@cpmai/cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2167 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
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";
15
+
16
+ // src/adapters/base.ts
17
+ var PlatformAdapter = class {
18
+ };
19
+
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
+ ]
31
+ }
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
+ }
54
+ }
55
+ results.push({
56
+ platform,
57
+ detected,
58
+ configPath
59
+ });
60
+ }
61
+ return results;
62
+ }
63
+ async function getDetectedPlatforms(projectPath = process.cwd()) {
64
+ const detections = await detectPlatforms(projectPath);
65
+ return detections.filter((d) => d.detected).map((d) => d.platform);
66
+ }
67
+ function getClaudeCodeHome() {
68
+ return path.join(os.homedir(), ".claude");
69
+ }
70
+ function getRulesPath(platform) {
71
+ if (platform !== "claude-code") {
72
+ throw new Error(`Rules path is not supported for platform: ${platform}`);
73
+ }
74
+ return path.join(getClaudeCodeHome(), "rules");
75
+ }
76
+ function getSkillsPath() {
77
+ return path.join(getClaudeCodeHome(), "skills");
78
+ }
79
+
80
+ // src/utils/logger.ts
81
+ import { createConsola } from "consola";
82
+ function getLogLevel(options) {
83
+ if (options.quiet) return 0;
84
+ if (options.verbose) return 4;
85
+ return 3;
86
+ }
87
+ function createLogger(options = {}) {
88
+ const consola = createConsola({
89
+ level: getLogLevel(options),
90
+ formatOptions: {
91
+ date: false,
92
+ colors: true,
93
+ compact: true
94
+ }
95
+ });
96
+ 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
+ /**
110
+ * Success message
111
+ */
112
+ success: (message, ...args) => {
113
+ consola.success(message, ...args);
114
+ },
115
+ /**
116
+ * Warning message
117
+ */
118
+ warn: (message, ...args) => {
119
+ consola.warn(message, ...args);
120
+ },
121
+ /**
122
+ * Error message
123
+ */
124
+ error: (message, ...args) => {
125
+ consola.error(message, ...args);
126
+ },
127
+ /**
128
+ * Plain output (always shown, no prefix)
129
+ * Use for formatted CLI output like tables, lists
130
+ */
131
+ log: (message) => {
132
+ if (!options.quiet) {
133
+ console.log(message);
134
+ }
135
+ },
136
+ /**
137
+ * Output a blank line
138
+ */
139
+ newline: () => {
140
+ if (!options.quiet) {
141
+ console.log();
142
+ }
143
+ },
144
+ /**
145
+ * Check if in quiet mode
146
+ */
147
+ isQuiet: () => options.quiet ?? false,
148
+ /**
149
+ * Check if in verbose mode
150
+ */
151
+ isVerbose: () => options.verbose ?? false
152
+ };
153
+ }
154
+ var loggerOptions = {};
155
+ var loggerInstance = createLogger(loggerOptions);
156
+ function configureLogger(options) {
157
+ loggerOptions = options;
158
+ loggerInstance = createLogger(options);
159
+ }
160
+ var logger = {
161
+ debug: (message, ...args) => loggerInstance.debug(message, ...args),
162
+ info: (message, ...args) => loggerInstance.info(message, ...args),
163
+ success: (message, ...args) => loggerInstance.success(message, ...args),
164
+ warn: (message, ...args) => loggerInstance.warn(message, ...args),
165
+ error: (message, ...args) => loggerInstance.error(message, ...args),
166
+ log: (message) => loggerInstance.log(message),
167
+ newline: () => loggerInstance.newline(),
168
+ isQuiet: () => loggerInstance.isQuiet(),
169
+ isVerbose: () => loggerInstance.isVerbose()
170
+ };
171
+
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
+ ];
195
+ function validateMcpConfig(mcp) {
196
+ if (!mcp?.command) {
197
+ return { valid: false, error: "MCP command is required" };
198
+ }
199
+ const baseCommand = path2.basename(mcp.command);
200
+ if (!ALLOWED_MCP_COMMANDS.includes(
201
+ baseCommand
202
+ )) {
203
+ return {
204
+ valid: false,
205
+ error: `MCP command '${baseCommand}' is not allowed. Allowed: ${ALLOWED_MCP_COMMANDS.join(", ")}`
206
+ };
207
+ }
208
+ 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
+ }
217
+ }
218
+ }
219
+ return { valid: true };
220
+ }
221
+ function sanitizeFileName(fileName) {
222
+ if (!fileName || typeof fileName !== "string") {
223
+ return { safe: false, sanitized: "", error: "File name cannot be empty" };
224
+ }
225
+ const baseName = path2.basename(fileName);
226
+ if (baseName.includes("\0")) {
227
+ return {
228
+ safe: false,
229
+ sanitized: "",
230
+ error: "File name contains null bytes"
231
+ };
232
+ }
233
+ if (baseName.startsWith(".") && baseName !== ".md") {
234
+ return { safe: false, sanitized: "", error: "Hidden files not allowed" };
235
+ }
236
+ const sanitized = baseName.replace(/[<>:"|?*\\]/g, "_");
237
+ if (sanitized.includes("..") || sanitized.includes("/") || sanitized.includes("\\")) {
238
+ return {
239
+ safe: false,
240
+ sanitized: "",
241
+ error: "Path traversal detected in file name"
242
+ };
243
+ }
244
+ if (!sanitized.endsWith(".md")) {
245
+ return { safe: false, sanitized: "", error: "Only .md files allowed" };
246
+ }
247
+ return { safe: true, sanitized };
248
+ }
249
+ function sanitizeFolderName(name) {
250
+ if (!name || typeof name !== "string") {
251
+ throw new Error("Package name cannot be empty");
252
+ }
253
+ let decoded = name;
254
+ try {
255
+ decoded = decodeURIComponent(name);
256
+ } catch {
257
+ }
258
+ if (decoded.includes("\0")) {
259
+ throw new Error("Invalid package name: contains null bytes");
260
+ }
261
+ let sanitized = decoded.includes("/") ? decoded.split("/").pop() || decoded : decoded.replace(/^@/, "");
262
+ sanitized = sanitized.replace(/\.\./g, "");
263
+ sanitized = sanitized.replace(/%2e%2e/gi, "");
264
+ sanitized = sanitized.replace(/%2f/gi, "");
265
+ sanitized = sanitized.replace(/%5c/gi, "");
266
+ sanitized = sanitized.replace(/[<>:"|?*\\]/g, "");
267
+ if (!sanitized || sanitized.startsWith(".")) {
268
+ throw new Error(`Invalid package name: ${name}`);
269
+ }
270
+ const normalized = path2.normalize(sanitized);
271
+ if (normalized !== sanitized || normalized.includes("..")) {
272
+ throw new Error(`Invalid package name (path traversal detected): ${name}`);
273
+ }
274
+ const testPath = path2.join("/test", sanitized);
275
+ const resolved = path2.resolve(testPath);
276
+ if (!resolved.startsWith("/test/")) {
277
+ throw new Error(`Invalid package name (path traversal detected): ${name}`);
278
+ }
279
+ return sanitized;
280
+ }
281
+ 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;
285
+ }
286
+ async function writePackageMetadata(packageDir, manifest) {
287
+ const metadata = {
288
+ name: manifest.name,
289
+ version: manifest.version,
290
+ type: manifest.type || "unknown",
291
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
292
+ };
293
+ const metadataPath = path2.join(packageDir, ".cpm.json");
294
+ try {
295
+ await fs2.writeJson(metadataPath, metadata, { spaces: 2 });
296
+ } catch (error) {
297
+ logger.warn(
298
+ `Could not write metadata: ${error instanceof Error ? error.message : "Unknown error"}`
299
+ );
300
+ }
301
+ return metadataPath;
302
+ }
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
+ }
401
+ /**
402
+ * Remove an MCP server configuration from ~/.claude.json
403
+ */
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) {
430
+ const filesWritten = [];
431
+ const rulesBaseDir = getRulesPath("claude-code");
432
+ const folderName = sanitizeFolderName(manifest.name);
433
+ const rulesDir = path2.join(rulesBaseDir, folderName);
434
+ await fs2.ensureDir(rulesDir);
435
+ if (packagePath && await fs2.pathExists(packagePath)) {
436
+ const files = await fs2.readdir(packagePath);
437
+ const mdFiles = files.filter(
438
+ (f) => f.endsWith(".md") && f.toLowerCase() !== "cpm.yaml"
439
+ );
440
+ if (mdFiles.length > 0) {
441
+ for (const file of mdFiles) {
442
+ const validation = sanitizeFileName(file);
443
+ if (!validation.safe) {
444
+ logger.warn(`Skipping unsafe file: ${file} (${validation.error})`);
445
+ continue;
446
+ }
447
+ const srcPath = path2.join(packagePath, file);
448
+ const destPath = path2.join(rulesDir, validation.sanitized);
449
+ if (!isPathWithinDirectory(destPath, rulesDir)) {
450
+ logger.warn(`Blocked path traversal attempt: ${file}`);
451
+ continue;
452
+ }
453
+ await fs2.copy(srcPath, destPath);
454
+ filesWritten.push(destPath);
455
+ }
456
+ const metadataPath2 = await writePackageMetadata(rulesDir, manifest);
457
+ filesWritten.push(metadataPath2);
458
+ return filesWritten;
459
+ }
460
+ }
461
+ const rulesContent = manifest.universal?.rules || manifest.universal?.prompt;
462
+ if (!rulesContent) return filesWritten;
463
+ const rulesPath = path2.join(rulesDir, "RULES.md");
464
+ const content = `# ${manifest.name}
465
+
466
+ ${manifest.description}
467
+
468
+ ${rulesContent.trim()}
469
+ `;
470
+ await fs2.writeFile(rulesPath, content, "utf-8");
471
+ filesWritten.push(rulesPath);
472
+ const metadataPath = await writePackageMetadata(rulesDir, manifest);
473
+ filesWritten.push(metadataPath);
474
+ return filesWritten;
475
+ }
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
+ }
507
+ }
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}
521
+
522
+ ${content.trim()}
523
+ `;
524
+ await fs2.writeFile(skillPath, skillContent, "utf-8");
525
+ filesWritten.push(skillPath);
526
+ const metadataPath = await writePackageMetadata(skillDir, manifest);
527
+ filesWritten.push(metadataPath);
528
+ }
529
+ return filesWritten;
530
+ }
531
+ async installMcp(manifest, _projectPath) {
532
+ const filesWritten = [];
533
+ if (!manifest.mcp) return filesWritten;
534
+ const mcpValidation = validateMcpConfig(manifest.mcp);
535
+ if (!mcpValidation.valid) {
536
+ throw new Error(`MCP security validation failed: ${mcpValidation.error}`);
537
+ }
538
+ const claudeHome = getClaudeCodeHome();
539
+ const mcpConfigPath = path2.join(path2.dirname(claudeHome), ".claude.json");
540
+ let existingConfig = {};
541
+ if (await fs2.pathExists(mcpConfigPath)) {
542
+ try {
543
+ existingConfig = await fs2.readJson(mcpConfigPath);
544
+ } catch {
545
+ logger.warn(`Could not parse ${mcpConfigPath}, creating new config`);
546
+ existingConfig = {};
547
+ }
548
+ }
549
+ const sanitizedName = sanitizeFolderName(manifest.name);
550
+ const existingMcpServers = existingConfig.mcpServers || {};
551
+ const updatedConfig = {
552
+ ...existingConfig,
553
+ mcpServers: {
554
+ ...existingMcpServers,
555
+ [sanitizedName]: {
556
+ command: manifest.mcp.command,
557
+ args: manifest.mcp.args,
558
+ env: manifest.mcp.env
559
+ }
560
+ }
561
+ };
562
+ await fs2.writeJson(mcpConfigPath, updatedConfig, { spaces: 2 });
563
+ filesWritten.push(mcpConfigPath);
564
+ return filesWritten;
565
+ }
566
+ formatSkillMd(manifest) {
567
+ if (!manifest.skill) {
568
+ throw new Error(
569
+ "Cannot format skill markdown: manifest.skill is undefined"
570
+ );
571
+ }
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}
584
+
585
+ ## Instructions
586
+
587
+ ${content.trim()}
588
+ `;
589
+ }
590
+ };
591
+
592
+ // src/adapters/index.ts
593
+ var adapters = {
594
+ "claude-code": new ClaudeCodeAdapter()
595
+ };
596
+ function getAdapter(platform) {
597
+ return adapters[platform];
598
+ }
599
+
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
+ // src/utils/registry.ts
614
+ 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
641
+ 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;
645
+ var Registry = class {
646
+ registryUrl;
647
+ cache = null;
648
+ cacheTimestamp = 0;
649
+ constructor(registryUrl = DEFAULT_REGISTRY_URL) {
650
+ this.registryUrl = registryUrl;
651
+ }
652
+ /**
653
+ * Fetch the registry data (with caching)
654
+ */
655
+ async fetch(forceRefresh = false) {
656
+ if (!forceRefresh && this.cache && Date.now() - this.cacheTimestamp < CACHE_TTL) {
657
+ return this.cache;
658
+ }
659
+ 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
+ }
670
+ }
671
+ } catch {
672
+ }
673
+ }
674
+ try {
675
+ const response = await got(this.registryUrl, {
676
+ timeout: { request: 1e4 },
677
+ responseType: "json"
678
+ });
679
+ 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
+ }
687
+ return data;
688
+ } catch {
689
+ if (this.cache) {
690
+ return this.cache;
691
+ }
692
+ try {
693
+ if (await fs4.pathExists(CACHE_FILE)) {
694
+ const cached = await fs4.readJson(CACHE_FILE);
695
+ this.cache = cached;
696
+ return cached;
697
+ }
698
+ } catch {
699
+ }
700
+ return this.getFallbackRegistry();
701
+ }
702
+ }
703
+ /**
704
+ * Search for packages
705
+ */
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;
721
+ }
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
+ }
738
+ });
739
+ const total = packages.length;
740
+ const offset = options.offset || 0;
741
+ const limit = options.limit || 10;
742
+ packages = packages.slice(offset, offset + limit);
743
+ return { packages, total };
744
+ }
745
+ /**
746
+ * Get a specific package by name
747
+ */
748
+ async getPackage(name) {
749
+ const data = await this.fetch();
750
+ return data.packages.find((pkg) => pkg.name === name) || null;
751
+ }
752
+ /**
753
+ * Get package manifest from GitHub
754
+ */
755
+ async getManifest(pkg) {
756
+ if (!pkg.repository) {
757
+ return null;
758
+ }
759
+ 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 }
767
+ });
768
+ const yaml2 = await import("yaml");
769
+ return yaml2.parse(response.body);
770
+ } catch {
771
+ return null;
772
+ }
773
+ }
774
+ /**
775
+ * Fallback registry data when network is unavailable
776
+ */
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"]
911
+ }
912
+ ]
913
+ };
914
+ }
915
+ };
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
+
926
+ // src/utils/embedded-packages.ts
927
+ var EMBEDDED_PACKAGES = {
928
+ "@cpm/nextjs-rules": {
929
+ name: "@cpm/nextjs-rules",
930
+ version: "1.0.0",
931
+ description: "Next.js 14+ App Router conventions and best practices for Claude Code",
932
+ type: "rules",
933
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
934
+ license: "MIT",
935
+ keywords: ["nextjs", "react", "typescript", "app-router"],
936
+ universal: {
937
+ globs: ["**/*.tsx", "**/*.ts", "app/**/*", "src/**/*"],
938
+ rules: `# Next.js 14+ Best Practices
939
+
940
+ You are an expert Next.js developer specializing in the App Router architecture.
941
+
942
+ ## Core Principles
943
+
944
+ 1. **App Router First**: Always use the App Router (\`app/\` directory), never the Pages Router
945
+ 2. **Server Components by Default**: Components are Server Components unless marked with \`'use client'\`
946
+ 3. **TypeScript Required**: Use TypeScript with strict mode enabled
947
+ 4. **Colocation**: Keep related files together (components, styles, tests)
948
+
949
+ ## File Conventions
950
+
951
+ - \`page.tsx\` - Unique UI for a route
952
+ - \`layout.tsx\` - Shared UI for a segment and children
953
+ - \`loading.tsx\` - Loading UI with Suspense
954
+ - \`error.tsx\` - Error boundary with recovery
955
+ - \`not-found.tsx\` - 404 UI for a segment
956
+
957
+ ## Data Fetching
958
+
959
+ - Use \`fetch()\` in Server Components with proper caching
960
+ - Implement \`generateStaticParams\` for static generation
961
+ - Use \`revalidatePath\` and \`revalidateTag\` for on-demand revalidation
962
+ - Prefer Server Actions for mutations
963
+
964
+ ## Component Patterns
965
+
966
+ \`\`\`typescript
967
+ // Server Component (default)
968
+ async function ProductList() {
969
+ const products = await getProducts();
970
+ return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
971
+ }
972
+
973
+ // Client Component (interactive)
974
+ 'use client';
975
+ function Counter() {
976
+ const [count, setCount] = useState(0);
977
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
978
+ }
979
+ \`\`\`
980
+
981
+ ## Best Practices
982
+
983
+ - Use \`next/image\` for optimized images
984
+ - Implement proper metadata with \`generateMetadata\`
985
+ - Use route groups \`(group)\` for organization without URL impact
986
+ - Implement parallel routes for complex UIs
987
+ - Use intercepting routes for modals`
988
+ }
989
+ },
990
+ "@cpm/typescript-strict": {
991
+ name: "@cpm/typescript-strict",
992
+ version: "1.0.0",
993
+ description: "TypeScript strict mode best practices and conventions",
994
+ type: "rules",
995
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
996
+ license: "MIT",
997
+ keywords: ["typescript", "strict", "types"],
998
+ universal: {
999
+ globs: ["**/*.ts", "**/*.tsx"],
1000
+ rules: `# TypeScript Strict Mode Best Practices
1001
+
1002
+ You are an expert TypeScript developer who prioritizes type safety and clean code.
1003
+
1004
+ ## Strict Mode Requirements
1005
+
1006
+ Always ensure these compiler options are enabled:
1007
+ - \`strict: true\` (enables all strict checks)
1008
+ - \`noUncheckedIndexedAccess: true\`
1009
+ - \`noImplicitReturns: true\`
1010
+ - \`noFallthroughCasesInSwitch: true\`
1011
+
1012
+ ## Type Safety Principles
1013
+
1014
+ 1. **No \`any\`**: Never use \`any\`. Use \`unknown\` with type guards instead.
1015
+ 2. **Explicit Return Types**: Always declare return types for functions.
1016
+ 3. **Readonly by Default**: Use \`readonly\` and \`Readonly<T>\` wherever possible.
1017
+ 4. **Discriminated Unions**: Prefer discriminated unions over optional properties.
1018
+
1019
+ ## Patterns
1020
+
1021
+ \`\`\`typescript
1022
+ // Good: Discriminated union
1023
+ type Result<T> =
1024
+ | { success: true; data: T }
1025
+ | { success: false; error: Error };
1026
+
1027
+ // Good: Type guard
1028
+ function isString(value: unknown): value is string {
1029
+ return typeof value === 'string';
1030
+ }
1031
+
1032
+ // Good: Explicit return type
1033
+ function getUser(id: string): Promise<User | null> {
1034
+ return db.users.find(id);
1035
+ }
1036
+
1037
+ // Good: Readonly
1038
+ function processItems(items: readonly Item[]): void {
1039
+ // Cannot mutate items
1040
+ }
1041
+ \`\`\`
1042
+
1043
+ ## Avoid
1044
+
1045
+ - \`as\` type assertions (use type guards)
1046
+ - Non-null assertion \`!\` (use proper null checks)
1047
+ - \`Object\`, \`Function\`, \`{}\` types (be specific)
1048
+ - Implicit \`any\` from untyped imports`
1049
+ }
1050
+ },
1051
+ "@cpm/code-review": {
1052
+ name: "@cpm/code-review",
1053
+ version: "1.0.0",
1054
+ description: "Automated code review skill for Claude Code",
1055
+ type: "skill",
1056
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1057
+ license: "MIT",
1058
+ keywords: ["code-review", "quality", "skill"],
1059
+ skill: {
1060
+ command: "/review",
1061
+ description: "Review code for bugs, performance, and best practices"
1062
+ },
1063
+ universal: {
1064
+ prompt: `# Code Review Skill
1065
+
1066
+ You are an expert code reviewer. When the user invokes /review, analyze the provided code thoroughly.
1067
+
1068
+ ## Review Checklist
1069
+
1070
+ 1. **Bugs & Logic Errors**
1071
+ - Off-by-one errors
1072
+ - Null/undefined handling
1073
+ - Race conditions
1074
+ - Error handling gaps
1075
+
1076
+ 2. **Performance**
1077
+ - Unnecessary re-renders (React)
1078
+ - N+1 queries
1079
+ - Memory leaks
1080
+ - Inefficient algorithms
1081
+
1082
+ 3. **Security**
1083
+ - Input validation
1084
+ - SQL injection risks
1085
+ - XSS vulnerabilities
1086
+ - Secrets in code
1087
+
1088
+ 4. **Maintainability**
1089
+ - Code complexity
1090
+ - Naming conventions
1091
+ - DRY violations
1092
+ - Missing documentation
1093
+
1094
+ 5. **Best Practices**
1095
+ - Framework conventions
1096
+ - Design patterns
1097
+ - Error boundaries
1098
+ - Testing considerations
1099
+
1100
+ ## Output Format
1101
+
1102
+ \`\`\`
1103
+ ## Code Review Summary
1104
+
1105
+ ### Critical Issues
1106
+ - [ ] Issue description with line reference
1107
+
1108
+ ### Warnings
1109
+ - [ ] Warning description
1110
+
1111
+ ### Suggestions
1112
+ - [ ] Suggestion for improvement
1113
+
1114
+ ### Positive Notes
1115
+ - What's done well
1116
+ \`\`\``
1117
+ }
1118
+ },
1119
+ "@cpm/git-commit": {
1120
+ name: "@cpm/git-commit",
1121
+ version: "1.0.0",
1122
+ description: "Smart commit message generation skill",
1123
+ type: "skill",
1124
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1125
+ license: "MIT",
1126
+ keywords: ["git", "commit", "messages", "skill"],
1127
+ skill: {
1128
+ command: "/commit",
1129
+ description: "Generate a commit message for staged changes"
1130
+ },
1131
+ universal: {
1132
+ prompt: `# Git Commit Message Skill
1133
+
1134
+ Generate clear, conventional commit messages based on staged changes.
1135
+
1136
+ ## Commit Format
1137
+
1138
+ \`\`\`
1139
+ <type>(<scope>): <subject>
1140
+
1141
+ <body>
1142
+
1143
+ <footer>
1144
+ \`\`\`
1145
+
1146
+ ## Types
1147
+
1148
+ - **feat**: New feature
1149
+ - **fix**: Bug fix
1150
+ - **docs**: Documentation changes
1151
+ - **style**: Formatting, no code change
1152
+ - **refactor**: Code restructuring
1153
+ - **perf**: Performance improvement
1154
+ - **test**: Adding tests
1155
+ - **chore**: Maintenance tasks
1156
+
1157
+ ## Guidelines
1158
+
1159
+ 1. Subject line max 50 characters
1160
+ 2. Use imperative mood ("Add feature" not "Added feature")
1161
+ 3. No period at end of subject
1162
+ 4. Body explains what and why (not how)
1163
+ 5. Reference issues in footer
1164
+
1165
+ ## Examples
1166
+
1167
+ \`\`\`
1168
+ feat(auth): add OAuth2 login support
1169
+
1170
+ Implement Google and GitHub OAuth providers using NextAuth.js.
1171
+ This allows users to sign in without creating a password.
1172
+
1173
+ Closes #123
1174
+ \`\`\``
1175
+ }
1176
+ },
1177
+ "@cpm/react-patterns": {
1178
+ name: "@cpm/react-patterns",
1179
+ version: "1.0.0",
1180
+ description: "React component patterns and best practices",
1181
+ type: "rules",
1182
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1183
+ license: "MIT",
1184
+ keywords: ["react", "components", "hooks", "patterns"],
1185
+ universal: {
1186
+ globs: ["**/*.tsx", "**/*.jsx"],
1187
+ rules: `# React Component Patterns
1188
+
1189
+ You are an expert React developer following modern best practices.
1190
+
1191
+ ## Component Design
1192
+
1193
+ 1. **Single Responsibility**: Each component does one thing well
1194
+ 2. **Composition over Inheritance**: Use children and render props
1195
+ 3. **Controlled Components**: Form inputs controlled by state
1196
+ 4. **Hooks for Logic**: Extract reusable logic into custom hooks
1197
+
1198
+ ## Patterns
1199
+
1200
+ ### Custom Hook Pattern
1201
+ \`\`\`tsx
1202
+ function useUser(id: string) {
1203
+ const [user, setUser] = useState<User | null>(null);
1204
+ const [loading, setLoading] = useState(true);
1205
+
1206
+ useEffect(() => {
1207
+ fetchUser(id).then(setUser).finally(() => setLoading(false));
1208
+ }, [id]);
1209
+
1210
+ return { user, loading };
1211
+ }
1212
+ \`\`\`
1213
+
1214
+ ### Compound Components
1215
+ \`\`\`tsx
1216
+ function Tabs({ children }) { /* ... */ }
1217
+ Tabs.Tab = function Tab({ children }) { /* ... */ };
1218
+ Tabs.Panel = function Panel({ children }) { /* ... */ };
1219
+ \`\`\`
1220
+
1221
+ ## Performance
1222
+
1223
+ - Use \`React.memo\` for expensive pure components
1224
+ - Use \`useMemo\` for expensive calculations
1225
+ - Use \`useCallback\` for stable function references
1226
+ - Avoid inline objects/arrays in props`
1227
+ }
1228
+ },
1229
+ "@cpm/refactor": {
1230
+ name: "@cpm/refactor",
1231
+ version: "1.0.0",
1232
+ description: "Code refactoring assistant skill",
1233
+ type: "skill",
1234
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1235
+ license: "MIT",
1236
+ keywords: ["refactor", "clean-code", "skill"],
1237
+ skill: {
1238
+ command: "/refactor",
1239
+ description: "Suggest and apply code refactoring improvements"
1240
+ },
1241
+ universal: {
1242
+ prompt: `# Code Refactoring Skill
1243
+
1244
+ You are an expert at improving code quality through refactoring.
1245
+
1246
+ ## Refactoring Techniques
1247
+
1248
+ 1. **Extract Function**: Pull out complex logic into named functions
1249
+ 2. **Extract Variable**: Name complex expressions
1250
+ 3. **Inline Variable**: Remove unnecessary intermediates
1251
+ 4. **Rename**: Use clear, descriptive names
1252
+ 5. **Move Function**: Place code where it belongs
1253
+
1254
+ ## Code Smells to Address
1255
+
1256
+ - Long functions (>20 lines)
1257
+ - Deep nesting (>3 levels)
1258
+ - Duplicate code
1259
+ - God objects
1260
+ - Feature envy
1261
+
1262
+ ## Process
1263
+
1264
+ 1. Understand the current behavior
1265
+ 2. Write tests if missing
1266
+ 3. Make small, incremental changes
1267
+ 4. Verify tests pass after each change
1268
+ 5. Commit frequently`
1269
+ }
1270
+ },
1271
+ "@cpm/explain": {
1272
+ name: "@cpm/explain",
1273
+ version: "1.0.0",
1274
+ description: "Code explanation and documentation skill",
1275
+ type: "skill",
1276
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1277
+ license: "MIT",
1278
+ keywords: ["explain", "documentation", "skill"],
1279
+ skill: {
1280
+ command: "/explain",
1281
+ description: "Explain code in detail with examples"
1282
+ },
1283
+ universal: {
1284
+ prompt: `# Code Explanation Skill
1285
+
1286
+ You are an expert at explaining code clearly and thoroughly.
1287
+
1288
+ ## Explanation Structure
1289
+
1290
+ 1. **Overview**: What does this code do at a high level?
1291
+ 2. **Key Concepts**: What patterns/techniques are used?
1292
+ 3. **Line-by-Line**: Detailed walkthrough of important parts
1293
+ 4. **Examples**: Show how to use or modify this code
1294
+ 5. **Gotchas**: Common pitfalls or edge cases
1295
+
1296
+ ## Adapt to Audience
1297
+
1298
+ - **Beginner**: More context, simpler terms, more examples
1299
+ - **Intermediate**: Focus on patterns and best practices
1300
+ - **Expert**: Focus on edge cases and optimization`
1301
+ }
1302
+ },
1303
+ "@cpm/api-design": {
1304
+ name: "@cpm/api-design",
1305
+ version: "1.0.0",
1306
+ description: "REST and GraphQL API design conventions",
1307
+ type: "rules",
1308
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1309
+ license: "MIT",
1310
+ keywords: ["api", "rest", "graphql", "design"],
1311
+ universal: {
1312
+ globs: ["**/api/**/*", "**/routes/**/*", "**/graphql/**/*"],
1313
+ rules: `# API Design Best Practices
1314
+
1315
+ You are an expert API designer following industry best practices.
1316
+
1317
+ ## REST Conventions
1318
+
1319
+ ### URL Structure
1320
+ - Use nouns, not verbs: \`/users\` not \`/getUsers\`
1321
+ - Use plural nouns: \`/users\` not \`/user\`
1322
+ - Nest for relationships: \`/users/{id}/posts\`
1323
+ - Use kebab-case: \`/user-profiles\`
1324
+
1325
+ ### HTTP Methods
1326
+ - GET: Read (idempotent)
1327
+ - POST: Create
1328
+ - PUT: Full update (idempotent)
1329
+ - PATCH: Partial update
1330
+ - DELETE: Remove (idempotent)
1331
+
1332
+ ### Status Codes
1333
+ - 200: Success
1334
+ - 201: Created
1335
+ - 400: Bad Request
1336
+ - 401: Unauthorized
1337
+ - 404: Not Found
1338
+ - 500: Server Error`
1339
+ }
1340
+ },
1341
+ "@cpm/testing-patterns": {
1342
+ name: "@cpm/testing-patterns",
1343
+ version: "1.0.0",
1344
+ description: "Testing best practices for JavaScript/TypeScript projects",
1345
+ type: "rules",
1346
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1347
+ license: "MIT",
1348
+ keywords: ["testing", "jest", "vitest", "patterns"],
1349
+ universal: {
1350
+ globs: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
1351
+ rules: `# Testing Best Practices
1352
+
1353
+ You are an expert in writing maintainable, reliable tests.
1354
+
1355
+ ## Testing Principles
1356
+
1357
+ 1. **Test Behavior, Not Implementation**
1358
+ 2. **Arrange-Act-Assert Pattern**
1359
+ 3. **One Assertion per Test** (when practical)
1360
+ 4. **Tests Should Be Independent**
1361
+ 5. **Use Descriptive Test Names**
1362
+
1363
+ ## Test Structure
1364
+
1365
+ \`\`\`typescript
1366
+ describe('UserService', () => {
1367
+ describe('createUser', () => {
1368
+ it('should create a user with valid data', async () => {
1369
+ // Arrange
1370
+ const userData = { name: 'John', email: 'john@example.com' };
1371
+
1372
+ // Act
1373
+ const user = await userService.createUser(userData);
1374
+
1375
+ // Assert
1376
+ expect(user.name).toBe('John');
1377
+ });
1378
+ });
1379
+ });
1380
+ \`\`\`
1381
+
1382
+ ## Mocking
1383
+
1384
+ - Mock external dependencies, not internal modules
1385
+ - Use dependency injection for testability
1386
+ - Reset mocks between tests`
1387
+ }
1388
+ },
1389
+ "@cpm/github-mcp": {
1390
+ name: "@cpm/github-mcp",
1391
+ version: "1.0.0",
1392
+ description: "GitHub API integration MCP server for Claude Code",
1393
+ type: "mcp",
1394
+ author: { name: "CPM Team", url: "https://cpm-ai.dev" },
1395
+ license: "MIT",
1396
+ keywords: ["github", "mcp", "api", "integration"],
1397
+ mcp: {
1398
+ transport: "stdio",
1399
+ command: "npx",
1400
+ args: ["-y", "@modelcontextprotocol/server-github"],
1401
+ env: {
1402
+ GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
1403
+ }
1404
+ }
1405
+ }
1406
+ };
1407
+ function getEmbeddedManifest(packageName) {
1408
+ return EMBEDDED_PACKAGES[packageName] ?? null;
1409
+ }
1410
+
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}`);
1437
+ }
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
+ };
1471
+ }
1472
+ }
1473
+ async function cleanupTempDir(tempDir) {
1474
+ try {
1475
+ if (tempDir.startsWith(TEMP_DIR)) {
1476
+ await fs5.remove(tempDir);
1477
+ }
1478
+ } catch {
1479
+ }
1480
+ }
1481
+ async function fetchManifestFromRepo(repoUrl) {
1482
+ try {
1483
+ const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
1484
+ if (!match) return null;
1485
+ const [, owner, repo] = match;
1486
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/cpm.yaml`;
1487
+ const response = await got2(rawUrl, {
1488
+ timeout: { request: TIMEOUTS.MANIFEST_FETCH }
1489
+ });
1490
+ return yaml.parse(response.body);
1491
+ } catch {
1492
+ return null;
1493
+ }
1494
+ }
1495
+ async function downloadAndExtractTarball(pkg, tempDir) {
1496
+ if (!pkg.tarball) return null;
1497
+ try {
1498
+ const parsedUrl = new URL(pkg.tarball);
1499
+ if (parsedUrl.protocol !== "https:") {
1500
+ throw new Error("Only HTTPS URLs are allowed for downloads");
1501
+ }
1502
+ const response = await got2(pkg.tarball, {
1503
+ timeout: { request: TIMEOUTS.TARBALL_DOWNLOAD },
1504
+ followRedirect: true,
1505
+ responseType: "buffer"
1506
+ });
1507
+ const tarballPath = path5.join(tempDir, "package.tar.gz");
1508
+ await fs5.writeFile(tarballPath, response.body);
1509
+ await extractTarball(tarballPath, tempDir);
1510
+ const manifestPath = path5.join(tempDir, "cpm.yaml");
1511
+ if (await fs5.pathExists(manifestPath)) {
1512
+ const content = await fs5.readFile(manifestPath, "utf-8");
1513
+ return yaml.parse(content);
1514
+ }
1515
+ return null;
1516
+ } catch {
1517
+ return null;
1518
+ }
1519
+ }
1520
+ async function extractTarball(tarballPath, destDir) {
1521
+ await fs5.ensureDir(destDir);
1522
+ const resolvedDestDir = path5.resolve(destDir);
1523
+ await tar.extract({
1524
+ file: tarballPath,
1525
+ cwd: destDir,
1526
+ strip: 1,
1527
+ filter: (entryPath) => {
1528
+ const resolvedPath = path5.resolve(destDir, entryPath);
1529
+ const isWithinDest = resolvedPath.startsWith(resolvedDestDir + path5.sep) || resolvedPath === resolvedDestDir;
1530
+ if (!isWithinDest) {
1531
+ logger.warn(`Blocked path traversal in tarball: ${entryPath}`);
1532
+ return false;
1533
+ }
1534
+ return true;
1535
+ }
1536
+ });
1537
+ }
1538
+ async function fetchPackageFromPath(pkg, tempDir) {
1539
+ if (!pkg.path) return null;
1540
+ try {
1541
+ const safePath = validatePackagePath(pkg.path);
1542
+ const githubInfo = parseGitHubInfo(PACKAGES_BASE_URL);
1543
+ if (!githubInfo) {
1544
+ return fetchSingleFileFromPath(pkg);
1545
+ }
1546
+ const apiUrl = `https://api.github.com/repos/${githubInfo.owner}/${githubInfo.repo}/contents/${safePath}`;
1547
+ const response = await got2(apiUrl, {
1548
+ timeout: { request: TIMEOUTS.API_REQUEST },
1549
+ headers: {
1550
+ Accept: "application/vnd.github.v3+json",
1551
+ "User-Agent": "cpm-cli"
1552
+ },
1553
+ responseType: "json"
1554
+ });
1555
+ const files = response.body;
1556
+ let mainContent = "";
1557
+ const pkgType = resolvePackageType(pkg);
1558
+ const contentFile = getContentFileName(pkgType);
1559
+ for (const file of files) {
1560
+ if (file.type === "file" && file.download_url) {
1561
+ const safeFileName = sanitizeFileName2(file.name);
1562
+ const destPath = path5.join(tempDir, safeFileName);
1563
+ validatePathWithinDir(destPath, tempDir);
1564
+ const fileResponse = await got2(file.download_url, {
1565
+ timeout: { request: TIMEOUTS.API_REQUEST }
1566
+ });
1567
+ await fs5.writeFile(destPath, fileResponse.body, "utf-8");
1568
+ if (file.name === contentFile) {
1569
+ mainContent = fileResponse.body;
1570
+ }
1571
+ }
1572
+ }
1573
+ return createManifestWithContent(pkg, mainContent);
1574
+ } catch {
1575
+ return fetchSingleFileFromPath(pkg);
1576
+ }
1577
+ }
1578
+ async function fetchSingleFileFromPath(pkg) {
1579
+ if (!pkg.path) return null;
1580
+ try {
1581
+ const safePath = validatePackagePath(pkg.path);
1582
+ const pkgType = resolvePackageType(pkg);
1583
+ const contentFile = getContentFileName(pkgType);
1584
+ const contentUrl = `${PACKAGES_BASE_URL}/${safePath}/${contentFile}`;
1585
+ const response = await got2(contentUrl, {
1586
+ timeout: { request: TIMEOUTS.API_REQUEST }
1587
+ });
1588
+ return createManifestWithContent(pkg, response.body);
1589
+ } catch {
1590
+ return null;
1591
+ }
1592
+ }
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
+
1642
+ ${pkg.description}`
1643
+ }
1644
+ };
1645
+ }
1646
+
1647
+ // src/commands/install.ts
1648
+ var VALID_PLATFORMS = ["claude-code"];
1649
+ var MAX_PACKAGE_NAME_LENGTH = 214;
1650
+ function validatePackageName(name) {
1651
+ if (!name || typeof name !== "string") {
1652
+ return { valid: false, error: "Package name cannot be empty" };
1653
+ }
1654
+ let decoded = name;
1655
+ try {
1656
+ decoded = decodeURIComponent(name);
1657
+ } catch {
1658
+ }
1659
+ if (decoded.length > MAX_PACKAGE_NAME_LENGTH) {
1660
+ return { valid: false, error: `Package name too long (max ${MAX_PACKAGE_NAME_LENGTH} characters)` };
1661
+ }
1662
+ if (decoded.includes("\0")) {
1663
+ return { valid: false, error: "Invalid characters in package name" };
1664
+ }
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) {
1667
+ return { valid: false, error: "Invalid characters in package name" };
1668
+ }
1669
+ const packageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
1670
+ if (!packageNameRegex.test(name.toLowerCase())) {
1671
+ return { valid: false, error: "Invalid package name format" };
1672
+ }
1673
+ return { valid: true };
1674
+ }
1675
+ function isValidPlatform(platform) {
1676
+ return VALID_PLATFORMS.includes(platform);
1677
+ }
1678
+ function normalizePackageName(name) {
1679
+ if (name.startsWith("@")) {
1680
+ return name;
1681
+ }
1682
+ return `@cpm/${name}`;
1683
+ }
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"];
1694
+ }
1695
+ platforms = platforms.filter((p) => p === "claude-code");
1696
+ if (platforms.length === 0) {
1697
+ platforms = ["claude-code"];
1698
+ }
1699
+ return platforms;
1700
+ }
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
+ );
1708
+ }
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
+ }
1717
+ }
1718
+ logger.newline();
1719
+ displayUsageHints(manifest);
1720
+ }
1721
+ function displayUsageHints(manifest) {
1722
+ switch (manifest.type) {
1723
+ case "skill":
1724
+ if (manifest.skill?.command) {
1725
+ logger.log(
1726
+ ` ${chalk.cyan("Usage:")} Type ${chalk.yellow(manifest.skill.command)} in Claude Code`
1727
+ );
1728
+ }
1729
+ break;
1730
+ case "rules":
1731
+ logger.log(
1732
+ ` ${chalk.cyan("Usage:")} Rules are automatically applied to matching files`
1733
+ );
1734
+ break;
1735
+ case "mcp":
1736
+ logger.log(
1737
+ ` ${chalk.cyan("Usage:")} MCP server configured. Restart Claude Code to activate.`
1738
+ );
1739
+ if (manifest.mcp?.env) {
1740
+ const envVars = Object.keys(manifest.mcp.env);
1741
+ if (envVars.length > 0) {
1742
+ logger.log(chalk.yellow(`
1743
+ Required environment variables:`));
1744
+ for (const envVar of envVars) {
1745
+ logger.log(chalk.dim(` - ${envVar}`));
1746
+ }
1747
+ }
1748
+ }
1749
+ break;
1750
+ }
1751
+ }
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}`));
1757
+ }
1758
+ }
1759
+ async function installCommand(packageName, options) {
1760
+ const validation = validatePackageName(packageName);
1761
+ if (!validation.valid) {
1762
+ logger.error(`Invalid package name: ${validation.error}`);
1763
+ return;
1764
+ }
1765
+ const spinner = logger.isQuiet() ? null : ora(`Installing ${chalk.cyan(packageName)}...`).start();
1766
+ let tempDir;
1767
+ try {
1768
+ const normalizedName = normalizePackageName(packageName);
1769
+ if (spinner) spinner.text = `Searching for ${chalk.cyan(normalizedName)}...`;
1770
+ const pkg = await registry.getPackage(normalizedName);
1771
+ 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(/^@[^/]+\//, "")}`));
1776
+ return;
1777
+ }
1778
+ if (spinner) spinner.text = `Downloading ${chalk.cyan(pkg.name)}@${pkg.version}...`;
1779
+ const downloadResult = await downloadPackage(pkg);
1780
+ 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}`);
1783
+ return;
1784
+ }
1785
+ 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(", ")}...`;
1794
+ await ensureClaudeDirs();
1795
+ const results = await installToPlatforms(
1796
+ downloadResult.manifest,
1797
+ tempDir,
1798
+ targetPlatforms
1799
+ );
1800
+ const successful = results.filter((r) => r.success);
1801
+ const failed = results.filter((r) => !r.success);
1802
+ 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);
1811
+ }
1812
+ displayWarnings(failed);
1813
+ } catch (error) {
1814
+ if (spinner) spinner.fail(`Failed to install ${packageName}`);
1815
+ else logger.error(`Failed to install ${packageName}`);
1816
+ if (error instanceof Error) {
1817
+ logger.error(error.message);
1818
+ }
1819
+ } finally {
1820
+ if (tempDir) {
1821
+ await cleanupTempDir(tempDir);
1822
+ }
1823
+ }
1824
+ }
1825
+
1826
+ // 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));
1853
+ 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();
1866
+ 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"));
1870
+ return;
1871
+ }
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>")}`));
1898
+ } catch (error) {
1899
+ if (spinner) spinner.fail("Search failed");
1900
+ else logger.error("Search failed");
1901
+ logger.error(error instanceof Error ? error.message : "Unknown error");
1902
+ }
1903
+ }
1904
+ function formatNumber(num) {
1905
+ if (num >= 1e3) {
1906
+ return `${(num / 1e3).toFixed(1)}k`;
1907
+ }
1908
+ return num.toString();
1909
+ }
1910
+
1911
+ // 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
+ };
1921
+ async function readPackageMetadata(packageDir) {
1922
+ const metadataPath = path7.join(packageDir, ".cpm.json");
1923
+ try {
1924
+ if (await fs6.pathExists(metadataPath)) {
1925
+ return await fs6.readJson(metadataPath);
1926
+ }
1927
+ } catch {
1928
+ }
1929
+ return null;
1930
+ }
1931
+ async function scanInstalledPackages() {
1932
+ 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
+ }
1950
+ }
1951
+ }
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
+ }
1969
+ }
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 {
1984
+ }
1985
+ }
1986
+ return items;
1987
+ }
1988
+ async function listCommand() {
1989
+ try {
1990
+ const packages = await scanInstalledPackages();
1991
+ 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`));
1995
+ return;
1996
+ }
1997
+ logger.log(chalk3.bold(`
1998
+ Installed packages (${packages.length}):
1999
+ `));
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"));
2015
+ } catch (error) {
2016
+ logger.error("Failed to list packages");
2017
+ logger.error(error instanceof Error ? error.message : "Unknown error");
2018
+ }
2019
+ }
2020
+
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");
2101
+ }
2102
+ }
2103
+
2104
+ // src/commands/uninstall.ts
2105
+ import chalk5 from "chalk";
2106
+ import ora3 from "ora";
2107
+ async function uninstallCommand(packageName) {
2108
+ const spinner = logger.isQuiet() ? null : ora3(`Uninstalling ${chalk5.cyan(packageName)}...`).start();
2109
+ try {
2110
+ const folderName = packageName.includes("/") ? packageName.split("/").pop() || packageName : packageName.replace(/^@/, "");
2111
+ const adapter = getAdapter("claude-code");
2112
+ const result = await adapter.uninstall(folderName, process.cwd());
2113
+ 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
+ }
2120
+ } else if (result.success) {
2121
+ if (spinner) spinner.warn(`Package ${packageName} was not found`);
2122
+ else logger.warn(`Package ${packageName} was not found`);
2123
+ } else {
2124
+ if (spinner) spinner.fail(`Failed to uninstall: ${result.error}`);
2125
+ else logger.error(`Failed to uninstall: ${result.error}`);
2126
+ }
2127
+ } catch (error) {
2128
+ if (spinner) spinner.fail(`Failed to uninstall ${packageName}`);
2129
+ else logger.error(`Failed to uninstall ${packageName}`);
2130
+ logger.error(error instanceof Error ? error.message : "Unknown error");
2131
+ }
2132
+ }
2133
+
2134
+ // src/index.ts
2135
+ var program = new Command();
2136
+ 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")}
2143
+ `;
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) => {
2147
+ const opts = thisCommand.optsWithGlobals();
2148
+ configureLogger({
2149
+ quiet: opts.quiet,
2150
+ verbose: opts.verbose
2151
+ });
2152
+ });
2153
+ program.command("install <package>").alias("i").description("Install a package").option("-p, --platform <platform>", "Target platform (claude-code)", "all").action(installCommand);
2154
+ program.command("uninstall <package>").alias("rm").description("Uninstall a package").option("-p, --platform <platform>", "Target platform").action(uninstallCommand);
2155
+ 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
+ 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
+ program.parse();