@cpmai/cli 0.2.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 +763 -423
- 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,
|
|
@@ -148,10 +47,7 @@ var SEARCH_SORT_OPTIONS = [
|
|
|
148
47
|
"name"
|
|
149
48
|
// Alphabetical order
|
|
150
49
|
];
|
|
151
|
-
var VALID_PLATFORMS = [
|
|
152
|
-
"claude-code"
|
|
153
|
-
// Currently the only supported platform
|
|
154
|
-
];
|
|
50
|
+
var VALID_PLATFORMS = ["claude-code", "cursor"];
|
|
155
51
|
var ALLOWED_MCP_COMMANDS = [
|
|
156
52
|
"npx",
|
|
157
53
|
// Node package executor
|
|
@@ -171,15 +67,19 @@ var ALLOWED_MCP_COMMANDS = [
|
|
|
171
67
|
var BLOCKED_MCP_ARG_PATTERNS = [
|
|
172
68
|
/--eval/i,
|
|
173
69
|
// Node.js eval flag
|
|
174
|
-
/-e
|
|
175
|
-
// Short eval flag with space
|
|
176
|
-
|
|
177
|
-
//
|
|
70
|
+
/-e(?:\s|$)/,
|
|
71
|
+
// Short eval flag (with space or at end)
|
|
72
|
+
/^-e\S/,
|
|
73
|
+
// Concatenated eval flag (e.g., -eCODE)
|
|
74
|
+
/-c(?:\s|$)/,
|
|
75
|
+
// Command flag (with space or at end)
|
|
76
|
+
/^-c\S/,
|
|
77
|
+
// Concatenated command flag (e.g., -cCODE)
|
|
178
78
|
/\bcurl\b/i,
|
|
179
79
|
// curl command (data exfiltration)
|
|
180
80
|
/\bwget\b/i,
|
|
181
81
|
// wget command (data exfiltration)
|
|
182
|
-
/\brm
|
|
82
|
+
/\brm(?:\s|$)/i,
|
|
183
83
|
// rm command (file deletion)
|
|
184
84
|
/\bsudo\b/i,
|
|
185
85
|
// sudo command (privilege escalation)
|
|
@@ -187,8 +87,44 @@ var BLOCKED_MCP_ARG_PATTERNS = [
|
|
|
187
87
|
// chmod command (permission changes)
|
|
188
88
|
/\bchown\b/i,
|
|
189
89
|
// chown command (ownership changes)
|
|
190
|
-
/[|;&`$]
|
|
90
|
+
/[|;&`$]/,
|
|
191
91
|
// Shell metacharacters (command chaining/injection)
|
|
92
|
+
/--inspect/i,
|
|
93
|
+
// Node.js debugger (remote code execution)
|
|
94
|
+
/--allow-all/i,
|
|
95
|
+
// Deno sandbox bypass
|
|
96
|
+
/--allow-run/i,
|
|
97
|
+
// Deno run permission
|
|
98
|
+
/--allow-write/i,
|
|
99
|
+
// Deno write permission
|
|
100
|
+
/--allow-net/i,
|
|
101
|
+
// Deno net permission
|
|
102
|
+
/^https?:\/\//i
|
|
103
|
+
// Remote URLs as standalone args (script loading)
|
|
104
|
+
];
|
|
105
|
+
var BLOCKED_MCP_ENV_KEYS = [
|
|
106
|
+
"PATH",
|
|
107
|
+
"LD_PRELOAD",
|
|
108
|
+
"LD_LIBRARY_PATH",
|
|
109
|
+
"DYLD_INSERT_LIBRARIES",
|
|
110
|
+
"DYLD_LIBRARY_PATH",
|
|
111
|
+
"NODE_OPTIONS",
|
|
112
|
+
"NODE_PATH",
|
|
113
|
+
"PYTHONPATH",
|
|
114
|
+
"PYTHONSTARTUP",
|
|
115
|
+
"PYTHONHOME",
|
|
116
|
+
"RUBYOPT",
|
|
117
|
+
"PERL5OPT",
|
|
118
|
+
"BASH_ENV",
|
|
119
|
+
"ENV",
|
|
120
|
+
"CDPATH",
|
|
121
|
+
"HOME",
|
|
122
|
+
"USERPROFILE",
|
|
123
|
+
"NPM_CONFIG_REGISTRY",
|
|
124
|
+
"NPM_CONFIG_PREFIX",
|
|
125
|
+
"NPM_CONFIG_GLOBALCONFIG",
|
|
126
|
+
"DENO_DIR",
|
|
127
|
+
"BUN_INSTALL"
|
|
192
128
|
];
|
|
193
129
|
|
|
194
130
|
// src/types.ts
|
|
@@ -210,15 +146,15 @@ function isSkillManifest(manifest) {
|
|
|
210
146
|
function isMcpManifest(manifest) {
|
|
211
147
|
return manifest.type === "mcp";
|
|
212
148
|
}
|
|
213
|
-
function getTypeFromPath(
|
|
214
|
-
if (
|
|
215
|
-
if (
|
|
216
|
-
if (
|
|
217
|
-
if (
|
|
218
|
-
if (
|
|
219
|
-
if (
|
|
220
|
-
if (
|
|
221
|
-
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";
|
|
222
158
|
return null;
|
|
223
159
|
}
|
|
224
160
|
function resolvePackageType(pkg) {
|
|
@@ -230,8 +166,110 @@ function resolvePackageType(pkg) {
|
|
|
230
166
|
throw new Error(`Cannot determine type for package: ${pkg.name}`);
|
|
231
167
|
}
|
|
232
168
|
|
|
169
|
+
// src/adapters/base.ts
|
|
170
|
+
var PlatformAdapter = class {
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/adapters/handlers/handler-registry.ts
|
|
174
|
+
var HandlerRegistry = class {
|
|
175
|
+
/**
|
|
176
|
+
* Internal storage for handlers.
|
|
177
|
+
* Maps package type strings to their handler instances.
|
|
178
|
+
* Using Map for O(1) lookup performance.
|
|
179
|
+
*/
|
|
180
|
+
handlers = /* @__PURE__ */ new Map();
|
|
181
|
+
/**
|
|
182
|
+
* Register a handler for a specific package type.
|
|
183
|
+
*
|
|
184
|
+
* When you create a new handler (like RulesHandler), you call this method
|
|
185
|
+
* to add it to the registry so it can be found later.
|
|
186
|
+
*
|
|
187
|
+
* @param handler - The handler instance to register. The handler's
|
|
188
|
+
* packageType property determines which type it handles.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const rulesHandler = new RulesHandler();
|
|
193
|
+
* registry.register(rulesHandler);
|
|
194
|
+
* // Now "rules" type packages will use RulesHandler
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
register(handler) {
|
|
198
|
+
this.handlers.set(handler.packageType, handler);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the handler for a specific package type.
|
|
202
|
+
*
|
|
203
|
+
* Use this when you need to install or uninstall a package.
|
|
204
|
+
* It returns the appropriate handler based on the package type.
|
|
205
|
+
*
|
|
206
|
+
* @param type - The package type to find a handler for (e.g., "rules", "skill", "mcp")
|
|
207
|
+
* @returns The handler that can process this package type
|
|
208
|
+
* @throws Error if no handler is registered for the given type
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const handler = registry.getHandler("skill");
|
|
213
|
+
* const files = await handler.install(manifest, context);
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
getHandler(type) {
|
|
217
|
+
const handler = this.handlers.get(type);
|
|
218
|
+
if (!handler) {
|
|
219
|
+
throw new Error(`No handler registered for package type: ${type}`);
|
|
220
|
+
}
|
|
221
|
+
return handler;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Check if a handler exists for a specific package type.
|
|
225
|
+
*
|
|
226
|
+
* Useful when you want to check availability before attempting
|
|
227
|
+
* to get a handler, avoiding the need for try-catch blocks.
|
|
228
|
+
*
|
|
229
|
+
* @param type - The package type to check
|
|
230
|
+
* @returns true if a handler is registered, false otherwise
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* if (registry.hasHandler("mcp")) {
|
|
235
|
+
* const handler = registry.getHandler("mcp");
|
|
236
|
+
* // ... use handler
|
|
237
|
+
* } else {
|
|
238
|
+
* console.log("MCP packages not supported");
|
|
239
|
+
* }
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
hasHandler(type) {
|
|
243
|
+
return this.handlers.has(type);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get a list of all registered package types.
|
|
247
|
+
*
|
|
248
|
+
* Useful for debugging, displaying supported types to users,
|
|
249
|
+
* or iterating over all available handlers.
|
|
250
|
+
*
|
|
251
|
+
* @returns Array of package type strings that have registered handlers
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const types = registry.getRegisteredTypes();
|
|
256
|
+
* console.log("Supported types:", types.join(", "));
|
|
257
|
+
* // Output: "Supported types: rules, skill, mcp"
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
getRegisteredTypes() {
|
|
261
|
+
return Array.from(this.handlers.keys());
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var handlerRegistry = new HandlerRegistry();
|
|
265
|
+
|
|
266
|
+
// src/adapters/handlers/rules-handler.ts
|
|
267
|
+
import fs3 from "fs-extra";
|
|
268
|
+
import path6 from "path";
|
|
269
|
+
|
|
233
270
|
// src/utils/platform.ts
|
|
234
271
|
import path2 from "path";
|
|
272
|
+
import os2 from "os";
|
|
235
273
|
|
|
236
274
|
// src/utils/config.ts
|
|
237
275
|
import path from "path";
|
|
@@ -245,13 +283,28 @@ async function ensureClaudeDirs() {
|
|
|
245
283
|
await fs.ensureDir(path.join(claudeHome, "rules"));
|
|
246
284
|
await fs.ensureDir(path.join(claudeHome, "skills"));
|
|
247
285
|
}
|
|
286
|
+
async function ensureCursorDirs(projectPath) {
|
|
287
|
+
await fs.ensureDir(path.join(projectPath, ".cursor", "rules"));
|
|
288
|
+
}
|
|
248
289
|
|
|
249
290
|
// src/utils/platform.ts
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
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");
|
|
253
300
|
}
|
|
254
|
-
|
|
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}`);
|
|
255
308
|
}
|
|
256
309
|
function getSkillsPath() {
|
|
257
310
|
return path2.join(getClaudeHome(), "skills");
|
|
@@ -331,14 +384,22 @@ var logger = {
|
|
|
331
384
|
};
|
|
332
385
|
|
|
333
386
|
// src/security/mcp-validator.ts
|
|
334
|
-
import path3 from "path";
|
|
335
387
|
function isAllowedCommand(command) {
|
|
336
|
-
|
|
388
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
337
391
|
return ALLOWED_MCP_COMMANDS.includes(
|
|
338
|
-
|
|
392
|
+
command
|
|
339
393
|
);
|
|
340
394
|
}
|
|
341
395
|
function containsBlockedPattern(args) {
|
|
396
|
+
for (const arg of args) {
|
|
397
|
+
for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
|
|
398
|
+
if (pattern.test(arg)) {
|
|
399
|
+
return pattern;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
342
403
|
const argsString = args.join(" ");
|
|
343
404
|
for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
|
|
344
405
|
if (pattern.test(argsString)) {
|
|
@@ -347,15 +408,23 @@ function containsBlockedPattern(args) {
|
|
|
347
408
|
}
|
|
348
409
|
return null;
|
|
349
410
|
}
|
|
411
|
+
function containsBlockedEnvKey(env) {
|
|
412
|
+
const blockedSet = new Set(BLOCKED_MCP_ENV_KEYS.map((k) => k.toUpperCase()));
|
|
413
|
+
for (const key of Object.keys(env)) {
|
|
414
|
+
if (blockedSet.has(key.toUpperCase())) {
|
|
415
|
+
return key;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
350
420
|
function validateMcpConfig(mcp) {
|
|
351
421
|
if (!mcp?.command) {
|
|
352
422
|
return { valid: false, error: "MCP command is required" };
|
|
353
423
|
}
|
|
354
|
-
const baseCommand = path3.basename(mcp.command);
|
|
355
424
|
if (!isAllowedCommand(mcp.command)) {
|
|
356
425
|
return {
|
|
357
426
|
valid: false,
|
|
358
|
-
error: `MCP command '${
|
|
427
|
+
error: `MCP command '${mcp.command}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
|
|
359
428
|
};
|
|
360
429
|
}
|
|
361
430
|
if (mcp.args) {
|
|
@@ -367,16 +436,25 @@ function validateMcpConfig(mcp) {
|
|
|
367
436
|
};
|
|
368
437
|
}
|
|
369
438
|
}
|
|
439
|
+
if (mcp.env) {
|
|
440
|
+
const blockedKey = containsBlockedEnvKey(mcp.env);
|
|
441
|
+
if (blockedKey) {
|
|
442
|
+
return {
|
|
443
|
+
valid: false,
|
|
444
|
+
error: `MCP environment variable '${blockedKey}' is not allowed. It could be used to bypass command security restrictions.`
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
370
448
|
return { valid: true };
|
|
371
449
|
}
|
|
372
450
|
|
|
373
451
|
// src/security/file-sanitizer.ts
|
|
374
|
-
import
|
|
452
|
+
import path3 from "path";
|
|
375
453
|
function sanitizeFileName(fileName) {
|
|
376
454
|
if (!fileName || typeof fileName !== "string") {
|
|
377
455
|
return { valid: false, error: "File name cannot be empty", sanitized: "" };
|
|
378
456
|
}
|
|
379
|
-
const baseName =
|
|
457
|
+
const baseName = path3.basename(fileName);
|
|
380
458
|
if (baseName.includes("\0")) {
|
|
381
459
|
return {
|
|
382
460
|
valid: false,
|
|
@@ -421,12 +499,12 @@ function sanitizeFolderName(name) {
|
|
|
421
499
|
if (!sanitized || sanitized.startsWith(".")) {
|
|
422
500
|
throw new Error(`Invalid package name: ${name}`);
|
|
423
501
|
}
|
|
424
|
-
const normalized =
|
|
502
|
+
const normalized = path3.normalize(sanitized);
|
|
425
503
|
if (normalized !== sanitized || normalized.includes("..")) {
|
|
426
504
|
throw new Error(`Invalid package name (path traversal detected): ${name}`);
|
|
427
505
|
}
|
|
428
|
-
const testPath =
|
|
429
|
-
const resolved =
|
|
506
|
+
const testPath = path3.join("/test", sanitized);
|
|
507
|
+
const resolved = path3.resolve(testPath);
|
|
430
508
|
if (!resolved.startsWith("/test/")) {
|
|
431
509
|
throw new Error(`Invalid package name (path traversal detected): ${name}`);
|
|
432
510
|
}
|
|
@@ -434,26 +512,79 @@ function sanitizeFolderName(name) {
|
|
|
434
512
|
}
|
|
435
513
|
|
|
436
514
|
// src/security/path-validator.ts
|
|
437
|
-
import
|
|
515
|
+
import path4 from "path";
|
|
438
516
|
function isPathWithinDirectory(filePath, directory) {
|
|
439
|
-
const resolvedPath =
|
|
440
|
-
const resolvedDir =
|
|
441
|
-
return resolvedPath.startsWith(resolvedDir +
|
|
517
|
+
const resolvedPath = path4.resolve(filePath);
|
|
518
|
+
const resolvedDir = path4.resolve(directory);
|
|
519
|
+
return resolvedPath.startsWith(resolvedDir + path4.sep) || resolvedPath === resolvedDir;
|
|
520
|
+
}
|
|
521
|
+
|
|
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 };
|
|
442
575
|
}
|
|
443
576
|
|
|
444
|
-
// src/adapters/handlers/
|
|
577
|
+
// src/adapters/handlers/metadata.ts
|
|
578
|
+
import fs2 from "fs-extra";
|
|
579
|
+
import path5 from "path";
|
|
445
580
|
async function writePackageMetadata(packageDir, manifest) {
|
|
446
581
|
const metadata = {
|
|
447
582
|
name: manifest.name,
|
|
448
|
-
// e.g., "@cpm/typescript-strict"
|
|
449
583
|
version: manifest.version,
|
|
450
|
-
// e.g., "1.0.0"
|
|
451
584
|
type: manifest.type,
|
|
452
|
-
// e.g., "rules"
|
|
453
585
|
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
454
|
-
// ISO timestamp for when it was installed
|
|
455
586
|
};
|
|
456
|
-
const metadataPath =
|
|
587
|
+
const metadataPath = path5.join(packageDir, ".cpm.json");
|
|
457
588
|
try {
|
|
458
589
|
await fs2.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
459
590
|
} catch (error) {
|
|
@@ -463,6 +594,8 @@ async function writePackageMetadata(packageDir, manifest) {
|
|
|
463
594
|
}
|
|
464
595
|
return metadataPath;
|
|
465
596
|
}
|
|
597
|
+
|
|
598
|
+
// src/adapters/handlers/rules-handler.ts
|
|
466
599
|
var RulesHandler = class {
|
|
467
600
|
/**
|
|
468
601
|
* Identifies this handler as handling "rules" type packages.
|
|
@@ -487,9 +620,9 @@ var RulesHandler = class {
|
|
|
487
620
|
const rulesBaseDir = getRulesPath("claude-code");
|
|
488
621
|
const folderName = sanitizeFolderName(manifest.name);
|
|
489
622
|
const rulesDir = path6.join(rulesBaseDir, folderName);
|
|
490
|
-
await
|
|
491
|
-
if (context.packagePath && await
|
|
492
|
-
const files = await
|
|
623
|
+
await fs3.ensureDir(rulesDir);
|
|
624
|
+
if (context.packagePath && await fs3.pathExists(context.packagePath)) {
|
|
625
|
+
const files = await fs3.readdir(context.packagePath);
|
|
493
626
|
const mdFiles = files.filter(
|
|
494
627
|
(f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
|
|
495
628
|
);
|
|
@@ -506,7 +639,12 @@ var RulesHandler = class {
|
|
|
506
639
|
logger.warn(`Blocked path traversal attempt: ${file}`);
|
|
507
640
|
continue;
|
|
508
641
|
}
|
|
509
|
-
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);
|
|
510
648
|
filesWritten.push(destPath);
|
|
511
649
|
}
|
|
512
650
|
const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
|
|
@@ -523,7 +661,7 @@ ${manifest.description}
|
|
|
523
661
|
|
|
524
662
|
${rulesContent.trim()}
|
|
525
663
|
`;
|
|
526
|
-
await
|
|
664
|
+
await fs3.writeFile(rulesPath, content, "utf-8");
|
|
527
665
|
filesWritten.push(rulesPath);
|
|
528
666
|
const metadataPath = await writePackageMetadata(rulesDir, manifest);
|
|
529
667
|
filesWritten.push(metadataPath);
|
|
@@ -543,8 +681,8 @@ ${rulesContent.trim()}
|
|
|
543
681
|
const folderName = sanitizeFolderName(packageName);
|
|
544
682
|
const rulesBaseDir = getRulesPath("claude-code");
|
|
545
683
|
const rulesPath = path6.join(rulesBaseDir, folderName);
|
|
546
|
-
if (await
|
|
547
|
-
await
|
|
684
|
+
if (await fs3.pathExists(rulesPath)) {
|
|
685
|
+
await fs3.remove(rulesPath);
|
|
548
686
|
filesRemoved.push(rulesPath);
|
|
549
687
|
}
|
|
550
688
|
return filesRemoved;
|
|
@@ -568,29 +706,8 @@ ${rulesContent.trim()}
|
|
|
568
706
|
};
|
|
569
707
|
|
|
570
708
|
// src/adapters/handlers/skill-handler.ts
|
|
571
|
-
import
|
|
709
|
+
import fs4 from "fs-extra";
|
|
572
710
|
import path7 from "path";
|
|
573
|
-
async function writePackageMetadata2(packageDir, manifest) {
|
|
574
|
-
const metadata = {
|
|
575
|
-
name: manifest.name,
|
|
576
|
-
// e.g., "@cpm/commit-skill"
|
|
577
|
-
version: manifest.version,
|
|
578
|
-
// e.g., "1.0.0"
|
|
579
|
-
type: manifest.type,
|
|
580
|
-
// e.g., "skill"
|
|
581
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
582
|
-
// ISO timestamp for when it was installed
|
|
583
|
-
};
|
|
584
|
-
const metadataPath = path7.join(packageDir, ".cpm.json");
|
|
585
|
-
try {
|
|
586
|
-
await fs3.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
587
|
-
} catch (error) {
|
|
588
|
-
logger.warn(
|
|
589
|
-
`Could not write metadata: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
return metadataPath;
|
|
593
|
-
}
|
|
594
711
|
function formatSkillMd(manifest) {
|
|
595
712
|
const skill = manifest.skill;
|
|
596
713
|
const content = manifest.universal?.prompt || manifest.universal?.rules || "";
|
|
@@ -634,9 +751,9 @@ var SkillHandler = class {
|
|
|
634
751
|
const skillsDir = getSkillsPath();
|
|
635
752
|
const folderName = sanitizeFolderName(manifest.name);
|
|
636
753
|
const skillDir = path7.join(skillsDir, folderName);
|
|
637
|
-
await
|
|
638
|
-
if (context.packagePath && await
|
|
639
|
-
const files = await
|
|
754
|
+
await fs4.ensureDir(skillDir);
|
|
755
|
+
if (context.packagePath && await fs4.pathExists(context.packagePath)) {
|
|
756
|
+
const files = await fs4.readdir(context.packagePath);
|
|
640
757
|
const contentFiles = files.filter(
|
|
641
758
|
(f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
|
|
642
759
|
);
|
|
@@ -653,10 +770,15 @@ var SkillHandler = class {
|
|
|
653
770
|
logger.warn(`Blocked path traversal attempt: ${file}`);
|
|
654
771
|
continue;
|
|
655
772
|
}
|
|
656
|
-
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);
|
|
657
779
|
filesWritten.push(destPath);
|
|
658
780
|
}
|
|
659
|
-
const metadataPath = await
|
|
781
|
+
const metadataPath = await writePackageMetadata(skillDir, manifest);
|
|
660
782
|
filesWritten.push(metadataPath);
|
|
661
783
|
return filesWritten;
|
|
662
784
|
}
|
|
@@ -664,9 +786,9 @@ var SkillHandler = class {
|
|
|
664
786
|
if (isSkillManifest(manifest)) {
|
|
665
787
|
const skillContent = formatSkillMd(manifest);
|
|
666
788
|
const skillPath = path7.join(skillDir, "SKILL.md");
|
|
667
|
-
await
|
|
789
|
+
await fs4.writeFile(skillPath, skillContent, "utf-8");
|
|
668
790
|
filesWritten.push(skillPath);
|
|
669
|
-
const metadataPath = await
|
|
791
|
+
const metadataPath = await writePackageMetadata(skillDir, manifest);
|
|
670
792
|
filesWritten.push(metadataPath);
|
|
671
793
|
} else {
|
|
672
794
|
const content = this.getUniversalContent(manifest);
|
|
@@ -678,9 +800,9 @@ ${manifest.description}
|
|
|
678
800
|
|
|
679
801
|
${content.trim()}
|
|
680
802
|
`;
|
|
681
|
-
await
|
|
803
|
+
await fs4.writeFile(skillPath, skillContent, "utf-8");
|
|
682
804
|
filesWritten.push(skillPath);
|
|
683
|
-
const metadataPath = await
|
|
805
|
+
const metadataPath = await writePackageMetadata(skillDir, manifest);
|
|
684
806
|
filesWritten.push(metadataPath);
|
|
685
807
|
}
|
|
686
808
|
}
|
|
@@ -700,8 +822,8 @@ ${content.trim()}
|
|
|
700
822
|
const folderName = sanitizeFolderName(packageName);
|
|
701
823
|
const skillsDir = getSkillsPath();
|
|
702
824
|
const skillPath = path7.join(skillsDir, folderName);
|
|
703
|
-
if (await
|
|
704
|
-
await
|
|
825
|
+
if (await fs4.pathExists(skillPath)) {
|
|
826
|
+
await fs4.remove(skillPath);
|
|
705
827
|
filesRemoved.push(skillPath);
|
|
706
828
|
}
|
|
707
829
|
return filesRemoved;
|
|
@@ -724,28 +846,66 @@ ${content.trim()}
|
|
|
724
846
|
};
|
|
725
847
|
|
|
726
848
|
// src/adapters/handlers/mcp-handler.ts
|
|
727
|
-
import
|
|
849
|
+
import path9 from "path";
|
|
850
|
+
|
|
851
|
+
// src/adapters/handlers/base-mcp-handler.ts
|
|
852
|
+
import fs6 from "fs-extra";
|
|
728
853
|
import path8 from "path";
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 {
|
|
734
908
|
packageType = "mcp";
|
|
735
|
-
/**
|
|
736
|
-
* Install an MCP package.
|
|
737
|
-
*
|
|
738
|
-
* The installation process:
|
|
739
|
-
* 1. Validate the MCP configuration for security
|
|
740
|
-
* 2. Read the existing ~/.claude.json configuration
|
|
741
|
-
* 3. Add the new MCP server to the mcpServers section
|
|
742
|
-
* 4. Write the updated configuration back
|
|
743
|
-
*
|
|
744
|
-
* @param manifest - The package manifest with MCP configuration
|
|
745
|
-
* @param _context - Install context (not used for MCP, but required by interface)
|
|
746
|
-
* @returns Array containing the path to the modified config file
|
|
747
|
-
* @throws Error if MCP configuration fails security validation
|
|
748
|
-
*/
|
|
749
909
|
async install(manifest, _context) {
|
|
750
910
|
const filesWritten = [];
|
|
751
911
|
if (!isMcpManifest(manifest)) {
|
|
@@ -755,70 +915,68 @@ var McpHandler = class {
|
|
|
755
915
|
if (!mcpValidation.valid) {
|
|
756
916
|
throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
|
|
757
917
|
}
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
// Add/update this package's MCP server
|
|
779
|
-
command: manifest.mcp.command,
|
|
780
|
-
// e.g., "npx"
|
|
781
|
-
args: manifest.mcp.args,
|
|
782
|
-
// e.g., ["-y", "@supabase/mcp"]
|
|
783
|
-
env: manifest.mcp.env
|
|
784
|
-
// e.g., { "SUPABASE_URL": "..." }
|
|
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)) {
|
|
923
|
+
try {
|
|
924
|
+
existingConfig = await fs6.readJson(mcpConfigPath);
|
|
925
|
+
} catch {
|
|
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 = {};
|
|
785
938
|
}
|
|
786
939
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
+
}
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
await fs6.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
|
|
954
|
+
filesWritten.push(mcpConfigPath);
|
|
955
|
+
});
|
|
790
956
|
return filesWritten;
|
|
791
957
|
}
|
|
792
|
-
/**
|
|
793
|
-
* Uninstall an MCP package.
|
|
794
|
-
*
|
|
795
|
-
* This removes the MCP server entry from ~/.claude.json
|
|
796
|
-
*
|
|
797
|
-
* @param packageName - The name of the package to remove
|
|
798
|
-
* @param _context - Uninstall context (not used for MCP, but required by interface)
|
|
799
|
-
* @returns Array containing the path to the modified config file
|
|
800
|
-
*/
|
|
801
958
|
async uninstall(packageName, _context) {
|
|
802
959
|
const filesWritten = [];
|
|
803
960
|
const folderName = sanitizeFolderName(packageName);
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
if (!await fs4.pathExists(mcpConfigPath)) {
|
|
961
|
+
const mcpConfigPath = this.getConfigPath();
|
|
962
|
+
if (!await fs6.pathExists(mcpConfigPath)) {
|
|
807
963
|
return filesWritten;
|
|
808
964
|
}
|
|
809
965
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
+
});
|
|
822
980
|
} catch (error) {
|
|
823
981
|
logger.warn(
|
|
824
982
|
`Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -828,6 +986,14 @@ var McpHandler = class {
|
|
|
828
986
|
}
|
|
829
987
|
};
|
|
830
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
|
+
|
|
831
997
|
// src/adapters/handlers/index.ts
|
|
832
998
|
function initializeHandlers() {
|
|
833
999
|
handlerRegistry.register(new RulesHandler());
|
|
@@ -837,8 +1003,6 @@ function initializeHandlers() {
|
|
|
837
1003
|
initializeHandlers();
|
|
838
1004
|
|
|
839
1005
|
// src/adapters/claude-code.ts
|
|
840
|
-
import fs5 from "fs-extra";
|
|
841
|
-
import path9 from "path";
|
|
842
1006
|
var ClaudeCodeAdapter = class extends PlatformAdapter {
|
|
843
1007
|
platform = "claude-code";
|
|
844
1008
|
displayName = "Claude Code";
|
|
@@ -865,23 +1029,17 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
|
|
|
865
1029
|
};
|
|
866
1030
|
}
|
|
867
1031
|
}
|
|
868
|
-
async uninstall(packageName,
|
|
1032
|
+
async uninstall(packageName, projectPath) {
|
|
869
1033
|
const filesWritten = [];
|
|
870
|
-
const
|
|
1034
|
+
const context = { projectPath };
|
|
871
1035
|
try {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const skillsDir = getSkillsPath();
|
|
879
|
-
const skillPath = path9.join(skillsDir, folderName);
|
|
880
|
-
if (await fs5.pathExists(skillPath)) {
|
|
881
|
-
await fs5.remove(skillPath);
|
|
882
|
-
filesWritten.push(skillPath);
|
|
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
|
+
}
|
|
883
1042
|
}
|
|
884
|
-
await this.removeMcpServer(folderName, filesWritten);
|
|
885
1043
|
return {
|
|
886
1044
|
success: true,
|
|
887
1045
|
platform: "claude-code",
|
|
@@ -904,63 +1062,248 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
|
|
|
904
1062
|
return this.installFallback(manifest, context);
|
|
905
1063
|
}
|
|
906
1064
|
async installFallback(manifest, context) {
|
|
907
|
-
|
|
1065
|
+
logger.warn(
|
|
1066
|
+
`No handler registered for type "${manifest.type}", attempting content-based detection`
|
|
1067
|
+
);
|
|
1068
|
+
if (isSkillManifest(manifest)) {
|
|
908
1069
|
const handler = handlerRegistry.getHandler("skill");
|
|
909
1070
|
return handler.install(manifest, context);
|
|
910
1071
|
}
|
|
911
|
-
if (
|
|
1072
|
+
if (isMcpManifest(manifest)) {
|
|
912
1073
|
const handler = handlerRegistry.getHandler("mcp");
|
|
913
1074
|
return handler.install(manifest, context);
|
|
914
1075
|
}
|
|
915
|
-
if (
|
|
1076
|
+
if (isRulesManifest(manifest)) {
|
|
916
1077
|
const handler = handlerRegistry.getHandler("rules");
|
|
917
1078
|
return handler.install(manifest, context);
|
|
918
1079
|
}
|
|
919
1080
|
return [];
|
|
920
1081
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1082
|
+
};
|
|
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;
|
|
926
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 = [];
|
|
927
1206
|
try {
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
};
|
|
932
1217
|
}
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1218
|
+
const result = await this.installByType(manifest, context);
|
|
1219
|
+
filesWritten.push(...result);
|
|
1220
|
+
return {
|
|
1221
|
+
success: true,
|
|
1222
|
+
platform: "cursor",
|
|
1223
|
+
filesWritten
|
|
937
1224
|
};
|
|
938
|
-
await fs5.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
|
|
939
|
-
filesWritten.push(mcpConfigPath);
|
|
940
1225
|
} catch (error) {
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
943
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 [];
|
|
944
1270
|
}
|
|
945
1271
|
}
|
|
946
1272
|
};
|
|
947
1273
|
|
|
948
1274
|
// src/adapters/index.ts
|
|
949
1275
|
var adapters = {
|
|
950
|
-
"claude-code": new ClaudeCodeAdapter()
|
|
1276
|
+
"claude-code": new ClaudeCodeAdapter(),
|
|
1277
|
+
cursor: new CursorAdapter()
|
|
951
1278
|
};
|
|
952
1279
|
function getAdapter(platform) {
|
|
953
|
-
|
|
1280
|
+
const adapter = adapters[platform];
|
|
1281
|
+
if (!adapter) {
|
|
1282
|
+
throw new Error(`No adapter available for platform: ${platform}`);
|
|
1283
|
+
}
|
|
1284
|
+
return adapter;
|
|
954
1285
|
}
|
|
955
1286
|
|
|
956
1287
|
// src/utils/registry.ts
|
|
957
1288
|
import got from "got";
|
|
958
|
-
import
|
|
959
|
-
import
|
|
960
|
-
import
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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");
|
|
964
1307
|
var comparators = {
|
|
965
1308
|
downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
|
|
966
1309
|
stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
|
|
@@ -1022,11 +1365,11 @@ var Registry = class {
|
|
|
1022
1365
|
}
|
|
1023
1366
|
async loadFileCache() {
|
|
1024
1367
|
try {
|
|
1025
|
-
await
|
|
1026
|
-
if (await
|
|
1027
|
-
const stat = await
|
|
1368
|
+
await fs8.ensureDir(CACHE_DIR);
|
|
1369
|
+
if (await fs8.pathExists(CACHE_FILE)) {
|
|
1370
|
+
const stat = await fs8.stat(CACHE_FILE);
|
|
1028
1371
|
if (Date.now() - stat.mtimeMs < LIMITS.CACHE_TTL_MS) {
|
|
1029
|
-
return await
|
|
1372
|
+
return await fs8.readJson(CACHE_FILE);
|
|
1030
1373
|
}
|
|
1031
1374
|
}
|
|
1032
1375
|
} catch {
|
|
@@ -1035,8 +1378,8 @@ var Registry = class {
|
|
|
1035
1378
|
}
|
|
1036
1379
|
async saveFileCache(data) {
|
|
1037
1380
|
try {
|
|
1038
|
-
await
|
|
1039
|
-
await
|
|
1381
|
+
await fs8.ensureDir(CACHE_DIR);
|
|
1382
|
+
await fs8.writeJson(CACHE_FILE, data, { spaces: 2 });
|
|
1040
1383
|
} catch {
|
|
1041
1384
|
}
|
|
1042
1385
|
}
|
|
@@ -1059,8 +1402,8 @@ var Registry = class {
|
|
|
1059
1402
|
return this.cache;
|
|
1060
1403
|
}
|
|
1061
1404
|
try {
|
|
1062
|
-
if (await
|
|
1063
|
-
const cached = await
|
|
1405
|
+
if (await fs8.pathExists(CACHE_FILE)) {
|
|
1406
|
+
const cached = await fs8.readJson(CACHE_FILE);
|
|
1064
1407
|
this.cache = cached;
|
|
1065
1408
|
return cached;
|
|
1066
1409
|
}
|
|
@@ -1100,9 +1443,9 @@ var Registry = class {
|
|
|
1100
1443
|
var registry = new Registry();
|
|
1101
1444
|
|
|
1102
1445
|
// src/utils/downloader.ts
|
|
1103
|
-
import
|
|
1104
|
-
import
|
|
1105
|
-
import
|
|
1446
|
+
import fs10 from "fs-extra";
|
|
1447
|
+
import path13 from "path";
|
|
1448
|
+
import os4 from "os";
|
|
1106
1449
|
|
|
1107
1450
|
// src/sources/manifest-resolver.ts
|
|
1108
1451
|
var ManifestResolver = class {
|
|
@@ -1195,76 +1538,45 @@ var ManifestResolver = class {
|
|
|
1195
1538
|
// src/sources/repository-source.ts
|
|
1196
1539
|
import got2 from "got";
|
|
1197
1540
|
import yaml from "yaml";
|
|
1541
|
+
var PACKAGES_REPO_BASE = "https://raw.githubusercontent.com/cpmai-dev/packages/main/packages";
|
|
1198
1542
|
var RepositorySource = class {
|
|
1199
|
-
/**
|
|
1200
|
-
* Name of this source for logging and debugging.
|
|
1201
|
-
*/
|
|
1202
1543
|
name = "repository";
|
|
1203
|
-
/**
|
|
1204
|
-
* Priority 1 - this is the first source tried.
|
|
1205
|
-
* Repository fetching is fast and provides complete manifests.
|
|
1206
|
-
*/
|
|
1207
1544
|
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
1545
|
canFetch(pkg) {
|
|
1230
|
-
return !!pkg.repository?.includes("github.com");
|
|
1546
|
+
return !!pkg.path || !!pkg.repository?.includes("github.com");
|
|
1231
1547
|
}
|
|
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
1548
|
async fetch(pkg, _context) {
|
|
1259
|
-
if (
|
|
1549
|
+
if (pkg.path) {
|
|
1550
|
+
const manifest = await this.fetchFromPackagesRepo(pkg.path);
|
|
1551
|
+
if (manifest) return manifest;
|
|
1552
|
+
}
|
|
1553
|
+
if (pkg.repository) {
|
|
1554
|
+
return this.fetchFromStandaloneRepo(pkg.repository);
|
|
1555
|
+
}
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
async fetchFromPackagesRepo(packagePath) {
|
|
1559
|
+
if (packagePath.includes("..") || packagePath.startsWith("/") || packagePath.includes("\\")) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const rawUrl = `${PACKAGES_REPO_BASE}/${packagePath}/cpm.yaml`;
|
|
1563
|
+
try {
|
|
1564
|
+
const response = await got2(rawUrl, {
|
|
1565
|
+
timeout: { request: TIMEOUTS.MANIFEST_FETCH }
|
|
1566
|
+
});
|
|
1567
|
+
return yaml.parse(response.body);
|
|
1568
|
+
} catch {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async fetchFromStandaloneRepo(repository) {
|
|
1260
1573
|
try {
|
|
1261
|
-
const match =
|
|
1574
|
+
const match = repository.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1262
1575
|
if (!match) return null;
|
|
1263
1576
|
const [, owner, repo] = match;
|
|
1264
1577
|
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/cpm.yaml`;
|
|
1265
1578
|
const response = await got2(rawUrl, {
|
|
1266
1579
|
timeout: { request: TIMEOUTS.MANIFEST_FETCH }
|
|
1267
|
-
// 5 second timeout
|
|
1268
1580
|
});
|
|
1269
1581
|
return yaml.parse(response.body);
|
|
1270
1582
|
} catch {
|
|
@@ -1275,8 +1587,8 @@ var RepositorySource = class {
|
|
|
1275
1587
|
|
|
1276
1588
|
// src/sources/tarball-source.ts
|
|
1277
1589
|
import got3 from "got";
|
|
1278
|
-
import
|
|
1279
|
-
import
|
|
1590
|
+
import fs9 from "fs-extra";
|
|
1591
|
+
import path12 from "path";
|
|
1280
1592
|
import * as tar from "tar";
|
|
1281
1593
|
import yaml2 from "yaml";
|
|
1282
1594
|
var TarballSource = class {
|
|
@@ -1328,12 +1640,12 @@ var TarballSource = class {
|
|
|
1328
1640
|
responseType: "buffer"
|
|
1329
1641
|
// Get raw binary data
|
|
1330
1642
|
});
|
|
1331
|
-
const tarballPath =
|
|
1332
|
-
await
|
|
1643
|
+
const tarballPath = path12.join(context.tempDir, "package.tar.gz");
|
|
1644
|
+
await fs9.writeFile(tarballPath, response.body);
|
|
1333
1645
|
await this.extractTarball(tarballPath, context.tempDir);
|
|
1334
|
-
const manifestPath =
|
|
1335
|
-
if (await
|
|
1336
|
-
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");
|
|
1337
1649
|
return yaml2.parse(content);
|
|
1338
1650
|
}
|
|
1339
1651
|
return null;
|
|
@@ -1355,8 +1667,8 @@ var TarballSource = class {
|
|
|
1355
1667
|
* @param destDir - Directory to extract to
|
|
1356
1668
|
*/
|
|
1357
1669
|
async extractTarball(tarballPath, destDir) {
|
|
1358
|
-
await
|
|
1359
|
-
const resolvedDestDir =
|
|
1670
|
+
await fs9.ensureDir(destDir);
|
|
1671
|
+
const resolvedDestDir = path12.resolve(destDir);
|
|
1360
1672
|
await tar.extract({
|
|
1361
1673
|
file: tarballPath,
|
|
1362
1674
|
// The tarball file to extract
|
|
@@ -1366,8 +1678,8 @@ var TarballSource = class {
|
|
|
1366
1678
|
// Remove the top-level directory (e.g., "package-1.0.0/")
|
|
1367
1679
|
// Security filter: check each entry before extracting
|
|
1368
1680
|
filter: (entryPath) => {
|
|
1369
|
-
const resolvedPath =
|
|
1370
|
-
const isWithinDest = resolvedPath.startsWith(resolvedDestDir +
|
|
1681
|
+
const resolvedPath = path12.resolve(destDir, entryPath);
|
|
1682
|
+
const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path12.sep) || resolvedPath === resolvedDestDir;
|
|
1371
1683
|
if (!isWithinDest) {
|
|
1372
1684
|
logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
|
|
1373
1685
|
return false;
|
|
@@ -2048,15 +2360,15 @@ function createDefaultResolver() {
|
|
|
2048
2360
|
var defaultResolver = createDefaultResolver();
|
|
2049
2361
|
|
|
2050
2362
|
// src/utils/downloader.ts
|
|
2051
|
-
var TEMP_DIR =
|
|
2363
|
+
var TEMP_DIR = path13.join(os4.tmpdir(), "cpm-downloads");
|
|
2052
2364
|
async function downloadPackage(pkg) {
|
|
2053
2365
|
try {
|
|
2054
|
-
await
|
|
2055
|
-
const packageTempDir =
|
|
2366
|
+
await fs10.ensureDir(TEMP_DIR);
|
|
2367
|
+
const packageTempDir = path13.join(
|
|
2056
2368
|
TEMP_DIR,
|
|
2057
2369
|
`${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
|
|
2058
2370
|
);
|
|
2059
|
-
await
|
|
2371
|
+
await fs10.ensureDir(packageTempDir);
|
|
2060
2372
|
const manifest = await defaultResolver.resolve(pkg, {
|
|
2061
2373
|
tempDir: packageTempDir
|
|
2062
2374
|
});
|
|
@@ -2071,7 +2383,7 @@ async function downloadPackage(pkg) {
|
|
|
2071
2383
|
async function cleanupTempDir(tempDir) {
|
|
2072
2384
|
try {
|
|
2073
2385
|
if (tempDir.startsWith(TEMP_DIR)) {
|
|
2074
|
-
await
|
|
2386
|
+
await fs10.remove(tempDir);
|
|
2075
2387
|
}
|
|
2076
2388
|
} catch {
|
|
2077
2389
|
}
|
|
@@ -2198,7 +2510,7 @@ var SEMANTIC_COLORS = {
|
|
|
2198
2510
|
|
|
2199
2511
|
// src/commands/ui/formatters.ts
|
|
2200
2512
|
import chalk2 from "chalk";
|
|
2201
|
-
import
|
|
2513
|
+
import path14 from "path";
|
|
2202
2514
|
function formatNumber(num) {
|
|
2203
2515
|
if (num >= 1e6) {
|
|
2204
2516
|
return `${(num / 1e6).toFixed(1)}M`;
|
|
@@ -2209,7 +2521,7 @@ function formatNumber(num) {
|
|
|
2209
2521
|
return num.toString();
|
|
2210
2522
|
}
|
|
2211
2523
|
function formatPath(filePath) {
|
|
2212
|
-
const relativePath =
|
|
2524
|
+
const relativePath = path14.relative(process.cwd(), filePath);
|
|
2213
2525
|
if (relativePath.startsWith("..")) {
|
|
2214
2526
|
return filePath;
|
|
2215
2527
|
}
|
|
@@ -2271,8 +2583,12 @@ function formatUsageHints(manifest) {
|
|
|
2271
2583
|
if ("mcp" in manifest && manifest.mcp?.env) {
|
|
2272
2584
|
const envVars = Object.keys(manifest.mcp.env);
|
|
2273
2585
|
if (envVars.length > 0) {
|
|
2274
|
-
hints.push(
|
|
2275
|
-
|
|
2586
|
+
hints.push(
|
|
2587
|
+
chalk2.yellow(
|
|
2588
|
+
`
|
|
2589
|
+
Configure these environment variables in ~/.claude.json:`
|
|
2590
|
+
)
|
|
2591
|
+
);
|
|
2276
2592
|
for (const envVar of envVars) {
|
|
2277
2593
|
hints.push(chalk2.dim(` - ${envVar}`));
|
|
2278
2594
|
}
|
|
@@ -2463,7 +2779,11 @@ async function installCommand(packageName, rawOptions) {
|
|
|
2463
2779
|
tempDir = downloadResult.tempDir;
|
|
2464
2780
|
const targetPlatforms = [options.platform];
|
|
2465
2781
|
spinner.update(`Installing to ${targetPlatforms.join(", ")}...`);
|
|
2466
|
-
|
|
2782
|
+
if (options.platform === "cursor") {
|
|
2783
|
+
await ensureCursorDirs(process.cwd());
|
|
2784
|
+
} else {
|
|
2785
|
+
await ensureClaudeDirs();
|
|
2786
|
+
}
|
|
2467
2787
|
const results = await installToPlatforms(
|
|
2468
2788
|
manifest,
|
|
2469
2789
|
tempDir,
|
|
@@ -2539,28 +2859,28 @@ async function searchCommand(query, rawOptions) {
|
|
|
2539
2859
|
|
|
2540
2860
|
// src/commands/list.ts
|
|
2541
2861
|
import chalk4 from "chalk";
|
|
2542
|
-
import
|
|
2543
|
-
import
|
|
2544
|
-
import
|
|
2862
|
+
import fs11 from "fs-extra";
|
|
2863
|
+
import path15 from "path";
|
|
2864
|
+
import os5 from "os";
|
|
2545
2865
|
async function readPackageMetadata(packageDir) {
|
|
2546
|
-
const metadataPath =
|
|
2866
|
+
const metadataPath = path15.join(packageDir, ".cpm.json");
|
|
2547
2867
|
try {
|
|
2548
|
-
if (await
|
|
2549
|
-
return await
|
|
2868
|
+
if (await fs11.pathExists(metadataPath)) {
|
|
2869
|
+
return await fs11.readJson(metadataPath);
|
|
2550
2870
|
}
|
|
2551
2871
|
} catch {
|
|
2552
2872
|
}
|
|
2553
2873
|
return null;
|
|
2554
2874
|
}
|
|
2555
|
-
async function scanDirectory(dir, type) {
|
|
2875
|
+
async function scanDirectory(dir, type, platform) {
|
|
2556
2876
|
const items = [];
|
|
2557
|
-
if (!await
|
|
2877
|
+
if (!await fs11.pathExists(dir)) {
|
|
2558
2878
|
return items;
|
|
2559
2879
|
}
|
|
2560
|
-
const entries = await
|
|
2880
|
+
const entries = await fs11.readdir(dir);
|
|
2561
2881
|
for (const entry of entries) {
|
|
2562
|
-
const entryPath =
|
|
2563
|
-
const stat = await
|
|
2882
|
+
const entryPath = path15.join(dir, entry);
|
|
2883
|
+
const stat = await fs11.stat(entryPath);
|
|
2564
2884
|
if (stat.isDirectory()) {
|
|
2565
2885
|
const metadata = await readPackageMetadata(entryPath);
|
|
2566
2886
|
items.push({
|
|
@@ -2568,27 +2888,28 @@ async function scanDirectory(dir, type) {
|
|
|
2568
2888
|
folderName: entry,
|
|
2569
2889
|
type,
|
|
2570
2890
|
version: metadata?.version,
|
|
2571
|
-
path: entryPath
|
|
2891
|
+
path: entryPath,
|
|
2892
|
+
platform
|
|
2572
2893
|
});
|
|
2573
2894
|
}
|
|
2574
2895
|
}
|
|
2575
2896
|
return items;
|
|
2576
2897
|
}
|
|
2577
|
-
async function
|
|
2898
|
+
async function scanMcpServersFromConfig(configPath, platform) {
|
|
2578
2899
|
const items = [];
|
|
2579
|
-
|
|
2580
|
-
if (!await fs9.pathExists(configPath)) {
|
|
2900
|
+
if (!await fs11.pathExists(configPath)) {
|
|
2581
2901
|
return items;
|
|
2582
2902
|
}
|
|
2583
2903
|
try {
|
|
2584
|
-
const config = await
|
|
2904
|
+
const config = await fs11.readJson(configPath);
|
|
2585
2905
|
const mcpServers = config.mcpServers || {};
|
|
2586
2906
|
for (const name of Object.keys(mcpServers)) {
|
|
2587
2907
|
items.push({
|
|
2588
2908
|
name,
|
|
2589
2909
|
folderName: name,
|
|
2590
2910
|
type: "mcp",
|
|
2591
|
-
path: configPath
|
|
2911
|
+
path: configPath,
|
|
2912
|
+
platform
|
|
2592
2913
|
});
|
|
2593
2914
|
}
|
|
2594
2915
|
} catch {
|
|
@@ -2596,13 +2917,24 @@ async function scanMcpServers() {
|
|
|
2596
2917
|
return items;
|
|
2597
2918
|
}
|
|
2598
2919
|
async function scanInstalledPackages() {
|
|
2599
|
-
const claudeHome =
|
|
2600
|
-
const
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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")
|
|
2604
2930
|
]);
|
|
2605
|
-
return [
|
|
2931
|
+
return [
|
|
2932
|
+
...claudeRules,
|
|
2933
|
+
...claudeSkills,
|
|
2934
|
+
...claudeMcp,
|
|
2935
|
+
...cursorRules,
|
|
2936
|
+
...cursorMcp
|
|
2937
|
+
];
|
|
2606
2938
|
}
|
|
2607
2939
|
function groupByType(packages) {
|
|
2608
2940
|
return packages.reduce(
|
|
@@ -2615,8 +2947,9 @@ function groupByType(packages) {
|
|
|
2615
2947
|
}
|
|
2616
2948
|
function displayPackage(pkg) {
|
|
2617
2949
|
const version = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
|
|
2950
|
+
const platform = pkg.platform ? SEMANTIC_COLORS.dim(` [${pkg.platform}]`) : "";
|
|
2618
2951
|
logger.log(
|
|
2619
|
-
` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}`
|
|
2952
|
+
` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}${platform}`
|
|
2620
2953
|
);
|
|
2621
2954
|
}
|
|
2622
2955
|
function displayByType(byType) {
|
|
@@ -2679,15 +3012,22 @@ async function uninstallCommand(packageName) {
|
|
|
2679
3012
|
const spinner = createSpinner(spinnerText("Uninstalling", packageName));
|
|
2680
3013
|
try {
|
|
2681
3014
|
const folderName = extractFolderName(packageName);
|
|
2682
|
-
const
|
|
2683
|
-
const
|
|
2684
|
-
|
|
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) {
|
|
2685
3027
|
spinner.succeed(successText("Uninstalled", packageName));
|
|
2686
|
-
displayRemovedFiles(
|
|
2687
|
-
} else if (result.success) {
|
|
2688
|
-
spinner.warn(`Package ${packageName} was not found`);
|
|
3028
|
+
displayRemovedFiles(allFilesRemoved);
|
|
2689
3029
|
} else {
|
|
2690
|
-
spinner.
|
|
3030
|
+
spinner.warn(`Package ${packageName} was not found`);
|
|
2691
3031
|
}
|
|
2692
3032
|
} catch (error) {
|
|
2693
3033
|
spinner.fail(failText("Failed to uninstall", packageName));
|