@cpmai/cli 0.1.3 → 0.2.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.
- package/README.md +1 -54
- package/dist/index.js +1742 -1186
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,79 +2,259 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
// src/commands/install.ts
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import ora from "ora";
|
|
10
|
-
import path6 from "path";
|
|
11
|
-
|
|
12
|
-
// src/adapters/claude-code.ts
|
|
13
|
-
import fs2 from "fs-extra";
|
|
14
|
-
import path2 from "path";
|
|
5
|
+
import chalk5 from "chalk";
|
|
15
6
|
|
|
16
7
|
// src/adapters/base.ts
|
|
17
8
|
var PlatformAdapter = class {
|
|
18
9
|
};
|
|
19
10
|
|
|
20
|
-
// src/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
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}`);
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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());
|
|
60
100
|
}
|
|
61
|
-
|
|
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
|
+
// src/constants.ts
|
|
109
|
+
var TIMEOUTS = {
|
|
110
|
+
MANIFEST_FETCH: 5e3,
|
|
111
|
+
// 5 seconds - quick manifest lookups
|
|
112
|
+
TARBALL_DOWNLOAD: 3e4,
|
|
113
|
+
// 30 seconds - allow time for large downloads
|
|
114
|
+
REGISTRY_FETCH: 1e4
|
|
115
|
+
// 10 seconds - registry can be slow sometimes
|
|
116
|
+
};
|
|
117
|
+
var LIMITS = {
|
|
118
|
+
MAX_PACKAGE_NAME_LENGTH: 214,
|
|
119
|
+
// npm standard maximum
|
|
120
|
+
CACHE_TTL_MS: 5 * 60 * 1e3
|
|
121
|
+
// 5 minutes in milliseconds
|
|
122
|
+
};
|
|
123
|
+
var PACKAGE_TYPES = [
|
|
124
|
+
"rules",
|
|
125
|
+
// Coding guidelines and best practices
|
|
126
|
+
"mcp",
|
|
127
|
+
// Model Context Protocol servers
|
|
128
|
+
"skill",
|
|
129
|
+
// Slash commands for Claude Code
|
|
130
|
+
"agent",
|
|
131
|
+
// AI agent definitions
|
|
132
|
+
"hook",
|
|
133
|
+
// Git or lifecycle hooks
|
|
134
|
+
"workflow",
|
|
135
|
+
// Multi-step workflows
|
|
136
|
+
"template",
|
|
137
|
+
// Project templates
|
|
138
|
+
"bundle"
|
|
139
|
+
// Collections of packages
|
|
140
|
+
];
|
|
141
|
+
var SEARCH_SORT_OPTIONS = [
|
|
142
|
+
"downloads",
|
|
143
|
+
// Most downloaded first
|
|
144
|
+
"stars",
|
|
145
|
+
// Most starred first
|
|
146
|
+
"recent",
|
|
147
|
+
// Most recently published first
|
|
148
|
+
"name"
|
|
149
|
+
// Alphabetical order
|
|
150
|
+
];
|
|
151
|
+
var VALID_PLATFORMS = [
|
|
152
|
+
"claude-code"
|
|
153
|
+
// Currently the only supported platform
|
|
154
|
+
];
|
|
155
|
+
var ALLOWED_MCP_COMMANDS = [
|
|
156
|
+
"npx",
|
|
157
|
+
// Node package executor
|
|
158
|
+
"node",
|
|
159
|
+
// Node.js runtime
|
|
160
|
+
"python",
|
|
161
|
+
// Python 2.x interpreter
|
|
162
|
+
"python3",
|
|
163
|
+
// Python 3.x interpreter
|
|
164
|
+
"deno",
|
|
165
|
+
// Deno runtime
|
|
166
|
+
"bun",
|
|
167
|
+
// Bun runtime
|
|
168
|
+
"uvx"
|
|
169
|
+
// Python package runner
|
|
170
|
+
];
|
|
171
|
+
var BLOCKED_MCP_ARG_PATTERNS = [
|
|
172
|
+
/--eval/i,
|
|
173
|
+
// Node.js eval flag
|
|
174
|
+
/-e\s/,
|
|
175
|
+
// Short eval flag with space
|
|
176
|
+
/-c\s/,
|
|
177
|
+
// Command flag with space
|
|
178
|
+
/\bcurl\b/i,
|
|
179
|
+
// curl command (data exfiltration)
|
|
180
|
+
/\bwget\b/i,
|
|
181
|
+
// wget command (data exfiltration)
|
|
182
|
+
/\brm\s/i,
|
|
183
|
+
// rm command (file deletion)
|
|
184
|
+
/\bsudo\b/i,
|
|
185
|
+
// sudo command (privilege escalation)
|
|
186
|
+
/\bchmod\b/i,
|
|
187
|
+
// chmod command (permission changes)
|
|
188
|
+
/\bchown\b/i,
|
|
189
|
+
// chown command (ownership changes)
|
|
190
|
+
/[|;&`$]/
|
|
191
|
+
// Shell metacharacters (command chaining/injection)
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
// src/types.ts
|
|
195
|
+
function isPackageType(value) {
|
|
196
|
+
return PACKAGE_TYPES.includes(value);
|
|
197
|
+
}
|
|
198
|
+
function isValidPlatform(value) {
|
|
199
|
+
return VALID_PLATFORMS.includes(value);
|
|
200
|
+
}
|
|
201
|
+
function isSearchSort(value) {
|
|
202
|
+
return SEARCH_SORT_OPTIONS.includes(value);
|
|
203
|
+
}
|
|
204
|
+
function isRulesManifest(manifest) {
|
|
205
|
+
return manifest.type === "rules";
|
|
206
|
+
}
|
|
207
|
+
function isSkillManifest(manifest) {
|
|
208
|
+
return manifest.type === "skill";
|
|
62
209
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
210
|
+
function isMcpManifest(manifest) {
|
|
211
|
+
return manifest.type === "mcp";
|
|
212
|
+
}
|
|
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";
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function resolvePackageType(pkg) {
|
|
225
|
+
if (pkg.type) return pkg.type;
|
|
226
|
+
if (pkg.path) {
|
|
227
|
+
const derived = getTypeFromPath(pkg.path);
|
|
228
|
+
if (derived) return derived;
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`Cannot determine type for package: ${pkg.name}`);
|
|
66
231
|
}
|
|
67
|
-
|
|
232
|
+
|
|
233
|
+
// src/utils/platform.ts
|
|
234
|
+
import path2 from "path";
|
|
235
|
+
|
|
236
|
+
// src/utils/config.ts
|
|
237
|
+
import path from "path";
|
|
238
|
+
import os from "os";
|
|
239
|
+
import fs from "fs-extra";
|
|
240
|
+
function getClaudeHome() {
|
|
68
241
|
return path.join(os.homedir(), ".claude");
|
|
69
242
|
}
|
|
243
|
+
async function ensureClaudeDirs() {
|
|
244
|
+
const claudeHome = getClaudeHome();
|
|
245
|
+
await fs.ensureDir(path.join(claudeHome, "rules"));
|
|
246
|
+
await fs.ensureDir(path.join(claudeHome, "skills"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/utils/platform.ts
|
|
70
250
|
function getRulesPath(platform) {
|
|
71
251
|
if (platform !== "claude-code") {
|
|
72
252
|
throw new Error(`Rules path is not supported for platform: ${platform}`);
|
|
73
253
|
}
|
|
74
|
-
return
|
|
254
|
+
return path2.join(getClaudeHome(), "rules");
|
|
75
255
|
}
|
|
76
256
|
function getSkillsPath() {
|
|
77
|
-
return
|
|
257
|
+
return path2.join(getClaudeHome(), "skills");
|
|
78
258
|
}
|
|
79
259
|
|
|
80
260
|
// src/utils/logger.ts
|
|
@@ -94,18 +274,6 @@ function createLogger(options = {}) {
|
|
|
94
274
|
}
|
|
95
275
|
});
|
|
96
276
|
return {
|
|
97
|
-
/**
|
|
98
|
-
* Debug message (only shown in verbose mode)
|
|
99
|
-
*/
|
|
100
|
-
debug: (message, ...args) => {
|
|
101
|
-
consola.debug(message, ...args);
|
|
102
|
-
},
|
|
103
|
-
/**
|
|
104
|
-
* Informational message
|
|
105
|
-
*/
|
|
106
|
-
info: (message, ...args) => {
|
|
107
|
-
consola.info(message, ...args);
|
|
108
|
-
},
|
|
109
277
|
/**
|
|
110
278
|
* Success message
|
|
111
279
|
*/
|
|
@@ -144,11 +312,7 @@ function createLogger(options = {}) {
|
|
|
144
312
|
/**
|
|
145
313
|
* Check if in quiet mode
|
|
146
314
|
*/
|
|
147
|
-
isQuiet: () => options.quiet ?? false
|
|
148
|
-
/**
|
|
149
|
-
* Check if in verbose mode
|
|
150
|
-
*/
|
|
151
|
-
isVerbose: () => options.verbose ?? false
|
|
315
|
+
isQuiet: () => options.quiet ?? false
|
|
152
316
|
};
|
|
153
317
|
}
|
|
154
318
|
var loggerOptions = {};
|
|
@@ -158,93 +322,83 @@ function configureLogger(options) {
|
|
|
158
322
|
loggerInstance = createLogger(options);
|
|
159
323
|
}
|
|
160
324
|
var logger = {
|
|
161
|
-
debug: (message, ...args) => loggerInstance.debug(message, ...args),
|
|
162
|
-
info: (message, ...args) => loggerInstance.info(message, ...args),
|
|
163
325
|
success: (message, ...args) => loggerInstance.success(message, ...args),
|
|
164
326
|
warn: (message, ...args) => loggerInstance.warn(message, ...args),
|
|
165
327
|
error: (message, ...args) => loggerInstance.error(message, ...args),
|
|
166
328
|
log: (message) => loggerInstance.log(message),
|
|
167
329
|
newline: () => loggerInstance.newline(),
|
|
168
|
-
isQuiet: () => loggerInstance.isQuiet()
|
|
169
|
-
isVerbose: () => loggerInstance.isVerbose()
|
|
330
|
+
isQuiet: () => loggerInstance.isQuiet()
|
|
170
331
|
};
|
|
171
332
|
|
|
172
|
-
// src/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/\bsudo\b/i,
|
|
190
|
-
/\bchmod\b/i,
|
|
191
|
-
/\bchown\b/i,
|
|
192
|
-
/[|;&`$]/
|
|
193
|
-
// Shell metacharacters
|
|
194
|
-
];
|
|
333
|
+
// src/security/mcp-validator.ts
|
|
334
|
+
import path3 from "path";
|
|
335
|
+
function isAllowedCommand(command) {
|
|
336
|
+
const baseCommand = path3.basename(command);
|
|
337
|
+
return ALLOWED_MCP_COMMANDS.includes(
|
|
338
|
+
baseCommand
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function containsBlockedPattern(args) {
|
|
342
|
+
const argsString = args.join(" ");
|
|
343
|
+
for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
|
|
344
|
+
if (pattern.test(argsString)) {
|
|
345
|
+
return pattern;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
195
350
|
function validateMcpConfig(mcp) {
|
|
196
351
|
if (!mcp?.command) {
|
|
197
352
|
return { valid: false, error: "MCP command is required" };
|
|
198
353
|
}
|
|
199
|
-
const baseCommand =
|
|
200
|
-
if (!
|
|
201
|
-
baseCommand
|
|
202
|
-
)) {
|
|
354
|
+
const baseCommand = path3.basename(mcp.command);
|
|
355
|
+
if (!isAllowedCommand(mcp.command)) {
|
|
203
356
|
return {
|
|
204
357
|
valid: false,
|
|
205
358
|
error: `MCP command '${baseCommand}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
|
|
206
359
|
};
|
|
207
360
|
}
|
|
208
361
|
if (mcp.args) {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
};
|
|
216
|
-
}
|
|
362
|
+
const blockedPattern = containsBlockedPattern(mcp.args);
|
|
363
|
+
if (blockedPattern) {
|
|
364
|
+
return {
|
|
365
|
+
valid: false,
|
|
366
|
+
error: `MCP arguments contain blocked pattern: ${blockedPattern.source}`
|
|
367
|
+
};
|
|
217
368
|
}
|
|
218
369
|
}
|
|
219
370
|
return { valid: true };
|
|
220
371
|
}
|
|
372
|
+
|
|
373
|
+
// src/security/file-sanitizer.ts
|
|
374
|
+
import path4 from "path";
|
|
221
375
|
function sanitizeFileName(fileName) {
|
|
222
376
|
if (!fileName || typeof fileName !== "string") {
|
|
223
|
-
return {
|
|
377
|
+
return { valid: false, error: "File name cannot be empty", sanitized: "" };
|
|
224
378
|
}
|
|
225
|
-
const baseName =
|
|
379
|
+
const baseName = path4.basename(fileName);
|
|
226
380
|
if (baseName.includes("\0")) {
|
|
227
381
|
return {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
382
|
+
valid: false,
|
|
383
|
+
error: "File name contains null bytes",
|
|
384
|
+
sanitized: ""
|
|
231
385
|
};
|
|
232
386
|
}
|
|
233
387
|
if (baseName.startsWith(".") && baseName !== ".md") {
|
|
234
|
-
return {
|
|
388
|
+
return { valid: false, error: "Hidden files not allowed", sanitized: "" };
|
|
235
389
|
}
|
|
236
390
|
const sanitized = baseName.replace(/[<>:"|?*\\]/g, "_");
|
|
237
391
|
if (sanitized.includes("..") || sanitized.includes("/") || sanitized.includes("\\")) {
|
|
238
392
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
393
|
+
valid: false,
|
|
394
|
+
error: "Path traversal detected in file name",
|
|
395
|
+
sanitized: ""
|
|
242
396
|
};
|
|
243
397
|
}
|
|
244
398
|
if (!sanitized.endsWith(".md")) {
|
|
245
|
-
return {
|
|
399
|
+
return { valid: false, error: "Only .md files allowed", sanitized: "" };
|
|
246
400
|
}
|
|
247
|
-
return {
|
|
401
|
+
return { valid: true, sanitized };
|
|
248
402
|
}
|
|
249
403
|
function sanitizeFolderName(name) {
|
|
250
404
|
if (!name || typeof name !== "string") {
|
|
@@ -267,30 +421,39 @@ function sanitizeFolderName(name) {
|
|
|
267
421
|
if (!sanitized || sanitized.startsWith(".")) {
|
|
268
422
|
throw new Error(`Invalid package name: ${name}`);
|
|
269
423
|
}
|
|
270
|
-
const normalized =
|
|
424
|
+
const normalized = path4.normalize(sanitized);
|
|
271
425
|
if (normalized !== sanitized || normalized.includes("..")) {
|
|
272
426
|
throw new Error(`Invalid package name (path traversal detected): ${name}`);
|
|
273
427
|
}
|
|
274
|
-
const testPath =
|
|
275
|
-
const resolved =
|
|
428
|
+
const testPath = path4.join("/test", sanitized);
|
|
429
|
+
const resolved = path4.resolve(testPath);
|
|
276
430
|
if (!resolved.startsWith("/test/")) {
|
|
277
431
|
throw new Error(`Invalid package name (path traversal detected): ${name}`);
|
|
278
432
|
}
|
|
279
433
|
return sanitized;
|
|
280
434
|
}
|
|
435
|
+
|
|
436
|
+
// src/security/path-validator.ts
|
|
437
|
+
import path5 from "path";
|
|
281
438
|
function isPathWithinDirectory(filePath, directory) {
|
|
282
|
-
const resolvedPath =
|
|
283
|
-
const resolvedDir =
|
|
284
|
-
return resolvedPath.startsWith(resolvedDir +
|
|
439
|
+
const resolvedPath = path5.resolve(filePath);
|
|
440
|
+
const resolvedDir = path5.resolve(directory);
|
|
441
|
+
return resolvedPath.startsWith(resolvedDir + path5.sep) || resolvedPath === resolvedDir;
|
|
285
442
|
}
|
|
443
|
+
|
|
444
|
+
// src/adapters/handlers/rules-handler.ts
|
|
286
445
|
async function writePackageMetadata(packageDir, manifest) {
|
|
287
446
|
const metadata = {
|
|
288
447
|
name: manifest.name,
|
|
448
|
+
// e.g., "@cpm/typescript-strict"
|
|
289
449
|
version: manifest.version,
|
|
290
|
-
|
|
450
|
+
// e.g., "1.0.0"
|
|
451
|
+
type: manifest.type,
|
|
452
|
+
// e.g., "rules"
|
|
291
453
|
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
454
|
+
// ISO timestamp for when it was installed
|
|
292
455
|
};
|
|
293
|
-
const metadataPath =
|
|
456
|
+
const metadataPath = path6.join(packageDir, ".cpm.json");
|
|
294
457
|
try {
|
|
295
458
|
await fs2.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
296
459
|
} catch (error) {
|
|
@@ -300,152 +463,45 @@ async function writePackageMetadata(packageDir, manifest) {
|
|
|
300
463
|
}
|
|
301
464
|
return metadataPath;
|
|
302
465
|
}
|
|
303
|
-
var
|
|
304
|
-
platform = "claude-code";
|
|
305
|
-
displayName = "Claude Code";
|
|
306
|
-
async isAvailable(_projectPath) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
async install(manifest, projectPath, packagePath) {
|
|
310
|
-
const filesWritten = [];
|
|
311
|
-
try {
|
|
312
|
-
switch (manifest.type) {
|
|
313
|
-
case "rules": {
|
|
314
|
-
const rulesResult = await this.installRules(
|
|
315
|
-
manifest,
|
|
316
|
-
projectPath,
|
|
317
|
-
packagePath
|
|
318
|
-
);
|
|
319
|
-
filesWritten.push(...rulesResult);
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
case "skill": {
|
|
323
|
-
const skillResult = await this.installSkill(
|
|
324
|
-
manifest,
|
|
325
|
-
projectPath,
|
|
326
|
-
packagePath
|
|
327
|
-
);
|
|
328
|
-
filesWritten.push(...skillResult);
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
case "mcp": {
|
|
332
|
-
const mcpResult = await this.installMcp(manifest, projectPath);
|
|
333
|
-
filesWritten.push(...mcpResult);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
default:
|
|
337
|
-
if (manifest.skill) {
|
|
338
|
-
const skillResult = await this.installSkill(
|
|
339
|
-
manifest,
|
|
340
|
-
projectPath,
|
|
341
|
-
packagePath
|
|
342
|
-
);
|
|
343
|
-
filesWritten.push(...skillResult);
|
|
344
|
-
} else if (manifest.mcp) {
|
|
345
|
-
const mcpResult = await this.installMcp(manifest, projectPath);
|
|
346
|
-
filesWritten.push(...mcpResult);
|
|
347
|
-
} else if (manifest.universal?.rules) {
|
|
348
|
-
const rulesResult = await this.installRules(
|
|
349
|
-
manifest,
|
|
350
|
-
projectPath,
|
|
351
|
-
packagePath
|
|
352
|
-
);
|
|
353
|
-
filesWritten.push(...rulesResult);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
success: true,
|
|
358
|
-
platform: "claude-code",
|
|
359
|
-
filesWritten
|
|
360
|
-
};
|
|
361
|
-
} catch (error) {
|
|
362
|
-
return {
|
|
363
|
-
success: false,
|
|
364
|
-
platform: "claude-code",
|
|
365
|
-
filesWritten,
|
|
366
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
async uninstall(packageName, _projectPath) {
|
|
371
|
-
const filesWritten = [];
|
|
372
|
-
const folderName = sanitizeFolderName(packageName);
|
|
373
|
-
try {
|
|
374
|
-
const rulesBaseDir = getRulesPath("claude-code");
|
|
375
|
-
const rulesPath = path2.join(rulesBaseDir, folderName);
|
|
376
|
-
if (await fs2.pathExists(rulesPath)) {
|
|
377
|
-
await fs2.remove(rulesPath);
|
|
378
|
-
filesWritten.push(rulesPath);
|
|
379
|
-
}
|
|
380
|
-
const skillsDir = getSkillsPath();
|
|
381
|
-
const skillPath = path2.join(skillsDir, folderName);
|
|
382
|
-
if (await fs2.pathExists(skillPath)) {
|
|
383
|
-
await fs2.remove(skillPath);
|
|
384
|
-
filesWritten.push(skillPath);
|
|
385
|
-
}
|
|
386
|
-
await this.removeMcpServer(folderName, filesWritten);
|
|
387
|
-
return {
|
|
388
|
-
success: true,
|
|
389
|
-
platform: "claude-code",
|
|
390
|
-
filesWritten
|
|
391
|
-
};
|
|
392
|
-
} catch (error) {
|
|
393
|
-
return {
|
|
394
|
-
success: false,
|
|
395
|
-
platform: "claude-code",
|
|
396
|
-
filesWritten,
|
|
397
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|
|
466
|
+
var RulesHandler = class {
|
|
401
467
|
/**
|
|
402
|
-
*
|
|
468
|
+
* Identifies this handler as handling "rules" type packages.
|
|
469
|
+
* The registry uses this to route rules packages to this handler.
|
|
403
470
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
mcpServers: remainingServers
|
|
420
|
-
};
|
|
421
|
-
await fs2.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
|
|
422
|
-
filesWritten.push(mcpConfigPath);
|
|
423
|
-
} catch (error) {
|
|
424
|
-
logger.warn(
|
|
425
|
-
`Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
async installRules(manifest, _projectPath, packagePath) {
|
|
471
|
+
packageType = "rules";
|
|
472
|
+
/**
|
|
473
|
+
* Install a rules package.
|
|
474
|
+
*
|
|
475
|
+
* The installation process:
|
|
476
|
+
* 1. Determine the target directory (~/.claude/rules/<package-name>/)
|
|
477
|
+
* 2. If package files exist in packagePath, copy all .md files
|
|
478
|
+
* 3. Otherwise, create a RULES.md from the manifest content
|
|
479
|
+
* 4. Write metadata file for tracking
|
|
480
|
+
*
|
|
481
|
+
* @param manifest - The package manifest with name, content, etc.
|
|
482
|
+
* @param context - Contains projectPath and optional packagePath
|
|
483
|
+
* @returns Array of file paths that were created
|
|
484
|
+
*/
|
|
485
|
+
async install(manifest, context) {
|
|
430
486
|
const filesWritten = [];
|
|
431
487
|
const rulesBaseDir = getRulesPath("claude-code");
|
|
432
488
|
const folderName = sanitizeFolderName(manifest.name);
|
|
433
|
-
const rulesDir =
|
|
489
|
+
const rulesDir = path6.join(rulesBaseDir, folderName);
|
|
434
490
|
await fs2.ensureDir(rulesDir);
|
|
435
|
-
if (packagePath && await fs2.pathExists(packagePath)) {
|
|
436
|
-
const files = await fs2.readdir(packagePath);
|
|
491
|
+
if (context.packagePath && await fs2.pathExists(context.packagePath)) {
|
|
492
|
+
const files = await fs2.readdir(context.packagePath);
|
|
437
493
|
const mdFiles = files.filter(
|
|
438
494
|
(f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
|
|
439
495
|
);
|
|
440
496
|
if (mdFiles.length > 0) {
|
|
441
497
|
for (const file of mdFiles) {
|
|
442
498
|
const validation = sanitizeFileName(file);
|
|
443
|
-
if (!validation.
|
|
499
|
+
if (!validation.valid) {
|
|
444
500
|
logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
|
|
445
501
|
continue;
|
|
446
502
|
}
|
|
447
|
-
const srcPath =
|
|
448
|
-
const destPath =
|
|
503
|
+
const srcPath = path6.join(context.packagePath, file);
|
|
504
|
+
const destPath = path6.join(rulesDir, validation.sanitized);
|
|
449
505
|
if (!isPathWithinDirectory(destPath, rulesDir)) {
|
|
450
506
|
logger.warn(`Blocked path traversal attempt: ${file}`);
|
|
451
507
|
continue;
|
|
@@ -458,9 +514,9 @@ var ClaudeCodeAdapter = class extends PlatformAdapter {
|
|
|
458
514
|
return filesWritten;
|
|
459
515
|
}
|
|
460
516
|
}
|
|
461
|
-
const rulesContent =
|
|
517
|
+
const rulesContent = this.getRulesContent(manifest);
|
|
462
518
|
if (!rulesContent) return filesWritten;
|
|
463
|
-
const rulesPath =
|
|
519
|
+
const rulesPath = path6.join(rulesDir, "RULES.md");
|
|
464
520
|
const content = `# ${manifest.name}
|
|
465
521
|
|
|
466
522
|
${manifest.description}
|
|
@@ -473,74 +529,238 @@ ${rulesContent.trim()}
|
|
|
473
529
|
filesWritten.push(metadataPath);
|
|
474
530
|
return filesWritten;
|
|
475
531
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
const srcPath = path2.join(packagePath, file);
|
|
495
|
-
const destPath = path2.join(skillDir, validation.sanitized);
|
|
496
|
-
if (!isPathWithinDirectory(destPath, skillDir)) {
|
|
497
|
-
logger.warn(`Blocked path traversal attempt: ${file}`);
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
await fs2.copy(srcPath, destPath);
|
|
501
|
-
filesWritten.push(destPath);
|
|
502
|
-
}
|
|
503
|
-
const metadataPath = await writePackageMetadata(skillDir, manifest);
|
|
504
|
-
filesWritten.push(metadataPath);
|
|
505
|
-
return filesWritten;
|
|
506
|
-
}
|
|
532
|
+
/**
|
|
533
|
+
* Uninstall a rules package.
|
|
534
|
+
*
|
|
535
|
+
* This removes the entire package directory from ~/.claude/rules/
|
|
536
|
+
*
|
|
537
|
+
* @param packageName - The name of the package to remove
|
|
538
|
+
* @param _context - Uninstall context (not used for rules, but required by interface)
|
|
539
|
+
* @returns Array of paths that were removed
|
|
540
|
+
*/
|
|
541
|
+
async uninstall(packageName, _context) {
|
|
542
|
+
const filesRemoved = [];
|
|
543
|
+
const folderName = sanitizeFolderName(packageName);
|
|
544
|
+
const rulesBaseDir = getRulesPath("claude-code");
|
|
545
|
+
const rulesPath = path6.join(rulesBaseDir, folderName);
|
|
546
|
+
if (await fs2.pathExists(rulesPath)) {
|
|
547
|
+
await fs2.remove(rulesPath);
|
|
548
|
+
filesRemoved.push(rulesPath);
|
|
507
549
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
550
|
+
return filesRemoved;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Extract rules content from the manifest.
|
|
554
|
+
*
|
|
555
|
+
* Rules content can come from:
|
|
556
|
+
* - manifest.universal.rules (primary)
|
|
557
|
+
* - manifest.universal.prompt (fallback)
|
|
558
|
+
*
|
|
559
|
+
* @param manifest - The package manifest
|
|
560
|
+
* @returns The rules content string, or undefined if none exists
|
|
561
|
+
*/
|
|
562
|
+
getRulesContent(manifest) {
|
|
563
|
+
if (isRulesManifest(manifest)) {
|
|
564
|
+
return manifest.universal.rules || manifest.universal.prompt;
|
|
565
|
+
}
|
|
566
|
+
return void 0;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/adapters/handlers/skill-handler.ts
|
|
571
|
+
import fs3 from "fs-extra";
|
|
572
|
+
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
|
+
function formatSkillMd(manifest) {
|
|
595
|
+
const skill = manifest.skill;
|
|
596
|
+
const content = manifest.universal?.prompt || manifest.universal?.rules || "";
|
|
597
|
+
return `---
|
|
598
|
+
name: ${manifest.name}
|
|
599
|
+
command: ${skill.command || `/${manifest.name}`}
|
|
600
|
+
description: ${skill.description || manifest.description}
|
|
601
|
+
version: ${manifest.version}
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
# ${manifest.name}
|
|
605
|
+
|
|
606
|
+
${manifest.description}
|
|
607
|
+
|
|
608
|
+
## Instructions
|
|
521
609
|
|
|
522
610
|
${content.trim()}
|
|
523
611
|
`;
|
|
524
|
-
|
|
612
|
+
}
|
|
613
|
+
var SkillHandler = class {
|
|
614
|
+
/**
|
|
615
|
+
* Identifies this handler as handling "skill" type packages.
|
|
616
|
+
* The registry uses this to route skill packages to this handler.
|
|
617
|
+
*/
|
|
618
|
+
packageType = "skill";
|
|
619
|
+
/**
|
|
620
|
+
* Install a skill package.
|
|
621
|
+
*
|
|
622
|
+
* The installation process:
|
|
623
|
+
* 1. Determine the target directory (~/.claude/skills/<package-name>/)
|
|
624
|
+
* 2. If package files exist in packagePath, copy all .md files
|
|
625
|
+
* 3. Otherwise, create a SKILL.md from the manifest content
|
|
626
|
+
* 4. Write metadata file for tracking
|
|
627
|
+
*
|
|
628
|
+
* @param manifest - The package manifest with name, content, etc.
|
|
629
|
+
* @param context - Contains projectPath and optional packagePath
|
|
630
|
+
* @returns Array of file paths that were created
|
|
631
|
+
*/
|
|
632
|
+
async install(manifest, context) {
|
|
633
|
+
const filesWritten = [];
|
|
634
|
+
const skillsDir = getSkillsPath();
|
|
635
|
+
const folderName = sanitizeFolderName(manifest.name);
|
|
636
|
+
const skillDir = path7.join(skillsDir, folderName);
|
|
637
|
+
await fs3.ensureDir(skillDir);
|
|
638
|
+
if (context.packagePath && await fs3.pathExists(context.packagePath)) {
|
|
639
|
+
const files = await fs3.readdir(context.packagePath);
|
|
640
|
+
const contentFiles = files.filter(
|
|
641
|
+
(f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
|
|
642
|
+
);
|
|
643
|
+
if (contentFiles.length > 0) {
|
|
644
|
+
for (const file of contentFiles) {
|
|
645
|
+
const validation = sanitizeFileName(file);
|
|
646
|
+
if (!validation.valid) {
|
|
647
|
+
logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const srcPath = path7.join(context.packagePath, file);
|
|
651
|
+
const destPath = path7.join(skillDir, validation.sanitized);
|
|
652
|
+
if (!isPathWithinDirectory(destPath, skillDir)) {
|
|
653
|
+
logger.warn(`Blocked path traversal attempt: ${file}`);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
await fs3.copy(srcPath, destPath);
|
|
657
|
+
filesWritten.push(destPath);
|
|
658
|
+
}
|
|
659
|
+
const metadataPath = await writePackageMetadata2(skillDir, manifest);
|
|
660
|
+
filesWritten.push(metadataPath);
|
|
661
|
+
return filesWritten;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (isSkillManifest(manifest)) {
|
|
665
|
+
const skillContent = formatSkillMd(manifest);
|
|
666
|
+
const skillPath = path7.join(skillDir, "SKILL.md");
|
|
667
|
+
await fs3.writeFile(skillPath, skillContent, "utf-8");
|
|
525
668
|
filesWritten.push(skillPath);
|
|
526
|
-
const metadataPath = await
|
|
669
|
+
const metadataPath = await writePackageMetadata2(skillDir, manifest);
|
|
527
670
|
filesWritten.push(metadataPath);
|
|
671
|
+
} else {
|
|
672
|
+
const content = this.getUniversalContent(manifest);
|
|
673
|
+
if (content) {
|
|
674
|
+
const skillPath = path7.join(skillDir, "SKILL.md");
|
|
675
|
+
const skillContent = `# ${manifest.name}
|
|
676
|
+
|
|
677
|
+
${manifest.description}
|
|
678
|
+
|
|
679
|
+
${content.trim()}
|
|
680
|
+
`;
|
|
681
|
+
await fs3.writeFile(skillPath, skillContent, "utf-8");
|
|
682
|
+
filesWritten.push(skillPath);
|
|
683
|
+
const metadataPath = await writePackageMetadata2(skillDir, manifest);
|
|
684
|
+
filesWritten.push(metadataPath);
|
|
685
|
+
}
|
|
528
686
|
}
|
|
529
687
|
return filesWritten;
|
|
530
688
|
}
|
|
531
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Uninstall a skill package.
|
|
691
|
+
*
|
|
692
|
+
* This removes the entire package directory from ~/.claude/skills/
|
|
693
|
+
*
|
|
694
|
+
* @param packageName - The name of the package to remove
|
|
695
|
+
* @param _context - Uninstall context (not used for skills, but required by interface)
|
|
696
|
+
* @returns Array of paths that were removed
|
|
697
|
+
*/
|
|
698
|
+
async uninstall(packageName, _context) {
|
|
699
|
+
const filesRemoved = [];
|
|
700
|
+
const folderName = sanitizeFolderName(packageName);
|
|
701
|
+
const skillsDir = getSkillsPath();
|
|
702
|
+
const skillPath = path7.join(skillsDir, folderName);
|
|
703
|
+
if (await fs3.pathExists(skillPath)) {
|
|
704
|
+
await fs3.remove(skillPath);
|
|
705
|
+
filesRemoved.push(skillPath);
|
|
706
|
+
}
|
|
707
|
+
return filesRemoved;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Extract universal content from the manifest.
|
|
711
|
+
*
|
|
712
|
+
* This is used as a fallback when the manifest doesn't have
|
|
713
|
+
* proper skill configuration but does have universal content.
|
|
714
|
+
*
|
|
715
|
+
* @param manifest - The package manifest
|
|
716
|
+
* @returns The universal content string, or undefined if none exists
|
|
717
|
+
*/
|
|
718
|
+
getUniversalContent(manifest) {
|
|
719
|
+
if ("universal" in manifest && manifest.universal) {
|
|
720
|
+
return manifest.universal.prompt || manifest.universal.rules;
|
|
721
|
+
}
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// src/adapters/handlers/mcp-handler.ts
|
|
727
|
+
import fs4 from "fs-extra";
|
|
728
|
+
import path8 from "path";
|
|
729
|
+
var McpHandler = class {
|
|
730
|
+
/**
|
|
731
|
+
* Identifies this handler as handling "mcp" type packages.
|
|
732
|
+
* The registry uses this to route MCP packages to this handler.
|
|
733
|
+
*/
|
|
734
|
+
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
|
+
async install(manifest, _context) {
|
|
532
750
|
const filesWritten = [];
|
|
533
|
-
if (!manifest
|
|
751
|
+
if (!isMcpManifest(manifest)) {
|
|
752
|
+
return filesWritten;
|
|
753
|
+
}
|
|
534
754
|
const mcpValidation = validateMcpConfig(manifest.mcp);
|
|
535
755
|
if (!mcpValidation.valid) {
|
|
536
756
|
throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
|
|
537
757
|
}
|
|
538
|
-
const claudeHome =
|
|
539
|
-
const mcpConfigPath =
|
|
758
|
+
const claudeHome = getClaudeHome();
|
|
759
|
+
const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
|
|
540
760
|
let existingConfig = {};
|
|
541
|
-
if (await
|
|
761
|
+
if (await fs4.pathExists(mcpConfigPath)) {
|
|
542
762
|
try {
|
|
543
|
-
existingConfig = await
|
|
763
|
+
existingConfig = await fs4.readJson(mcpConfigPath);
|
|
544
764
|
} catch {
|
|
545
765
|
logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
|
|
546
766
|
existingConfig = {};
|
|
@@ -550,42 +770,178 @@ ${content.trim()}
|
|
|
550
770
|
const existingMcpServers = existingConfig.mcpServers || {};
|
|
551
771
|
const updatedConfig = {
|
|
552
772
|
...existingConfig,
|
|
773
|
+
// Preserve all other config settings
|
|
553
774
|
mcpServers: {
|
|
554
775
|
...existingMcpServers,
|
|
776
|
+
// Preserve other MCP servers
|
|
555
777
|
[sanitizedName]: {
|
|
778
|
+
// Add/update this package's MCP server
|
|
556
779
|
command: manifest.mcp.command,
|
|
780
|
+
// e.g., "npx"
|
|
557
781
|
args: manifest.mcp.args,
|
|
782
|
+
// e.g., ["-y", "@supabase/mcp"]
|
|
558
783
|
env: manifest.mcp.env
|
|
784
|
+
// e.g., { "SUPABASE_URL": "..." }
|
|
559
785
|
}
|
|
560
786
|
}
|
|
561
787
|
};
|
|
562
|
-
await
|
|
788
|
+
await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
|
|
563
789
|
filesWritten.push(mcpConfigPath);
|
|
564
790
|
return filesWritten;
|
|
565
791
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
async uninstall(packageName, _context) {
|
|
802
|
+
const filesWritten = [];
|
|
803
|
+
const folderName = sanitizeFolderName(packageName);
|
|
804
|
+
const claudeHome = getClaudeHome();
|
|
805
|
+
const mcpConfigPath = path8.join(path8.dirname(claudeHome), ".claude.json");
|
|
806
|
+
if (!await fs4.pathExists(mcpConfigPath)) {
|
|
807
|
+
return filesWritten;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const config = await fs4.readJson(mcpConfigPath);
|
|
811
|
+
const mcpServers = config.mcpServers;
|
|
812
|
+
if (!mcpServers || !mcpServers[folderName]) {
|
|
813
|
+
return filesWritten;
|
|
814
|
+
}
|
|
815
|
+
const { [folderName]: _removed, ...remainingServers } = mcpServers;
|
|
816
|
+
const updatedConfig = {
|
|
817
|
+
...config,
|
|
818
|
+
mcpServers: remainingServers
|
|
819
|
+
};
|
|
820
|
+
await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
|
|
821
|
+
filesWritten.push(mcpConfigPath);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
logger.warn(
|
|
824
|
+
`Could not update MCP config: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
570
825
|
);
|
|
571
826
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
name: ${manifest.name}
|
|
576
|
-
command: ${skill.command || `/${manifest.name}`}
|
|
577
|
-
description: ${skill.description || manifest.description}
|
|
578
|
-
version: ${manifest.version}
|
|
579
|
-
---
|
|
580
|
-
|
|
581
|
-
# ${manifest.name}
|
|
582
|
-
|
|
583
|
-
${manifest.description}
|
|
827
|
+
return filesWritten;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
584
830
|
|
|
585
|
-
|
|
831
|
+
// src/adapters/handlers/index.ts
|
|
832
|
+
function initializeHandlers() {
|
|
833
|
+
handlerRegistry.register(new RulesHandler());
|
|
834
|
+
handlerRegistry.register(new SkillHandler());
|
|
835
|
+
handlerRegistry.register(new McpHandler());
|
|
836
|
+
}
|
|
837
|
+
initializeHandlers();
|
|
586
838
|
|
|
587
|
-
|
|
588
|
-
|
|
839
|
+
// src/adapters/claude-code.ts
|
|
840
|
+
import fs5 from "fs-extra";
|
|
841
|
+
import path9 from "path";
|
|
842
|
+
var ClaudeCodeAdapter = class extends PlatformAdapter {
|
|
843
|
+
platform = "claude-code";
|
|
844
|
+
displayName = "Claude Code";
|
|
845
|
+
async isAvailable(_projectPath) {
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
async install(manifest, projectPath, packagePath) {
|
|
849
|
+
const filesWritten = [];
|
|
850
|
+
try {
|
|
851
|
+
const context = { projectPath, packagePath };
|
|
852
|
+
const result = await this.installByType(manifest, context);
|
|
853
|
+
filesWritten.push(...result);
|
|
854
|
+
return {
|
|
855
|
+
success: true,
|
|
856
|
+
platform: "claude-code",
|
|
857
|
+
filesWritten
|
|
858
|
+
};
|
|
859
|
+
} catch (error) {
|
|
860
|
+
return {
|
|
861
|
+
success: false,
|
|
862
|
+
platform: "claude-code",
|
|
863
|
+
filesWritten,
|
|
864
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async uninstall(packageName, _projectPath) {
|
|
869
|
+
const filesWritten = [];
|
|
870
|
+
const folderName = sanitizeFolderName(packageName);
|
|
871
|
+
try {
|
|
872
|
+
const rulesBaseDir = getRulesPath("claude-code");
|
|
873
|
+
const rulesPath = path9.join(rulesBaseDir, folderName);
|
|
874
|
+
if (await fs5.pathExists(rulesPath)) {
|
|
875
|
+
await fs5.remove(rulesPath);
|
|
876
|
+
filesWritten.push(rulesPath);
|
|
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);
|
|
883
|
+
}
|
|
884
|
+
await this.removeMcpServer(folderName, filesWritten);
|
|
885
|
+
return {
|
|
886
|
+
success: true,
|
|
887
|
+
platform: "claude-code",
|
|
888
|
+
filesWritten
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return {
|
|
892
|
+
success: false,
|
|
893
|
+
platform: "claude-code",
|
|
894
|
+
filesWritten,
|
|
895
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async installByType(manifest, context) {
|
|
900
|
+
if (handlerRegistry.hasHandler(manifest.type)) {
|
|
901
|
+
const handler = handlerRegistry.getHandler(manifest.type);
|
|
902
|
+
return handler.install(manifest, context);
|
|
903
|
+
}
|
|
904
|
+
return this.installFallback(manifest, context);
|
|
905
|
+
}
|
|
906
|
+
async installFallback(manifest, context) {
|
|
907
|
+
if ("skill" in manifest && manifest.skill) {
|
|
908
|
+
const handler = handlerRegistry.getHandler("skill");
|
|
909
|
+
return handler.install(manifest, context);
|
|
910
|
+
}
|
|
911
|
+
if ("mcp" in manifest && manifest.mcp) {
|
|
912
|
+
const handler = handlerRegistry.getHandler("mcp");
|
|
913
|
+
return handler.install(manifest, context);
|
|
914
|
+
}
|
|
915
|
+
if ("universal" in manifest && manifest.universal?.rules) {
|
|
916
|
+
const handler = handlerRegistry.getHandler("rules");
|
|
917
|
+
return handler.install(manifest, context);
|
|
918
|
+
}
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
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
|
+
}
|
|
589
945
|
}
|
|
590
946
|
};
|
|
591
947
|
|
|
@@ -597,51 +953,20 @@ function getAdapter(platform) {
|
|
|
597
953
|
return adapters[platform];
|
|
598
954
|
}
|
|
599
955
|
|
|
600
|
-
// src/utils/config.ts
|
|
601
|
-
import path3 from "path";
|
|
602
|
-
import os2 from "os";
|
|
603
|
-
import fs3 from "fs-extra";
|
|
604
|
-
function getClaudeHome() {
|
|
605
|
-
return path3.join(os2.homedir(), ".claude");
|
|
606
|
-
}
|
|
607
|
-
async function ensureClaudeDirs() {
|
|
608
|
-
const claudeHome = getClaudeHome();
|
|
609
|
-
await fs3.ensureDir(path3.join(claudeHome, "rules"));
|
|
610
|
-
await fs3.ensureDir(path3.join(claudeHome, "skills"));
|
|
611
|
-
}
|
|
612
|
-
|
|
613
956
|
// src/utils/registry.ts
|
|
614
957
|
import got from "got";
|
|
615
|
-
import
|
|
616
|
-
import
|
|
617
|
-
import
|
|
618
|
-
|
|
619
|
-
// src/types.ts
|
|
620
|
-
function getTypeFromPath(path9) {
|
|
621
|
-
if (path9.startsWith("skills/")) return "skill";
|
|
622
|
-
if (path9.startsWith("rules/")) return "rules";
|
|
623
|
-
if (path9.startsWith("mcp/")) return "mcp";
|
|
624
|
-
if (path9.startsWith("agents/")) return "agent";
|
|
625
|
-
if (path9.startsWith("hooks/")) return "hook";
|
|
626
|
-
if (path9.startsWith("workflows/")) return "workflow";
|
|
627
|
-
if (path9.startsWith("templates/")) return "template";
|
|
628
|
-
if (path9.startsWith("bundles/")) return "bundle";
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
function resolvePackageType(pkg) {
|
|
632
|
-
if (pkg.type) return pkg.type;
|
|
633
|
-
if (pkg.path) {
|
|
634
|
-
const derived = getTypeFromPath(pkg.path);
|
|
635
|
-
if (derived) return derived;
|
|
636
|
-
}
|
|
637
|
-
throw new Error(`Cannot determine type for package: ${pkg.name}`);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// src/utils/registry.ts
|
|
958
|
+
import fs6 from "fs-extra";
|
|
959
|
+
import path10 from "path";
|
|
960
|
+
import os2 from "os";
|
|
641
961
|
var DEFAULT_REGISTRY_URL = process.env.CPM_REGISTRY_URL || "https://raw.githubusercontent.com/cpmai-dev/packages/main/registry.json";
|
|
642
|
-
var CACHE_DIR =
|
|
643
|
-
var CACHE_FILE =
|
|
644
|
-
var
|
|
962
|
+
var CACHE_DIR = path10.join(os2.homedir(), ".cpm", "cache");
|
|
963
|
+
var CACHE_FILE = path10.join(CACHE_DIR, "registry.json");
|
|
964
|
+
var comparators = {
|
|
965
|
+
downloads: (a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
|
|
966
|
+
stars: (a, b) => (b.stars ?? 0) - (a.stars ?? 0),
|
|
967
|
+
recent: (a, b) => new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime(),
|
|
968
|
+
name: (a, b) => (a.name ?? "").localeCompare(b.name ?? "")
|
|
969
|
+
};
|
|
645
970
|
var Registry = class {
|
|
646
971
|
registryUrl;
|
|
647
972
|
cache = null;
|
|
@@ -653,275 +978,405 @@ var Registry = class {
|
|
|
653
978
|
* Fetch the registry data (with caching)
|
|
654
979
|
*/
|
|
655
980
|
async fetch(forceRefresh = false) {
|
|
656
|
-
if (
|
|
981
|
+
if (this.hasValidMemoryCache() && !forceRefresh) {
|
|
657
982
|
return this.cache;
|
|
658
983
|
}
|
|
659
984
|
if (!forceRefresh) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
985
|
+
const fileCache = await this.loadFileCache();
|
|
986
|
+
if (fileCache) {
|
|
987
|
+
this.updateMemoryCache(fileCache);
|
|
988
|
+
return fileCache;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return this.fetchFromNetwork();
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Search for packages
|
|
995
|
+
*/
|
|
996
|
+
async search(options = {}) {
|
|
997
|
+
const data = await this.fetch();
|
|
998
|
+
let packages = [...data.packages];
|
|
999
|
+
packages = this.filterByQuery(packages, options.query);
|
|
1000
|
+
packages = this.filterByType(packages, options.type);
|
|
1001
|
+
packages = this.sortPackages(packages, options.sort || "downloads");
|
|
1002
|
+
const total = packages.length;
|
|
1003
|
+
packages = this.paginate(packages, options.offset, options.limit);
|
|
1004
|
+
return { packages, total };
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Get a specific package by name
|
|
1008
|
+
*/
|
|
1009
|
+
async getPackage(name) {
|
|
1010
|
+
const data = await this.fetch();
|
|
1011
|
+
return data.packages.find((pkg) => pkg.name === name) || null;
|
|
1012
|
+
}
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
// Private Helpers
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
hasValidMemoryCache() {
|
|
1017
|
+
return this.cache !== null && Date.now() - this.cacheTimestamp < LIMITS.CACHE_TTL_MS;
|
|
1018
|
+
}
|
|
1019
|
+
updateMemoryCache(data) {
|
|
1020
|
+
this.cache = data;
|
|
1021
|
+
this.cacheTimestamp = Date.now();
|
|
1022
|
+
}
|
|
1023
|
+
async loadFileCache() {
|
|
1024
|
+
try {
|
|
1025
|
+
await fs6.ensureDir(CACHE_DIR);
|
|
1026
|
+
if (await fs6.pathExists(CACHE_FILE)) {
|
|
1027
|
+
const stat = await fs6.stat(CACHE_FILE);
|
|
1028
|
+
if (Date.now() - stat.mtimeMs < LIMITS.CACHE_TTL_MS) {
|
|
1029
|
+
return await fs6.readJson(CACHE_FILE);
|
|
670
1030
|
}
|
|
671
|
-
} catch {
|
|
672
1031
|
}
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
async saveFileCache(data) {
|
|
1037
|
+
try {
|
|
1038
|
+
await fs6.ensureDir(CACHE_DIR);
|
|
1039
|
+
await fs6.writeJson(CACHE_FILE, data, { spaces: 2 });
|
|
1040
|
+
} catch {
|
|
673
1041
|
}
|
|
1042
|
+
}
|
|
1043
|
+
async fetchFromNetwork() {
|
|
674
1044
|
try {
|
|
675
1045
|
const response = await got(this.registryUrl, {
|
|
676
|
-
timeout: { request:
|
|
1046
|
+
timeout: { request: TIMEOUTS.REGISTRY_FETCH },
|
|
677
1047
|
responseType: "json"
|
|
678
1048
|
});
|
|
679
1049
|
const data = response.body;
|
|
680
|
-
this.
|
|
681
|
-
this.
|
|
682
|
-
try {
|
|
683
|
-
await fs4.ensureDir(CACHE_DIR);
|
|
684
|
-
await fs4.writeJson(CACHE_FILE, data, { spaces: 2 });
|
|
685
|
-
} catch {
|
|
686
|
-
}
|
|
1050
|
+
this.updateMemoryCache(data);
|
|
1051
|
+
await this.saveFileCache(data);
|
|
687
1052
|
return data;
|
|
688
1053
|
} catch {
|
|
689
|
-
|
|
690
|
-
|
|
1054
|
+
return this.handleNetworkError();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async handleNetworkError() {
|
|
1058
|
+
if (this.cache) {
|
|
1059
|
+
return this.cache;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
if (await fs6.pathExists(CACHE_FILE)) {
|
|
1063
|
+
const cached = await fs6.readJson(CACHE_FILE);
|
|
1064
|
+
this.cache = cached;
|
|
1065
|
+
return cached;
|
|
691
1066
|
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
"Unable to fetch package registry. Please check your internet connection and try again."
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
filterByQuery(packages, query) {
|
|
1074
|
+
if (!query) return packages;
|
|
1075
|
+
const lowerQuery = query.toLowerCase();
|
|
1076
|
+
return packages.filter(
|
|
1077
|
+
(pkg) => pkg.name?.toLowerCase().includes(lowerQuery) || pkg.description?.toLowerCase().includes(lowerQuery) || pkg.keywords?.some((k) => k?.toLowerCase().includes(lowerQuery))
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
filterByType(packages, type) {
|
|
1081
|
+
if (!type) return packages;
|
|
1082
|
+
return packages.filter((pkg) => {
|
|
692
1083
|
try {
|
|
693
|
-
|
|
694
|
-
const cached = await fs4.readJson(CACHE_FILE);
|
|
695
|
-
this.cache = cached;
|
|
696
|
-
return cached;
|
|
697
|
-
}
|
|
1084
|
+
return resolvePackageType(pkg) === type;
|
|
698
1085
|
} catch {
|
|
1086
|
+
return false;
|
|
699
1087
|
}
|
|
700
|
-
|
|
701
|
-
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
sortPackages(packages, sort) {
|
|
1091
|
+
const comparator = comparators[sort];
|
|
1092
|
+
return [...packages].sort(comparator);
|
|
702
1093
|
}
|
|
1094
|
+
paginate(packages, offset, limit) {
|
|
1095
|
+
const start = offset || 0;
|
|
1096
|
+
const end = start + (limit || 10);
|
|
1097
|
+
return packages.slice(start, end);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var registry = new Registry();
|
|
1101
|
+
|
|
1102
|
+
// src/utils/downloader.ts
|
|
1103
|
+
import fs8 from "fs-extra";
|
|
1104
|
+
import path12 from "path";
|
|
1105
|
+
import os3 from "os";
|
|
1106
|
+
|
|
1107
|
+
// src/sources/manifest-resolver.ts
|
|
1108
|
+
var ManifestResolver = class {
|
|
703
1109
|
/**
|
|
704
|
-
*
|
|
1110
|
+
* List of sources, sorted by priority (lowest first).
|
|
1111
|
+
* This array is created once during construction and never modified.
|
|
705
1112
|
*/
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1113
|
+
sources;
|
|
1114
|
+
/**
|
|
1115
|
+
* Create a new ManifestResolver with the given sources.
|
|
1116
|
+
*
|
|
1117
|
+
* The sources are automatically sorted by priority (lowest first)
|
|
1118
|
+
* so they're tried in the correct order during resolution.
|
|
1119
|
+
*
|
|
1120
|
+
* @param sources - Array of manifest sources to use
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* const resolver = new ManifestResolver([
|
|
1125
|
+
* new RepositorySource(), // priority: 1
|
|
1126
|
+
* new TarballSource(), // priority: 2
|
|
1127
|
+
* new RegistrySource(), // priority: 4
|
|
1128
|
+
* ]);
|
|
1129
|
+
* // Sources will be tried in order: Repository, Tarball, Registry
|
|
1130
|
+
* ```
|
|
1131
|
+
*/
|
|
1132
|
+
constructor(sources) {
|
|
1133
|
+
this.sources = [...sources].sort((a, b) => a.priority - b.priority);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Resolve the manifest for a package.
|
|
1137
|
+
*
|
|
1138
|
+
* This method tries each source in priority order until one
|
|
1139
|
+
* successfully returns a manifest. If all sources fail, an
|
|
1140
|
+
* error is thrown.
|
|
1141
|
+
*
|
|
1142
|
+
* The resolution process:
|
|
1143
|
+
* 1. For each source (in priority order):
|
|
1144
|
+
* a. Check if the source can fetch this package (canFetch)
|
|
1145
|
+
* b. If yes, try to fetch the manifest
|
|
1146
|
+
* c. If fetch returns a manifest, return it immediately
|
|
1147
|
+
* d. If fetch returns null, continue to the next source
|
|
1148
|
+
* 2. If no source returned a manifest, throw an error
|
|
1149
|
+
*
|
|
1150
|
+
* @param pkg - The registry package to resolve
|
|
1151
|
+
* @param context - Context with temp directory for downloads
|
|
1152
|
+
* @returns The resolved package manifest
|
|
1153
|
+
* @throws Error if no source can provide a manifest
|
|
1154
|
+
*
|
|
1155
|
+
* @example
|
|
1156
|
+
* ```typescript
|
|
1157
|
+
* const pkg = await registry.getPackage("@cpm/typescript-rules");
|
|
1158
|
+
* const context = { tempDir: "/tmp/cpm-download-123" };
|
|
1159
|
+
*
|
|
1160
|
+
* const manifest = await resolver.resolve(pkg, context);
|
|
1161
|
+
* console.log(`Package type: ${manifest.type}`);
|
|
1162
|
+
* ```
|
|
1163
|
+
*/
|
|
1164
|
+
async resolve(pkg, context) {
|
|
1165
|
+
for (const source of this.sources) {
|
|
1166
|
+
if (source.canFetch(pkg)) {
|
|
1167
|
+
const manifest = await source.fetch(pkg, context);
|
|
1168
|
+
if (manifest) {
|
|
1169
|
+
return manifest;
|
|
721
1170
|
}
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
const sort = options.sort || "downloads";
|
|
725
|
-
packages.sort((a, b) => {
|
|
726
|
-
switch (sort) {
|
|
727
|
-
case "downloads":
|
|
728
|
-
return (b.downloads ?? 0) - (a.downloads ?? 0);
|
|
729
|
-
case "stars":
|
|
730
|
-
return (b.stars ?? 0) - (a.stars ?? 0);
|
|
731
|
-
case "recent":
|
|
732
|
-
return new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime();
|
|
733
|
-
case "name":
|
|
734
|
-
return (a.name ?? "").localeCompare(b.name ?? "");
|
|
735
|
-
default:
|
|
736
|
-
return 0;
|
|
737
1171
|
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const offset = options.offset || 0;
|
|
741
|
-
const limit = options.limit || 10;
|
|
742
|
-
packages = packages.slice(offset, offset + limit);
|
|
743
|
-
return { packages, total };
|
|
1172
|
+
}
|
|
1173
|
+
throw new Error(`No manifest found for package: ${pkg.name}`);
|
|
744
1174
|
}
|
|
745
1175
|
/**
|
|
746
|
-
* Get
|
|
1176
|
+
* Get the names of all registered sources.
|
|
1177
|
+
*
|
|
1178
|
+
* Useful for debugging and displaying information about
|
|
1179
|
+
* which sources are configured.
|
|
1180
|
+
*
|
|
1181
|
+
* @returns Array of source names in priority order
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* ```typescript
|
|
1185
|
+
* const names = resolver.getSourceNames();
|
|
1186
|
+
* console.log("Configured sources:", names.join(", "));
|
|
1187
|
+
* // Output: "Configured sources: repository, tarball, embedded, registry"
|
|
1188
|
+
* ```
|
|
747
1189
|
*/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1190
|
+
getSourceNames() {
|
|
1191
|
+
return this.sources.map((s) => s.name);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// src/sources/repository-source.ts
|
|
1196
|
+
import got2 from "got";
|
|
1197
|
+
import yaml from "yaml";
|
|
1198
|
+
var RepositorySource = class {
|
|
1199
|
+
/**
|
|
1200
|
+
* Name of this source for logging and debugging.
|
|
1201
|
+
*/
|
|
1202
|
+
name = "repository";
|
|
1203
|
+
/**
|
|
1204
|
+
* Priority 1 - this is the first source tried.
|
|
1205
|
+
* Repository fetching is fast and provides complete manifests.
|
|
1206
|
+
*/
|
|
1207
|
+
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
|
+
canFetch(pkg) {
|
|
1230
|
+
return !!pkg.repository?.includes("github.com");
|
|
751
1231
|
}
|
|
752
1232
|
/**
|
|
753
|
-
*
|
|
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
|
+
* ```
|
|
754
1257
|
*/
|
|
755
|
-
async
|
|
756
|
-
if (!pkg.repository)
|
|
1258
|
+
async fetch(pkg, _context) {
|
|
1259
|
+
if (!pkg.repository) return null;
|
|
1260
|
+
try {
|
|
1261
|
+
const match = pkg.repository.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1262
|
+
if (!match) return null;
|
|
1263
|
+
const [, owner, repo] = match;
|
|
1264
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/cpm.yaml`;
|
|
1265
|
+
const response = await got2(rawUrl, {
|
|
1266
|
+
timeout: { request: TIMEOUTS.MANIFEST_FETCH }
|
|
1267
|
+
// 5 second timeout
|
|
1268
|
+
});
|
|
1269
|
+
return yaml.parse(response.body);
|
|
1270
|
+
} catch {
|
|
757
1271
|
return null;
|
|
758
1272
|
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/sources/tarball-source.ts
|
|
1277
|
+
import got3 from "got";
|
|
1278
|
+
import fs7 from "fs-extra";
|
|
1279
|
+
import path11 from "path";
|
|
1280
|
+
import * as tar from "tar";
|
|
1281
|
+
import yaml2 from "yaml";
|
|
1282
|
+
var TarballSource = class {
|
|
1283
|
+
/**
|
|
1284
|
+
* Name of this source for logging and debugging.
|
|
1285
|
+
*/
|
|
1286
|
+
name = "tarball";
|
|
1287
|
+
/**
|
|
1288
|
+
* Priority 2 - tried after repository source.
|
|
1289
|
+
* Tarball downloading is slower but provides the full package.
|
|
1290
|
+
*/
|
|
1291
|
+
priority = 2;
|
|
1292
|
+
/**
|
|
1293
|
+
* Check if this source can fetch the given package.
|
|
1294
|
+
*
|
|
1295
|
+
* We can only fetch if the package has a tarball URL.
|
|
1296
|
+
*
|
|
1297
|
+
* @param pkg - The registry package to check
|
|
1298
|
+
* @returns true if the package has a tarball URL
|
|
1299
|
+
*/
|
|
1300
|
+
canFetch(pkg) {
|
|
1301
|
+
return !!pkg.tarball;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Fetch the manifest by downloading and extracting the tarball.
|
|
1305
|
+
*
|
|
1306
|
+
* This method:
|
|
1307
|
+
* 1. Validates the tarball URL (must be HTTPS)
|
|
1308
|
+
* 2. Downloads the tarball to the temp directory
|
|
1309
|
+
* 3. Extracts it with zip slip protection
|
|
1310
|
+
* 4. Reads and parses the cpm.yaml file
|
|
1311
|
+
*
|
|
1312
|
+
* @param pkg - The registry package to fetch
|
|
1313
|
+
* @param context - Context containing the temp directory path
|
|
1314
|
+
* @returns The parsed manifest, or null if fetch fails
|
|
1315
|
+
*/
|
|
1316
|
+
async fetch(pkg, context) {
|
|
1317
|
+
if (!pkg.tarball) return null;
|
|
759
1318
|
try {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
"
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
1319
|
+
const parsedUrl = new URL(pkg.tarball);
|
|
1320
|
+
if (parsedUrl.protocol !== "https:") {
|
|
1321
|
+
throw new Error("Only HTTPS URLs are allowed for downloads");
|
|
1322
|
+
}
|
|
1323
|
+
const response = await got3(pkg.tarball, {
|
|
1324
|
+
timeout: { request: TIMEOUTS.TARBALL_DOWNLOAD },
|
|
1325
|
+
// 30 second timeout
|
|
1326
|
+
followRedirect: true,
|
|
1327
|
+
// Follow redirects (common for CDN URLs)
|
|
1328
|
+
responseType: "buffer"
|
|
1329
|
+
// Get raw binary data
|
|
767
1330
|
});
|
|
768
|
-
const
|
|
769
|
-
|
|
1331
|
+
const tarballPath = path11.join(context.tempDir, "package.tar.gz");
|
|
1332
|
+
await fs7.writeFile(tarballPath, response.body);
|
|
1333
|
+
await this.extractTarball(tarballPath, context.tempDir);
|
|
1334
|
+
const manifestPath = path11.join(context.tempDir, "cpm.yaml");
|
|
1335
|
+
if (await fs7.pathExists(manifestPath)) {
|
|
1336
|
+
const content = await fs7.readFile(manifestPath, "utf-8");
|
|
1337
|
+
return yaml2.parse(content);
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
770
1340
|
} catch {
|
|
771
1341
|
return null;
|
|
772
1342
|
}
|
|
773
1343
|
}
|
|
774
1344
|
/**
|
|
775
|
-
*
|
|
1345
|
+
* Extract a tarball to a destination directory with security checks.
|
|
1346
|
+
*
|
|
1347
|
+
* This method extracts the tarball while protecting against:
|
|
1348
|
+
* - Zip slip attacks (files extracting outside the target directory)
|
|
1349
|
+
* - Path traversal via ".." in file paths
|
|
1350
|
+
*
|
|
1351
|
+
* The strip: 1 option removes the top-level directory from the tarball,
|
|
1352
|
+
* which is common in GitHub release tarballs (e.g., "package-1.0.0/").
|
|
1353
|
+
*
|
|
1354
|
+
* @param tarballPath - Path to the .tar.gz file
|
|
1355
|
+
* @param destDir - Directory to extract to
|
|
776
1356
|
*/
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
tarball:
|
|
793
|
-
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
name: "@cpm/typescript-strict",
|
|
797
|
-
version: "1.0.0",
|
|
798
|
-
description: "TypeScript strict mode best practices and conventions",
|
|
799
|
-
type: "rules",
|
|
800
|
-
author: "cpm",
|
|
801
|
-
downloads: 980,
|
|
802
|
-
stars: 67,
|
|
803
|
-
verified: true,
|
|
804
|
-
repository: "https://github.com/cpm-ai/typescript-strict",
|
|
805
|
-
tarball: "https://github.com/cpm-ai/typescript-strict/releases/download/v1.0.0/package.tar.gz",
|
|
806
|
-
keywords: ["typescript", "strict", "types"]
|
|
807
|
-
},
|
|
808
|
-
{
|
|
809
|
-
name: "@cpm/react-patterns",
|
|
810
|
-
version: "1.0.0",
|
|
811
|
-
description: "React component patterns and best practices",
|
|
812
|
-
type: "rules",
|
|
813
|
-
author: "cpm",
|
|
814
|
-
downloads: 875,
|
|
815
|
-
stars: 54,
|
|
816
|
-
verified: true,
|
|
817
|
-
repository: "https://github.com/cpm-ai/react-patterns",
|
|
818
|
-
tarball: "https://github.com/cpm-ai/react-patterns/releases/download/v1.0.0/package.tar.gz",
|
|
819
|
-
keywords: ["react", "components", "hooks", "patterns"]
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
name: "@cpm/code-review",
|
|
823
|
-
version: "1.0.0",
|
|
824
|
-
description: "Automated code review skill for Claude Code",
|
|
825
|
-
type: "skill",
|
|
826
|
-
author: "cpm",
|
|
827
|
-
downloads: 2100,
|
|
828
|
-
stars: 156,
|
|
829
|
-
verified: true,
|
|
830
|
-
repository: "https://github.com/cpm-ai/code-review",
|
|
831
|
-
tarball: "https://github.com/cpm-ai/code-review/releases/download/v1.0.0/package.tar.gz",
|
|
832
|
-
keywords: ["code-review", "quality", "skill"]
|
|
833
|
-
},
|
|
834
|
-
{
|
|
835
|
-
name: "@cpm/git-commit",
|
|
836
|
-
version: "1.0.0",
|
|
837
|
-
description: "Smart commit message generation skill",
|
|
838
|
-
type: "skill",
|
|
839
|
-
author: "cpm",
|
|
840
|
-
downloads: 1800,
|
|
841
|
-
stars: 112,
|
|
842
|
-
verified: true,
|
|
843
|
-
repository: "https://github.com/cpm-ai/git-commit",
|
|
844
|
-
tarball: "https://github.com/cpm-ai/git-commit/releases/download/v1.0.0/package.tar.gz",
|
|
845
|
-
keywords: ["git", "commit", "messages", "skill"]
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
name: "@cpm/api-design",
|
|
849
|
-
version: "1.0.0",
|
|
850
|
-
description: "REST and GraphQL API design conventions",
|
|
851
|
-
type: "rules",
|
|
852
|
-
author: "cpm",
|
|
853
|
-
downloads: 650,
|
|
854
|
-
stars: 43,
|
|
855
|
-
verified: true,
|
|
856
|
-
repository: "https://github.com/cpm-ai/api-design",
|
|
857
|
-
tarball: "https://github.com/cpm-ai/api-design/releases/download/v1.0.0/package.tar.gz",
|
|
858
|
-
keywords: ["api", "rest", "graphql", "design"]
|
|
859
|
-
},
|
|
860
|
-
{
|
|
861
|
-
name: "@cpm/testing-patterns",
|
|
862
|
-
version: "1.0.0",
|
|
863
|
-
description: "Testing best practices for JavaScript/TypeScript projects",
|
|
864
|
-
type: "rules",
|
|
865
|
-
author: "cpm",
|
|
866
|
-
downloads: 720,
|
|
867
|
-
stars: 51,
|
|
868
|
-
verified: true,
|
|
869
|
-
repository: "https://github.com/cpm-ai/testing-patterns",
|
|
870
|
-
tarball: "https://github.com/cpm-ai/testing-patterns/releases/download/v1.0.0/package.tar.gz",
|
|
871
|
-
keywords: ["testing", "jest", "vitest", "patterns"]
|
|
872
|
-
},
|
|
873
|
-
{
|
|
874
|
-
name: "@cpm/refactor",
|
|
875
|
-
version: "1.0.0",
|
|
876
|
-
description: "Code refactoring assistant skill",
|
|
877
|
-
type: "skill",
|
|
878
|
-
author: "cpm",
|
|
879
|
-
downloads: 1450,
|
|
880
|
-
stars: 98,
|
|
881
|
-
verified: true,
|
|
882
|
-
repository: "https://github.com/cpm-ai/refactor",
|
|
883
|
-
tarball: "https://github.com/cpm-ai/refactor/releases/download/v1.0.0/package.tar.gz",
|
|
884
|
-
keywords: ["refactor", "clean-code", "skill"]
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
name: "@cpm/explain",
|
|
888
|
-
version: "1.0.0",
|
|
889
|
-
description: "Code explanation and documentation skill",
|
|
890
|
-
type: "skill",
|
|
891
|
-
author: "cpm",
|
|
892
|
-
downloads: 1320,
|
|
893
|
-
stars: 87,
|
|
894
|
-
verified: true,
|
|
895
|
-
repository: "https://github.com/cpm-ai/explain",
|
|
896
|
-
tarball: "https://github.com/cpm-ai/explain/releases/download/v1.0.0/package.tar.gz",
|
|
897
|
-
keywords: ["explain", "documentation", "skill"]
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
name: "@cpm/github-mcp",
|
|
901
|
-
version: "1.0.0",
|
|
902
|
-
description: "GitHub API integration MCP server for Claude Code",
|
|
903
|
-
type: "mcp",
|
|
904
|
-
author: "cpm",
|
|
905
|
-
downloads: 890,
|
|
906
|
-
stars: 72,
|
|
907
|
-
verified: true,
|
|
908
|
-
repository: "https://github.com/cpm-ai/github-mcp",
|
|
909
|
-
tarball: "https://github.com/cpm-ai/github-mcp/releases/download/v1.0.0/package.tar.gz",
|
|
910
|
-
keywords: ["github", "mcp", "api", "integration"]
|
|
1357
|
+
async extractTarball(tarballPath, destDir) {
|
|
1358
|
+
await fs7.ensureDir(destDir);
|
|
1359
|
+
const resolvedDestDir = path11.resolve(destDir);
|
|
1360
|
+
await tar.extract({
|
|
1361
|
+
file: tarballPath,
|
|
1362
|
+
// The tarball file to extract
|
|
1363
|
+
cwd: destDir,
|
|
1364
|
+
// Extract to this directory
|
|
1365
|
+
strip: 1,
|
|
1366
|
+
// Remove the top-level directory (e.g., "package-1.0.0/")
|
|
1367
|
+
// Security filter: check each entry before extracting
|
|
1368
|
+
filter: (entryPath) => {
|
|
1369
|
+
const resolvedPath = path11.resolve(destDir, entryPath);
|
|
1370
|
+
const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path11.sep) || resolvedPath === resolvedDestDir;
|
|
1371
|
+
if (!isWithinDest) {
|
|
1372
|
+
logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
|
|
1373
|
+
return false;
|
|
911
1374
|
}
|
|
912
|
-
|
|
913
|
-
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
914
1378
|
}
|
|
915
1379
|
};
|
|
916
|
-
var registry = new Registry();
|
|
917
|
-
|
|
918
|
-
// src/utils/downloader.ts
|
|
919
|
-
import got2 from "got";
|
|
920
|
-
import fs5 from "fs-extra";
|
|
921
|
-
import path5 from "path";
|
|
922
|
-
import os4 from "os";
|
|
923
|
-
import * as tar from "tar";
|
|
924
|
-
import yaml from "yaml";
|
|
925
1380
|
|
|
926
1381
|
// src/utils/embedded-packages.ts
|
|
927
1382
|
var EMBEDDED_PACKAGES = {
|
|
@@ -1408,245 +1863,243 @@ function getEmbeddedManifest(packageName) {
|
|
|
1408
1863
|
return EMBEDDED_PACKAGES[packageName] ?? null;
|
|
1409
1864
|
}
|
|
1410
1865
|
|
|
1411
|
-
// src/
|
|
1412
|
-
var
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1866
|
+
// src/sources/embedded-source.ts
|
|
1867
|
+
var EmbeddedSource = class {
|
|
1868
|
+
/**
|
|
1869
|
+
* Name of this source for logging and debugging.
|
|
1870
|
+
*/
|
|
1871
|
+
name = "embedded";
|
|
1872
|
+
/**
|
|
1873
|
+
* Priority 3 - used as a fallback after network sources.
|
|
1874
|
+
* Embedded packages are tried after repository and tarball sources
|
|
1875
|
+
* because they may be older versions.
|
|
1876
|
+
*/
|
|
1877
|
+
priority = 3;
|
|
1878
|
+
/**
|
|
1879
|
+
* Check if this source can fetch the given package.
|
|
1880
|
+
*
|
|
1881
|
+
* We can only fetch packages that are bundled into CPM.
|
|
1882
|
+
* This check looks up the package name in the embedded packages map.
|
|
1883
|
+
*
|
|
1884
|
+
* @param pkg - The registry package to check
|
|
1885
|
+
* @returns true if the package is bundled with CPM
|
|
1886
|
+
*
|
|
1887
|
+
* @example
|
|
1888
|
+
* ```typescript
|
|
1889
|
+
* // Bundled package
|
|
1890
|
+
* canFetch({ name: "@cpm/typescript-strict" }) // true (if bundled)
|
|
1891
|
+
*
|
|
1892
|
+
* // Not bundled
|
|
1893
|
+
* canFetch({ name: "@custom/my-package" }) // false
|
|
1894
|
+
* ```
|
|
1895
|
+
*/
|
|
1896
|
+
canFetch(pkg) {
|
|
1897
|
+
return getEmbeddedManifest(pkg.name) !== null;
|
|
1437
1898
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
manifest = getEmbeddedManifest(pkg.name);
|
|
1460
|
-
}
|
|
1461
|
-
if (!manifest) {
|
|
1462
|
-
manifest = createManifestFromRegistry(pkg);
|
|
1463
|
-
}
|
|
1464
|
-
return { success: true, manifest, tempDir: packageTempDir };
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
return {
|
|
1467
|
-
success: false,
|
|
1468
|
-
manifest: {},
|
|
1469
|
-
error: error instanceof Error ? error.message : "Download failed"
|
|
1470
|
-
};
|
|
1899
|
+
/**
|
|
1900
|
+
* Fetch the manifest from embedded packages.
|
|
1901
|
+
*
|
|
1902
|
+
* This method simply looks up the package in the embedded packages
|
|
1903
|
+
* map and returns the manifest. It's synchronous internally but
|
|
1904
|
+
* returns a Promise to match the interface.
|
|
1905
|
+
*
|
|
1906
|
+
* @param pkg - The registry package to fetch
|
|
1907
|
+
* @param _context - Fetch context (not used by this source)
|
|
1908
|
+
* @returns The embedded manifest, or null if not found
|
|
1909
|
+
*
|
|
1910
|
+
* @example
|
|
1911
|
+
* ```typescript
|
|
1912
|
+
* const manifest = await source.fetch(pkg, context);
|
|
1913
|
+
* if (manifest) {
|
|
1914
|
+
* console.log("Using bundled manifest for:", manifest.name);
|
|
1915
|
+
* }
|
|
1916
|
+
* ```
|
|
1917
|
+
*/
|
|
1918
|
+
async fetch(pkg, _context) {
|
|
1919
|
+
return getEmbeddedManifest(pkg.name);
|
|
1471
1920
|
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// src/sources/registry-source.ts
|
|
1924
|
+
var RegistrySource = class {
|
|
1925
|
+
/**
|
|
1926
|
+
* Name of this source for logging and debugging.
|
|
1927
|
+
*/
|
|
1928
|
+
name = "registry";
|
|
1929
|
+
/**
|
|
1930
|
+
* Priority 4 - the lowest priority (last resort).
|
|
1931
|
+
* This source generates manifests rather than fetching them,
|
|
1932
|
+
* so it's only used when all other sources fail.
|
|
1933
|
+
*/
|
|
1934
|
+
priority = 4;
|
|
1935
|
+
/**
|
|
1936
|
+
* Check if this source can fetch the given package.
|
|
1937
|
+
*
|
|
1938
|
+
* This source always returns true because it generates manifests
|
|
1939
|
+
* from registry data. It doesn't need anything special from the package.
|
|
1940
|
+
*
|
|
1941
|
+
* @param _pkg - The registry package to check (ignored)
|
|
1942
|
+
* @returns Always true - we can always generate a manifest
|
|
1943
|
+
*/
|
|
1944
|
+
canFetch(_pkg) {
|
|
1945
|
+
return true;
|
|
1479
1946
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1947
|
+
/**
|
|
1948
|
+
* Fetch (generate) the manifest from registry data.
|
|
1949
|
+
*
|
|
1950
|
+
* This method creates a manifest based on:
|
|
1951
|
+
* - The package's registry metadata (name, version, etc.)
|
|
1952
|
+
* - The inferred or explicit package type
|
|
1953
|
+
*
|
|
1954
|
+
* Different manifest structures are created for different types:
|
|
1955
|
+
* - "rules": Creates universal.rules with basic content
|
|
1956
|
+
* - "skill": Creates skill config with slash command
|
|
1957
|
+
* - "mcp": Creates mcp config with npx command
|
|
1958
|
+
*
|
|
1959
|
+
* @param pkg - The registry package to create a manifest for
|
|
1960
|
+
* @param _context - Fetch context (not used by this source)
|
|
1961
|
+
* @returns A generated manifest (never null)
|
|
1962
|
+
*/
|
|
1963
|
+
async fetch(pkg, _context) {
|
|
1964
|
+
return this.createManifestFromRegistry(pkg);
|
|
1493
1965
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1966
|
+
/**
|
|
1967
|
+
* Create a manifest from registry package data.
|
|
1968
|
+
*
|
|
1969
|
+
* This method builds a type-appropriate manifest using the
|
|
1970
|
+
* information available in the registry entry. The content
|
|
1971
|
+
* is auto-generated based on the package name and description.
|
|
1972
|
+
*
|
|
1973
|
+
* @param pkg - The registry package to create a manifest from
|
|
1974
|
+
* @returns A complete PackageManifest object
|
|
1975
|
+
*/
|
|
1976
|
+
createManifestFromRegistry(pkg) {
|
|
1977
|
+
const packageType = resolvePackageType(pkg);
|
|
1978
|
+
const baseFields = {
|
|
1979
|
+
name: pkg.name,
|
|
1980
|
+
// e.g., "@cpm/typescript-strict"
|
|
1981
|
+
version: pkg.version,
|
|
1982
|
+
// e.g., "1.0.0"
|
|
1983
|
+
description: pkg.description,
|
|
1984
|
+
// e.g., "TypeScript strict mode rules"
|
|
1985
|
+
author: { name: pkg.author },
|
|
1986
|
+
// e.g., { name: "cpm" }
|
|
1987
|
+
repository: pkg.repository,
|
|
1988
|
+
// e.g., "https://github.com/..."
|
|
1989
|
+
keywords: pkg.keywords
|
|
1990
|
+
// e.g., ["typescript", "rules"]
|
|
1991
|
+
};
|
|
1992
|
+
if (packageType === "mcp") {
|
|
1993
|
+
return {
|
|
1994
|
+
...baseFields,
|
|
1995
|
+
type: "mcp",
|
|
1996
|
+
mcp: {
|
|
1997
|
+
command: "npx",
|
|
1998
|
+
// Default to npx for Node packages
|
|
1999
|
+
args: []
|
|
2000
|
+
// Empty args - user will need to configure
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
1514
2003
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
return false;
|
|
1533
|
-
}
|
|
1534
|
-
return true;
|
|
2004
|
+
if (packageType === "skill") {
|
|
2005
|
+
return {
|
|
2006
|
+
...baseFields,
|
|
2007
|
+
type: "skill",
|
|
2008
|
+
skill: {
|
|
2009
|
+
// Create slash command from package name
|
|
2010
|
+
// e.g., "@cpm/commit-skill" becomes "/commit-skill"
|
|
2011
|
+
command: `/${pkg.name.replace(/^@[^/]+\//, "")}`,
|
|
2012
|
+
description: pkg.description
|
|
2013
|
+
},
|
|
2014
|
+
universal: {
|
|
2015
|
+
// Auto-generate prompt content from package info
|
|
2016
|
+
prompt: `# ${pkg.name}
|
|
2017
|
+
|
|
2018
|
+
${pkg.description}`
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
1535
2021
|
}
|
|
1536
|
-
|
|
2022
|
+
return {
|
|
2023
|
+
...baseFields,
|
|
2024
|
+
type: "rules",
|
|
2025
|
+
universal: {
|
|
2026
|
+
// Auto-generate rules content from package info
|
|
2027
|
+
rules: `# ${pkg.name}
|
|
2028
|
+
|
|
2029
|
+
${pkg.description}`
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
// src/sources/index.ts
|
|
2036
|
+
function createDefaultResolver() {
|
|
2037
|
+
return new ManifestResolver([
|
|
2038
|
+
// Priority 1: Try GitHub repository first (fastest, most complete)
|
|
2039
|
+
new RepositorySource(),
|
|
2040
|
+
// Priority 2: Download tarball if repo fails
|
|
2041
|
+
new TarballSource(),
|
|
2042
|
+
// Priority 3: Use bundled packages as fallback
|
|
2043
|
+
new EmbeddedSource(),
|
|
2044
|
+
// Priority 4: Generate from registry data as last resort
|
|
2045
|
+
new RegistrySource()
|
|
2046
|
+
]);
|
|
1537
2047
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
2048
|
+
var defaultResolver = createDefaultResolver();
|
|
2049
|
+
|
|
2050
|
+
// src/utils/downloader.ts
|
|
2051
|
+
var TEMP_DIR = path12.join(os3.tmpdir(), "cpm-downloads");
|
|
2052
|
+
async function downloadPackage(pkg) {
|
|
1540
2053
|
try {
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
headers: {
|
|
1550
|
-
Accept: "application/vnd.github.v3+json",
|
|
1551
|
-
"User-Agent": "cpm-cli"
|
|
1552
|
-
},
|
|
1553
|
-
responseType: "json"
|
|
2054
|
+
await fs8.ensureDir(TEMP_DIR);
|
|
2055
|
+
const packageTempDir = path12.join(
|
|
2056
|
+
TEMP_DIR,
|
|
2057
|
+
`${pkg.name.replace(/[@/]/g, "_")}-${Date.now()}`
|
|
2058
|
+
);
|
|
2059
|
+
await fs8.ensureDir(packageTempDir);
|
|
2060
|
+
const manifest = await defaultResolver.resolve(pkg, {
|
|
2061
|
+
tempDir: packageTempDir
|
|
1554
2062
|
});
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
const safeFileName = sanitizeFileName2(file.name);
|
|
1562
|
-
const destPath = path5.join(tempDir, safeFileName);
|
|
1563
|
-
validatePathWithinDir(destPath, tempDir);
|
|
1564
|
-
const fileResponse = await got2(file.download_url, {
|
|
1565
|
-
timeout: { request: TIMEOUTS.API_REQUEST }
|
|
1566
|
-
});
|
|
1567
|
-
await fs5.writeFile(destPath, fileResponse.body, "utf-8");
|
|
1568
|
-
if (file.name === contentFile) {
|
|
1569
|
-
mainContent = fileResponse.body;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
return createManifestWithContent(pkg, mainContent);
|
|
1574
|
-
} catch {
|
|
1575
|
-
return fetchSingleFileFromPath(pkg);
|
|
2063
|
+
return { success: true, manifest, tempDir: packageTempDir };
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
return {
|
|
2066
|
+
success: false,
|
|
2067
|
+
error: error instanceof Error ? error.message : "Download failed"
|
|
2068
|
+
};
|
|
1576
2069
|
}
|
|
1577
2070
|
}
|
|
1578
|
-
async function
|
|
1579
|
-
if (!pkg.path) return null;
|
|
2071
|
+
async function cleanupTempDir(tempDir) {
|
|
1580
2072
|
try {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const contentUrl = `${PACKAGES_BASE_URL}/${safePath}/${contentFile}`;
|
|
1585
|
-
const response = await got2(contentUrl, {
|
|
1586
|
-
timeout: { request: TIMEOUTS.API_REQUEST }
|
|
1587
|
-
});
|
|
1588
|
-
return createManifestWithContent(pkg, response.body);
|
|
2073
|
+
if (tempDir.startsWith(TEMP_DIR)) {
|
|
2074
|
+
await fs8.remove(tempDir);
|
|
2075
|
+
}
|
|
1589
2076
|
} catch {
|
|
1590
|
-
return null;
|
|
1591
2077
|
}
|
|
1592
2078
|
}
|
|
1593
|
-
function getContentFileName(type) {
|
|
1594
|
-
const fileNames = {
|
|
1595
|
-
skill: "SKILL.md",
|
|
1596
|
-
rules: "RULES.md",
|
|
1597
|
-
mcp: "MCP.md",
|
|
1598
|
-
agent: "AGENT.md",
|
|
1599
|
-
hook: "HOOK.md",
|
|
1600
|
-
workflow: "WORKFLOW.md",
|
|
1601
|
-
template: "TEMPLATE.md",
|
|
1602
|
-
bundle: "BUNDLE.md"
|
|
1603
|
-
};
|
|
1604
|
-
return fileNames[type] || "README.md";
|
|
1605
|
-
}
|
|
1606
|
-
function parseGitHubInfo(baseUrl) {
|
|
1607
|
-
const match = baseUrl.match(/github(?:usercontent)?\.com\/([^/]+)\/([^/]+)/);
|
|
1608
|
-
if (!match) return null;
|
|
1609
|
-
return { owner: match[1], repo: match[2] };
|
|
1610
|
-
}
|
|
1611
|
-
function createManifestWithContent(pkg, content) {
|
|
1612
|
-
const pkgType = resolvePackageType(pkg);
|
|
1613
|
-
return {
|
|
1614
|
-
name: pkg.name,
|
|
1615
|
-
version: pkg.version,
|
|
1616
|
-
description: pkg.description,
|
|
1617
|
-
type: pkgType,
|
|
1618
|
-
author: { name: pkg.author },
|
|
1619
|
-
keywords: pkg.keywords,
|
|
1620
|
-
universal: {
|
|
1621
|
-
rules: content,
|
|
1622
|
-
prompt: content
|
|
1623
|
-
},
|
|
1624
|
-
skill: pkgType === "skill" ? {
|
|
1625
|
-
command: `/${pkg.name.split("/").pop()}`,
|
|
1626
|
-
description: pkg.description
|
|
1627
|
-
} : void 0
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
function createManifestFromRegistry(pkg) {
|
|
1631
|
-
return {
|
|
1632
|
-
name: pkg.name,
|
|
1633
|
-
version: pkg.version,
|
|
1634
|
-
description: pkg.description,
|
|
1635
|
-
type: resolvePackageType(pkg),
|
|
1636
|
-
author: { name: pkg.author },
|
|
1637
|
-
repository: pkg.repository,
|
|
1638
|
-
keywords: pkg.keywords,
|
|
1639
|
-
universal: {
|
|
1640
|
-
rules: `# ${pkg.name}
|
|
1641
2079
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2080
|
+
// src/validation/package-name.ts
|
|
2081
|
+
var PACKAGE_NAME_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
2082
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
2083
|
+
"..",
|
|
2084
|
+
// Parent directory
|
|
2085
|
+
"\\",
|
|
2086
|
+
// Windows path separator
|
|
2087
|
+
"%2e",
|
|
2088
|
+
// URL-encoded "." (lowercase)
|
|
2089
|
+
"%2E",
|
|
2090
|
+
// URL-encoded "." (uppercase)
|
|
2091
|
+
"%5c",
|
|
2092
|
+
// URL-encoded "\" (lowercase)
|
|
2093
|
+
"%5C",
|
|
2094
|
+
// URL-encoded "\" (uppercase)
|
|
2095
|
+
"%2f",
|
|
2096
|
+
// URL-encoded "/" (lowercase)
|
|
2097
|
+
"%2F"
|
|
2098
|
+
// URL-encoded "/" (uppercase)
|
|
2099
|
+
];
|
|
2100
|
+
function hasPathTraversal(value) {
|
|
2101
|
+
return PATH_TRAVERSAL_PATTERNS.some((pattern) => value.includes(pattern));
|
|
1645
2102
|
}
|
|
1646
|
-
|
|
1647
|
-
// src/commands/install.ts
|
|
1648
|
-
var VALID_PLATFORMS = ["claude-code"];
|
|
1649
|
-
var MAX_PACKAGE_NAME_LENGTH = 214;
|
|
1650
2103
|
function validatePackageName(name) {
|
|
1651
2104
|
if (!name || typeof name !== "string") {
|
|
1652
2105
|
return { valid: false, error: "Package name cannot be empty" };
|
|
@@ -1656,163 +2109,378 @@ function validatePackageName(name) {
|
|
|
1656
2109
|
decoded = decodeURIComponent(name);
|
|
1657
2110
|
} catch {
|
|
1658
2111
|
}
|
|
1659
|
-
if (decoded.length > MAX_PACKAGE_NAME_LENGTH) {
|
|
1660
|
-
return {
|
|
2112
|
+
if (decoded.length > LIMITS.MAX_PACKAGE_NAME_LENGTH) {
|
|
2113
|
+
return {
|
|
2114
|
+
valid: false,
|
|
2115
|
+
error: `Package name too long (max ${LIMITS.MAX_PACKAGE_NAME_LENGTH} characters)`
|
|
2116
|
+
};
|
|
1661
2117
|
}
|
|
1662
2118
|
if (decoded.includes("\0")) {
|
|
1663
2119
|
return { valid: false, error: "Invalid characters in package name" };
|
|
1664
2120
|
}
|
|
1665
|
-
|
|
1666
|
-
if (hasPathTraversal) {
|
|
2121
|
+
if (hasPathTraversal(decoded)) {
|
|
1667
2122
|
return { valid: false, error: "Invalid characters in package name" };
|
|
1668
2123
|
}
|
|
1669
|
-
|
|
1670
|
-
if (!packageNameRegex.test(name.toLowerCase())) {
|
|
2124
|
+
if (!PACKAGE_NAME_REGEX.test(name.toLowerCase())) {
|
|
1671
2125
|
return { valid: false, error: "Invalid package name format" };
|
|
1672
2126
|
}
|
|
1673
2127
|
return { valid: true };
|
|
1674
2128
|
}
|
|
1675
|
-
function isValidPlatform(platform) {
|
|
1676
|
-
return VALID_PLATFORMS.includes(platform);
|
|
1677
|
-
}
|
|
1678
2129
|
function normalizePackageName(name) {
|
|
1679
2130
|
if (name.startsWith("@")) {
|
|
1680
2131
|
return name;
|
|
1681
2132
|
}
|
|
1682
2133
|
return `@cpm/${name}`;
|
|
1683
2134
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2135
|
+
|
|
2136
|
+
// src/commands/ui/colors.ts
|
|
2137
|
+
import chalk from "chalk";
|
|
2138
|
+
var TYPE_COLORS = {
|
|
2139
|
+
rules: chalk.yellow,
|
|
2140
|
+
// Yellow - like warning/guidelines
|
|
2141
|
+
skill: chalk.blue,
|
|
2142
|
+
// Blue - quick actions
|
|
2143
|
+
mcp: chalk.magenta,
|
|
2144
|
+
// Magenta - special/protocol
|
|
2145
|
+
agent: chalk.green,
|
|
2146
|
+
// Green - AI/active
|
|
2147
|
+
hook: chalk.cyan,
|
|
2148
|
+
// Cyan - lifecycle
|
|
2149
|
+
workflow: chalk.red,
|
|
2150
|
+
// Red - processes
|
|
2151
|
+
template: chalk.white,
|
|
2152
|
+
// White - neutral/base
|
|
2153
|
+
bundle: chalk.gray
|
|
2154
|
+
// Gray - collection
|
|
2155
|
+
};
|
|
2156
|
+
function getTypeColor(type) {
|
|
2157
|
+
return TYPE_COLORS[type] ?? chalk.white;
|
|
2158
|
+
}
|
|
2159
|
+
var TYPE_EMOJIS = {
|
|
2160
|
+
rules: "\u{1F4DC}",
|
|
2161
|
+
// Scroll - coding rules/guidelines
|
|
2162
|
+
skill: "\u26A1",
|
|
2163
|
+
// Lightning - quick commands
|
|
2164
|
+
mcp: "\u{1F50C}",
|
|
2165
|
+
// Plug - server connections
|
|
2166
|
+
agent: "\u{1F916}",
|
|
2167
|
+
// Robot - AI agents
|
|
2168
|
+
hook: "\u{1FA9D}",
|
|
2169
|
+
// Hook - lifecycle events
|
|
2170
|
+
workflow: "\u{1F4CB}",
|
|
2171
|
+
// Clipboard - workflows
|
|
2172
|
+
template: "\u{1F4C1}",
|
|
2173
|
+
// Folder - project templates
|
|
2174
|
+
bundle: "\u{1F4E6}"
|
|
2175
|
+
// Package - bundles
|
|
2176
|
+
};
|
|
2177
|
+
function getTypeEmoji(type) {
|
|
2178
|
+
return TYPE_EMOJIS[type] ?? "\u{1F4E6}";
|
|
2179
|
+
}
|
|
2180
|
+
var SEMANTIC_COLORS = {
|
|
2181
|
+
/** Success messages and indicators */
|
|
2182
|
+
success: chalk.green,
|
|
2183
|
+
/** Error messages */
|
|
2184
|
+
error: chalk.red,
|
|
2185
|
+
/** Warning messages */
|
|
2186
|
+
warning: chalk.yellow,
|
|
2187
|
+
/** Informational messages */
|
|
2188
|
+
info: chalk.cyan,
|
|
2189
|
+
/** Dimmed/secondary text */
|
|
2190
|
+
dim: chalk.dim,
|
|
2191
|
+
/** Bold emphasis */
|
|
2192
|
+
bold: chalk.bold,
|
|
2193
|
+
/** Package names and commands */
|
|
2194
|
+
highlight: chalk.cyan,
|
|
2195
|
+
/** Version numbers */
|
|
2196
|
+
version: chalk.dim
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
// src/commands/ui/formatters.ts
|
|
2200
|
+
import chalk2 from "chalk";
|
|
2201
|
+
import path13 from "path";
|
|
2202
|
+
function formatNumber(num) {
|
|
2203
|
+
if (num >= 1e6) {
|
|
2204
|
+
return `${(num / 1e6).toFixed(1)}M`;
|
|
1694
2205
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
platforms = ["claude-code"];
|
|
2206
|
+
if (num >= 1e3) {
|
|
2207
|
+
return `${(num / 1e3).toFixed(1)}k`;
|
|
1698
2208
|
}
|
|
1699
|
-
return
|
|
2209
|
+
return num.toString();
|
|
1700
2210
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
);
|
|
2211
|
+
function formatPath(filePath) {
|
|
2212
|
+
const relativePath = path13.relative(process.cwd(), filePath);
|
|
2213
|
+
if (relativePath.startsWith("..")) {
|
|
2214
|
+
return filePath;
|
|
2215
|
+
}
|
|
2216
|
+
return relativePath;
|
|
1708
2217
|
}
|
|
1709
|
-
function
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
logger.log(chalk.dim(` + ${path6.relative(process.cwd(), file)}`));
|
|
1716
|
-
}
|
|
2218
|
+
function formatPackageHeader(pkg) {
|
|
2219
|
+
const pkgType = resolvePackageType(pkg);
|
|
2220
|
+
const emoji = getTypeEmoji(pkgType);
|
|
2221
|
+
const badges = [];
|
|
2222
|
+
if (pkg.verified) {
|
|
2223
|
+
badges.push(chalk2.green("\u2713 verified"));
|
|
1717
2224
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
2225
|
+
const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
2226
|
+
return `${emoji} ${chalk2.bold.white(pkg.name)} ${chalk2.dim(`v${pkg.version}`)}${badgeStr}`;
|
|
2227
|
+
}
|
|
2228
|
+
function formatPackageMetadata(pkg) {
|
|
2229
|
+
const pkgType = resolvePackageType(pkg);
|
|
2230
|
+
const typeColor = getTypeColor(pkgType);
|
|
2231
|
+
const parts = [
|
|
2232
|
+
typeColor(pkgType),
|
|
2233
|
+
chalk2.dim(`\u2193 ${formatNumber(pkg.downloads ?? 0)}`),
|
|
2234
|
+
pkg.stars !== void 0 ? chalk2.dim(`\u2605 ${pkg.stars}`) : null,
|
|
2235
|
+
chalk2.dim(`@${pkg.author}`)
|
|
2236
|
+
].filter(Boolean);
|
|
2237
|
+
return parts.join(chalk2.dim(" \xB7 "));
|
|
1720
2238
|
}
|
|
1721
|
-
function
|
|
2239
|
+
function formatPackageEntry(pkg) {
|
|
2240
|
+
return [
|
|
2241
|
+
formatPackageHeader(pkg),
|
|
2242
|
+
` ${chalk2.dim(pkg.description)}`,
|
|
2243
|
+
` ${formatPackageMetadata(pkg)}`
|
|
2244
|
+
];
|
|
2245
|
+
}
|
|
2246
|
+
function formatCreatedFiles(files) {
|
|
2247
|
+
return files.map((file) => chalk2.dim(` + ${formatPath(file)}`));
|
|
2248
|
+
}
|
|
2249
|
+
function formatRemovedFiles(files) {
|
|
2250
|
+
return files.map((file) => chalk2.dim(` - ${file}`));
|
|
2251
|
+
}
|
|
2252
|
+
function formatUsageHints(manifest) {
|
|
2253
|
+
const hints = [];
|
|
1722
2254
|
switch (manifest.type) {
|
|
1723
2255
|
case "skill":
|
|
1724
|
-
if (manifest.skill?.command) {
|
|
1725
|
-
|
|
1726
|
-
` ${
|
|
2256
|
+
if ("skill" in manifest && manifest.skill?.command) {
|
|
2257
|
+
hints.push(
|
|
2258
|
+
` ${SEMANTIC_COLORS.info("Usage:")} Type ${chalk2.yellow(manifest.skill.command)} in your editor`
|
|
1727
2259
|
);
|
|
1728
2260
|
}
|
|
1729
2261
|
break;
|
|
1730
2262
|
case "rules":
|
|
1731
|
-
|
|
1732
|
-
` ${
|
|
2263
|
+
hints.push(
|
|
2264
|
+
` ${SEMANTIC_COLORS.info("Usage:")} Rules are automatically applied to matching files`
|
|
1733
2265
|
);
|
|
1734
2266
|
break;
|
|
1735
2267
|
case "mcp":
|
|
1736
|
-
|
|
1737
|
-
` ${
|
|
2268
|
+
hints.push(
|
|
2269
|
+
` ${SEMANTIC_COLORS.info("Usage:")} MCP server configured. Restart your editor to activate.`
|
|
1738
2270
|
);
|
|
1739
|
-
if (manifest.mcp?.env) {
|
|
2271
|
+
if ("mcp" in manifest && manifest.mcp?.env) {
|
|
1740
2272
|
const envVars = Object.keys(manifest.mcp.env);
|
|
1741
2273
|
if (envVars.length > 0) {
|
|
1742
|
-
|
|
2274
|
+
hints.push(chalk2.yellow(`
|
|
1743
2275
|
Required environment variables:`));
|
|
1744
2276
|
for (const envVar of envVars) {
|
|
1745
|
-
|
|
2277
|
+
hints.push(chalk2.dim(` - ${envVar}`));
|
|
1746
2278
|
}
|
|
1747
2279
|
}
|
|
1748
2280
|
}
|
|
1749
2281
|
break;
|
|
1750
2282
|
}
|
|
2283
|
+
return hints;
|
|
2284
|
+
}
|
|
2285
|
+
function formatSeparator(width = 50) {
|
|
2286
|
+
return chalk2.dim("\u2500".repeat(width));
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/commands/ui/spinner.ts
|
|
2290
|
+
import ora from "ora";
|
|
2291
|
+
import chalk3 from "chalk";
|
|
2292
|
+
var ActiveSpinner = class {
|
|
2293
|
+
constructor(spinner) {
|
|
2294
|
+
this.spinner = spinner;
|
|
2295
|
+
}
|
|
2296
|
+
update(text) {
|
|
2297
|
+
this.spinner.text = text;
|
|
2298
|
+
}
|
|
2299
|
+
succeed(text) {
|
|
2300
|
+
this.spinner.succeed(text);
|
|
2301
|
+
}
|
|
2302
|
+
fail(text) {
|
|
2303
|
+
this.spinner.fail(text);
|
|
2304
|
+
}
|
|
2305
|
+
warn(text) {
|
|
2306
|
+
this.spinner.warn(text);
|
|
2307
|
+
}
|
|
2308
|
+
stop() {
|
|
2309
|
+
this.spinner.stop();
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
var QuietSpinner = class {
|
|
2313
|
+
update(_text) {
|
|
2314
|
+
}
|
|
2315
|
+
succeed(text) {
|
|
2316
|
+
logger.success(text.replace(chalk3.green(""), "").trim());
|
|
2317
|
+
}
|
|
2318
|
+
fail(text) {
|
|
2319
|
+
logger.error(text.replace(chalk3.red(""), "").trim());
|
|
2320
|
+
}
|
|
2321
|
+
warn(text) {
|
|
2322
|
+
logger.warn(text.replace(chalk3.yellow(""), "").trim());
|
|
2323
|
+
}
|
|
2324
|
+
stop() {
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
function createSpinner(initialText) {
|
|
2328
|
+
if (logger.isQuiet()) {
|
|
2329
|
+
return new QuietSpinner();
|
|
2330
|
+
}
|
|
2331
|
+
const spinner = ora(initialText).start();
|
|
2332
|
+
return new ActiveSpinner(spinner);
|
|
2333
|
+
}
|
|
2334
|
+
function spinnerText(action, packageName) {
|
|
2335
|
+
return `${action} ${chalk3.cyan(packageName)}...`;
|
|
2336
|
+
}
|
|
2337
|
+
function successText(action, packageName, version) {
|
|
2338
|
+
const versionStr = version ? `@${chalk3.dim(version)}` : "";
|
|
2339
|
+
return `${action} ${chalk3.green(packageName)}${versionStr}`;
|
|
2340
|
+
}
|
|
2341
|
+
function failText(action, packageName, error) {
|
|
2342
|
+
const errorStr = error ? `: ${error}` : "";
|
|
2343
|
+
return `${action} ${chalk3.red(packageName)}${errorStr}`;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/commands/types.ts
|
|
2347
|
+
function parseInstallOptions(raw) {
|
|
2348
|
+
if (raw.platform && raw.platform !== "all") {
|
|
2349
|
+
if (!isValidPlatform(raw.platform)) {
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
platform: raw.platform,
|
|
2354
|
+
version: raw.version
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
return {
|
|
2358
|
+
platform: "claude-code",
|
|
2359
|
+
version: raw.version
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
var SEARCH_DEFAULTS = {
|
|
2363
|
+
limit: 10,
|
|
2364
|
+
minLimit: 1,
|
|
2365
|
+
maxLimit: 100
|
|
2366
|
+
};
|
|
2367
|
+
function parseSearchOptions(query, raw) {
|
|
2368
|
+
const parsedLimit = parseInt(raw.limit || "", 10);
|
|
2369
|
+
const limit = Number.isNaN(parsedLimit) ? SEARCH_DEFAULTS.limit : Math.max(
|
|
2370
|
+
SEARCH_DEFAULTS.minLimit,
|
|
2371
|
+
Math.min(parsedLimit, SEARCH_DEFAULTS.maxLimit)
|
|
2372
|
+
);
|
|
2373
|
+
const options = {
|
|
2374
|
+
query,
|
|
2375
|
+
limit
|
|
2376
|
+
};
|
|
2377
|
+
if (raw.type && isPackageType(raw.type)) {
|
|
2378
|
+
options.type = raw.type;
|
|
2379
|
+
}
|
|
2380
|
+
if (raw.sort && isSearchSort(raw.sort)) {
|
|
2381
|
+
options.sort = raw.sort;
|
|
2382
|
+
}
|
|
2383
|
+
return options;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// src/commands/install.ts
|
|
2387
|
+
async function installToPlatforms(manifest, tempDir, platforms) {
|
|
2388
|
+
return Promise.all(
|
|
2389
|
+
platforms.map(async (platform) => {
|
|
2390
|
+
const adapter = getAdapter(platform);
|
|
2391
|
+
return adapter.install(manifest, process.cwd(), tempDir);
|
|
2392
|
+
})
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
function partitionResults(results) {
|
|
2396
|
+
const successful = results.filter((r) => r.success);
|
|
2397
|
+
const failed = results.filter((r) => !r.success);
|
|
2398
|
+
return [successful, failed];
|
|
2399
|
+
}
|
|
2400
|
+
function displaySuccess(manifest, results) {
|
|
2401
|
+
logger.log(SEMANTIC_COLORS.dim(`
|
|
2402
|
+
${manifest.description}`));
|
|
2403
|
+
logger.log(SEMANTIC_COLORS.dim("\n Files created:"));
|
|
2404
|
+
const allFiles = results.flatMap((r) => r.filesWritten);
|
|
2405
|
+
const formattedFiles = formatCreatedFiles(allFiles);
|
|
2406
|
+
formattedFiles.forEach((line) => logger.log(line));
|
|
2407
|
+
logger.newline();
|
|
2408
|
+
const hints = formatUsageHints(manifest);
|
|
2409
|
+
hints.forEach((line) => logger.log(line));
|
|
1751
2410
|
}
|
|
1752
|
-
function displayWarnings(
|
|
1753
|
-
if (
|
|
1754
|
-
logger.log(
|
|
1755
|
-
for (const result of
|
|
1756
|
-
logger.log(
|
|
2411
|
+
function displayWarnings(results) {
|
|
2412
|
+
if (results.length === 0) return;
|
|
2413
|
+
logger.log(SEMANTIC_COLORS.warning("\n Warnings:"));
|
|
2414
|
+
for (const result of results) {
|
|
2415
|
+
logger.log(
|
|
2416
|
+
SEMANTIC_COLORS.warning(` - ${result.platform}: ${result.error}`)
|
|
2417
|
+
);
|
|
1757
2418
|
}
|
|
1758
2419
|
}
|
|
1759
|
-
|
|
2420
|
+
function displayNotFound(packageName) {
|
|
2421
|
+
logger.log(SEMANTIC_COLORS.dim("\nTry searching for packages:"));
|
|
2422
|
+
logger.log(
|
|
2423
|
+
SEMANTIC_COLORS.dim(` cpm search ${packageName.replace(/^@[^/]+\//, "")}`)
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
function displayInvalidPlatform() {
|
|
2427
|
+
logger.log(
|
|
2428
|
+
SEMANTIC_COLORS.dim(`Valid platforms: ${VALID_PLATFORMS.join(", ")}`)
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
async function installCommand(packageName, rawOptions) {
|
|
1760
2432
|
const validation = validatePackageName(packageName);
|
|
1761
2433
|
if (!validation.valid) {
|
|
1762
2434
|
logger.error(`Invalid package name: ${validation.error}`);
|
|
1763
2435
|
return;
|
|
1764
2436
|
}
|
|
1765
|
-
const
|
|
2437
|
+
const options = parseInstallOptions(rawOptions);
|
|
2438
|
+
if (!options) {
|
|
2439
|
+
logger.error(`Invalid platform: ${rawOptions.platform}`);
|
|
2440
|
+
displayInvalidPlatform();
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
const spinner = createSpinner(spinnerText("Installing", packageName));
|
|
1766
2444
|
let tempDir;
|
|
1767
2445
|
try {
|
|
1768
2446
|
const normalizedName = normalizePackageName(packageName);
|
|
1769
|
-
|
|
2447
|
+
spinner.update(spinnerText("Searching for", normalizedName));
|
|
1770
2448
|
const pkg = await registry.getPackage(normalizedName);
|
|
1771
2449
|
if (!pkg) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
logger.log(chalk.dim("\nTry searching for packages:"));
|
|
1775
|
-
logger.log(chalk.dim(` cpm search ${packageName.replace(/^@[^/]+\//, "")}`));
|
|
2450
|
+
spinner.fail(failText("Package not found", normalizedName));
|
|
2451
|
+
displayNotFound(packageName);
|
|
1776
2452
|
return;
|
|
1777
2453
|
}
|
|
1778
|
-
|
|
2454
|
+
spinner.update(spinnerText("Downloading", `${pkg.name}@${pkg.version}`));
|
|
1779
2455
|
const downloadResult = await downloadPackage(pkg);
|
|
1780
2456
|
if (!downloadResult.success) {
|
|
1781
|
-
|
|
1782
|
-
|
|
2457
|
+
spinner.fail(
|
|
2458
|
+
failText("Failed to download", pkg.name, downloadResult.error)
|
|
2459
|
+
);
|
|
1783
2460
|
return;
|
|
1784
2461
|
}
|
|
2462
|
+
const manifest = downloadResult.manifest;
|
|
1785
2463
|
tempDir = downloadResult.tempDir;
|
|
1786
|
-
const targetPlatforms =
|
|
1787
|
-
|
|
1788
|
-
if (spinner) spinner.fail(`Invalid platform: ${options.platform}`);
|
|
1789
|
-
else logger.error(`Invalid platform: ${options.platform}`);
|
|
1790
|
-
logger.log(chalk.dim(`Valid platforms: ${VALID_PLATFORMS.join(", ")}`));
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
if (spinner) spinner.text = `Installing to ${targetPlatforms.join(", ")}...`;
|
|
2464
|
+
const targetPlatforms = [options.platform];
|
|
2465
|
+
spinner.update(`Installing to ${targetPlatforms.join(", ")}...`);
|
|
1794
2466
|
await ensureClaudeDirs();
|
|
1795
2467
|
const results = await installToPlatforms(
|
|
1796
|
-
|
|
2468
|
+
manifest,
|
|
1797
2469
|
tempDir,
|
|
1798
2470
|
targetPlatforms
|
|
1799
2471
|
);
|
|
1800
|
-
const successful = results
|
|
1801
|
-
const failed = results.filter((r) => !r.success);
|
|
2472
|
+
const [successful, failed] = partitionResults(results);
|
|
1802
2473
|
if (successful.length > 0) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
}
|
|
1810
|
-
displaySuccessMessage(downloadResult.manifest, successful);
|
|
2474
|
+
spinner.succeed(
|
|
2475
|
+
successText("Installed", manifest.name, manifest.version)
|
|
2476
|
+
);
|
|
2477
|
+
displaySuccess(manifest, successful);
|
|
2478
|
+
} else {
|
|
2479
|
+
spinner.fail(failText("Failed to install", manifest.name));
|
|
1811
2480
|
}
|
|
1812
2481
|
displayWarnings(failed);
|
|
1813
2482
|
} catch (error) {
|
|
1814
|
-
|
|
1815
|
-
else logger.error(`Failed to install ${packageName}`);
|
|
2483
|
+
spinner.fail(failText("Failed to install", packageName));
|
|
1816
2484
|
if (error instanceof Error) {
|
|
1817
2485
|
logger.error(error.message);
|
|
1818
2486
|
}
|
|
@@ -1824,309 +2492,205 @@ async function installCommand(packageName, options) {
|
|
|
1824
2492
|
}
|
|
1825
2493
|
|
|
1826
2494
|
// src/commands/search.ts
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
2495
|
+
function displayResults(packages, total) {
|
|
2496
|
+
logger.log(SEMANTIC_COLORS.dim(`
|
|
2497
|
+
Found ${total} package(s)
|
|
2498
|
+
`));
|
|
2499
|
+
for (const pkg of packages) {
|
|
2500
|
+
const lines = formatPackageEntry(pkg);
|
|
2501
|
+
lines.forEach((line) => logger.log(line));
|
|
2502
|
+
logger.newline();
|
|
2503
|
+
}
|
|
2504
|
+
logger.log(formatSeparator());
|
|
2505
|
+
logger.log(
|
|
2506
|
+
SEMANTIC_COLORS.dim(
|
|
2507
|
+
`Install with: ${SEMANTIC_COLORS.highlight("cpm install <package-name>")}`
|
|
2508
|
+
)
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
function displayNoResults(query) {
|
|
2512
|
+
logger.warn(`No packages found for "${query}"`);
|
|
2513
|
+
logger.log(
|
|
2514
|
+
SEMANTIC_COLORS.dim("\nAvailable package types: rules, skill, mcp")
|
|
2515
|
+
);
|
|
2516
|
+
logger.log(SEMANTIC_COLORS.dim("Try: cpm search react --type rules"));
|
|
2517
|
+
}
|
|
2518
|
+
async function searchCommand(query, rawOptions) {
|
|
2519
|
+
const options = parseSearchOptions(query, rawOptions);
|
|
2520
|
+
const spinner = createSpinner(`Searching for "${query}"...`);
|
|
1853
2521
|
try {
|
|
1854
|
-
const
|
|
1855
|
-
query,
|
|
1856
|
-
limit
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (options.sort) {
|
|
1862
|
-
searchOptions.sort = options.sort;
|
|
1863
|
-
}
|
|
1864
|
-
const results = await registry.search(searchOptions);
|
|
1865
|
-
if (spinner) spinner.stop();
|
|
2522
|
+
const results = await registry.search({
|
|
2523
|
+
query: options.query,
|
|
2524
|
+
limit: options.limit,
|
|
2525
|
+
type: options.type,
|
|
2526
|
+
sort: options.sort
|
|
2527
|
+
});
|
|
2528
|
+
spinner.stop();
|
|
1866
2529
|
if (results.packages.length === 0) {
|
|
1867
|
-
|
|
1868
|
-
logger.log(chalk2.dim("\nAvailable package types: rules, skill, mcp"));
|
|
1869
|
-
logger.log(chalk2.dim("Try: cpm search react --type rules"));
|
|
2530
|
+
displayNoResults(query);
|
|
1870
2531
|
return;
|
|
1871
2532
|
}
|
|
1872
|
-
|
|
1873
|
-
Found ${results.total} package(s)
|
|
1874
|
-
`));
|
|
1875
|
-
for (const pkg of results.packages) {
|
|
1876
|
-
const pkgType = resolvePackageType(pkg);
|
|
1877
|
-
const typeColor = typeColors[pkgType] || chalk2.white;
|
|
1878
|
-
const emoji = typeEmoji[pkgType] || "\u{1F4E6}";
|
|
1879
|
-
const badges = [];
|
|
1880
|
-
if (pkg.verified) {
|
|
1881
|
-
badges.push(chalk2.green("\u2713 verified"));
|
|
1882
|
-
}
|
|
1883
|
-
logger.log(
|
|
1884
|
-
`${emoji} ${chalk2.bold.white(pkg.name)} ${chalk2.dim(`v${pkg.version}`)}` + (badges.length > 0 ? ` ${badges.join(" ")}` : "")
|
|
1885
|
-
);
|
|
1886
|
-
logger.log(` ${chalk2.dim(pkg.description)}`);
|
|
1887
|
-
const meta = [
|
|
1888
|
-
typeColor(pkgType),
|
|
1889
|
-
chalk2.dim(`\u2193 ${formatNumber(pkg.downloads ?? 0)}`),
|
|
1890
|
-
pkg.stars !== void 0 ? chalk2.dim(`\u2605 ${pkg.stars}`) : null,
|
|
1891
|
-
chalk2.dim(`@${pkg.author}`)
|
|
1892
|
-
].filter(Boolean);
|
|
1893
|
-
logger.log(` ${meta.join(chalk2.dim(" \xB7 "))}`);
|
|
1894
|
-
logger.newline();
|
|
1895
|
-
}
|
|
1896
|
-
logger.log(chalk2.dim("\u2500".repeat(50)));
|
|
1897
|
-
logger.log(chalk2.dim(`Install with: ${chalk2.cyan("cpm install <package-name>")}`));
|
|
2533
|
+
displayResults(results.packages, results.total);
|
|
1898
2534
|
} catch (error) {
|
|
1899
|
-
|
|
1900
|
-
else logger.error("Search failed");
|
|
2535
|
+
spinner.fail("Search failed");
|
|
1901
2536
|
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
1902
2537
|
}
|
|
1903
2538
|
}
|
|
1904
|
-
function formatNumber(num) {
|
|
1905
|
-
if (num >= 1e3) {
|
|
1906
|
-
return `${(num / 1e3).toFixed(1)}k`;
|
|
1907
|
-
}
|
|
1908
|
-
return num.toString();
|
|
1909
|
-
}
|
|
1910
2539
|
|
|
1911
2540
|
// src/commands/list.ts
|
|
1912
|
-
import
|
|
1913
|
-
import
|
|
1914
|
-
import
|
|
1915
|
-
import
|
|
1916
|
-
var typeColors2 = {
|
|
1917
|
-
rules: chalk3.yellow,
|
|
1918
|
-
skill: chalk3.blue,
|
|
1919
|
-
mcp: chalk3.magenta
|
|
1920
|
-
};
|
|
2541
|
+
import chalk4 from "chalk";
|
|
2542
|
+
import fs9 from "fs-extra";
|
|
2543
|
+
import path14 from "path";
|
|
2544
|
+
import os4 from "os";
|
|
1921
2545
|
async function readPackageMetadata(packageDir) {
|
|
1922
|
-
const metadataPath =
|
|
2546
|
+
const metadataPath = path14.join(packageDir, ".cpm.json");
|
|
1923
2547
|
try {
|
|
1924
|
-
if (await
|
|
1925
|
-
return await
|
|
2548
|
+
if (await fs9.pathExists(metadataPath)) {
|
|
2549
|
+
return await fs9.readJson(metadataPath);
|
|
1926
2550
|
}
|
|
1927
2551
|
} catch {
|
|
1928
2552
|
}
|
|
1929
2553
|
return null;
|
|
1930
2554
|
}
|
|
1931
|
-
async function
|
|
2555
|
+
async function scanDirectory(dir, type) {
|
|
1932
2556
|
const items = [];
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
}
|
|
2557
|
+
if (!await fs9.pathExists(dir)) {
|
|
2558
|
+
return items;
|
|
2559
|
+
}
|
|
2560
|
+
const entries = await fs9.readdir(dir);
|
|
2561
|
+
for (const entry of entries) {
|
|
2562
|
+
const entryPath = path14.join(dir, entry);
|
|
2563
|
+
const stat = await fs9.stat(entryPath);
|
|
2564
|
+
if (stat.isDirectory()) {
|
|
2565
|
+
const metadata = await readPackageMetadata(entryPath);
|
|
2566
|
+
items.push({
|
|
2567
|
+
name: metadata?.name || entry,
|
|
2568
|
+
folderName: entry,
|
|
2569
|
+
type,
|
|
2570
|
+
version: metadata?.version,
|
|
2571
|
+
path: entryPath
|
|
2572
|
+
});
|
|
1950
2573
|
}
|
|
1951
2574
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
const metadata = await readPackageMetadata(skillPath);
|
|
1960
|
-
items.push({
|
|
1961
|
-
name: metadata?.name || dir,
|
|
1962
|
-
folderName: dir,
|
|
1963
|
-
type: "skill",
|
|
1964
|
-
version: metadata?.version,
|
|
1965
|
-
path: skillPath
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
2575
|
+
return items;
|
|
2576
|
+
}
|
|
2577
|
+
async function scanMcpServers() {
|
|
2578
|
+
const items = [];
|
|
2579
|
+
const configPath = path14.join(os4.homedir(), ".claude.json");
|
|
2580
|
+
if (!await fs9.pathExists(configPath)) {
|
|
2581
|
+
return items;
|
|
1969
2582
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
path: mcpConfigPath
|
|
1981
|
-
});
|
|
1982
|
-
}
|
|
1983
|
-
} catch {
|
|
2583
|
+
try {
|
|
2584
|
+
const config = await fs9.readJson(configPath);
|
|
2585
|
+
const mcpServers = config.mcpServers || {};
|
|
2586
|
+
for (const name of Object.keys(mcpServers)) {
|
|
2587
|
+
items.push({
|
|
2588
|
+
name,
|
|
2589
|
+
folderName: name,
|
|
2590
|
+
type: "mcp",
|
|
2591
|
+
path: configPath
|
|
2592
|
+
});
|
|
1984
2593
|
}
|
|
2594
|
+
} catch {
|
|
1985
2595
|
}
|
|
1986
2596
|
return items;
|
|
1987
2597
|
}
|
|
2598
|
+
async function scanInstalledPackages() {
|
|
2599
|
+
const claudeHome = path14.join(os4.homedir(), ".claude");
|
|
2600
|
+
const [rules, skills, mcp] = await Promise.all([
|
|
2601
|
+
scanDirectory(path14.join(claudeHome, "rules"), "rules"),
|
|
2602
|
+
scanDirectory(path14.join(claudeHome, "skills"), "skill"),
|
|
2603
|
+
scanMcpServers()
|
|
2604
|
+
]);
|
|
2605
|
+
return [...rules, ...skills, ...mcp];
|
|
2606
|
+
}
|
|
2607
|
+
function groupByType(packages) {
|
|
2608
|
+
return packages.reduce(
|
|
2609
|
+
(acc, pkg) => ({
|
|
2610
|
+
...acc,
|
|
2611
|
+
[pkg.type]: [...acc[pkg.type] || [], pkg]
|
|
2612
|
+
}),
|
|
2613
|
+
{}
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
function displayPackage(pkg) {
|
|
2617
|
+
const version = pkg.version ? SEMANTIC_COLORS.dim(` v${pkg.version}`) : "";
|
|
2618
|
+
logger.log(
|
|
2619
|
+
` ${SEMANTIC_COLORS.success("\u25C9")} ${chalk4.bold(pkg.name)}${version}`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
function displayByType(byType) {
|
|
2623
|
+
for (const [type, items] of Object.entries(byType)) {
|
|
2624
|
+
const typeColor = getTypeColor(type);
|
|
2625
|
+
logger.log(typeColor(` ${type.toUpperCase()}`));
|
|
2626
|
+
for (const item of items) {
|
|
2627
|
+
displayPackage(item);
|
|
2628
|
+
}
|
|
2629
|
+
logger.newline();
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function displayEmpty() {
|
|
2633
|
+
logger.warn("No packages installed");
|
|
2634
|
+
logger.log(
|
|
2635
|
+
SEMANTIC_COLORS.dim(
|
|
2636
|
+
`
|
|
2637
|
+
Run ${SEMANTIC_COLORS.highlight("cpm install <package>")} to install a package`
|
|
2638
|
+
)
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
function displayFooter() {
|
|
2642
|
+
logger.log(
|
|
2643
|
+
SEMANTIC_COLORS.dim("Run cpm uninstall <package-name> to remove a package")
|
|
2644
|
+
);
|
|
2645
|
+
logger.log(SEMANTIC_COLORS.dim(" e.g., cpm uninstall backend-patterns"));
|
|
2646
|
+
}
|
|
1988
2647
|
async function listCommand() {
|
|
1989
2648
|
try {
|
|
1990
2649
|
const packages = await scanInstalledPackages();
|
|
1991
2650
|
if (packages.length === 0) {
|
|
1992
|
-
|
|
1993
|
-
logger.log(chalk3.dim(`
|
|
1994
|
-
Run ${chalk3.cyan("cpm install <package>")} to install a package`));
|
|
2651
|
+
displayEmpty();
|
|
1995
2652
|
return;
|
|
1996
2653
|
}
|
|
1997
|
-
logger.log(
|
|
2654
|
+
logger.log(chalk4.bold(`
|
|
1998
2655
|
Installed packages (${packages.length}):
|
|
1999
2656
|
`));
|
|
2000
|
-
const byType = packages
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
}), {});
|
|
2004
|
-
for (const [type, items] of Object.entries(byType)) {
|
|
2005
|
-
const typeColor = typeColors2[type] || chalk3.white;
|
|
2006
|
-
logger.log(typeColor(` ${type.toUpperCase()}`));
|
|
2007
|
-
for (const item of items) {
|
|
2008
|
-
const version = item.version ? chalk3.dim(` v${item.version}`) : "";
|
|
2009
|
-
logger.log(` ${chalk3.green("\u25C9")} ${chalk3.bold(item.name)}${version}`);
|
|
2010
|
-
}
|
|
2011
|
-
logger.newline();
|
|
2012
|
-
}
|
|
2013
|
-
logger.log(chalk3.dim("Run cpm uninstall <package-name> to remove a package"));
|
|
2014
|
-
logger.log(chalk3.dim(" e.g., cpm uninstall backend-patterns"));
|
|
2657
|
+
const byType = groupByType(packages);
|
|
2658
|
+
displayByType(byType);
|
|
2659
|
+
displayFooter();
|
|
2015
2660
|
} catch (error) {
|
|
2016
2661
|
logger.error("Failed to list packages");
|
|
2017
2662
|
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
2018
2663
|
}
|
|
2019
2664
|
}
|
|
2020
2665
|
|
|
2021
|
-
// src/commands/
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
var TEMPLATE = `# Package manifest for cpm
|
|
2026
|
-
# https://cpm-ai.dev/docs/packages
|
|
2027
|
-
|
|
2028
|
-
name: my-package
|
|
2029
|
-
version: 0.1.0
|
|
2030
|
-
description: A brief description of your package
|
|
2031
|
-
type: rules # rules | skill | mcp | agent | hook | workflow | template | bundle
|
|
2032
|
-
|
|
2033
|
-
author:
|
|
2034
|
-
name: Your Name
|
|
2035
|
-
email: you@example.com
|
|
2036
|
-
url: https://github.com/yourusername
|
|
2037
|
-
|
|
2038
|
-
repository: https://github.com/yourusername/my-package
|
|
2039
|
-
license: MIT
|
|
2040
|
-
|
|
2041
|
-
keywords:
|
|
2042
|
-
- keyword1
|
|
2043
|
-
- keyword2
|
|
2044
|
-
|
|
2045
|
-
# Universal content (works on all platforms)
|
|
2046
|
-
universal:
|
|
2047
|
-
# File patterns this applies to
|
|
2048
|
-
globs:
|
|
2049
|
-
- "**/*.ts"
|
|
2050
|
-
- "**/*.tsx"
|
|
2051
|
-
|
|
2052
|
-
# Rules/instructions (markdown)
|
|
2053
|
-
rules: |
|
|
2054
|
-
You are an expert developer.
|
|
2055
|
-
|
|
2056
|
-
## Guidelines
|
|
2057
|
-
|
|
2058
|
-
- Follow best practices
|
|
2059
|
-
- Write clean, maintainable code
|
|
2060
|
-
- Include proper error handling
|
|
2061
|
-
|
|
2062
|
-
# Platform-specific configurations (optional)
|
|
2063
|
-
# platforms:
|
|
2064
|
-
# cursor:
|
|
2065
|
-
# settings:
|
|
2066
|
-
# alwaysApply: true
|
|
2067
|
-
# claude-code:
|
|
2068
|
-
# skill:
|
|
2069
|
-
# command: /my-command
|
|
2070
|
-
# description: What this skill does
|
|
2071
|
-
|
|
2072
|
-
# MCP server configuration (if type: mcp)
|
|
2073
|
-
# mcp:
|
|
2074
|
-
# command: npx
|
|
2075
|
-
# args: ["your-mcp-server"]
|
|
2076
|
-
# env:
|
|
2077
|
-
# API_KEY: "\${API_KEY}"
|
|
2078
|
-
`;
|
|
2079
|
-
async function initCommand(_options) {
|
|
2080
|
-
const manifestPath = path8.join(process.cwd(), "cpm.yaml");
|
|
2081
|
-
if (await fs7.pathExists(manifestPath)) {
|
|
2082
|
-
logger.warn("cpm.yaml already exists in this directory");
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
try {
|
|
2086
|
-
await fs7.writeFile(manifestPath, TEMPLATE, "utf-8");
|
|
2087
|
-
logger.success("Created cpm.yaml");
|
|
2088
|
-
logger.newline();
|
|
2089
|
-
logger.log("Next steps:");
|
|
2090
|
-
logger.log(chalk4.dim(" 1. Edit cpm.yaml to configure your package"));
|
|
2091
|
-
logger.log(chalk4.dim(" 2. Run cpm publish to publish to the registry"));
|
|
2092
|
-
logger.newline();
|
|
2093
|
-
logger.log(
|
|
2094
|
-
chalk4.dim(
|
|
2095
|
-
`Learn more: ${chalk4.cyan("https://cpm-ai.dev/docs/publishing")}`
|
|
2096
|
-
)
|
|
2097
|
-
);
|
|
2098
|
-
} catch (error) {
|
|
2099
|
-
logger.error("Failed to create cpm.yaml");
|
|
2100
|
-
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
2666
|
+
// src/commands/uninstall.ts
|
|
2667
|
+
function extractFolderName(packageName) {
|
|
2668
|
+
if (packageName.includes("/")) {
|
|
2669
|
+
return packageName.split("/").pop() || packageName;
|
|
2101
2670
|
}
|
|
2671
|
+
return packageName.replace(/^@/, "");
|
|
2672
|
+
}
|
|
2673
|
+
function displayRemovedFiles(files) {
|
|
2674
|
+
logger.log(SEMANTIC_COLORS.dim("\nFiles removed:"));
|
|
2675
|
+
const formatted = formatRemovedFiles(files);
|
|
2676
|
+
formatted.forEach((line) => logger.log(line));
|
|
2102
2677
|
}
|
|
2103
|
-
|
|
2104
|
-
// src/commands/uninstall.ts
|
|
2105
|
-
import chalk5 from "chalk";
|
|
2106
|
-
import ora3 from "ora";
|
|
2107
2678
|
async function uninstallCommand(packageName) {
|
|
2108
|
-
const spinner =
|
|
2679
|
+
const spinner = createSpinner(spinnerText("Uninstalling", packageName));
|
|
2109
2680
|
try {
|
|
2110
|
-
const folderName =
|
|
2681
|
+
const folderName = extractFolderName(packageName);
|
|
2111
2682
|
const adapter = getAdapter("claude-code");
|
|
2112
2683
|
const result = await adapter.uninstall(folderName, process.cwd());
|
|
2113
2684
|
if (result.success && result.filesWritten.length > 0) {
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
logger.log(chalk5.dim("\nFiles removed:"));
|
|
2117
|
-
for (const file of result.filesWritten) {
|
|
2118
|
-
logger.log(chalk5.dim(` - ${file}`));
|
|
2119
|
-
}
|
|
2685
|
+
spinner.succeed(successText("Uninstalled", packageName));
|
|
2686
|
+
displayRemovedFiles(result.filesWritten);
|
|
2120
2687
|
} else if (result.success) {
|
|
2121
|
-
|
|
2122
|
-
else logger.warn(`Package ${packageName} was not found`);
|
|
2688
|
+
spinner.warn(`Package ${packageName} was not found`);
|
|
2123
2689
|
} else {
|
|
2124
|
-
|
|
2125
|
-
else logger.error(`Failed to uninstall: ${result.error}`);
|
|
2690
|
+
spinner.fail(failText("Failed to uninstall", packageName, result.error));
|
|
2126
2691
|
}
|
|
2127
2692
|
} catch (error) {
|
|
2128
|
-
|
|
2129
|
-
else logger.error(`Failed to uninstall ${packageName}`);
|
|
2693
|
+
spinner.fail(failText("Failed to uninstall", packageName));
|
|
2130
2694
|
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
2131
2695
|
}
|
|
2132
2696
|
}
|
|
@@ -2134,16 +2698,18 @@ async function uninstallCommand(packageName) {
|
|
|
2134
2698
|
// src/index.ts
|
|
2135
2699
|
var program = new Command();
|
|
2136
2700
|
var logo = `
|
|
2137
|
-
${
|
|
2138
|
-
${
|
|
2139
|
-
${
|
|
2140
|
-
${
|
|
2141
|
-
${
|
|
2142
|
-
${
|
|
2701
|
+
${chalk5.hex("#22d3ee")("\u2591\u2588\u2588\u2588\u2588\u2588\u2557\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2591\u2588\u2588\u2588\u2557\u2591\u2591\u2591\u2588\u2588\u2588\u2557")}
|
|
2702
|
+
${chalk5.hex("#22d3ee")("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557\u2591\u2588\u2588\u2588\u2588\u2551")}
|
|
2703
|
+
${chalk5.hex("#4ade80")("\u2588\u2588\u2551\u2591\u2591\u255A\u2550\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551")}
|
|
2704
|
+
${chalk5.hex("#4ade80")("\u2588\u2588\u2551\u2591\u2591\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u255D\u2591\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551")}
|
|
2705
|
+
${chalk5.hex("#a3e635")("\u255A\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2551\u2591\u255A\u2550\u255D\u2591\u2588\u2588\u2551")}
|
|
2706
|
+
${chalk5.hex("#a3e635")("\u2591\u255A\u2550\u2550\u2550\u2550\u255D\u2591\u255A\u2550\u255D\u2591\u2591\u2591\u2591\u2591\u255A\u2550\u255D\u2591\u2591\u2591\u2591\u2591\u255A\u2550\u255D")}
|
|
2143
2707
|
`;
|
|
2144
|
-
program.name("cpm").description(
|
|
2145
|
-
|
|
2146
|
-
|
|
2708
|
+
program.name("cpm").description(
|
|
2709
|
+
`${logo}
|
|
2710
|
+
${chalk5.dim("Package manager for AI coding assistants")}
|
|
2711
|
+
`
|
|
2712
|
+
).version("0.2.0-beta.1").option("-q, --quiet", "Suppress all output except errors").option("-v, --verbose", "Enable verbose output for debugging").hook("preAction", (thisCommand) => {
|
|
2147
2713
|
const opts = thisCommand.optsWithGlobals();
|
|
2148
2714
|
configureLogger({
|
|
2149
2715
|
quiet: opts.quiet,
|
|
@@ -2154,14 +2720,4 @@ program.command("install <package>").alias("i").description("Install a package")
|
|
|
2154
2720
|
program.command("uninstall <package>").alias("rm").description("Uninstall a package").option("-p, --platform <platform>", "Target platform").action(uninstallCommand);
|
|
2155
2721
|
program.command("search <query>").alias("s").description("Search for packages").option("-t, --type <type>", "Filter by type (rules, mcp, skill, agent)").option("-l, --limit <number>", "Limit results", "10").action(searchCommand);
|
|
2156
2722
|
program.command("list").alias("ls").description("List installed packages").action(listCommand);
|
|
2157
|
-
program.command("init").description("Create a new cpm package").option("-y, --yes", "Skip prompts and use defaults").action(initCommand);
|
|
2158
|
-
program.command("info <package>").description("Show package details").action(async () => {
|
|
2159
|
-
logger.warn("Coming soon: package info");
|
|
2160
|
-
});
|
|
2161
|
-
program.command("update").alias("up").description("Update installed packages").action(async () => {
|
|
2162
|
-
logger.warn("Coming soon: package updates");
|
|
2163
|
-
});
|
|
2164
|
-
program.command("publish").description("Publish a package to the registry").action(async () => {
|
|
2165
|
-
logger.warn("Coming soon: package publishing");
|
|
2166
|
-
});
|
|
2167
2723
|
program.parse();
|