@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.
Files changed (3) hide show
  1. package/README.md +1 -54
  2. package/dist/index.js +1762 -1179
  3. 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 chalk6 from "chalk";
5
+ import chalk5 from "chalk";
6
6
 
7
- // src/commands/install.ts
8
- import chalk from "chalk";
9
- import ora from "ora";
10
- import path6 from "path";
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/adapters/claude-code.ts
13
- import fs2 from "fs-extra";
14
- import path2 from "path";
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/utils/platform.ts
21
- import fs from "fs-extra";
22
- import path from "path";
23
- import os from "os";
24
- var detectionRules = {
25
- "claude-code": {
26
- paths: [".claude", ".claude.json"],
27
- global: [
28
- path.join(os.homedir(), ".claude.json"),
29
- path.join(os.homedir(), ".claude")
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
- async function detectPlatforms(projectPath = process.cwd()) {
34
- const results = [];
35
- for (const [platform, rules] of Object.entries(detectionRules)) {
36
- let detected = false;
37
- let configPath;
38
- for (const p of rules.paths) {
39
- const fullPath = path.join(projectPath, p);
40
- if (await fs.pathExists(fullPath)) {
41
- detected = true;
42
- configPath = fullPath;
43
- break;
44
- }
45
- }
46
- if (!detected) {
47
- for (const p of rules.global) {
48
- if (await fs.pathExists(p)) {
49
- detected = true;
50
- configPath = p;
51
- break;
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
- results.push({
56
- platform,
57
- detected,
58
- configPath
59
- });
222
+ return handler;
60
223
  }
61
- return results;
62
- }
63
- async function getDetectedPlatforms(projectPath = process.cwd()) {
64
- const detections = await detectPlatforms(projectPath);
65
- return detections.filter((d) => d.detected).map((d) => d.platform);
66
- }
67
- function getClaudeCodeHome() {
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 path.join(getClaudeCodeHome(), "rules");
292
+ return path2.join(getClaudeHome(), "rules");
75
293
  }
76
294
  function getSkillsPath() {
77
- return path.join(getClaudeCodeHome(), "skills");
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/adapters/claude-code.ts
173
- var ALLOWED_MCP_COMMANDS = [
174
- "npx",
175
- "node",
176
- "python",
177
- "python3",
178
- "deno",
179
- "bun",
180
- "uvx"
181
- ];
182
- var BLOCKED_MCP_ARG_PATTERNS = [
183
- /--eval/i,
184
- /-e\s/,
185
- /-c\s/,
186
- /\bcurl\b/i,
187
- /\bwget\b/i,
188
- /\brm\s/i,
189
- /\bsudo\b/i,
190
- /\bchmod\b/i,
191
- /\bchown\b/i,
192
- /[|;&`$]/
193
- // Shell metacharacters
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
- const baseCommand = path2.basename(mcp.command);
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 '${baseCommand}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
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 argsString = mcp.args.join(" ");
210
- for (const pattern of BLOCKED_MCP_ARG_PATTERNS) {
211
- if (pattern.test(argsString)) {
212
- return {
213
- valid: false,
214
- error: `MCP arguments contain blocked pattern: ${pattern.source}`
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 { safe: false, sanitized: "", error: "File name cannot be empty" };
440
+ return { valid: false, error: "File name cannot be empty", sanitized: "" };
224
441
  }
225
- const baseName = path2.basename(fileName);
442
+ const baseName = path3.basename(fileName);
226
443
  if (baseName.includes("\0")) {
227
444
  return {
228
- safe: false,
229
- sanitized: "",
230
- error: "File name contains null bytes"
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 { safe: false, sanitized: "", error: "Hidden files not allowed" };
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
- safe: false,
240
- sanitized: "",
241
- error: "Path traversal detected in file name"
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 { safe: false, sanitized: "", error: "Only .md files allowed" };
462
+ return { valid: false, error: "Only .md files allowed", sanitized: "" };
246
463
  }
247
- return { safe: true, sanitized };
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 = path2.normalize(sanitized);
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 = path2.join("/test", sanitized);
275
- const resolved = path2.resolve(testPath);
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 = path2.resolve(filePath);
283
- const resolvedDir = path2.resolve(directory);
284
- return resolvedPath.startsWith(resolvedDir + path2.sep) || resolvedPath === 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
- type: manifest.type || "unknown",
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 = path2.join(packageDir, ".cpm.json");
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 ClaudeCodeAdapter = class extends PlatformAdapter {
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
- * Remove an MCP server configuration from ~/.claude.json
531
+ * Identifies this handler as handling "rules" type packages.
532
+ * The registry uses this to route rules packages to this handler.
403
533
  */
404
- async removeMcpServer(serverName, filesWritten) {
405
- const claudeHome = getClaudeCodeHome();
406
- const mcpConfigPath = path2.join(path2.dirname(claudeHome), ".claude.json");
407
- if (!await fs2.pathExists(mcpConfigPath)) {
408
- return;
409
- }
410
- try {
411
- const config = await fs2.readJson(mcpConfigPath);
412
- const mcpServers = config.mcpServers;
413
- if (!mcpServers || !mcpServers[serverName]) {
414
- return;
415
- }
416
- const { [serverName]: _removed, ...remainingServers } = mcpServers;
417
- const updatedConfig = {
418
- ...config,
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 = path2.join(rulesBaseDir, folderName);
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.safe) {
562
+ if (!validation.valid) {
444
563
  logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
445
564
  continue;
446
565
  }
447
- const srcPath = path2.join(packagePath, file);
448
- const destPath = path2.join(rulesDir, validation.sanitized);
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 = manifest.universal?.rules || manifest.universal?.prompt;
580
+ const rulesContent = this.getRulesContent(manifest);
462
581
  if (!rulesContent) return filesWritten;
463
- const rulesPath = path2.join(rulesDir, "RULES.md");
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
- async installSkill(manifest, _projectPath, packagePath) {
477
- const filesWritten = [];
478
- const skillsDir = getSkillsPath();
479
- const folderName = sanitizeFolderName(manifest.name);
480
- const skillDir = path2.join(skillsDir, folderName);
481
- await fs2.ensureDir(skillDir);
482
- if (packagePath && await fs2.pathExists(packagePath)) {
483
- const files = await fs2.readdir(packagePath);
484
- const contentFiles = files.filter(
485
- (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
486
- );
487
- if (contentFiles.length > 0) {
488
- for (const file of contentFiles) {
489
- const validation = sanitizeFileName(file);
490
- if (!validation.safe) {
491
- logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
492
- continue;
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
- if (manifest.skill) {
509
- const skillContent = this.formatSkillMd(manifest);
510
- const skillPath = path2.join(skillDir, "SKILL.md");
511
- await fs2.writeFile(skillPath, skillContent, "utf-8");
512
- filesWritten.push(skillPath);
513
- const metadataPath = await writePackageMetadata(skillDir, manifest);
514
- filesWritten.push(metadataPath);
515
- } else if (manifest.universal?.prompt || manifest.universal?.rules) {
516
- const content = manifest.universal.prompt || manifest.universal.rules || "";
517
- const skillPath = path2.join(skillDir, "SKILL.md");
518
- const skillContent = `# ${manifest.name}
519
-
520
- ${manifest.description}
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
- await fs2.writeFile(skillPath, skillContent, "utf-8");
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 writePackageMetadata(skillDir, manifest);
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
- async installMcp(manifest, _projectPath) {
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.mcp) return filesWritten;
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 = getClaudeCodeHome();
539
- const mcpConfigPath = path2.join(path2.dirname(claudeHome), ".claude.json");
821
+ const claudeHome = getClaudeHome();
822
+ const mcpConfigPath = path7.join(path7.dirname(claudeHome), ".claude.json");
540
823
  let existingConfig = {};
541
- if (await fs2.pathExists(mcpConfigPath)) {
824
+ if (await fs4.pathExists(mcpConfigPath)) {
542
825
  try {
543
- existingConfig = await fs2.readJson(mcpConfigPath);
826
+ existingConfig = await fs4.readJson(mcpConfigPath);
544
827
  } catch {
545
- logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
828
+ const backupPath = `${mcpConfigPath}.backup.${Date.now()}`;
829
+ try {
830
+ await fs4.copy(mcpConfigPath, backupPath);
831
+ logger.warn(
832
+ `Could not parse ${mcpConfigPath}, backup saved to ${backupPath}`
833
+ );
834
+ } catch {
835
+ logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
836
+ }
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 fs2.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
859
+ await fs4.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
563
860
  filesWritten.push(mcpConfigPath);
564
861
  return filesWritten;
565
862
  }
566
- formatSkillMd(manifest) {
567
- if (!manifest.skill) {
568
- throw new Error(
569
- "Cannot format skill markdown: manifest.skill is undefined"
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
- const skill = manifest.skill;
573
- const content = manifest.universal?.prompt || manifest.universal?.rules || "";
574
- return `---
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
- ## Instructions
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
- ${content.trim()}
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 fs4 from "fs-extra";
616
- import path4 from "path";
617
- import os3 from "os";
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 = path4.join(os3.homedir(), ".cpm", "cache");
643
- var CACHE_FILE = path4.join(CACHE_DIR, "registry.json");
644
- var CACHE_TTL = 5 * 60 * 1e3;
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 (!forceRefresh && this.cache && Date.now() - this.cacheTimestamp < CACHE_TTL) {
1035
+ if (this.hasValidMemoryCache() && !forceRefresh) {
657
1036
  return this.cache;
658
1037
  }
659
1038
  if (!forceRefresh) {
660
- try {
661
- await fs4.ensureDir(CACHE_DIR);
662
- if (await fs4.pathExists(CACHE_FILE)) {
663
- const stat = await fs4.stat(CACHE_FILE);
664
- if (Date.now() - stat.mtimeMs < CACHE_TTL) {
665
- const cached = await fs4.readJson(CACHE_FILE);
666
- this.cache = cached;
667
- this.cacheTimestamp = Date.now();
668
- return cached;
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: 1e4 },
1100
+ timeout: { request: TIMEOUTS.REGISTRY_FETCH },
677
1101
  responseType: "json"
678
1102
  });
679
1103
  const data = response.body;
680
- this.cache = data;
681
- this.cacheTimestamp = Date.now();
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
- if (this.cache) {
690
- return this.cache;
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
- if (await fs4.pathExists(CACHE_FILE)) {
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
- * Search for packages
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
- async search(options = {}) {
707
- const data = await this.fetch();
708
- let packages = [...data.packages];
709
- if (options.query) {
710
- const query = options.query.toLowerCase();
711
- packages = packages.filter(
712
- (pkg) => pkg.name?.toLowerCase().includes(query) || pkg.description?.toLowerCase().includes(query) || pkg.keywords?.some((k) => k?.toLowerCase().includes(query))
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 (options.type) {
716
- packages = packages.filter((pkg) => {
717
- try {
718
- return resolvePackageType(pkg) === options.type;
719
- } catch {
720
- return false;
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
- * Get a specific package by name
1307
+ * Name of this source for logging and debugging.
747
1308
  */
748
- async getPackage(name) {
749
- const data = await this.fetch();
750
- return data.packages.find((pkg) => pkg.name === name) || null;
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
- * Get package manifest from GitHub
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 getManifest(pkg) {
756
- if (!pkg.repository) {
757
- return null;
758
- }
1339
+ async fetch(pkg, context) {
1340
+ if (!pkg.tarball) return null;
759
1341
  try {
760
- const repoUrl = pkg.repository.replace(
761
- "github.com",
762
- "raw.githubusercontent.com"
763
- );
764
- const manifestUrl = `${repoUrl}/main/cpm.yaml`;
765
- const response = await got(manifestUrl, {
766
- timeout: { request: 1e4 }
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 yaml2 = await import("yaml");
769
- return yaml2.parse(response.body);
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
- * Fallback registry data when network is unavailable
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
- getFallbackRegistry() {
778
- return {
779
- version: 1,
780
- updated: (/* @__PURE__ */ new Date()).toISOString(),
781
- packages: [
782
- {
783
- name: "@cpm/nextjs-rules",
784
- version: "1.0.0",
785
- description: "Next.js 14+ App Router conventions and best practices for Claude Code",
786
- type: "rules",
787
- author: "cpm",
788
- downloads: 1250,
789
- stars: 89,
790
- verified: true,
791
- repository: "https://github.com/cpm-ai/nextjs-rules",
792
- tarball: "https://github.com/cpm-ai/nextjs-rules/releases/download/v1.0.0/package.tar.gz",
793
- keywords: ["nextjs", "react", "typescript", "app-router"]
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/utils/downloader.ts
1412
- var TEMP_DIR = path5.join(os4.tmpdir(), "cpm-downloads");
1413
- var PACKAGES_BASE_URL = process.env.CPM_PACKAGES_URL || "https://raw.githubusercontent.com/cpmai-dev/packages/main";
1414
- var TIMEOUTS = {
1415
- MANIFEST_FETCH: 5e3,
1416
- TARBALL_DOWNLOAD: 3e4,
1417
- API_REQUEST: 1e4
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
- function sanitizeFileName2(fileName) {
1420
- const sanitized = path5.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, "_");
1421
- if (!sanitized || sanitized.includes("..") || sanitized.startsWith(".")) {
1422
- throw new Error(`Invalid file name: ${fileName}`);
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
- return sanitized;
1425
- }
1426
- function validatePathWithinDir(destPath, allowedDir) {
1427
- const resolvedDest = path5.resolve(destPath);
1428
- const resolvedDir = path5.resolve(allowedDir);
1429
- if (!resolvedDest.startsWith(resolvedDir + path5.sep) && resolvedDest !== resolvedDir) {
1430
- throw new Error(`Path traversal detected: ${destPath}`);
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
- function validatePackagePath(pkgPath) {
1434
- const normalized = path5.normalize(pkgPath).replace(/\\/g, "/");
1435
- if (normalized.includes("..") || normalized.startsWith("/")) {
1436
- throw new Error(`Invalid package path: ${pkgPath}`);
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
- return normalized;
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 fs5.ensureDir(TEMP_DIR);
1443
- const packageTempDir = path5.join(
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 fs5.ensureDir(packageTempDir);
1448
- let manifest = null;
1449
- if (pkg.path) {
1450
- manifest = await fetchPackageFromPath(pkg, packageTempDir);
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 fs5.remove(tempDir);
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
- ${pkg.description}`
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 { valid: false, error: `Package name too long (max ${MAX_PACKAGE_NAME_LENGTH} characters)` };
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
- const hasPathTraversal = decoded.includes("..") || decoded.includes("\\") || decoded.includes("%2e") || decoded.includes("%2E") || decoded.includes("%5c") || decoded.includes("%5C") || decoded.includes("%2f") || decoded.includes("%2F");
1666
- if (hasPathTraversal) {
2144
+ if (hasPathTraversal(decoded)) {
1667
2145
  return { valid: false, error: "Invalid characters in package name" };
1668
2146
  }
1669
- const packageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
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
- async function resolveTargetPlatforms(options) {
1685
- if (options.platform && options.platform !== "all") {
1686
- if (!isValidPlatform(options.platform)) {
1687
- return null;
1688
- }
1689
- return [options.platform];
1690
- }
1691
- let platforms = await getDetectedPlatforms();
1692
- if (platforms.length === 0) {
1693
- platforms = ["claude-code"];
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
- platforms = platforms.filter((p) => p === "claude-code");
1696
- if (platforms.length === 0) {
1697
- platforms = ["claude-code"];
2229
+ if (num >= 1e3) {
2230
+ return `${(num / 1e3).toFixed(1)}k`;
1698
2231
  }
1699
- return platforms;
2232
+ return num.toString();
1700
2233
  }
1701
- async function installToPlatforms(manifest, tempDir, platforms) {
1702
- return Promise.all(
1703
- platforms.map(async (platform) => {
1704
- const adapter = getAdapter(platform);
1705
- return adapter.install(manifest, process.cwd(), tempDir);
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 displaySuccessMessage(manifest, successfulResults) {
1710
- logger.log(chalk.dim(`
1711
- ${manifest.description}`));
1712
- logger.log(chalk.dim("\n Files created:"));
1713
- for (const result of successfulResults) {
1714
- for (const file of result.filesWritten) {
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
- logger.newline();
1719
- displayUsageHints(manifest);
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 displayUsageHints(manifest) {
2275
+ function formatUsageHints(manifest) {
2276
+ const hints = [];
1722
2277
  switch (manifest.type) {
1723
2278
  case "skill":
1724
- if (manifest.skill?.command) {
1725
- logger.log(
1726
- ` ${chalk.cyan("Usage:")} Type ${chalk.yellow(manifest.skill.command)} in Claude Code`
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
- logger.log(
1732
- ` ${chalk.cyan("Usage:")} Rules are automatically applied to matching files`
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
- logger.log(
1737
- ` ${chalk.cyan("Usage:")} MCP server configured. Restart Claude Code to activate.`
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
- logger.log(chalk.yellow(`
1743
- Required environment variables:`));
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
- logger.log(chalk.dim(` - ${envVar}`));
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 displayWarnings(failedResults) {
1753
- if (failedResults.length === 0) return;
1754
- logger.log(chalk.yellow("\n Warnings:"));
1755
- for (const result of failedResults) {
1756
- logger.log(chalk.yellow(` - ${result.platform}: ${result.error}`));
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
- async function installCommand(packageName, options) {
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 spinner = logger.isQuiet() ? null : ora(`Installing ${chalk.cyan(packageName)}...`).start();
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
- if (spinner) spinner.text = `Searching for ${chalk.cyan(normalizedName)}...`;
2474
+ spinner.update(spinnerText("Searching for", normalizedName));
1770
2475
  const pkg = await registry.getPackage(normalizedName);
1771
2476
  if (!pkg) {
1772
- if (spinner) spinner.fail(`Package ${chalk.red(normalizedName)} not found`);
1773
- else logger.error(`Package ${normalizedName} not found`);
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
- if (spinner) spinner.text = `Downloading ${chalk.cyan(pkg.name)}@${pkg.version}...`;
2481
+ spinner.update(spinnerText("Downloading", `${pkg.name}@${pkg.version}`));
1779
2482
  const downloadResult = await downloadPackage(pkg);
1780
2483
  if (!downloadResult.success) {
1781
- if (spinner) spinner.fail(`Failed to download ${pkg.name}: ${downloadResult.error}`);
1782
- else logger.error(`Failed to download ${pkg.name}: ${downloadResult.error}`);
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 = await resolveTargetPlatforms(options);
1787
- if (!targetPlatforms) {
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
- downloadResult.manifest,
2495
+ manifest,
1797
2496
  tempDir,
1798
2497
  targetPlatforms
1799
2498
  );
1800
- const successful = results.filter((r) => r.success);
1801
- const failed = results.filter((r) => !r.success);
2499
+ const [successful, failed] = partitionResults(results);
1802
2500
  if (successful.length > 0) {
1803
- if (spinner) {
1804
- spinner.succeed(
1805
- `Installed ${chalk.green(downloadResult.manifest.name)}@${chalk.dim(downloadResult.manifest.version)}`
1806
- );
1807
- } else {
1808
- logger.success(`Installed ${downloadResult.manifest.name}@${downloadResult.manifest.version}`);
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
- if (spinner) spinner.fail(`Failed to install ${packageName}`);
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
- import chalk2 from "chalk";
1828
- import ora2 from "ora";
1829
- var typeColors = {
1830
- rules: chalk2.yellow,
1831
- skill: chalk2.blue,
1832
- mcp: chalk2.magenta,
1833
- agent: chalk2.green,
1834
- hook: chalk2.cyan,
1835
- workflow: chalk2.red,
1836
- template: chalk2.white,
1837
- bundle: chalk2.gray
1838
- };
1839
- var typeEmoji = {
1840
- rules: "\u{1F4DC}",
1841
- skill: "\u26A1",
1842
- mcp: "\u{1F50C}",
1843
- agent: "\u{1F916}",
1844
- hook: "\u{1FA9D}",
1845
- workflow: "\u{1F4CB}",
1846
- template: "\u{1F4C1}",
1847
- bundle: "\u{1F4E6}"
1848
- };
1849
- async function searchCommand(query, options) {
1850
- const spinner = logger.isQuiet() ? null : ora2(`Searching for "${query}"...`).start();
1851
- const parsedLimit = parseInt(options.limit || "10", 10);
1852
- const limit = Number.isNaN(parsedLimit) ? 10 : Math.max(1, Math.min(parsedLimit, 100));
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 searchOptions = {
1855
- query,
1856
- limit
1857
- };
1858
- if (options.type) {
1859
- searchOptions.type = options.type;
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
- logger.warn(`No packages found for "${query}"`);
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
- logger.log(chalk2.dim(`
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
- if (spinner) spinner.fail("Search failed");
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 chalk3 from "chalk";
1913
- import fs6 from "fs-extra";
1914
- import path7 from "path";
1915
- import os5 from "os";
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 = path7.join(packageDir, ".cpm.json");
2573
+ const metadataPath = path13.join(packageDir, ".cpm.json");
1923
2574
  try {
1924
- if (await fs6.pathExists(metadataPath)) {
1925
- return await fs6.readJson(metadataPath);
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 scanInstalledPackages() {
2582
+ async function scanDirectory(dir, type) {
1932
2583
  const items = [];
1933
- const claudeHome = path7.join(os5.homedir(), ".claude");
1934
- const rulesDir = path7.join(claudeHome, "rules");
1935
- if (await fs6.pathExists(rulesDir)) {
1936
- const entries = await fs6.readdir(rulesDir);
1937
- for (const entry of entries) {
1938
- const entryPath = path7.join(rulesDir, entry);
1939
- const stat = await fs6.stat(entryPath);
1940
- if (stat.isDirectory()) {
1941
- const metadata = await readPackageMetadata(entryPath);
1942
- items.push({
1943
- name: metadata?.name || entry,
1944
- folderName: entry,
1945
- type: "rules",
1946
- version: metadata?.version,
1947
- path: entryPath
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
- const skillsDir = path7.join(claudeHome, "skills");
1953
- if (await fs6.pathExists(skillsDir)) {
1954
- const dirs = await fs6.readdir(skillsDir);
1955
- for (const dir of dirs) {
1956
- const skillPath = path7.join(skillsDir, dir);
1957
- const stat = await fs6.stat(skillPath);
1958
- if (stat.isDirectory()) {
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
- const mcpConfigPath = path7.join(os5.homedir(), ".claude.json");
1971
- if (await fs6.pathExists(mcpConfigPath)) {
1972
- try {
1973
- const config = await fs6.readJson(mcpConfigPath);
1974
- const mcpServers = config.mcpServers || {};
1975
- for (const name of Object.keys(mcpServers)) {
1976
- items.push({
1977
- name,
1978
- folderName: name,
1979
- type: "mcp",
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
- logger.warn("No packages installed");
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(chalk3.bold(`
2681
+ logger.log(chalk4.bold(`
1998
2682
  Installed packages (${packages.length}):
1999
2683
  `));
2000
- const byType = packages.reduce((acc, pkg) => ({
2001
- ...acc,
2002
- [pkg.type]: [...acc[pkg.type] || [], pkg]
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/init.ts
2022
- import chalk4 from "chalk";
2023
- import fs7 from "fs-extra";
2024
- import path8 from "path";
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 = logger.isQuiet() ? null : ora3(`Uninstalling ${chalk5.cyan(packageName)}...`).start();
2706
+ const spinner = createSpinner(spinnerText("Uninstalling", packageName));
2109
2707
  try {
2110
- const folderName = packageName.includes("/") ? packageName.split("/").pop() || packageName : packageName.replace(/^@/, "");
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
- if (spinner) spinner.succeed(`Uninstalled ${chalk5.green(packageName)}`);
2115
- else logger.success(`Uninstalled ${packageName}`);
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
- if (spinner) spinner.warn(`Package ${packageName} was not found`);
2122
- else logger.warn(`Package ${packageName} was not found`);
2715
+ spinner.warn(`Package ${packageName} was not found`);
2123
2716
  } else {
2124
- if (spinner) spinner.fail(`Failed to uninstall: ${result.error}`);
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
- if (spinner) spinner.fail(`Failed to uninstall ${packageName}`);
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
- ${chalk6.hex("#f97316")("\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")}
2138
- ${chalk6.hex("#f97316")("\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")}
2139
- ${chalk6.hex("#fb923c")("\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")}
2140
- ${chalk6.hex("#fb923c")("\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")}
2141
- ${chalk6.hex("#fbbf24")("\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")}
2142
- ${chalk6.hex("#fbbf24")("\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")}
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(`${logo}
2145
- ${chalk6.dim("The package manager for Claude Code")}
2146
- `).version("0.1.0").option("-q, --quiet", "Suppress all output except errors").option("-v, --verbose", "Enable verbose output for debugging").hook("preAction", (thisCommand) => {
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();