@cpmai/cli 0.1.3 → 0.2.0-beta.1

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