@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/README.md +157 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2167 -0
- package/package.json +65 -0
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();
|