@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.
- package/dist/index.js +557 -244
- 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(
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
154
|
-
if (
|
|
155
|
-
if (
|
|
156
|
-
if (
|
|
157
|
-
if (
|
|
158
|
-
if (
|
|
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
|
|
269
|
-
import
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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/
|
|
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 =
|
|
553
|
-
await
|
|
554
|
-
if (context.packagePath && await
|
|
555
|
-
const files = await
|
|
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 =
|
|
567
|
-
const destPath =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
609
|
-
if (await
|
|
610
|
-
await
|
|
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
|
|
635
|
-
import
|
|
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 =
|
|
700
|
-
await
|
|
701
|
-
if (context.packagePath && await
|
|
702
|
-
const files = await
|
|
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 =
|
|
714
|
-
const destPath =
|
|
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
|
|
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
|
|
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 =
|
|
730
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
803
|
+
await fs4.writeFile(skillPath, skillContent, "utf-8");
|
|
745
804
|
filesWritten.push(skillPath);
|
|
746
|
-
const metadataPath = await
|
|
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 =
|
|
766
|
-
if (await
|
|
767
|
-
await
|
|
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
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
|
831
|
-
logger.warn(
|
|
832
|
-
`Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
|
|
833
|
-
);
|
|
924
|
+
existingConfig = await fs6.readJson(mcpConfigPath);
|
|
834
925
|
} catch {
|
|
835
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
860
|
-
|
|
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
|
|
876
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
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
|
|
1013
|
-
import
|
|
1014
|
-
import
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1080
|
-
if (await
|
|
1081
|
-
const stat = await
|
|
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
|
|
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
|
|
1093
|
-
await
|
|
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
|
|
1117
|
-
const cached = await
|
|
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
|
|
1158
|
-
import
|
|
1159
|
-
import
|
|
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
|
|
1302
|
-
import
|
|
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 =
|
|
1355
|
-
await
|
|
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 =
|
|
1358
|
-
if (await
|
|
1359
|
-
const content = await
|
|
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
|
|
1382
|
-
const resolvedDestDir =
|
|
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 =
|
|
1393
|
-
const isWithinDest = resolvedPath.startsWith(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 =
|
|
2363
|
+
var TEMP_DIR = path13.join(os4.tmpdir(), "cpm-downloads");
|
|
2075
2364
|
async function downloadPackage(pkg) {
|
|
2076
2365
|
try {
|
|
2077
|
-
await
|
|
2078
|
-
const packageTempDir =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2570
|
-
import
|
|
2571
|
-
import
|
|
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 =
|
|
2866
|
+
const metadataPath = path15.join(packageDir, ".cpm.json");
|
|
2574
2867
|
try {
|
|
2575
|
-
if (await
|
|
2576
|
-
return await
|
|
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
|
|
2877
|
+
if (!await fs11.pathExists(dir)) {
|
|
2585
2878
|
return items;
|
|
2586
2879
|
}
|
|
2587
|
-
const entries = await
|
|
2880
|
+
const entries = await fs11.readdir(dir);
|
|
2588
2881
|
for (const entry of entries) {
|
|
2589
|
-
const entryPath =
|
|
2590
|
-
const stat = await
|
|
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
|
|
2898
|
+
async function scanMcpServersFromConfig(configPath, platform) {
|
|
2605
2899
|
const items = [];
|
|
2606
|
-
|
|
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
|
|
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 =
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
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 [
|
|
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
|
|
2710
|
-
const
|
|
2711
|
-
|
|
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(
|
|
2714
|
-
} else if (result.success) {
|
|
2715
|
-
spinner.warn(`Package ${packageName} was not found`);
|
|
3028
|
+
displayRemovedFiles(allFilesRemoved);
|
|
2716
3029
|
} else {
|
|
2717
|
-
spinner.
|
|
3030
|
+
spinner.warn(`Package ${packageName} was not found`);
|
|
2718
3031
|
}
|
|
2719
3032
|
} catch (error) {
|
|
2720
3033
|
spinner.fail(failText("Failed to uninstall", packageName));
|