@42ailab/42plugin 0.1.0-beta.1 → 0.1.2
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 +211 -68
- package/package.json +12 -7
- package/src/api.ts +447 -0
- package/src/cli.ts +33 -16
- package/src/commands/auth.ts +83 -69
- package/src/commands/check.ts +118 -0
- package/src/commands/completion.ts +210 -0
- package/src/commands/index.ts +13 -0
- package/src/commands/install-helper.ts +71 -0
- package/src/commands/install.ts +219 -300
- package/src/commands/list.ts +42 -66
- package/src/commands/publish.ts +121 -0
- package/src/commands/search.ts +89 -85
- package/src/commands/setup.ts +158 -0
- package/src/commands/uninstall.ts +53 -44
- package/src/config.ts +27 -36
- package/src/db.ts +593 -0
- package/src/errors.ts +40 -0
- package/src/index.ts +4 -31
- package/src/services/packager.ts +177 -0
- package/src/services/publisher.ts +237 -0
- package/src/services/upload.ts +52 -0
- package/src/services/version-manager.ts +65 -0
- package/src/types.ts +396 -0
- package/src/utils.ts +128 -0
- package/src/validators/plugin-validator.ts +635 -0
- package/src/commands/version.ts +0 -20
- package/src/db/client.ts +0 -180
- package/src/services/api.ts +0 -128
- package/src/services/auth.ts +0 -46
- package/src/services/cache.ts +0 -101
- package/src/services/download.ts +0 -148
- package/src/services/link.ts +0 -86
- package/src/services/project.ts +0 -179
- package/src/types/api.ts +0 -115
- package/src/types/db.ts +0 -31
- package/src/utils/errors.ts +0 -40
- package/src/utils/platform.ts +0 -6
- package/src/utils/target.ts +0 -114
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件结构验证器
|
|
3
|
+
*
|
|
4
|
+
* 支持的插件类型:
|
|
5
|
+
* - skill: 技能插件(目录或单文件)
|
|
6
|
+
* - agent: Agent 插件(仅单文件)
|
|
7
|
+
* - command: 命令插件(目录或单文件)
|
|
8
|
+
* - hook: Hook 插件(仅目录,需要 hooks.json)
|
|
9
|
+
* - mcp: MCP 服务器插件(仅目录,需要 mcp.json)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import matter from 'gray-matter';
|
|
15
|
+
import { ValidationError } from '../errors';
|
|
16
|
+
import type {
|
|
17
|
+
PluginMetadata,
|
|
18
|
+
PluginType,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
ValidationIssue,
|
|
21
|
+
HookConfig,
|
|
22
|
+
McpConfig,
|
|
23
|
+
} from '../types';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// 常量定义
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
30
|
+
const MAX_NAME_LENGTH = 64;
|
|
31
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
32
|
+
const MIN_DESCRIPTION_LENGTH = 20;
|
|
33
|
+
const MAX_TAG_LENGTH = 32;
|
|
34
|
+
const MAX_AGENT_FILE_SIZE = 50 * 1024; // 50KB
|
|
35
|
+
|
|
36
|
+
const VALID_TYPES: PluginType[] = ['skill', 'agent', 'command', 'hook', 'mcp'];
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// PluginValidator 类
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export class PluginValidator {
|
|
43
|
+
/**
|
|
44
|
+
* 完整验证(返回详细结果)
|
|
45
|
+
*/
|
|
46
|
+
async validateFull(pluginPath: string, overrideName?: string): Promise<ValidationResult> {
|
|
47
|
+
const result: ValidationResult = {
|
|
48
|
+
valid: true,
|
|
49
|
+
metadata: {
|
|
50
|
+
name: '',
|
|
51
|
+
type: 'skill',
|
|
52
|
+
sourcePath: '',
|
|
53
|
+
},
|
|
54
|
+
errors: [],
|
|
55
|
+
warnings: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const absPath = path.resolve(pluginPath);
|
|
59
|
+
|
|
60
|
+
// Phase 1: 路径检查
|
|
61
|
+
const stat = await fs.stat(absPath).catch(() => null);
|
|
62
|
+
if (!stat) {
|
|
63
|
+
result.errors.push({
|
|
64
|
+
code: 'E000',
|
|
65
|
+
message: `路径不存在: ${pluginPath}`,
|
|
66
|
+
suggestion: '请检查路径是否正确',
|
|
67
|
+
});
|
|
68
|
+
result.valid = false;
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Phase 2: 提取元信息并进行类型检测
|
|
73
|
+
try {
|
|
74
|
+
if (stat.isFile()) {
|
|
75
|
+
result.metadata = await this.extractFileMetadata(absPath, overrideName);
|
|
76
|
+
} else if (stat.isDirectory()) {
|
|
77
|
+
result.metadata = await this.extractDirectoryMetadata(absPath, overrideName);
|
|
78
|
+
} else {
|
|
79
|
+
result.errors.push({
|
|
80
|
+
code: 'E000',
|
|
81
|
+
message: '不支持的路径类型',
|
|
82
|
+
});
|
|
83
|
+
result.valid = false;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
result.errors.push({
|
|
88
|
+
code: 'E000',
|
|
89
|
+
message: (e as Error).message,
|
|
90
|
+
});
|
|
91
|
+
result.valid = false;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Phase 3: 通用 Schema 验证
|
|
96
|
+
this.validateSchema(result);
|
|
97
|
+
|
|
98
|
+
// Phase 4: 类型特定验证
|
|
99
|
+
await this.validateTypeSpecific(absPath, stat.isDirectory(), result);
|
|
100
|
+
|
|
101
|
+
// Phase 5: 引用检查(仅目录插件)
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
await this.validateReferences(absPath, result);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
result.valid = result.errors.length === 0;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 简单验证(向后兼容)
|
|
112
|
+
*/
|
|
113
|
+
async validate(pluginPath: string, overrideName?: string): Promise<PluginMetadata> {
|
|
114
|
+
const result = await this.validateFull(pluginPath, overrideName);
|
|
115
|
+
|
|
116
|
+
if (!result.valid) {
|
|
117
|
+
const firstError = result.errors[0];
|
|
118
|
+
throw new ValidationError(firstError.message, firstError.code);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result.metadata;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==========================================================================
|
|
125
|
+
// 元信息提取
|
|
126
|
+
// ==========================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 从单文件提取元信息
|
|
130
|
+
*/
|
|
131
|
+
private async extractFileMetadata(
|
|
132
|
+
filePath: string,
|
|
133
|
+
overrideName?: string
|
|
134
|
+
): Promise<PluginMetadata> {
|
|
135
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
136
|
+
const basename = path.basename(filePath, ext);
|
|
137
|
+
|
|
138
|
+
if (ext !== '.md' && ext !== '.yaml' && ext !== '.yml') {
|
|
139
|
+
throw new ValidationError(
|
|
140
|
+
`不支持的文件类型: ${ext}\n支持的类型: .md, .yaml, .yml`,
|
|
141
|
+
'UNSUPPORTED_FILE_TYPE'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
146
|
+
const parsed = matter(content);
|
|
147
|
+
const data = parsed.data as Record<string, unknown>;
|
|
148
|
+
|
|
149
|
+
// 推断类型
|
|
150
|
+
const type = this.inferTypeFromContent(content, data);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
name: overrideName || (data.name as string) || basename,
|
|
154
|
+
type,
|
|
155
|
+
title: data.title as string | undefined,
|
|
156
|
+
description: data.description as string | undefined,
|
|
157
|
+
tags: data.tags as string[] | undefined,
|
|
158
|
+
sourcePath: filePath,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 从目录提取元信息
|
|
164
|
+
*/
|
|
165
|
+
private async extractDirectoryMetadata(
|
|
166
|
+
dirPath: string,
|
|
167
|
+
overrideName?: string
|
|
168
|
+
): Promise<PluginMetadata> {
|
|
169
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
170
|
+
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
171
|
+
const lowerFiles = files.map((f) => f.toLowerCase());
|
|
172
|
+
const basename = path.basename(dirPath);
|
|
173
|
+
|
|
174
|
+
// 检测插件类型(基于特征文件)
|
|
175
|
+
let type: PluginType = 'skill';
|
|
176
|
+
let frontmatterData: Record<string, unknown> = {};
|
|
177
|
+
|
|
178
|
+
// MCP: 有 mcp.json 或 .mcp.json
|
|
179
|
+
if (lowerFiles.some((f) => f === 'mcp.json' || f === '.mcp.json')) {
|
|
180
|
+
type = 'mcp';
|
|
181
|
+
}
|
|
182
|
+
// Hook: 有 hooks.json 或 hook.json
|
|
183
|
+
else if (lowerFiles.some((f) => f === 'hooks.json' || f === 'hook.json')) {
|
|
184
|
+
type = 'hook';
|
|
185
|
+
}
|
|
186
|
+
// Skill: 有 SKILL.md 或 skill.md
|
|
187
|
+
else if (lowerFiles.some((f) => f === 'skill.md')) {
|
|
188
|
+
type = 'skill';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 尝试读取主文件获取元信息
|
|
192
|
+
const mainFiles = ['SKILL.md', 'skill.md', 'README.md', 'readme.md'];
|
|
193
|
+
let description: string | undefined;
|
|
194
|
+
let title: string | undefined;
|
|
195
|
+
let tags: string[] | undefined;
|
|
196
|
+
|
|
197
|
+
for (const file of mainFiles) {
|
|
198
|
+
if (files.includes(file)) {
|
|
199
|
+
try {
|
|
200
|
+
const content = await fs.readFile(path.join(dirPath, file), 'utf-8');
|
|
201
|
+
const parsed = matter(content);
|
|
202
|
+
frontmatterData = parsed.data as Record<string, unknown>;
|
|
203
|
+
|
|
204
|
+
if (frontmatterData.title) title = frontmatterData.title as string;
|
|
205
|
+
if (frontmatterData.description) description = frontmatterData.description as string;
|
|
206
|
+
if (frontmatterData.tags) tags = frontmatterData.tags as string[];
|
|
207
|
+
if (frontmatterData.type && VALID_TYPES.includes(frontmatterData.type as PluginType)) {
|
|
208
|
+
type = frontmatterData.type as PluginType;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 如果没有 frontmatter 描述,取第一段文字
|
|
212
|
+
if (!description && parsed.content) {
|
|
213
|
+
const firstPara = parsed.content.trim().split('\n\n')[0];
|
|
214
|
+
if (firstPara && !firstPara.startsWith('#')) {
|
|
215
|
+
description = firstPara.slice(0, 200);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
break;
|
|
220
|
+
} catch {
|
|
221
|
+
// 忽略读取错误
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
name: overrideName || (frontmatterData.name as string) || basename,
|
|
228
|
+
type,
|
|
229
|
+
title,
|
|
230
|
+
description,
|
|
231
|
+
tags,
|
|
232
|
+
sourcePath: dirPath,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 从内容推断插件类型
|
|
238
|
+
*/
|
|
239
|
+
private inferTypeFromContent(content: string, data: Record<string, unknown>): PluginType {
|
|
240
|
+
// 优先使用 frontmatter 中的 type
|
|
241
|
+
if (data.type && VALID_TYPES.includes(data.type as PluginType)) {
|
|
242
|
+
return data.type as PluginType;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 从内容推断
|
|
246
|
+
const lowerContent = content.toLowerCase();
|
|
247
|
+
|
|
248
|
+
if (lowerContent.includes('you are') || lowerContent.includes('你是')) {
|
|
249
|
+
return 'agent';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (lowerContent.includes('mcp') || lowerContent.includes('model context protocol')) {
|
|
253
|
+
return 'mcp';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 默认为 skill
|
|
257
|
+
return 'skill';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ==========================================================================
|
|
261
|
+
// Schema 验证
|
|
262
|
+
// ==========================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 通用 Schema 验证
|
|
266
|
+
*/
|
|
267
|
+
private validateSchema(result: ValidationResult): void {
|
|
268
|
+
const { metadata } = result;
|
|
269
|
+
|
|
270
|
+
// name 验证
|
|
271
|
+
if (!metadata.name) {
|
|
272
|
+
result.errors.push({
|
|
273
|
+
code: 'E001',
|
|
274
|
+
field: 'name',
|
|
275
|
+
message: '缺少必需字段: name',
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
// 格式验证
|
|
279
|
+
if (!NAME_PATTERN.test(metadata.name)) {
|
|
280
|
+
result.errors.push({
|
|
281
|
+
code: 'E002',
|
|
282
|
+
field: 'name',
|
|
283
|
+
message: `name "${metadata.name}" 格式无效`,
|
|
284
|
+
suggestion: '只能包含小写字母 (a-z)、数字 (0-9) 和连字符 (-)',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 长度验证
|
|
289
|
+
if (metadata.name.length > MAX_NAME_LENGTH) {
|
|
290
|
+
result.errors.push({
|
|
291
|
+
code: 'E003',
|
|
292
|
+
field: 'name',
|
|
293
|
+
message: `name 长度超限 (${metadata.name.length}/${MAX_NAME_LENGTH})`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// description 验证
|
|
299
|
+
if (!metadata.description) {
|
|
300
|
+
result.warnings.push({
|
|
301
|
+
code: 'W001',
|
|
302
|
+
field: 'description',
|
|
303
|
+
message: '建议添加 description 字段',
|
|
304
|
+
suggestion: '良好的描述有助于插件被发现',
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
if (metadata.description.length > MAX_DESCRIPTION_LENGTH) {
|
|
308
|
+
result.errors.push({
|
|
309
|
+
code: 'E020',
|
|
310
|
+
field: 'description',
|
|
311
|
+
message: `description 长度超限 (${metadata.description.length}/${MAX_DESCRIPTION_LENGTH})`,
|
|
312
|
+
});
|
|
313
|
+
} else if (metadata.description.length < MIN_DESCRIPTION_LENGTH) {
|
|
314
|
+
result.warnings.push({
|
|
315
|
+
code: 'W002',
|
|
316
|
+
field: 'description',
|
|
317
|
+
message: `description 过短 (${metadata.description.length}字符)`,
|
|
318
|
+
suggestion: '建议提供更详细的描述(至少20字符)',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// type 验证
|
|
324
|
+
if (metadata.type && !VALID_TYPES.includes(metadata.type)) {
|
|
325
|
+
result.errors.push({
|
|
326
|
+
code: 'E010',
|
|
327
|
+
field: 'type',
|
|
328
|
+
message: `无效的 type: ${metadata.type}`,
|
|
329
|
+
suggestion: `有效值: ${VALID_TYPES.join(', ')}`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// tags 验证
|
|
334
|
+
if (metadata.tags) {
|
|
335
|
+
if (!Array.isArray(metadata.tags)) {
|
|
336
|
+
result.warnings.push({
|
|
337
|
+
code: 'W010',
|
|
338
|
+
field: 'tags',
|
|
339
|
+
message: 'tags 应该是数组格式',
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
for (const tag of metadata.tags) {
|
|
343
|
+
if (typeof tag !== 'string') {
|
|
344
|
+
result.warnings.push({
|
|
345
|
+
code: 'W011',
|
|
346
|
+
field: 'tags',
|
|
347
|
+
message: `tag "${tag}" 不是字符串`,
|
|
348
|
+
});
|
|
349
|
+
} else if (tag.length > MAX_TAG_LENGTH) {
|
|
350
|
+
result.warnings.push({
|
|
351
|
+
code: 'W011',
|
|
352
|
+
field: 'tags',
|
|
353
|
+
message: `tag "${tag}" 过长 (${tag.length}/${MAX_TAG_LENGTH})`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ==========================================================================
|
|
362
|
+
// 类型特定验证
|
|
363
|
+
// ==========================================================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 类型特定验证
|
|
367
|
+
*/
|
|
368
|
+
private async validateTypeSpecific(
|
|
369
|
+
absPath: string,
|
|
370
|
+
isDirectory: boolean,
|
|
371
|
+
result: ValidationResult
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
const { type } = result.metadata;
|
|
374
|
+
|
|
375
|
+
switch (type) {
|
|
376
|
+
case 'agent':
|
|
377
|
+
await this.validateAgent(absPath, isDirectory, result);
|
|
378
|
+
break;
|
|
379
|
+
case 'hook':
|
|
380
|
+
await this.validateHook(absPath, isDirectory, result);
|
|
381
|
+
break;
|
|
382
|
+
case 'mcp':
|
|
383
|
+
await this.validateMcp(absPath, isDirectory, result);
|
|
384
|
+
break;
|
|
385
|
+
case 'skill':
|
|
386
|
+
case 'command':
|
|
387
|
+
// skill 和 command 支持单文件和目录,无特殊限制
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Agent 类型验证
|
|
394
|
+
*/
|
|
395
|
+
private async validateAgent(
|
|
396
|
+
absPath: string,
|
|
397
|
+
isDirectory: boolean,
|
|
398
|
+
result: ValidationResult
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
// Agent 只支持单文件
|
|
401
|
+
if (isDirectory) {
|
|
402
|
+
result.errors.push({
|
|
403
|
+
code: 'E030',
|
|
404
|
+
field: 'type',
|
|
405
|
+
message: 'Agent 类型只支持单文件格式',
|
|
406
|
+
suggestion: '请将 Agent 内容放在单个 .md 文件中',
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 检查文件大小
|
|
412
|
+
const stat = await fs.stat(absPath);
|
|
413
|
+
if (stat.size > MAX_AGENT_FILE_SIZE) {
|
|
414
|
+
result.warnings.push({
|
|
415
|
+
code: 'W031',
|
|
416
|
+
message: `Agent 文件过大 (${Math.round(stat.size / 1024)}KB)`,
|
|
417
|
+
suggestion: `建议 Agent 文件小于 ${MAX_AGENT_FILE_SIZE / 1024}KB`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 检查是否包含身份定义
|
|
422
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
423
|
+
const lowerContent = content.toLowerCase();
|
|
424
|
+
if (!lowerContent.includes('you are') && !lowerContent.includes('你是')) {
|
|
425
|
+
result.warnings.push({
|
|
426
|
+
code: 'W030',
|
|
427
|
+
message: 'Agent 文件建议包含身份定义语句',
|
|
428
|
+
suggestion: '添加 "You are..." 或 "你是..." 来定义 Agent 身份',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Hook 类型验证
|
|
435
|
+
*/
|
|
436
|
+
private async validateHook(
|
|
437
|
+
absPath: string,
|
|
438
|
+
isDirectory: boolean,
|
|
439
|
+
result: ValidationResult
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
// Hook 必须是目录
|
|
442
|
+
if (!isDirectory) {
|
|
443
|
+
result.errors.push({
|
|
444
|
+
code: 'E040',
|
|
445
|
+
field: 'type',
|
|
446
|
+
message: 'Hook 类型必须是目录格式',
|
|
447
|
+
suggestion: '请创建包含 hooks.json 的目录',
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 检查 hooks.json 是否存在
|
|
453
|
+
const hooksJsonPath = path.join(absPath, 'hooks.json');
|
|
454
|
+
const hookJsonPath = path.join(absPath, 'hook.json');
|
|
455
|
+
|
|
456
|
+
let configPath: string | null = null;
|
|
457
|
+
if (await this.exists(hooksJsonPath)) {
|
|
458
|
+
configPath = hooksJsonPath;
|
|
459
|
+
} else if (await this.exists(hookJsonPath)) {
|
|
460
|
+
configPath = hookJsonPath;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!configPath) {
|
|
464
|
+
result.errors.push({
|
|
465
|
+
code: 'E041',
|
|
466
|
+
message: '缺少 hooks.json 配置文件',
|
|
467
|
+
suggestion: '创建 hooks.json 文件定义钩子配置',
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 验证 hooks.json 内容
|
|
473
|
+
try {
|
|
474
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
475
|
+
const config = JSON.parse(content) as HookConfig;
|
|
476
|
+
|
|
477
|
+
// hooks 应该是一个对象,包含事件类型作为键
|
|
478
|
+
if (!config.hooks || typeof config.hooks !== 'object') {
|
|
479
|
+
result.errors.push({
|
|
480
|
+
code: 'E042',
|
|
481
|
+
message: 'hooks 字段为空或格式无效',
|
|
482
|
+
suggestion: '在 hooks.json 中定义 hooks 对象,包含事件类型作为键',
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
// 检查是否至少有一个事件类型定义
|
|
486
|
+
const eventTypes = Object.keys(config.hooks);
|
|
487
|
+
if (eventTypes.length === 0) {
|
|
488
|
+
result.errors.push({
|
|
489
|
+
code: 'E042',
|
|
490
|
+
message: 'hooks 对象为空',
|
|
491
|
+
suggestion: '在 hooks.json 中定义至少一个事件类型(如 PreToolUse, PostToolUse 等)',
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {
|
|
496
|
+
result.errors.push({
|
|
497
|
+
code: 'E041',
|
|
498
|
+
message: `hooks.json 解析失败: ${(e as Error).message}`,
|
|
499
|
+
suggestion: '请检查 JSON 格式是否正确',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* MCP 类型验证
|
|
506
|
+
*/
|
|
507
|
+
private async validateMcp(
|
|
508
|
+
absPath: string,
|
|
509
|
+
isDirectory: boolean,
|
|
510
|
+
result: ValidationResult
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
// MCP 必须是目录
|
|
513
|
+
if (!isDirectory) {
|
|
514
|
+
result.errors.push({
|
|
515
|
+
code: 'E050',
|
|
516
|
+
field: 'type',
|
|
517
|
+
message: 'MCP 类型必须是目录格式',
|
|
518
|
+
suggestion: '请创建包含 mcp.json 的目录',
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 检查 mcp.json 是否存在
|
|
524
|
+
const mcpJsonPath = path.join(absPath, 'mcp.json');
|
|
525
|
+
const dotMcpJsonPath = path.join(absPath, '.mcp.json');
|
|
526
|
+
|
|
527
|
+
let configPath: string | null = null;
|
|
528
|
+
if (await this.exists(mcpJsonPath)) {
|
|
529
|
+
configPath = mcpJsonPath;
|
|
530
|
+
} else if (await this.exists(dotMcpJsonPath)) {
|
|
531
|
+
configPath = dotMcpJsonPath;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!configPath) {
|
|
535
|
+
result.errors.push({
|
|
536
|
+
code: 'E051',
|
|
537
|
+
message: '缺少 mcp.json 配置文件',
|
|
538
|
+
suggestion: '创建 mcp.json 文件定义 MCP 服务器配置',
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 验证 mcp.json 内容
|
|
544
|
+
try {
|
|
545
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
546
|
+
const config = JSON.parse(content) as McpConfig;
|
|
547
|
+
|
|
548
|
+
// 建议指定 protocol_version
|
|
549
|
+
if (!config.protocol_version) {
|
|
550
|
+
result.warnings.push({
|
|
551
|
+
code: 'W050',
|
|
552
|
+
message: '建议指定 protocol_version',
|
|
553
|
+
suggestion: '添加 "protocol_version": "2024-11" 到 mcp.json',
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 建议声明 capabilities
|
|
558
|
+
if (!config.capabilities) {
|
|
559
|
+
result.warnings.push({
|
|
560
|
+
code: 'W051',
|
|
561
|
+
message: '建议声明 capabilities',
|
|
562
|
+
suggestion: '添加 capabilities 对象声明支持的功能',
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {
|
|
566
|
+
result.errors.push({
|
|
567
|
+
code: 'E051',
|
|
568
|
+
message: `mcp.json 解析失败: ${(e as Error).message}`,
|
|
569
|
+
suggestion: '请检查 JSON 格式是否正确',
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ==========================================================================
|
|
575
|
+
// 引用检查
|
|
576
|
+
// ==========================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 引用文件检查
|
|
580
|
+
*/
|
|
581
|
+
private async validateReferences(dirPath: string, result: ValidationResult): Promise<void> {
|
|
582
|
+
// 读取主文件内容
|
|
583
|
+
const mainFiles = ['SKILL.md', 'skill.md', 'README.md'];
|
|
584
|
+
let content = '';
|
|
585
|
+
|
|
586
|
+
for (const file of mainFiles) {
|
|
587
|
+
try {
|
|
588
|
+
content = await fs.readFile(path.join(dirPath, file), 'utf-8');
|
|
589
|
+
break;
|
|
590
|
+
} catch {
|
|
591
|
+
// 继续尝试下一个文件
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!content) return;
|
|
596
|
+
|
|
597
|
+
// 提取引用
|
|
598
|
+
const patterns = [/scripts\/[\w\-\.\/]+/g, /references\/[\w\-\.\/]+/g, /assets\/[\w\-\.\/]+/g];
|
|
599
|
+
|
|
600
|
+
const refs = new Set<string>();
|
|
601
|
+
for (const pattern of patterns) {
|
|
602
|
+
const matches = content.match(pattern);
|
|
603
|
+
if (matches) {
|
|
604
|
+
matches.forEach((m) => refs.add(m));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 检查引用是否存在
|
|
609
|
+
for (const ref of refs) {
|
|
610
|
+
const refPath = path.join(dirPath, ref);
|
|
611
|
+
const exists = await this.exists(refPath);
|
|
612
|
+
|
|
613
|
+
if (!exists) {
|
|
614
|
+
result.errors.push({
|
|
615
|
+
code: 'E100',
|
|
616
|
+
message: `引用文件不存在: ${ref}`,
|
|
617
|
+
suggestion: `创建文件 ${ref} 或从文档中移除引用`,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ==========================================================================
|
|
624
|
+
// 辅助方法
|
|
625
|
+
// ==========================================================================
|
|
626
|
+
|
|
627
|
+
private async exists(filePath: string): Promise<boolean> {
|
|
628
|
+
try {
|
|
629
|
+
await fs.access(filePath);
|
|
630
|
+
return true;
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
package/src/commands/version.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
export const versionCommand = new Command('version')
|
|
7
|
-
.description('显示版本信息')
|
|
8
|
-
.action(() => {
|
|
9
|
-
// Try to read version from package.json
|
|
10
|
-
try {
|
|
11
|
-
// Get the directory of the current module
|
|
12
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
14
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
15
|
-
console.log(`42plugin CLI v${pkg.version}`);
|
|
16
|
-
} catch {
|
|
17
|
-
// Fallback if package.json is not readable
|
|
18
|
-
console.log('42plugin CLI v0.1.0');
|
|
19
|
-
}
|
|
20
|
-
});
|