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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +289 -262
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,107 +4,6 @@
4
4
  import { Command } from "commander";
5
5
  import chalk5 from "chalk";
6
6
 
7
- // src/adapters/base.ts
8
- var PlatformAdapter = class {
9
- };
10
-
11
- // src/adapters/handlers/handler-registry.ts
12
- var HandlerRegistry = class {
13
- /**
14
- * Internal storage for handlers.
15
- * Maps package type strings to their handler instances.
16
- * Using Map for O(1) lookup performance.
17
- */
18
- handlers = /* @__PURE__ */ new Map();
19
- /**
20
- * Register a handler for a specific package type.
21
- *
22
- * When you create a new handler (like RulesHandler), you call this method
23
- * to add it to the registry so it can be found later.
24
- *
25
- * @param handler - The handler instance to register. The handler's
26
- * packageType property determines which type it handles.
27
- *
28
- * @example
29
- * ```typescript
30
- * const rulesHandler = new RulesHandler();
31
- * registry.register(rulesHandler);
32
- * // Now "rules" type packages will use RulesHandler
33
- * ```
34
- */
35
- register(handler) {
36
- this.handlers.set(handler.packageType, handler);
37
- }
38
- /**
39
- * Get the handler for a specific package type.
40
- *
41
- * Use this when you need to install or uninstall a package.
42
- * It returns the appropriate handler based on the package type.
43
- *
44
- * @param type - The package type to find a handler for (e.g., "rules", "skill", "mcp")
45
- * @returns The handler that can process this package type
46
- * @throws Error if no handler is registered for the given type
47
- *
48
- * @example
49
- * ```typescript
50
- * const handler = registry.getHandler("skill");
51
- * const files = await handler.install(manifest, context);
52
- * ```
53
- */
54
- getHandler(type) {
55
- const handler = this.handlers.get(type);
56
- if (!handler) {
57
- throw new Error(`No handler registered for package type: ${type}`);
58
- }
59
- return handler;
60
- }
61
- /**
62
- * Check if a handler exists for a specific package type.
63
- *
64
- * Useful when you want to check availability before attempting
65
- * to get a handler, avoiding the need for try-catch blocks.
66
- *
67
- * @param type - The package type to check
68
- * @returns true if a handler is registered, false otherwise
69
- *
70
- * @example
71
- * ```typescript
72
- * if (registry.hasHandler("mcp")) {
73
- * const handler = registry.getHandler("mcp");
74
- * // ... use handler
75
- * } else {
76
- * console.log("MCP packages not supported");
77
- * }
78
- * ```
79
- */
80
- hasHandler(type) {
81
- return this.handlers.has(type);
82
- }
83
- /**
84
- * Get a list of all registered package types.
85
- *
86
- * Useful for debugging, displaying supported types to users,
87
- * or iterating over all available handlers.
88
- *
89
- * @returns Array of package type strings that have registered handlers
90
- *
91
- * @example
92
- * ```typescript
93
- * const types = registry.getRegisteredTypes();
94
- * console.log("Supported types:", types.join(", "));
95
- * // Output: "Supported types: rules, skill, mcp"
96
- * ```
97
- */
98
- getRegisteredTypes() {
99
- return Array.from(this.handlers.keys());
100
- }
101
- };
102
- var handlerRegistry = new HandlerRegistry();
103
-
104
- // src/adapters/handlers/rules-handler.ts
105
- import fs2 from "fs-extra";
106
- import path6 from "path";
107
-
108
7
  // src/constants.ts
109
8
  var TIMEOUTS = {
110
9
  MANIFEST_FETCH: 5e3,
@@ -171,15 +70,17 @@ var ALLOWED_MCP_COMMANDS = [
171
70
  var BLOCKED_MCP_ARG_PATTERNS = [
172
71
  /--eval/i,
173
72
  // Node.js eval flag
174
- /-e\s/,
175
- // Short eval flag with space
176
- /-c\s/,
177
- // Command flag with space
73
+ /-e(?:\s|$)/,
74
+ // Short eval flag (with space or at end)
75
+ /^-e\S/,
76
+ // Concatenated eval flag (e.g., -eCODE)
77
+ /-c(?:\s|$)/,
78
+ // Command flag (with space or at end)
178
79
  /\bcurl\b/i,
179
80
  // curl command (data exfiltration)
180
81
  /\bwget\b/i,
181
82
  // wget command (data exfiltration)
182
- /\brm\s/i,
83
+ /\brm(?:\s|$)/i,
183
84
  // rm command (file deletion)
184
85
  /\bsudo\b/i,
185
86
  // sudo command (privilege escalation)
@@ -187,8 +88,44 @@ var BLOCKED_MCP_ARG_PATTERNS = [
187
88
  // chmod command (permission changes)
188
89
  /\bchown\b/i,
189
90
  // chown command (ownership changes)
190
- /[|;&`$]/
91
+ /[|;&`$]/,
191
92
  // Shell metacharacters (command chaining/injection)
93
+ /--inspect/i,
94
+ // Node.js debugger (remote code execution)
95
+ /--allow-all/i,
96
+ // Deno sandbox bypass
97
+ /--allow-run/i,
98
+ // Deno run permission
99
+ /--allow-write/i,
100
+ // Deno write permission
101
+ /--allow-net/i,
102
+ // Deno net permission
103
+ /^https?:\/\//i
104
+ // Remote URLs as standalone args (script loading)
105
+ ];
106
+ var BLOCKED_MCP_ENV_KEYS = [
107
+ "PATH",
108
+ "LD_PRELOAD",
109
+ "LD_LIBRARY_PATH",
110
+ "DYLD_INSERT_LIBRARIES",
111
+ "DYLD_LIBRARY_PATH",
112
+ "NODE_OPTIONS",
113
+ "NODE_PATH",
114
+ "PYTHONPATH",
115
+ "PYTHONSTARTUP",
116
+ "PYTHONHOME",
117
+ "RUBYOPT",
118
+ "PERL5OPT",
119
+ "BASH_ENV",
120
+ "ENV",
121
+ "CDPATH",
122
+ "HOME",
123
+ "USERPROFILE",
124
+ "NPM_CONFIG_REGISTRY",
125
+ "NPM_CONFIG_PREFIX",
126
+ "NPM_CONFIG_GLOBALCONFIG",
127
+ "DENO_DIR",
128
+ "BUN_INSTALL"
192
129
  ];
193
130
 
194
131
  // src/types.ts
@@ -210,15 +147,15 @@ function isSkillManifest(manifest) {
210
147
  function isMcpManifest(manifest) {
211
148
  return manifest.type === "mcp";
212
149
  }
213
- function getTypeFromPath(path15) {
214
- if (path15.startsWith("skills/")) return "skill";
215
- if (path15.startsWith("rules/")) return "rules";
216
- if (path15.startsWith("mcp/")) return "mcp";
217
- if (path15.startsWith("agents/")) return "agent";
218
- if (path15.startsWith("hooks/")) return "hook";
219
- if (path15.startsWith("workflows/")) return "workflow";
220
- if (path15.startsWith("templates/")) return "template";
221
- if (path15.startsWith("bundles/")) return "bundle";
150
+ function getTypeFromPath(path14) {
151
+ if (path14.startsWith("skills/")) return "skill";
152
+ if (path14.startsWith("rules/")) return "rules";
153
+ if (path14.startsWith("mcp/")) return "mcp";
154
+ if (path14.startsWith("agents/")) return "agent";
155
+ if (path14.startsWith("hooks/")) return "hook";
156
+ if (path14.startsWith("workflows/")) return "workflow";
157
+ if (path14.startsWith("templates/")) return "template";
158
+ if (path14.startsWith("bundles/")) return "bundle";
222
159
  return null;
223
160
  }
224
161
  function resolvePackageType(pkg) {
@@ -230,6 +167,107 @@ function resolvePackageType(pkg) {
230
167
  throw new Error(`Cannot determine type for package: ${pkg.name}`);
231
168
  }
232
169
 
170
+ // src/adapters/base.ts
171
+ var PlatformAdapter = class {
172
+ };
173
+
174
+ // src/adapters/handlers/handler-registry.ts
175
+ var HandlerRegistry = class {
176
+ /**
177
+ * Internal storage for handlers.
178
+ * Maps package type strings to their handler instances.
179
+ * Using Map for O(1) lookup performance.
180
+ */
181
+ handlers = /* @__PURE__ */ new Map();
182
+ /**
183
+ * Register a handler for a specific package type.
184
+ *
185
+ * When you create a new handler (like RulesHandler), you call this method
186
+ * to add it to the registry so it can be found later.
187
+ *
188
+ * @param handler - The handler instance to register. The handler's
189
+ * packageType property determines which type it handles.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const rulesHandler = new RulesHandler();
194
+ * registry.register(rulesHandler);
195
+ * // Now "rules" type packages will use RulesHandler
196
+ * ```
197
+ */
198
+ register(handler) {
199
+ this.handlers.set(handler.packageType, handler);
200
+ }
201
+ /**
202
+ * Get the handler for a specific package type.
203
+ *
204
+ * Use this when you need to install or uninstall a package.
205
+ * It returns the appropriate handler based on the package type.
206
+ *
207
+ * @param type - The package type to find a handler for (e.g., "rules", "skill", "mcp")
208
+ * @returns The handler that can process this package type
209
+ * @throws Error if no handler is registered for the given type
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const handler = registry.getHandler("skill");
214
+ * const files = await handler.install(manifest, context);
215
+ * ```
216
+ */
217
+ getHandler(type) {
218
+ const handler = this.handlers.get(type);
219
+ if (!handler) {
220
+ throw new Error(`No handler registered for package type: ${type}`);
221
+ }
222
+ return handler;
223
+ }
224
+ /**
225
+ * Check if a handler exists for a specific package type.
226
+ *
227
+ * Useful when you want to check availability before attempting
228
+ * to get a handler, avoiding the need for try-catch blocks.
229
+ *
230
+ * @param type - The package type to check
231
+ * @returns true if a handler is registered, false otherwise
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * if (registry.hasHandler("mcp")) {
236
+ * const handler = registry.getHandler("mcp");
237
+ * // ... use handler
238
+ * } else {
239
+ * console.log("MCP packages not supported");
240
+ * }
241
+ * ```
242
+ */
243
+ hasHandler(type) {
244
+ return this.handlers.has(type);
245
+ }
246
+ /**
247
+ * Get a list of all registered package types.
248
+ *
249
+ * Useful for debugging, displaying supported types to users,
250
+ * or iterating over all available handlers.
251
+ *
252
+ * @returns Array of package type strings that have registered handlers
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const types = registry.getRegisteredTypes();
257
+ * console.log("Supported types:", types.join(", "));
258
+ * // Output: "Supported types: rules, skill, mcp"
259
+ * ```
260
+ */
261
+ getRegisteredTypes() {
262
+ return Array.from(this.handlers.keys());
263
+ }
264
+ };
265
+ var handlerRegistry = new HandlerRegistry();
266
+
267
+ // src/adapters/handlers/rules-handler.ts
268
+ import fs2 from "fs-extra";
269
+ import path5 from "path";
270
+
233
271
  // src/utils/platform.ts
234
272
  import path2 from "path";
235
273
 
@@ -331,14 +369,22 @@ var logger = {
331
369
  };
332
370
 
333
371
  // src/security/mcp-validator.ts
334
- import path3 from "path";
335
372
  function isAllowedCommand(command) {
336
- const baseCommand = path3.basename(command);
373
+ if (command.includes("/") || command.includes("\\")) {
374
+ return false;
375
+ }
337
376
  return ALLOWED_MCP_COMMANDS.includes(
338
- baseCommand
377
+ command
339
378
  );
340
379
  }
341
380
  function containsBlockedPattern(args) {
381
+ for (const arg of args) {
382
+ for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
383
+ if (pattern.test(arg)) {
384
+ return pattern;
385
+ }
386
+ }
387
+ }
342
388
  const argsString = args.join(" ");
343
389
  for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
344
390
  if (pattern.test(argsString)) {
@@ -347,15 +393,23 @@ function containsBlockedPattern(args) {
347
393
  }
348
394
  return null;
349
395
  }
396
+ function containsBlockedEnvKey(env) {
397
+ const blockedSet = new Set(BLOCKED_MCP_ENV_KEYS.map((k) => k.toUpperCase()));
398
+ for (const key of Object.keys(env)) {
399
+ if (blockedSet.has(key.toUpperCase())) {
400
+ return key;
401
+ }
402
+ }
403
+ return null;
404
+ }
350
405
  function validateMcpConfig(mcp) {
351
406
  if (!mcp?.command) {
352
407
  return { valid: false, error: "MCP command is required" };
353
408
  }
354
- const baseCommand = path3.basename(mcp.command);
355
409
  if (!isAllowedCommand(mcp.command)) {
356
410
  return {
357
411
  valid: false,
358
- error: `MCP command '${baseCommand}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
412
+ error: `MCP command '${mcp.command}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
359
413
  };
360
414
  }
361
415
  if (mcp.args) {
@@ -367,16 +421,25 @@ function validateMcpConfig(mcp) {
367
421
  };
368
422
  }
369
423
  }
424
+ if (mcp.env) {
425
+ const blockedKey = containsBlockedEnvKey(mcp.env);
426
+ if (blockedKey) {
427
+ return {
428
+ valid: false,
429
+ error: `MCP environment variable '${blockedKey}' is not allowed. It could be used to bypass command security restrictions.`
430
+ };
431
+ }
432
+ }
370
433
  return { valid: true };
371
434
  }
372
435
 
373
436
  // src/security/file-sanitizer.ts
374
- import path4 from "path";
437
+ import path3 from "path";
375
438
  function sanitizeFileName(fileName) {
376
439
  if (!fileName || typeof fileName !== "string") {
377
440
  return { valid: false, error: "File name cannot be empty", sanitized: "" };
378
441
  }
379
- const baseName = path4.basename(fileName);
442
+ const baseName = path3.basename(fileName);
380
443
  if (baseName.includes("\0")) {
381
444
  return {
382
445
  valid: false,
@@ -421,12 +484,12 @@ function sanitizeFolderName(name) {
421
484
  if (!sanitized || sanitized.startsWith(".")) {
422
485
  throw new Error(`Invalid package name: ${name}`);
423
486
  }
424
- const normalized = path4.normalize(sanitized);
487
+ const normalized = path3.normalize(sanitized);
425
488
  if (normalized !== sanitized || normalized.includes("..")) {
426
489
  throw new Error(`Invalid package name (path traversal detected): ${name}`);
427
490
  }
428
- const testPath = path4.join("/test", sanitized);
429
- const resolved = path4.resolve(testPath);
491
+ const testPath = path3.join("/test", sanitized);
492
+ const resolved = path3.resolve(testPath);
430
493
  if (!resolved.startsWith("/test/")) {
431
494
  throw new Error(`Invalid package name (path traversal detected): ${name}`);
432
495
  }
@@ -434,11 +497,11 @@ function sanitizeFolderName(name) {
434
497
  }
435
498
 
436
499
  // src/security/path-validator.ts
437
- import path5 from "path";
500
+ import path4 from "path";
438
501
  function isPathWithinDirectory(filePath, directory) {
439
- const resolvedPath = path5.resolve(filePath);
440
- const resolvedDir = path5.resolve(directory);
441
- return resolvedPath.startsWith(resolvedDir + path5.sep) || resolvedPath === resolvedDir;
502
+ const resolvedPath = path4.resolve(filePath);
503
+ const resolvedDir = path4.resolve(directory);
504
+ return resolvedPath.startsWith(resolvedDir + path4.sep) || resolvedPath === resolvedDir;
442
505
  }
443
506
 
444
507
  // src/adapters/handlers/rules-handler.ts
@@ -453,7 +516,7 @@ async function writePackageMetadata(packageDir, manifest) {
453
516
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
454
517
  // ISO timestamp for when it was installed
455
518
  };
456
- const metadataPath = path6.join(packageDir, ".cpm.json");
519
+ const metadataPath = path5.join(packageDir, ".cpm.json");
457
520
  try {
458
521
  await fs2.writeJson(metadataPath, metadata, { spaces: 2 });
459
522
  } catch (error) {
@@ -486,7 +549,7 @@ var RulesHandler = class {
486
549
  const filesWritten = [];
487
550
  const rulesBaseDir = getRulesPath("claude-code");
488
551
  const folderName = sanitizeFolderName(manifest.name);
489
- const rulesDir = path6.join(rulesBaseDir, folderName);
552
+ const rulesDir = path5.join(rulesBaseDir, folderName);
490
553
  await fs2.ensureDir(rulesDir);
491
554
  if (context.packagePath && await fs2.pathExists(context.packagePath)) {
492
555
  const files = await fs2.readdir(context.packagePath);
@@ -500,8 +563,8 @@ var RulesHandler = class {
500
563
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
501
564
  continue;
502
565
  }
503
- const srcPath = path6.join(context.packagePath, file);
504
- const destPath = path6.join(rulesDir, validation.sanitized);
566
+ const srcPath = path5.join(context.packagePath, file);
567
+ const destPath = path5.join(rulesDir, validation.sanitized);
505
568
  if (!isPathWithinDirectory(destPath, rulesDir)) {
506
569
  logger.warn(`Blocked path traversal attempt: ${file}`);
507
570
  continue;
@@ -516,7 +579,7 @@ var RulesHandler = class {
516
579
  }
517
580
  const rulesContent = this.getRulesContent(manifest);
518
581
  if (!rulesContent) return filesWritten;
519
- const rulesPath = path6.join(rulesDir, "RULES.md");
582
+ const rulesPath = path5.join(rulesDir, "RULES.md");
520
583
  const content = `# ${manifest.name}
521
584
 
522
585
  ${manifest.description}
@@ -542,7 +605,7 @@ ${rulesContent.trim()}
542
605
  const filesRemoved = [];
543
606
  const folderName = sanitizeFolderName(packageName);
544
607
  const rulesBaseDir = getRulesPath("claude-code");
545
- const rulesPath = path6.join(rulesBaseDir, folderName);
608
+ const rulesPath = path5.join(rulesBaseDir, folderName);
546
609
  if (await fs2.pathExists(rulesPath)) {
547
610
  await fs2.remove(rulesPath);
548
611
  filesRemoved.push(rulesPath);
@@ -569,7 +632,7 @@ ${rulesContent.trim()}
569
632
 
570
633
  // src/adapters/handlers/skill-handler.ts
571
634
  import fs3 from "fs-extra";
572
- import path7 from "path";
635
+ import path6 from "path";
573
636
  async function writePackageMetadata2(packageDir, manifest) {
574
637
  const metadata = {
575
638
  name: manifest.name,
@@ -581,7 +644,7 @@ async function writePackageMetadata2(packageDir, manifest) {
581
644
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
582
645
  // ISO timestamp for when it was installed
583
646
  };
584
- const metadataPath = path7.join(packageDir, ".cpm.json");
647
+ const metadataPath = path6.join(packageDir, ".cpm.json");
585
648
  try {
586
649
  await fs3.writeJson(metadataPath, metadata, { spaces: 2 });
587
650
  } catch (error) {
@@ -633,7 +696,7 @@ var SkillHandler = class {
633
696
  const filesWritten = [];
634
697
  const skillsDir = getSkillsPath();
635
698
  const folderName = sanitizeFolderName(manifest.name);
636
- const skillDir = path7.join(skillsDir, folderName);
699
+ const skillDir = path6.join(skillsDir, folderName);
637
700
  await fs3.ensureDir(skillDir);
638
701
  if (context.packagePath && await fs3.pathExists(context.packagePath)) {
639
702
  const files = await fs3.readdir(context.packagePath);
@@ -647,8 +710,8 @@ var SkillHandler = class {
647
710
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
648
711
  continue;
649
712
  }
650
- const srcPath = path7.join(context.packagePath, file);
651
- const destPath = path7.join(skillDir, validation.sanitized);
713
+ const srcPath = path6.join(context.packagePath, file);
714
+ const destPath = path6.join(skillDir, validation.sanitized);
652
715
  if (!isPathWithinDirectory(destPath, skillDir)) {
653
716
  logger.warn(`Blocked path traversal attempt: ${file}`);
654
717
  continue;
@@ -663,7 +726,7 @@ var SkillHandler = class {
663
726
  }
664
727
  if (isSkillManifest(manifest)) {
665
728
  const skillContent = formatSkillMd(manifest);
666
- const skillPath = path7.join(skillDir, "SKILL.md");
729
+ const skillPath = path6.join(skillDir, "SKILL.md");
667
730
  await fs3.writeFile(skillPath, skillContent, "utf-8");
668
731
  filesWritten.push(skillPath);
669
732
  const metadataPath = await writePackageMetadata2(skillDir, manifest);
@@ -671,7 +734,7 @@ var SkillHandler = class {
671
734
  } else {
672
735
  const content = this.getUniversalContent(manifest);
673
736
  if (content) {
674
- const skillPath = path7.join(skillDir, "SKILL.md");
737
+ const skillPath = path6.join(skillDir, "SKILL.md");
675
738
  const skillContent = `# ${manifest.name}
676
739
 
677
740
  ${manifest.description}
@@ -699,7 +762,7 @@ ${content.trim()}
699
762
  const filesRemoved = [];
700
763
  const folderName = sanitizeFolderName(packageName);
701
764
  const skillsDir = getSkillsPath();
702
- const skillPath = path7.join(skillsDir, folderName);
765
+ const skillPath = path6.join(skillsDir, folderName);
703
766
  if (await fs3.pathExists(skillPath)) {
704
767
  await fs3.remove(skillPath);
705
768
  filesRemoved.push(skillPath);
@@ -725,7 +788,7 @@ ${content.trim()}
725
788
 
726
789
  // src/adapters/handlers/mcp-handler.ts
727
790
  import fs4 from "fs-extra";
728
- import path8 from "path";
791
+ import path7 from "path";
729
792
  var McpHandler = class {
730
793
  /**
731
794
  * Identifies this handler as handling "mcp" type packages.
@@ -756,13 +819,21 @@ var McpHandler = class {
756
819
  throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
757
820
  }
758
821
  const claudeHome = getClaudeHome();
759
- const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
822
+ const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
760
823
  let existingConfig = {};
761
824
  if (await fs4.pathExists(mcpConfigPath)) {
762
825
  try {
763
826
  existingConfig = await fs4.readJson(mcpConfigPath);
764
827
  } catch {
765
- logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
828
+ const backupPath = `${mcpConfigPath}.backup.${Date.now()}`;
829
+ try {
830
+ await fs4.copy(mcpConfigPath, backupPath);
831
+ logger.warn(
832
+ `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
833
+ );
834
+ } catch {
835
+ logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
836
+ }
766
837
  existingConfig = {};
767
838
  }
768
839
  }
@@ -802,7 +873,7 @@ var McpHandler = class {
802
873
  const filesWritten = [];
803
874
  const folderName = sanitizeFolderName(packageName);
804
875
  const claudeHome = getClaudeHome();
805
- const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
876
+ const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
806
877
  if (!await fs4.pathExists(mcpConfigPath)) {
807
878
  return filesWritten;
808
879
  }
@@ -838,7 +909,7 @@ initializeHandlers();
838
909
 
839
910
  // src/adapters/claude-code.ts
840
911
  import fs5 from "fs-extra";
841
- import path9 from "path";
912
+ import path8 from "path";
842
913
  var ClaudeCodeAdapter = class extends PlatformAdapter {
843
914
  platform = "claude-code";
844
915
  displayName = "Claude Code";
@@ -865,23 +936,28 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
865
936
  };
866
937
  }
867
938
  }
868
- async uninstall(packageName, _projectPath) {
939
+ async uninstall(packageName, projectPath) {
869
940
  const filesWritten = [];
870
941
  const folderName = sanitizeFolderName(packageName);
942
+ const context = { projectPath };
871
943
  try {
872
944
  const rulesBaseDir = getRulesPath("claude-code");
873
- const rulesPath = path9.join(rulesBaseDir, folderName);
945
+ const rulesPath = path8.join(rulesBaseDir, folderName);
874
946
  if (await fs5.pathExists(rulesPath)) {
875
947
  await fs5.remove(rulesPath);
876
948
  filesWritten.push(rulesPath);
877
949
  }
878
950
  const skillsDir = getSkillsPath();
879
- const skillPath = path9.join(skillsDir, folderName);
951
+ const skillPath = path8.join(skillsDir, folderName);
880
952
  if (await fs5.pathExists(skillPath)) {
881
953
  await fs5.remove(skillPath);
882
954
  filesWritten.push(skillPath);
883
955
  }
884
- await this.removeMcpServer(folderName, filesWritten);
956
+ if (handlerRegistry.hasHandler("mcp")) {
957
+ const mcpHandler = handlerRegistry.getHandler("mcp");
958
+ const mcpFiles = await mcpHandler.uninstall(packageName, context);
959
+ filesWritten.push(...mcpFiles);
960
+ }
885
961
  return {
886
962
  success: true,
887
963
  platform: "claude-code",
@@ -904,45 +980,23 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
904
980
  return this.installFallback(manifest, context);
905
981
  }
906
982
  async installFallback(manifest, context) {
907
- if ("skill" in manifest && manifest.skill) {
983
+ logger.warn(
984
+ `No handler registered for type "${manifest.type}", attempting content-based detection`
985
+ );
986
+ if (isSkillManifest(manifest)) {
908
987
  const handler = handlerRegistry.getHandler("skill");
909
988
  return handler.install(manifest, context);
910
989
  }
911
- if ("mcp" in manifest && manifest.mcp) {
990
+ if (isMcpManifest(manifest)) {
912
991
  const handler = handlerRegistry.getHandler("mcp");
913
992
  return handler.install(manifest, context);
914
993
  }
915
- if ("universal" in manifest && manifest.universal?.rules) {
994
+ if (isRulesManifest(manifest)) {
916
995
  const handler = handlerRegistry.getHandler("rules");
917
996
  return handler.install(manifest, context);
918
997
  }
919
998
  return [];
920
999
  }
921
- async removeMcpServer(serverName, filesWritten) {
922
- const claudeHome = getClaudeHome();
923
- const mcpConfigPath = path9.join(path9.dirname(claudeHome), ".claude.json");
924
- if (!await fs5.pathExists(mcpConfigPath)) {
925
- return;
926
- }
927
- try {
928
- const config = await fs5.readJson(mcpConfigPath);
929
- const mcpServers = config.mcpServers;
930
- if (!mcpServers || !mcpServers[serverName]) {
931
- return;
932
- }
933
- const { [serverName]: _removed, ...remainingServers } = mcpServers;
934
- const updatedConfig = {
935
- ...config,
936
- mcpServers: remainingServers
937
- };
938
- await fs5.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
939
- filesWritten.push(mcpConfigPath);
940
- } catch (error) {
941
- logger.warn(
942
- `Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
943
- );
944
- }
945
- }
946
1000
  };
947
1001
 
948
1002
  // src/adapters/index.ts
@@ -956,11 +1010,11 @@ function getAdapter(platform) {
956
1010
  // src/utils/registry.ts
957
1011
  import got from "got";
958
1012
  import fs6 from "fs-extra";
959
- import path10 from "path";
1013
+ import path9 from "path";
960
1014
  import os2 from "os";
961
1015
  var DEFAULT_REGISTRY_URL = process.env.CPM_REGISTRY_URL || "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
962
- var CACHE_DIR = path10.join(os2.homedir(), ".cpm", "cache");
963
- var CACHE_FILE = path10.join(CACHE_DIR, "registry.json");
1016
+ var CACHE_DIR = path9.join(os2.homedir(), ".cpm", "cache");
1017
+ var CACHE_FILE = path9.join(CACHE_DIR, "registry.json");
964
1018
  var comparators = {
965
1019
  downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
966
1020
  stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
@@ -1101,7 +1155,7 @@ var registry = new Registry();
1101
1155
 
1102
1156
  // src/utils/downloader.ts
1103
1157
  import fs8 from "fs-extra";
1104
- import path12 from "path";
1158
+ import path11 from "path";
1105
1159
  import os3 from "os";
1106
1160
 
1107
1161
  // src/sources/manifest-resolver.ts
@@ -1195,76 +1249,45 @@ var ManifestResolver = class {
1195
1249
  // src/sources/repository-source.ts
1196
1250
  import got2 from "got";
1197
1251
  import yaml from "yaml";
1252
+ var PACKAGES_REPO_BASE = "https://raw.githubusercontent.com/cpmai-dev/packages/main/packages";
1198
1253
  var RepositorySource = class {
1199
- /**
1200
- * Name of this source for logging and debugging.
1201
- */
1202
1254
  name = "repository";
1203
- /**
1204
- * Priority 1 - this is the first source tried.
1205
- * Repository fetching is fast and provides complete manifests.
1206
- */
1207
1255
  priority = 1;
1208
- /**
1209
- * Check if this source can fetch the given package.
1210
- *
1211
- * We can only fetch from GitHub repositories, so we check
1212
- * if the package has a repository URL that includes "github.com".
1213
- *
1214
- * @param pkg - The registry package to check
1215
- * @returns true if the package has a GitHub repository URL
1216
- *
1217
- * @example
1218
- * ```typescript
1219
- * // Package with GitHub repo
1220
- * canFetch({ repository: "https://github.com/cpm/rules" }) // true
1221
- *
1222
- * // Package without repo
1223
- * canFetch({ repository: undefined }) // false
1224
- *
1225
- * // Package with non-GitHub repo
1226
- * canFetch({ repository: "https://gitlab.com/..." }) // false
1227
- * ```
1228
- */
1229
1256
  canFetch(pkg) {
1230
- return !!pkg.repository?.includes("github.com");
1257
+ return !!pkg.path || !!pkg.repository?.includes("github.com");
1231
1258
  }
1232
- /**
1233
- * Fetch the manifest from the package's GitHub repository.
1234
- *
1235
- * This method:
1236
- * 1. Extracts the owner and repo name from the URL
1237
- * 2. Constructs a raw.githubusercontent.com URL for cpm.yaml
1238
- * 3. Fetches the file with a timeout
1239
- * 4. Parses the YAML content
1240
- *
1241
- * @param pkg - The registry package to fetch
1242
- * @param _context - Fetch context (not used by this source)
1243
- * @returns The parsed manifest, or null if fetch fails
1244
- *
1245
- * @example
1246
- * ```typescript
1247
- * const manifest = await source.fetch({
1248
- * name: "@cpm/typescript-rules",
1249
- * repository: "https://github.com/cpm/typescript-rules",
1250
- * // ... other fields
1251
- * }, context);
1252
- *
1253
- * if (manifest) {
1254
- * console.log("Fetched:", manifest.name, manifest.version);
1255
- * }
1256
- * ```
1257
- */
1258
1259
  async fetch(pkg, _context) {
1259
- if (!pkg.repository) return null;
1260
+ if (pkg.path) {
1261
+ const manifest = await this.fetchFromPackagesRepo(pkg.path);
1262
+ if (manifest) return manifest;
1263
+ }
1264
+ if (pkg.repository) {
1265
+ return this.fetchFromStandaloneRepo(pkg.repository);
1266
+ }
1267
+ return null;
1268
+ }
1269
+ async fetchFromPackagesRepo(packagePath) {
1270
+ if (packagePath.includes("..") || packagePath.startsWith("/") || packagePath.includes("\\")) {
1271
+ return null;
1272
+ }
1273
+ const rawUrl = `${PACKAGES_REPO_BASE}/${packagePath}/cpm.yaml`;
1260
1274
  try {
1261
- const match = pkg.repository.match(/github\.com\/([^/]+)\/([^/]+)/);
1275
+ const response = await got2(rawUrl, {
1276
+ timeout: { request: TIMEOUTS.MANIFEST_FETCH }
1277
+ });
1278
+ return yaml.parse(response.body);
1279
+ } catch {
1280
+ return null;
1281
+ }
1282
+ }
1283
+ async fetchFromStandaloneRepo(repository) {
1284
+ try {
1285
+ const match = repository.match(/github\.com\/([^/]+)\/([^/]+)/);
1262
1286
  if (!match) return null;
1263
1287
  const [, owner, repo] = match;
1264
1288
  const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/cpm.yaml`;
1265
1289
  const response = await got2(rawUrl, {
1266
1290
  timeout: { request: TIMEOUTS.MANIFEST_FETCH }
1267
- // 5 second timeout
1268
1291
  });
1269
1292
  return yaml.parse(response.body);
1270
1293
  } catch {
@@ -1276,7 +1299,7 @@ var RepositorySource = class {
1276
1299
  // src/sources/tarball-source.ts
1277
1300
  import got3 from "got";
1278
1301
  import fs7 from "fs-extra";
1279
- import path11 from "path";
1302
+ import path10 from "path";
1280
1303
  import * as tar from "tar";
1281
1304
  import yaml2 from "yaml";
1282
1305
  var TarballSource = class {
@@ -1328,10 +1351,10 @@ var TarballSource = class {
1328
1351
  responseType: "buffer"
1329
1352
  // Get raw binary data
1330
1353
  });
1331
- const tarballPath = path11.join(context.tempDir, "package.tar.gz");
1354
+ const tarballPath = path10.join(context.tempDir, "package.tar.gz");
1332
1355
  await fs7.writeFile(tarballPath, response.body);
1333
1356
  await this.extractTarball(tarballPath, context.tempDir);
1334
- const manifestPath = path11.join(context.tempDir, "cpm.yaml");
1357
+ const manifestPath = path10.join(context.tempDir, "cpm.yaml");
1335
1358
  if (await fs7.pathExists(manifestPath)) {
1336
1359
  const content = await fs7.readFile(manifestPath, "utf-8");
1337
1360
  return yaml2.parse(content);
@@ -1356,7 +1379,7 @@ var TarballSource = class {
1356
1379
  */
1357
1380
  async extractTarball(tarballPath, destDir) {
1358
1381
  await fs7.ensureDir(destDir);
1359
- const resolvedDestDir = path11.resolve(destDir);
1382
+ const resolvedDestDir = path10.resolve(destDir);
1360
1383
  await tar.extract({
1361
1384
  file: tarballPath,
1362
1385
  // The tarball file to extract
@@ -1366,8 +1389,8 @@ var TarballSource = class {
1366
1389
  // Remove the top-level directory (e.g., "package-1.0.0/")
1367
1390
  // Security filter: check each entry before extracting
1368
1391
  filter: (entryPath) => {
1369
- const resolvedPath = path11.resolve(destDir, entryPath);
1370
- const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path11.sep) || resolvedPath === resolvedDestDir;
1392
+ const resolvedPath = path10.resolve(destDir, entryPath);
1393
+ const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path10.sep) || resolvedPath === resolvedDestDir;
1371
1394
  if (!isWithinDest) {
1372
1395
  logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
1373
1396
  return false;
@@ -2048,11 +2071,11 @@ function createDefaultResolver() {
2048
2071
  var defaultResolver = createDefaultResolver();
2049
2072
 
2050
2073
  // src/utils/downloader.ts
2051
- var TEMP_DIR = path12.join(os3.tmpdir(), "cpm-downloads");
2074
+ var TEMP_DIR = path11.join(os3.tmpdir(), "cpm-downloads");
2052
2075
  async function downloadPackage(pkg) {
2053
2076
  try {
2054
2077
  await fs8.ensureDir(TEMP_DIR);
2055
- const packageTempDir = path12.join(
2078
+ const packageTempDir = path11.join(
2056
2079
  TEMP_DIR,
2057
2080
  `${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
2058
2081
  );
@@ -2198,7 +2221,7 @@ var SEMANTIC_COLORS = {
2198
2221
 
2199
2222
  // src/commands/ui/formatters.ts
2200
2223
  import chalk2 from "chalk";
2201
- import path13 from "path";
2224
+ import path12 from "path";
2202
2225
  function formatNumber(num) {
2203
2226
  if (num >= 1e6) {
2204
2227
  return `${(num / 1e6).toFixed(1)}M`;
@@ -2209,7 +2232,7 @@ function formatNumber(num) {
2209
2232
  return num.toString();
2210
2233
  }
2211
2234
  function formatPath(filePath) {
2212
- const relativePath = path13.relative(process.cwd(), filePath);
2235
+ const relativePath = path12.relative(process.cwd(), filePath);
2213
2236
  if (relativePath.startsWith("..")) {
2214
2237
  return filePath;
2215
2238
  }
@@ -2271,8 +2294,12 @@ function formatUsageHints(manifest) {
2271
2294
  if ("mcp" in manifest && manifest.mcp?.env) {
2272
2295
  const envVars = Object.keys(manifest.mcp.env);
2273
2296
  if (envVars.length > 0) {
2274
- hints.push(chalk2.yellow(`
2275
- Required environment variables:`));
2297
+ hints.push(
2298
+ chalk2.yellow(
2299
+ `
2300
+ Configure these environment variables in ~/.claude.json:`
2301
+ )
2302
+ );
2276
2303
  for (const envVar of envVars) {
2277
2304
  hints.push(chalk2.dim(` - ${envVar}`));
2278
2305
  }
@@ -2540,10 +2567,10 @@ async function searchCommand(query, rawOptions) {
2540
2567
  // src/commands/list.ts
2541
2568
  import chalk4 from "chalk";
2542
2569
  import fs9 from "fs-extra";
2543
- import path14 from "path";
2570
+ import path13 from "path";
2544
2571
  import os4 from "os";
2545
2572
  async function readPackageMetadata(packageDir) {
2546
- const metadataPath = path14.join(packageDir, ".cpm.json");
2573
+ const metadataPath = path13.join(packageDir, ".cpm.json");
2547
2574
  try {
2548
2575
  if (await fs9.pathExists(metadataPath)) {
2549
2576
  return await fs9.readJson(metadataPath);
@@ -2559,7 +2586,7 @@ async function scanDirectory(dir, type) {
2559
2586
  }
2560
2587
  const entries = await fs9.readdir(dir);
2561
2588
  for (const entry of entries) {
2562
- const entryPath = path14.join(dir, entry);
2589
+ const entryPath = path13.join(dir, entry);
2563
2590
  const stat = await fs9.stat(entryPath);
2564
2591
  if (stat.isDirectory()) {
2565
2592
  const metadata = await readPackageMetadata(entryPath);
@@ -2576,7 +2603,7 @@ async function scanDirectory(dir, type) {
2576
2603
  }
2577
2604
  async function scanMcpServers() {
2578
2605
  const items = [];
2579
- const configPath = path14.join(os4.homedir(), ".claude.json");
2606
+ const configPath = path13.join(os4.homedir(), ".claude.json");
2580
2607
  if (!await fs9.pathExists(configPath)) {
2581
2608
  return items;
2582
2609
  }
@@ -2596,10 +2623,10 @@ async function scanMcpServers() {
2596
2623
  return items;
2597
2624
  }
2598
2625
  async function scanInstalledPackages() {
2599
- const claudeHome = path14.join(os4.homedir(), ".claude");
2626
+ const claudeHome = path13.join(os4.homedir(), ".claude");
2600
2627
  const [rules, skills, mcp] = await Promise.all([
2601
- scanDirectory(path14.join(claudeHome, "rules"), "rules"),
2602
- scanDirectory(path14.join(claudeHome, "skills"), "skill"),
2628
+ scanDirectory(path13.join(claudeHome, "rules"), "rules"),
2629
+ scanDirectory(path13.join(claudeHome, "skills"), "skill"),
2603
2630
  scanMcpServers()
2604
2631
  ]);
2605
2632
  return [...rules, ...skills, ...mcp];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cpmai/cli",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "CPM CLI - cpm-ai.dev",
5
5
  "keywords": [
6
6
  "claude-code",