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