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