@cpmai/cli 0.3.0-beta.1 → 0.3.0-beta.3

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 +565 -249
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
  import chalk5 from "chalk";
6
+ import { createRequire } from "module";
6
7
 
7
8
  // src/constants.ts
8
9
  var TIMEOUTS = {
@@ -47,10 +48,7 @@ var SEARCH_SORT_OPTIONS = [
47
48
  "name"
48
49
  // Alphabetical order
49
50
  ];
50
- var VALID_PLATFORMS = [
51
- "claude-code"
52
- // Currently the only supported platform
53
- ];
51
+ var VALID_PLATFORMS = ["claude-code", "cursor"];
54
52
  var ALLOWED_MCP_COMMANDS = [
55
53
  "npx",
56
54
  // Node package executor
@@ -76,6 +74,8 @@ var BLOCKED_MCP_ARG_PATTERNS = [
76
74
  // Concatenated eval flag (e.g., -eCODE)
77
75
  /-c(?:\s|$)/,
78
76
  // Command flag (with space or at end)
77
+ /^-c\S/,
78
+ // Concatenated command flag (e.g., -cCODE)
79
79
  /\bcurl\b/i,
80
80
  // curl command (data exfiltration)
81
81
  /\bwget\b/i,
@@ -147,15 +147,15 @@ function isSkillManifest(manifest) {
147
147
  function isMcpManifest(manifest) {
148
148
  return manifest.type === "mcp";
149
149
  }
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";
150
+ function getTypeFromPath(path16) {
151
+ if (path16.startsWith("skills/")) return "skill";
152
+ if (path16.startsWith("rules/")) return "rules";
153
+ if (path16.startsWith("mcp/")) return "mcp";
154
+ if (path16.startsWith("agents/")) return "agent";
155
+ if (path16.startsWith("hooks/")) return "hook";
156
+ if (path16.startsWith("workflows/")) return "workflow";
157
+ if (path16.startsWith("templates/")) return "template";
158
+ if (path16.startsWith("bundles/")) return "bundle";
159
159
  return null;
160
160
  }
161
161
  function resolvePackageType(pkg) {
@@ -265,11 +265,12 @@ var HandlerRegistry = class {
265
265
  var handlerRegistry = new HandlerRegistry();
266
266
 
267
267
  // src/adapters/handlers/rules-handler.ts
268
- import fs2 from "fs-extra";
269
- import path5 from "path";
268
+ import fs3 from "fs-extra";
269
+ import path6 from "path";
270
270
 
271
271
  // src/utils/platform.ts
272
272
  import path2 from "path";
273
+ import os2 from "os";
273
274
 
274
275
  // src/utils/config.ts
275
276
  import path from "path";
@@ -283,13 +284,28 @@ async function ensureClaudeDirs() {
283
284
  await fs.ensureDir(path.join(claudeHome, "rules"));
284
285
  await fs.ensureDir(path.join(claudeHome, "skills"));
285
286
  }
287
+ async function ensureCursorDirs(projectPath) {
288
+ await fs.ensureDir(path.join(projectPath, ".cursor", "rules"));
289
+ }
286
290
 
287
291
  // 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}`);
292
+ function getCursorHome() {
293
+ return path2.join(os2.homedir(), ".cursor");
294
+ }
295
+ function getCursorMcpConfigPath() {
296
+ return path2.join(getCursorHome(), "mcp.json");
297
+ }
298
+ function getRulesPath(platform, projectPath) {
299
+ if (platform === "claude-code") {
300
+ return path2.join(getClaudeHome(), "rules");
291
301
  }
292
- return path2.join(getClaudeHome(), "rules");
302
+ if (platform === "cursor") {
303
+ if (!projectPath) {
304
+ return path2.join(process.cwd(), ".cursor", "rules");
305
+ }
306
+ return path2.join(projectPath, ".cursor", "rules");
307
+ }
308
+ throw new Error(`Rules path is not supported for platform: ${platform}`);
293
309
  }
294
310
  function getSkillsPath() {
295
311
  return path2.join(getClaudeHome(), "skills");
@@ -504,17 +520,70 @@ function isPathWithinDirectory(filePath, directory) {
504
520
  return resolvedPath.startsWith(resolvedDir + path4.sep) || resolvedPath === resolvedDir;
505
521
  }
506
522
 
507
- // src/adapters/handlers/rules-handler.ts
523
+ // src/security/glob-validator.ts
524
+ var BLOCKED_GLOB_PATTERNS = [
525
+ // Environment and secret files
526
+ { pattern: /\.env\b/i, reason: "targets environment/secret files" },
527
+ { pattern: /\.secret/i, reason: "targets secret files" },
528
+ { pattern: /credentials/i, reason: "targets credential files" },
529
+ { pattern: /\.pem$/i, reason: "targets PEM certificate/key files" },
530
+ { pattern: /\.key$/i, reason: "targets key files" },
531
+ { pattern: /\.p12$/i, reason: "targets PKCS12 certificate files" },
532
+ { pattern: /\.pfx$/i, reason: "targets PFX certificate files" },
533
+ // SSH and GPG
534
+ { pattern: /\.ssh\//i, reason: "targets SSH directory" },
535
+ { pattern: /id_rsa/i, reason: "targets SSH private keys" },
536
+ { pattern: /id_ed25519/i, reason: "targets SSH private keys" },
537
+ { pattern: /\.gnupg\//i, reason: "targets GPG directory" },
538
+ // Git internals
539
+ { pattern: /\.git\//, reason: "targets git internals" },
540
+ // Config files with potential secrets
541
+ { pattern: /\.claude\.json$/i, reason: "targets Claude Code config" },
542
+ { pattern: /\.npmrc$/i, reason: "targets npm config (may contain tokens)" },
543
+ { pattern: /\.pypirc$/i, reason: "targets PyPI config (may contain tokens)" },
544
+ // System files
545
+ { pattern: /\/etc\//, reason: "targets system configuration" },
546
+ { pattern: /\/passwd/, reason: "targets system password file" },
547
+ { pattern: /\/shadow/, reason: "targets system shadow file" },
548
+ // Path traversal in globs
549
+ { pattern: /\.\.\//, reason: "contains path traversal" }
550
+ ];
551
+ function validateGlob(glob) {
552
+ if (!glob || typeof glob !== "string") {
553
+ return { valid: false, error: "Glob pattern cannot be empty" };
554
+ }
555
+ if (glob.includes("\0")) {
556
+ return { valid: false, error: "Glob pattern contains null bytes" };
557
+ }
558
+ for (const { pattern, reason } of BLOCKED_GLOB_PATTERNS) {
559
+ if (pattern.test(glob)) {
560
+ return {
561
+ valid: false,
562
+ error: `Glob pattern "${glob}" is blocked: ${reason}`
563
+ };
564
+ }
565
+ }
566
+ return { valid: true };
567
+ }
568
+ function validateGlobs(globs) {
569
+ for (const glob of globs) {
570
+ const result = validateGlob(glob);
571
+ if (!result.valid) {
572
+ return result;
573
+ }
574
+ }
575
+ return { valid: true };
576
+ }
577
+
578
+ // src/adapters/handlers/metadata.ts
579
+ import fs2 from "fs-extra";
580
+ import path5 from "path";
508
581
  async function writePackageMetadata(packageDir, manifest) {
509
582
  const metadata = {
510
583
  name: manifest.name,
511
- // e.g., "@cpm/typescript-strict"
512
584
  version: manifest.version,
513
- // e.g., "1.0.0"
514
585
  type: manifest.type,
515
- // e.g., "rules"
516
586
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
517
- // ISO timestamp for when it was installed
518
587
  };
519
588
  const metadataPath = path5.join(packageDir, ".cpm.json");
520
589
  try {
@@ -526,6 +595,8 @@ async function writePackageMetadata(packageDir, manifest) {
526
595
  }
527
596
  return metadataPath;
528
597
  }
598
+
599
+ // src/adapters/handlers/rules-handler.ts
529
600
  var RulesHandler = class {
530
601
  /**
531
602
  * Identifies this handler as handling "rules" type packages.
@@ -549,10 +620,10 @@ var RulesHandler = class {
549
620
  const filesWritten = [];
550
621
  const rulesBaseDir = getRulesPath("claude-code");
551
622
  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);
623
+ const rulesDir = path6.join(rulesBaseDir, folderName);
624
+ await fs3.ensureDir(rulesDir);
625
+ if (context.packagePath && await fs3.pathExists(context.packagePath)) {
626
+ const files = await fs3.readdir(context.packagePath);
556
627
  const mdFiles = files.filter(
557
628
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
558
629
  );
@@ -563,13 +634,18 @@ var RulesHandler = class {
563
634
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
564
635
  continue;
565
636
  }
566
- const srcPath = path5.join(context.packagePath, file);
567
- const destPath = path5.join(rulesDir, validation.sanitized);
637
+ const srcPath = path6.join(context.packagePath, file);
638
+ const destPath = path6.join(rulesDir, validation.sanitized);
568
639
  if (!isPathWithinDirectory(destPath, rulesDir)) {
569
640
  logger.warn(`Blocked path traversal attempt: ${file}`);
570
641
  continue;
571
642
  }
572
- await fs2.copy(srcPath, destPath);
643
+ const srcStat = await fs3.lstat(srcPath);
644
+ if (srcStat.isSymbolicLink()) {
645
+ logger.warn(`Blocked symlink in package: ${file}`);
646
+ continue;
647
+ }
648
+ await fs3.copy(srcPath, destPath);
573
649
  filesWritten.push(destPath);
574
650
  }
575
651
  const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
@@ -579,14 +655,14 @@ var RulesHandler = class {
579
655
  }
580
656
  const rulesContent = this.getRulesContent(manifest);
581
657
  if (!rulesContent) return filesWritten;
582
- const rulesPath = path5.join(rulesDir, "RULES.md");
658
+ const rulesPath = path6.join(rulesDir, "RULES.md");
583
659
  const content = `# ${manifest.name}
584
660
 
585
661
  ${manifest.description}
586
662
 
587
663
  ${rulesContent.trim()}
588
664
  `;
589
- await fs2.writeFile(rulesPath, content, "utf-8");
665
+ await fs3.writeFile(rulesPath, content, "utf-8");
590
666
  filesWritten.push(rulesPath);
591
667
  const metadataPath = await writePackageMetadata(rulesDir, manifest);
592
668
  filesWritten.push(metadataPath);
@@ -605,9 +681,9 @@ ${rulesContent.trim()}
605
681
  const filesRemoved = [];
606
682
  const folderName = sanitizeFolderName(packageName);
607
683
  const rulesBaseDir = getRulesPath("claude-code");
608
- const rulesPath = path5.join(rulesBaseDir, folderName);
609
- if (await fs2.pathExists(rulesPath)) {
610
- await fs2.remove(rulesPath);
684
+ const rulesPath = path6.join(rulesBaseDir, folderName);
685
+ if (await fs3.pathExists(rulesPath)) {
686
+ await fs3.remove(rulesPath);
611
687
  filesRemoved.push(rulesPath);
612
688
  }
613
689
  return filesRemoved;
@@ -623,7 +699,7 @@ ${rulesContent.trim()}
623
699
  * @returns The rules content string, or undefined if none exists
624
700
  */
625
701
  getRulesContent(manifest) {
626
- if (isRulesManifest(manifest)) {
702
+ if (isRulesManifest(manifest) && manifest.universal) {
627
703
  return manifest.universal.rules || manifest.universal.prompt;
628
704
  }
629
705
  return void 0;
@@ -631,29 +707,8 @@ ${rulesContent.trim()}
631
707
  };
632
708
 
633
709
  // 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
- }
710
+ import fs4 from "fs-extra";
711
+ import path7 from "path";
657
712
  function formatSkillMd(manifest) {
658
713
  const skill = manifest.skill;
659
714
  const content = manifest.universal?.prompt || manifest.universal?.rules || "";
@@ -696,10 +751,10 @@ var SkillHandler = class {
696
751
  const filesWritten = [];
697
752
  const skillsDir = getSkillsPath();
698
753
  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);
754
+ const skillDir = path7.join(skillsDir, folderName);
755
+ await fs4.ensureDir(skillDir);
756
+ if (context.packagePath && await fs4.pathExists(context.packagePath)) {
757
+ const files = await fs4.readdir(context.packagePath);
703
758
  const contentFiles = files.filter(
704
759
  (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
705
760
  );
@@ -710,40 +765,45 @@ var SkillHandler = class {
710
765
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
711
766
  continue;
712
767
  }
713
- const srcPath = path6.join(context.packagePath, file);
714
- const destPath = path6.join(skillDir, validation.sanitized);
768
+ const srcPath = path7.join(context.packagePath, file);
769
+ const destPath = path7.join(skillDir, validation.sanitized);
715
770
  if (!isPathWithinDirectory(destPath, skillDir)) {
716
771
  logger.warn(`Blocked path traversal attempt: ${file}`);
717
772
  continue;
718
773
  }
719
- await fs3.copy(srcPath, destPath);
774
+ const srcStat = await fs4.lstat(srcPath);
775
+ if (srcStat.isSymbolicLink()) {
776
+ logger.warn(`Blocked symlink in package: ${file}`);
777
+ continue;
778
+ }
779
+ await fs4.copy(srcPath, destPath);
720
780
  filesWritten.push(destPath);
721
781
  }
722
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
782
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
723
783
  filesWritten.push(metadataPath);
724
784
  return filesWritten;
725
785
  }
726
786
  }
727
787
  if (isSkillManifest(manifest)) {
728
788
  const skillContent = formatSkillMd(manifest);
729
- const skillPath = path6.join(skillDir, "SKILL.md");
730
- await fs3.writeFile(skillPath, skillContent, "utf-8");
789
+ const skillPath = path7.join(skillDir, "SKILL.md");
790
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
731
791
  filesWritten.push(skillPath);
732
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
792
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
733
793
  filesWritten.push(metadataPath);
734
794
  } else {
735
795
  const content = this.getUniversalContent(manifest);
736
796
  if (content) {
737
- const skillPath = path6.join(skillDir, "SKILL.md");
797
+ const skillPath = path7.join(skillDir, "SKILL.md");
738
798
  const skillContent = `# ${manifest.name}
739
799
 
740
800
  ${manifest.description}
741
801
 
742
802
  ${content.trim()}
743
803
  `;
744
- await fs3.writeFile(skillPath, skillContent, "utf-8");
804
+ await fs4.writeFile(skillPath, skillContent, "utf-8");
745
805
  filesWritten.push(skillPath);
746
- const metadataPath = await writePackageMetadata2(skillDir, manifest);
806
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
747
807
  filesWritten.push(metadataPath);
748
808
  }
749
809
  }
@@ -762,9 +822,9 @@ ${content.trim()}
762
822
  const filesRemoved = [];
763
823
  const folderName = sanitizeFolderName(packageName);
764
824
  const skillsDir = getSkillsPath();
765
- const skillPath = path6.join(skillsDir, folderName);
766
- if (await fs3.pathExists(skillPath)) {
767
- await fs3.remove(skillPath);
825
+ const skillPath = path7.join(skillsDir, folderName);
826
+ if (await fs4.pathExists(skillPath)) {
827
+ await fs4.remove(skillPath);
768
828
  filesRemoved.push(skillPath);
769
829
  }
770
830
  return filesRemoved;
@@ -787,28 +847,66 @@ ${content.trim()}
787
847
  };
788
848
 
789
849
  // 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
- */
850
+ import path9 from "path";
851
+
852
+ // src/adapters/handlers/base-mcp-handler.ts
853
+ import fs6 from "fs-extra";
854
+ import path8 from "path";
855
+ import crypto from "crypto";
856
+
857
+ // src/utils/file-lock.ts
858
+ import fs5 from "fs-extra";
859
+ var LOCK_STALE_MS = 1e4;
860
+ var LOCK_RETRY_MS = 100;
861
+ var LOCK_MAX_RETRIES = 50;
862
+ async function acquireLock(filePath) {
863
+ const lockPath = `${filePath}.lock`;
864
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
865
+ try {
866
+ await fs5.writeFile(lockPath, String(Date.now()), { flag: "wx" });
867
+ return async () => {
868
+ try {
869
+ await fs5.remove(lockPath);
870
+ } catch {
871
+ }
872
+ };
873
+ } catch (error) {
874
+ const err = error;
875
+ if (err.code === "EEXIST") {
876
+ try {
877
+ const content = await fs5.readFile(lockPath, "utf-8");
878
+ const lockTime = parseInt(content, 10);
879
+ if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_STALE_MS) {
880
+ await fs5.remove(lockPath);
881
+ continue;
882
+ }
883
+ } catch {
884
+ await fs5.remove(lockPath).catch(() => {
885
+ });
886
+ continue;
887
+ }
888
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
889
+ continue;
890
+ }
891
+ throw error;
892
+ }
893
+ }
894
+ throw new Error(
895
+ `Could not acquire lock for ${filePath} after ${LOCK_MAX_RETRIES} retries`
896
+ );
897
+ }
898
+ async function withFileLock(filePath, fn) {
899
+ const release = await acquireLock(filePath);
900
+ try {
901
+ return await fn();
902
+ } finally {
903
+ await release();
904
+ }
905
+ }
906
+
907
+ // src/adapters/handlers/base-mcp-handler.ts
908
+ var BaseMcpHandler = class {
797
909
  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
910
  async install(manifest, _context) {
813
911
  const filesWritten = [];
814
912
  if (!isMcpManifest(manifest)) {
@@ -818,78 +916,68 @@ var McpHandler = class {
818
916
  if (!mcpValidation.valid) {
819
917
  throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
820
918
  }
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()}`;
919
+ const mcpConfigPath = this.getConfigPath();
920
+ await fs6.ensureDir(path8.dirname(mcpConfigPath));
921
+ await withFileLock(mcpConfigPath, async () => {
922
+ let existingConfig = {};
923
+ if (await fs6.pathExists(mcpConfigPath)) {
829
924
  try {
830
- await fs4.copy(mcpConfigPath, backupPath);
831
- logger.warn(
832
- `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
833
- );
925
+ existingConfig = await fs6.readJson(mcpConfigPath);
834
926
  } catch {
835
- logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
927
+ const backupPath = `${mcpConfigPath}.backup.${crypto.randomBytes(8).toString("hex")}`;
928
+ try {
929
+ await fs6.copy(mcpConfigPath, backupPath);
930
+ logger.warn(
931
+ `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
932
+ );
933
+ } catch {
934
+ logger.warn(
935
+ `Could not parse ${mcpConfigPath}, creating new config`
936
+ );
937
+ }
938
+ existingConfig = {};
836
939
  }
837
- existingConfig = {};
838
940
  }
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": "..." }
941
+ const sanitizedName = sanitizeFolderName(manifest.name);
942
+ const existingMcpServers = existingConfig.mcpServers || {};
943
+ const updatedConfig = {
944
+ ...existingConfig,
945
+ mcpServers: {
946
+ ...existingMcpServers,
947
+ [sanitizedName]: {
948
+ command: manifest.mcp.command,
949
+ args: manifest.mcp.args,
950
+ env: manifest.mcp.env
951
+ }
856
952
  }
857
- }
858
- };
859
- await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
860
- filesWritten.push(mcpConfigPath);
953
+ };
954
+ await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
955
+ filesWritten.push(mcpConfigPath);
956
+ });
861
957
  return filesWritten;
862
958
  }
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
959
  async uninstall(packageName, _context) {
873
960
  const filesWritten = [];
874
961
  const folderName = sanitizeFolderName(packageName);
875
- const claudeHome = getClaudeHome();
876
- const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
877
- if (!await fs4.pathExists(mcpConfigPath)) {
962
+ const mcpConfigPath = this.getConfigPath();
963
+ if (!await fs6.pathExists(mcpConfigPath)) {
878
964
  return filesWritten;
879
965
  }
880
966
  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);
967
+ await withFileLock(mcpConfigPath, async () => {
968
+ const config = await fs6.readJson(mcpConfigPath);
969
+ const mcpServers = config.mcpServers;
970
+ if (!mcpServers || !mcpServers[folderName]) {
971
+ return;
972
+ }
973
+ const { [folderName]: _removed, ...remainingServers } = mcpServers;
974
+ const updatedConfig = {
975
+ ...config,
976
+ mcpServers: remainingServers
977
+ };
978
+ await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
979
+ filesWritten.push(mcpConfigPath);
980
+ });
893
981
  } catch (error) {
894
982
  logger.warn(
895
983
  `Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -899,6 +987,14 @@ var McpHandler = class {
899
987
  }
900
988
  };
901
989
 
990
+ // src/adapters/handlers/mcp-handler.ts
991
+ var McpHandler = class extends BaseMcpHandler {
992
+ getConfigPath() {
993
+ const claudeHome = getClaudeHome();
994
+ return path9.join(path9.dirname(claudeHome), ".claude.json");
995
+ }
996
+ };
997
+
902
998
  // src/adapters/handlers/index.ts
903
999
  function initializeHandlers() {
904
1000
  handlerRegistry.register(new RulesHandler());
@@ -908,8 +1004,6 @@ function initializeHandlers() {
908
1004
  initializeHandlers();
909
1005
 
910
1006
  // src/adapters/claude-code.ts
911
- import fs5 from "fs-extra";
912
- import path8 from "path";
913
1007
  var ClaudeCodeAdapter = class extends PlatformAdapter {
914
1008
  platform = "claude-code";
915
1009
  displayName = "Claude Code";
@@ -938,25 +1032,14 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
938
1032
  }
939
1033
  async uninstall(packageName, projectPath) {
940
1034
  const filesWritten = [];
941
- const folderName = sanitizeFolderName(packageName);
942
1035
  const context = { projectPath };
943
1036
  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);
1037
+ for (const type of ["rules", "skill", "mcp"]) {
1038
+ if (handlerRegistry.hasHandler(type)) {
1039
+ const handler = handlerRegistry.getHandler(type);
1040
+ const files = await handler.uninstall(packageName, context);
1041
+ filesWritten.push(...files);
1042
+ }
960
1043
  }
961
1044
  return {
962
1045
  success: true,
@@ -999,22 +1082,229 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
999
1082
  }
1000
1083
  };
1001
1084
 
1085
+ // src/adapters/handlers/cursor-rules-handler.ts
1086
+ import fs7 from "fs-extra";
1087
+ import path10 from "path";
1088
+ function escapeYamlString(value) {
1089
+ if (/[\n\r\t\0:#{}[\]&*?|>!%@`"',]/.test(value) || value.trim() !== value) {
1090
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
1091
+ return `"${escaped}"`;
1092
+ }
1093
+ return value;
1094
+ }
1095
+ function toMdcContent(description, globs, rulesContent) {
1096
+ const alwaysApply = globs.length === 0;
1097
+ const frontmatter = [
1098
+ "---",
1099
+ `description: ${escapeYamlString(description)}`,
1100
+ `globs: ${JSON.stringify(globs)}`,
1101
+ `alwaysApply: ${alwaysApply}`,
1102
+ "---"
1103
+ ].join("\n");
1104
+ return `${frontmatter}
1105
+ ${rulesContent.trim()}
1106
+ `;
1107
+ }
1108
+ var CursorRulesHandler = class {
1109
+ packageType = "rules";
1110
+ async install(manifest, context) {
1111
+ const filesWritten = [];
1112
+ const rulesBaseDir = getRulesPath("cursor", context.projectPath);
1113
+ const folderName = sanitizeFolderName(manifest.name);
1114
+ const rulesDir = path10.join(rulesBaseDir, folderName);
1115
+ await fs7.ensureDir(rulesDir);
1116
+ const description = manifest.description || manifest.name;
1117
+ const globs = manifest.universal?.globs || [];
1118
+ if (globs.length > 0) {
1119
+ const globValidation = validateGlobs(globs);
1120
+ if (!globValidation.valid) {
1121
+ throw new Error(
1122
+ `Glob security validation failed: ${globValidation.error}`
1123
+ );
1124
+ }
1125
+ }
1126
+ if (context.packagePath && await fs7.pathExists(context.packagePath)) {
1127
+ const files = await fs7.readdir(context.packagePath);
1128
+ const mdFiles = files.filter(
1129
+ (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
1130
+ );
1131
+ if (mdFiles.length > 0) {
1132
+ for (const file of mdFiles) {
1133
+ const validation = sanitizeFileName(file);
1134
+ if (!validation.valid) {
1135
+ logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
1136
+ continue;
1137
+ }
1138
+ const srcPath = path10.join(context.packagePath, file);
1139
+ const mdcFileName = validation.sanitized.replace(/\.md$/, ".mdc");
1140
+ const destPath = path10.join(rulesDir, mdcFileName);
1141
+ if (!isPathWithinDirectory(destPath, rulesDir)) {
1142
+ logger.warn(`Blocked path traversal attempt: ${file}`);
1143
+ continue;
1144
+ }
1145
+ const srcStat = await fs7.lstat(srcPath);
1146
+ if (srcStat.isSymbolicLink()) {
1147
+ logger.warn(`Blocked symlink in package: ${file}`);
1148
+ continue;
1149
+ }
1150
+ const content = await fs7.readFile(srcPath, "utf-8");
1151
+ const mdcContent2 = toMdcContent(description, globs, content);
1152
+ await fs7.writeFile(destPath, mdcContent2, "utf-8");
1153
+ filesWritten.push(destPath);
1154
+ }
1155
+ const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
1156
+ filesWritten.push(metadataPath2);
1157
+ return filesWritten;
1158
+ }
1159
+ }
1160
+ const rulesContent = this.getRulesContent(manifest);
1161
+ if (!rulesContent) return filesWritten;
1162
+ const rulesPath = path10.join(rulesDir, "RULES.mdc");
1163
+ const mdcContent = toMdcContent(description, globs, rulesContent);
1164
+ await fs7.writeFile(rulesPath, mdcContent, "utf-8");
1165
+ filesWritten.push(rulesPath);
1166
+ const metadataPath = await writePackageMetadata(rulesDir, manifest);
1167
+ filesWritten.push(metadataPath);
1168
+ return filesWritten;
1169
+ }
1170
+ async uninstall(packageName, context) {
1171
+ const filesRemoved = [];
1172
+ const folderName = sanitizeFolderName(packageName);
1173
+ const rulesBaseDir = getRulesPath("cursor", context.projectPath);
1174
+ const rulesPath = path10.join(rulesBaseDir, folderName);
1175
+ if (await fs7.pathExists(rulesPath)) {
1176
+ await fs7.remove(rulesPath);
1177
+ filesRemoved.push(rulesPath);
1178
+ }
1179
+ return filesRemoved;
1180
+ }
1181
+ getRulesContent(manifest) {
1182
+ if (isRulesManifest(manifest) && manifest.universal) {
1183
+ return manifest.universal.rules || manifest.universal.prompt;
1184
+ }
1185
+ return void 0;
1186
+ }
1187
+ };
1188
+
1189
+ // src/adapters/handlers/cursor-mcp-handler.ts
1190
+ var CursorMcpHandler = class extends BaseMcpHandler {
1191
+ getConfigPath() {
1192
+ return getCursorMcpConfigPath();
1193
+ }
1194
+ };
1195
+
1196
+ // src/adapters/cursor.ts
1197
+ var cursorRulesHandler = new CursorRulesHandler();
1198
+ var cursorMcpHandler = new CursorMcpHandler();
1199
+ var CursorAdapter = class extends PlatformAdapter {
1200
+ platform = "cursor";
1201
+ displayName = "Cursor";
1202
+ async isAvailable(_projectPath) {
1203
+ return true;
1204
+ }
1205
+ async install(manifest, projectPath, packagePath) {
1206
+ const filesWritten = [];
1207
+ try {
1208
+ const context = { projectPath, packagePath };
1209
+ if (manifest.type === "skill") {
1210
+ logger.warn(
1211
+ `Package "${manifest.name}" is a skill package. Skills are not supported on Cursor \u2014 skipping.`
1212
+ );
1213
+ return {
1214
+ success: true,
1215
+ platform: "cursor",
1216
+ filesWritten
1217
+ };
1218
+ }
1219
+ const result = await this.installByType(manifest, context);
1220
+ filesWritten.push(...result);
1221
+ return {
1222
+ success: true,
1223
+ platform: "cursor",
1224
+ filesWritten
1225
+ };
1226
+ } catch (error) {
1227
+ return {
1228
+ success: false,
1229
+ platform: "cursor",
1230
+ filesWritten,
1231
+ error: error instanceof Error ? error.message : "Unknown error"
1232
+ };
1233
+ }
1234
+ }
1235
+ async uninstall(packageName, projectPath) {
1236
+ const filesWritten = [];
1237
+ const context = { projectPath };
1238
+ try {
1239
+ const rulesFiles = await cursorRulesHandler.uninstall(
1240
+ packageName,
1241
+ context
1242
+ );
1243
+ filesWritten.push(...rulesFiles);
1244
+ const mcpFiles = await cursorMcpHandler.uninstall(packageName, context);
1245
+ filesWritten.push(...mcpFiles);
1246
+ return {
1247
+ success: true,
1248
+ platform: "cursor",
1249
+ filesWritten
1250
+ };
1251
+ } catch (error) {
1252
+ return {
1253
+ success: false,
1254
+ platform: "cursor",
1255
+ filesWritten,
1256
+ error: error instanceof Error ? error.message : "Unknown error"
1257
+ };
1258
+ }
1259
+ }
1260
+ async installByType(manifest, context) {
1261
+ switch (manifest.type) {
1262
+ case "rules":
1263
+ return cursorRulesHandler.install(manifest, context);
1264
+ case "mcp":
1265
+ return cursorMcpHandler.install(manifest, context);
1266
+ default:
1267
+ logger.warn(
1268
+ `Package type "${manifest.type}" is not yet supported on Cursor`
1269
+ );
1270
+ return [];
1271
+ }
1272
+ }
1273
+ };
1274
+
1002
1275
  // src/adapters/index.ts
1003
1276
  var adapters = {
1004
- "claude-code": new ClaudeCodeAdapter()
1277
+ "claude-code": new ClaudeCodeAdapter(),
1278
+ cursor: new CursorAdapter()
1005
1279
  };
1006
1280
  function getAdapter(platform) {
1007
- return adapters[platform];
1281
+ const adapter = adapters[platform];
1282
+ if (!adapter) {
1283
+ throw new Error(`No adapter available for platform: ${platform}`);
1284
+ }
1285
+ return adapter;
1008
1286
  }
1009
1287
 
1010
1288
  // src/utils/registry.ts
1011
1289
  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");
1290
+ import fs8 from "fs-extra";
1291
+ import path11 from "path";
1292
+ import os3 from "os";
1293
+ function getRegistryUrl() {
1294
+ const envUrl = process.env.CPM_REGISTRY_URL;
1295
+ if (envUrl) {
1296
+ if (!envUrl.startsWith("https://")) {
1297
+ throw new Error(
1298
+ "CPM_REGISTRY_URL must use HTTPS. HTTP registries are not allowed for security reasons."
1299
+ );
1300
+ }
1301
+ return envUrl;
1302
+ }
1303
+ return "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
1304
+ }
1305
+ var DEFAULT_REGISTRY_URL = getRegistryUrl();
1306
+ var CACHE_DIR = path11.join(os3.homedir(), ".cpm", "cache");
1307
+ var CACHE_FILE = path11.join(CACHE_DIR, "registry.json");
1018
1308
  var comparators = {
1019
1309
  downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
1020
1310
  stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
@@ -1076,11 +1366,11 @@ var Registry = class {
1076
1366
  }
1077
1367
  async loadFileCache() {
1078
1368
  try {
1079
- await fs6.ensureDir(CACHE_DIR);
1080
- if (await fs6.pathExists(CACHE_FILE)) {
1081
- const stat = await fs6.stat(CACHE_FILE);
1369
+ await fs8.ensureDir(CACHE_DIR);
1370
+ if (await fs8.pathExists(CACHE_FILE)) {
1371
+ const stat = await fs8.stat(CACHE_FILE);
1082
1372
  if (Date.now() - stat.mtimeMs < LIMITS.CACHE_TTL_MS) {
1083
- return await fs6.readJson(CACHE_FILE);
1373
+ return await fs8.readJson(CACHE_FILE);
1084
1374
  }
1085
1375
  }
1086
1376
  } catch {
@@ -1089,8 +1379,8 @@ var Registry = class {
1089
1379
  }
1090
1380
  async saveFileCache(data) {
1091
1381
  try {
1092
- await fs6.ensureDir(CACHE_DIR);
1093
- await fs6.writeJson(CACHE_FILE, data, { spaces: 2 });
1382
+ await fs8.ensureDir(CACHE_DIR);
1383
+ await fs8.writeJson(CACHE_FILE, data, { spaces: 2 });
1094
1384
  } catch {
1095
1385
  }
1096
1386
  }
@@ -1113,8 +1403,8 @@ var Registry = class {
1113
1403
  return this.cache;
1114
1404
  }
1115
1405
  try {
1116
- if (await fs6.pathExists(CACHE_FILE)) {
1117
- const cached = await fs6.readJson(CACHE_FILE);
1406
+ if (await fs8.pathExists(CACHE_FILE)) {
1407
+ const cached = await fs8.readJson(CACHE_FILE);
1118
1408
  this.cache = cached;
1119
1409
  return cached;
1120
1410
  }
@@ -1154,9 +1444,9 @@ var Registry = class {
1154
1444
  var registry = new Registry();
1155
1445
 
1156
1446
  // src/utils/downloader.ts
1157
- import fs8 from "fs-extra";
1158
- import path11 from "path";
1159
- import os3 from "os";
1447
+ import fs10 from "fs-extra";
1448
+ import path13 from "path";
1449
+ import os4 from "os";
1160
1450
 
1161
1451
  // src/sources/manifest-resolver.ts
1162
1452
  var ManifestResolver = class {
@@ -1298,8 +1588,8 @@ var RepositorySource = class {
1298
1588
 
1299
1589
  // src/sources/tarball-source.ts
1300
1590
  import got3 from "got";
1301
- import fs7 from "fs-extra";
1302
- import path10 from "path";
1591
+ import fs9 from "fs-extra";
1592
+ import path12 from "path";
1303
1593
  import * as tar from "tar";
1304
1594
  import yaml2 from "yaml";
1305
1595
  var TarballSource = class {
@@ -1351,12 +1641,12 @@ var TarballSource = class {
1351
1641
  responseType: "buffer"
1352
1642
  // Get raw binary data
1353
1643
  });
1354
- const tarballPath = path10.join(context.tempDir, "package.tar.gz");
1355
- await fs7.writeFile(tarballPath, response.body);
1644
+ const tarballPath = path12.join(context.tempDir, "package.tar.gz");
1645
+ await fs9.writeFile(tarballPath, response.body);
1356
1646
  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");
1647
+ const manifestPath = path12.join(context.tempDir, "cpm.yaml");
1648
+ if (await fs9.pathExists(manifestPath)) {
1649
+ const content = await fs9.readFile(manifestPath, "utf-8");
1360
1650
  return yaml2.parse(content);
1361
1651
  }
1362
1652
  return null;
@@ -1378,8 +1668,8 @@ var TarballSource = class {
1378
1668
  * @param destDir - Directory to extract to
1379
1669
  */
1380
1670
  async extractTarball(tarballPath, destDir) {
1381
- await fs7.ensureDir(destDir);
1382
- const resolvedDestDir = path10.resolve(destDir);
1671
+ await fs9.ensureDir(destDir);
1672
+ const resolvedDestDir = path12.resolve(destDir);
1383
1673
  await tar.extract({
1384
1674
  file: tarballPath,
1385
1675
  // The tarball file to extract
@@ -1389,8 +1679,8 @@ var TarballSource = class {
1389
1679
  // Remove the top-level directory (e.g., "package-1.0.0/")
1390
1680
  // Security filter: check each entry before extracting
1391
1681
  filter: (entryPath) => {
1392
- const resolvedPath = path10.resolve(destDir, entryPath);
1393
- const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path10.sep) || resolvedPath === resolvedDestDir;
1682
+ const resolvedPath = path12.resolve(destDir, entryPath);
1683
+ const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path12.sep) || resolvedPath === resolvedDestDir;
1394
1684
  if (!isWithinDest) {
1395
1685
  logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
1396
1686
  return false;
@@ -2071,15 +2361,15 @@ function createDefaultResolver() {
2071
2361
  var defaultResolver = createDefaultResolver();
2072
2362
 
2073
2363
  // src/utils/downloader.ts
2074
- var TEMP_DIR = path11.join(os3.tmpdir(), "cpm-downloads");
2364
+ var TEMP_DIR = path13.join(os4.tmpdir(), "cpm-downloads");
2075
2365
  async function downloadPackage(pkg) {
2076
2366
  try {
2077
- await fs8.ensureDir(TEMP_DIR);
2078
- const packageTempDir = path11.join(
2367
+ await fs10.ensureDir(TEMP_DIR);
2368
+ const packageTempDir = path13.join(
2079
2369
  TEMP_DIR,
2080
2370
  `${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
2081
2371
  );
2082
- await fs8.ensureDir(packageTempDir);
2372
+ await fs10.ensureDir(packageTempDir);
2083
2373
  const manifest = await defaultResolver.resolve(pkg, {
2084
2374
  tempDir: packageTempDir
2085
2375
  });
@@ -2094,7 +2384,7 @@ async function downloadPackage(pkg) {
2094
2384
  async function cleanupTempDir(tempDir) {
2095
2385
  try {
2096
2386
  if (tempDir.startsWith(TEMP_DIR)) {
2097
- await fs8.remove(tempDir);
2387
+ await fs10.remove(tempDir);
2098
2388
  }
2099
2389
  } catch {
2100
2390
  }
@@ -2221,7 +2511,7 @@ var SEMANTIC_COLORS = {
2221
2511
 
2222
2512
  // src/commands/ui/formatters.ts
2223
2513
  import chalk2 from "chalk";
2224
- import path12 from "path";
2514
+ import path14 from "path";
2225
2515
  function formatNumber(num) {
2226
2516
  if (num >= 1e6) {
2227
2517
  return `${(num / 1e6).toFixed(1)}M`;
@@ -2232,7 +2522,7 @@ function formatNumber(num) {
2232
2522
  return num.toString();
2233
2523
  }
2234
2524
  function formatPath(filePath) {
2235
- const relativePath = path12.relative(process.cwd(), filePath);
2525
+ const relativePath = path14.relative(process.cwd(), filePath);
2236
2526
  if (relativePath.startsWith("..")) {
2237
2527
  return filePath;
2238
2528
  }
@@ -2361,8 +2651,8 @@ function createSpinner(initialText) {
2361
2651
  function spinnerText(action, packageName) {
2362
2652
  return `${action} ${chalk3.cyan(packageName)}...`;
2363
2653
  }
2364
- function successText(action, packageName, version) {
2365
- const versionStr = version ? `@${chalk3.dim(version)}` : "";
2654
+ function successText(action, packageName, version2) {
2655
+ const versionStr = version2 ? `@${chalk3.dim(version2)}` : "";
2366
2656
  return `${action} ${chalk3.green(packageName)}${versionStr}`;
2367
2657
  }
2368
2658
  function failText(action, packageName, error) {
@@ -2490,7 +2780,11 @@ async function installCommand(packageName, rawOptions) {
2490
2780
  tempDir = downloadResult.tempDir;
2491
2781
  const targetPlatforms = [options.platform];
2492
2782
  spinner.update(`Installing to ${targetPlatforms.join(", ")}...`);
2493
- await ensureClaudeDirs();
2783
+ if (options.platform === "cursor") {
2784
+ await ensureCursorDirs(process.cwd());
2785
+ } else {
2786
+ await ensureClaudeDirs();
2787
+ }
2494
2788
  const results = await installToPlatforms(
2495
2789
  manifest,
2496
2790
  tempDir,
@@ -2566,28 +2860,28 @@ async function searchCommand(query, rawOptions) {
2566
2860
 
2567
2861
  // src/commands/list.ts
2568
2862
  import chalk4 from "chalk";
2569
- import fs9 from "fs-extra";
2570
- import path13 from "path";
2571
- import os4 from "os";
2863
+ import fs11 from "fs-extra";
2864
+ import path15 from "path";
2865
+ import os5 from "os";
2572
2866
  async function readPackageMetadata(packageDir) {
2573
- const metadataPath = path13.join(packageDir, ".cpm.json");
2867
+ const metadataPath = path15.join(packageDir, ".cpm.json");
2574
2868
  try {
2575
- if (await fs9.pathExists(metadataPath)) {
2576
- return await fs9.readJson(metadataPath);
2869
+ if (await fs11.pathExists(metadataPath)) {
2870
+ return await fs11.readJson(metadataPath);
2577
2871
  }
2578
2872
  } catch {
2579
2873
  }
2580
2874
  return null;
2581
2875
  }
2582
- async function scanDirectory(dir, type) {
2876
+ async function scanDirectory(dir, type, platform) {
2583
2877
  const items = [];
2584
- if (!await fs9.pathExists(dir)) {
2878
+ if (!await fs11.pathExists(dir)) {
2585
2879
  return items;
2586
2880
  }
2587
- const entries = await fs9.readdir(dir);
2881
+ const entries = await fs11.readdir(dir);
2588
2882
  for (const entry of entries) {
2589
- const entryPath = path13.join(dir, entry);
2590
- const stat = await fs9.stat(entryPath);
2883
+ const entryPath = path15.join(dir, entry);
2884
+ const stat = await fs11.stat(entryPath);
2591
2885
  if (stat.isDirectory()) {
2592
2886
  const metadata = await readPackageMetadata(entryPath);
2593
2887
  items.push({
@@ -2595,27 +2889,28 @@ async function scanDirectory(dir, type) {
2595
2889
  folderName: entry,
2596
2890
  type,
2597
2891
  version: metadata?.version,
2598
- path: entryPath
2892
+ path: entryPath,
2893
+ platform
2599
2894
  });
2600
2895
  }
2601
2896
  }
2602
2897
  return items;
2603
2898
  }
2604
- async function scanMcpServers() {
2899
+ async function scanMcpServersFromConfig(configPath, platform) {
2605
2900
  const items = [];
2606
- const configPath = path13.join(os4.homedir(), ".claude.json");
2607
- if (!await fs9.pathExists(configPath)) {
2901
+ if (!await fs11.pathExists(configPath)) {
2608
2902
  return items;
2609
2903
  }
2610
2904
  try {
2611
- const config = await fs9.readJson(configPath);
2905
+ const config = await fs11.readJson(configPath);
2612
2906
  const mcpServers = config.mcpServers || {};
2613
2907
  for (const name of Object.keys(mcpServers)) {
2614
2908
  items.push({
2615
2909
  name,
2616
2910
  folderName: name,
2617
2911
  type: "mcp",
2618
- path: configPath
2912
+ path: configPath,
2913
+ platform
2619
2914
  });
2620
2915
  }
2621
2916
  } catch {
@@ -2623,13 +2918,24 @@ async function scanMcpServers() {
2623
2918
  return items;
2624
2919
  }
2625
2920
  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()
2921
+ const claudeHome = path15.join(os5.homedir(), ".claude");
2922
+ const cursorRulesDir = path15.join(process.cwd(), ".cursor", "rules");
2923
+ const cursorMcpConfig = getCursorMcpConfigPath();
2924
+ const claudeMcpConfig = path15.join(os5.homedir(), ".claude.json");
2925
+ const [claudeRules, claudeSkills, claudeMcp, cursorRules, cursorMcp] = await Promise.all([
2926
+ scanDirectory(path15.join(claudeHome, "rules"), "rules", "claude-code"),
2927
+ scanDirectory(path15.join(claudeHome, "skills"), "skill", "claude-code"),
2928
+ scanMcpServersFromConfig(claudeMcpConfig, "claude-code"),
2929
+ scanDirectory(cursorRulesDir, "rules", "cursor"),
2930
+ scanMcpServersFromConfig(cursorMcpConfig, "cursor")
2631
2931
  ]);
2632
- return [...rules, ...skills, ...mcp];
2932
+ return [
2933
+ ...claudeRules,
2934
+ ...claudeSkills,
2935
+ ...claudeMcp,
2936
+ ...cursorRules,
2937
+ ...cursorMcp
2938
+ ];
2633
2939
  }
2634
2940
  function groupByType(packages) {
2635
2941
  return packages.reduce(
@@ -2641,9 +2947,10 @@ function groupByType(packages) {
2641
2947
  );
2642
2948
  }
2643
2949
  function displayPackage(pkg) {
2644
- const version = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
2950
+ const version2 = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
2951
+ const platform = pkg.platform ? SEMANTIC_COLORS.dim(` [${pkg.platform}]`) : "";
2645
2952
  logger.log(
2646
- ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}`
2953
+ ` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version2}${platform}`
2647
2954
  );
2648
2955
  }
2649
2956
  function displayByType(byType) {
@@ -2706,15 +3013,22 @@ async function uninstallCommand(packageName) {
2706
3013
  const spinner = createSpinner(spinnerText("Uninstalling", packageName));
2707
3014
  try {
2708
3015
  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) {
3016
+ const allFilesRemoved = [];
3017
+ for (const platform of VALID_PLATFORMS) {
3018
+ try {
3019
+ const adapter = getAdapter(platform);
3020
+ const result = await adapter.uninstall(folderName, process.cwd());
3021
+ if (result.success) {
3022
+ allFilesRemoved.push(...result.filesWritten);
3023
+ }
3024
+ } catch {
3025
+ }
3026
+ }
3027
+ if (allFilesRemoved.length > 0) {
2712
3028
  spinner.succeed(successText("Uninstalled", packageName));
2713
- displayRemovedFiles(result.filesWritten);
2714
- } else if (result.success) {
2715
- spinner.warn(`Package ${packageName} was not found`);
3029
+ displayRemovedFiles(allFilesRemoved);
2716
3030
  } else {
2717
- spinner.fail(failText("Failed to uninstall", packageName, result.error));
3031
+ spinner.warn(`Package ${packageName} was not found`);
2718
3032
  }
2719
3033
  } catch (error) {
2720
3034
  spinner.fail(failText("Failed to uninstall", packageName));
@@ -2723,6 +3037,8 @@ async function uninstallCommand(packageName) {
2723
3037
  }
2724
3038
 
2725
3039
  // src/index.ts
3040
+ var require2 = createRequire(import.meta.url);
3041
+ var { version } = require2("../package.json");
2726
3042
  var program = new Command();
2727
3043
  var logo = `
2728
3044
  ${chalk5.hex("#22d3ee")("\u2591\u2588\u2588\u2588\u2588\u2588\u2557\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2591\u2588\u2588\u2588\u2557\u2591\u2591\u2591\u2588\u2588\u2588\u2557")}
@@ -2736,7 +3052,7 @@ program.name("cpm").description(
2736
3052
  `${logo}
2737
3053
  ${chalk5.dim("Package manager for AI coding assistants")}
2738
3054
  `
2739
- ).version("0.2.0-beta.1").option("-q, --quiet", "Suppress all output except errors").option("-v, --verbose", "Enable verbose output for debugging").hook("preAction", (thisCommand) => {
3055
+ ).version(version).option("-q, --quiet", "Suppress all output except errors").option("-v, --verbose", "Enable verbose output for debugging").hook("preAction", (thisCommand) => {
2740
3056
  const opts = thisCommand.optsWithGlobals();
2741
3057
  configureLogger({
2742
3058
  quiet: opts.quiet,
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.3",
4
4
  "description": "CPM CLI - cpm-ai.dev",
5
5
  "keywords": [
6
6
  "claude-code",