@42ailab/42plugin 0.1.21 → 0.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@42ailab/42plugin",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -4,10 +4,73 @@
4
4
 
5
5
  import { Command } from 'commander';
6
6
  import chalk from 'chalk';
7
+ import path from 'path';
8
+ import fs from 'fs/promises';
9
+ import { select } from '@inquirer/prompts';
7
10
  import { api } from '../api';
8
11
  import { Publisher } from '../services/publisher';
9
12
  import { ValidationError, UploadError, AuthRequiredError } from '../errors';
10
13
  import { getTypeIcon } from '../utils';
14
+ import type { PluginType } from '../types';
15
+
16
+ const VALID_TYPES: PluginType[] = ['skill', 'agent', 'command', 'hook', 'mcp'];
17
+
18
+ const TYPE_DESCRIPTIONS: Record<PluginType, string> = {
19
+ skill: 'skill(技能) - 包含 SKILL.md 的目录,为 Claude 提供特定能力',
20
+ agent: 'agent(代理) - 单个 .md 文件,定义 AI 角色和行为',
21
+ command: 'command(命令) - 单个 .md 文件,定义可执行的斜杠命令',
22
+ hook: 'hook(钩子) - 包含 hooks.json 的目录,响应事件触发',
23
+ mcp: 'mcp(MCP 服务) - 包含 mcp.json 的目录,Model Context Protocol 服务',
24
+ };
25
+
26
+ /**
27
+ * 从路径和文件特征推断建议类型(用于设置默认选项)
28
+ */
29
+ async function suggestType(pluginPath: string): Promise<PluginType> {
30
+ const absPath = path.resolve(pluginPath);
31
+ const normalizedPath = absPath.replace(/\\/g, '/').toLowerCase();
32
+
33
+ // 1. 从路径推断(Claude Code 目录约定)
34
+ if (normalizedPath.includes('/.claude/agents/') || normalizedPath.includes('.claude/agents/')) {
35
+ return 'agent';
36
+ }
37
+ if (normalizedPath.includes('/.claude/commands/') || normalizedPath.includes('.claude/commands/')) {
38
+ return 'command';
39
+ }
40
+ if (normalizedPath.includes('/.claude/skills/') || normalizedPath.includes('.claude/skills/')) {
41
+ return 'skill';
42
+ }
43
+ if (normalizedPath.includes('/.claude/hooks/') || normalizedPath.includes('.claude/hooks/')) {
44
+ return 'hook';
45
+ }
46
+ if (normalizedPath.includes('/.claude/mcp/') || normalizedPath.includes('.claude/mcp/')) {
47
+ return 'mcp';
48
+ }
49
+
50
+ // 2. 检查是否是目录,如果是则检查特征文件
51
+ try {
52
+ const stat = await fs.stat(absPath);
53
+ if (stat.isDirectory()) {
54
+ const files = await fs.readdir(absPath);
55
+ const lowerFiles = files.map((f) => f.toLowerCase());
56
+
57
+ if (lowerFiles.includes('mcp.json') || lowerFiles.includes('.mcp.json')) {
58
+ return 'mcp';
59
+ }
60
+ if (lowerFiles.includes('hooks.json') || lowerFiles.includes('hook.json')) {
61
+ return 'hook';
62
+ }
63
+ if (lowerFiles.includes('skill.md')) {
64
+ return 'skill';
65
+ }
66
+ }
67
+ } catch {
68
+ // 路径不存在,忽略
69
+ }
70
+
71
+ // 3. 默认返回 skill
72
+ return 'skill';
73
+ }
11
74
 
12
75
  export const publishCommand = new Command('publish')
13
76
  .alias('pub')
@@ -17,8 +80,31 @@ export const publishCommand = new Command('publish')
17
80
  .option('-f, --force', '强制发布(即使内容未变化)')
18
81
  .option('--public', '公开发布(默认仅自己可见)')
19
82
  .option('-n, --name <name>', '覆盖插件名称')
83
+ .option('-t, --type <type>', '指定插件类型 (skill|agent|command|hook|mcp)')
20
84
  .action(async (pluginPath, options) => {
21
85
  try {
86
+ // 验证 --type 参数
87
+ let selectedType: PluginType | undefined = options.type;
88
+ if (selectedType && !VALID_TYPES.includes(selectedType)) {
89
+ console.error(chalk.red(`无效的类型: ${selectedType}`));
90
+ console.log(chalk.gray(`有效类型: ${VALID_TYPES.join(', ')}`));
91
+ process.exit(1);
92
+ }
93
+
94
+ // 如果没有指定类型,让用户交互式选择(智能默认)
95
+ if (!selectedType) {
96
+ const suggested = await suggestType(pluginPath);
97
+ selectedType = await select({
98
+ message: '请选择插件类型:',
99
+ choices: VALID_TYPES.map((t) => ({
100
+ name: TYPE_DESCRIPTIONS[t],
101
+ value: t,
102
+ })),
103
+ default: suggested,
104
+ });
105
+ console.log();
106
+ }
107
+
22
108
  // 认证检查已在全局 hook 中完成
23
109
  // 执行发布
24
110
  const publisher = new Publisher();
@@ -28,6 +114,7 @@ export const publishCommand = new Command('publish')
28
114
  force: options.force,
29
115
  visibility: options.public ? 'public' : 'self',
30
116
  name: options.name,
117
+ type: selectedType,
31
118
  });
32
119
 
33
120
  // 显示结果
@@ -49,7 +49,7 @@ export class Publisher {
49
49
  try {
50
50
  // Phase 1: 完整验证
51
51
  spinner.text = '验证插件...';
52
- const validation = await this.validator.validateFull(options.path, options.name);
52
+ const validation = await this.validator.validateFull(options.path, options.name, options.type);
53
53
 
54
54
  // 显示验证结果
55
55
  spinner.stop();
package/src/types.ts CHANGED
@@ -289,6 +289,7 @@ export interface PublishOptions {
289
289
  force?: boolean;
290
290
  visibility?: 'self' | 'public';
291
291
  name?: string;
292
+ type?: PluginType; // 用户指定的类型,覆盖自动推断
292
293
  }
293
294
 
294
295
  export interface PluginMetadata {
@@ -42,8 +42,15 @@ const VALID_TYPES: PluginType[] = ['skill', 'agent', 'command', 'hook', 'mcp'];
42
42
  export class PluginValidator {
43
43
  /**
44
44
  * 完整验证(返回详细结果)
45
+ * @param pluginPath 插件路径
46
+ * @param overrideName 覆盖名称
47
+ * @param overrideType 覆盖类型(用户通过 -t 参数指定)
45
48
  */
46
- async validateFull(pluginPath: string, overrideName?: string): Promise<ValidationResult> {
49
+ async validateFull(
50
+ pluginPath: string,
51
+ overrideName?: string,
52
+ overrideType?: PluginType
53
+ ): Promise<ValidationResult> {
47
54
  const result: ValidationResult = {
48
55
  valid: true,
49
56
  metadata: {
@@ -72,9 +79,9 @@ export class PluginValidator {
72
79
  // Phase 2: 提取元信息并进行类型检测
73
80
  try {
74
81
  if (stat.isFile()) {
75
- result.metadata = await this.extractFileMetadata(absPath, overrideName);
82
+ result.metadata = await this.extractFileMetadata(absPath, overrideName, overrideType);
76
83
  } else if (stat.isDirectory()) {
77
- result.metadata = await this.extractDirectoryMetadata(absPath, overrideName);
84
+ result.metadata = await this.extractDirectoryMetadata(absPath, overrideName, overrideType);
78
85
  } else {
79
86
  result.errors.push({
80
87
  code: 'E000',
@@ -130,7 +137,8 @@ export class PluginValidator {
130
137
  */
131
138
  private async extractFileMetadata(
132
139
  filePath: string,
133
- overrideName?: string
140
+ overrideName?: string,
141
+ overrideType?: PluginType
134
142
  ): Promise<PluginMetadata> {
135
143
  const ext = path.extname(filePath).toLowerCase();
136
144
  const basename = path.basename(filePath, ext);
@@ -146,8 +154,9 @@ export class PluginValidator {
146
154
  const parsed = matter(content);
147
155
  const data = parsed.data as Record<string, unknown>;
148
156
 
149
- // 推断类型
150
- const type = this.inferTypeFromContent(content, data);
157
+ // 类型由用户通过 -t 参数或交互选择指定,必须提供
158
+ // 仅在未指定时使用 frontmatter 作为后备(向后兼容 validate 方法)
159
+ let type: PluginType = overrideType || (data.type as PluginType) || 'skill';
151
160
 
152
161
  return {
153
162
  name: overrideName || (data.name as string) || basename,
@@ -164,35 +173,23 @@ export class PluginValidator {
164
173
  */
165
174
  private async extractDirectoryMetadata(
166
175
  dirPath: string,
167
- overrideName?: string
176
+ overrideName?: string,
177
+ overrideType?: PluginType
168
178
  ): Promise<PluginMetadata> {
169
179
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
170
180
  const files = entries.filter((e) => e.isFile()).map((e) => e.name);
171
- const lowerFiles = files.map((f) => f.toLowerCase());
172
181
  const basename = path.basename(dirPath);
173
182
 
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
- }
183
+ // 类型由用户通过 -t 参数或交互选择指定,必须提供
184
+ // 仅在未指定时使用 'skill' 作为后备(向后兼容 validate 方法)
185
+ const type: PluginType = overrideType || 'skill';
190
186
 
191
187
  // 尝试读取主文件获取元信息
192
188
  const mainFiles = ['SKILL.md', 'skill.md', 'README.md', 'readme.md'];
193
189
  let description: string | undefined;
194
190
  let title: string | undefined;
195
191
  let tags: string[] | undefined;
192
+ let frontmatterData: Record<string, unknown> = {};
196
193
 
197
194
  for (const file of mainFiles) {
198
195
  if (files.includes(file)) {
@@ -204,9 +201,6 @@ export class PluginValidator {
204
201
  if (frontmatterData.title) title = frontmatterData.title as string;
205
202
  if (frontmatterData.description) description = frontmatterData.description as string;
206
203
  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
204
 
211
205
  // 如果没有 frontmatter 描述,取第一段文字
212
206
  if (!description && parsed.content) {
@@ -233,30 +227,6 @@ export class PluginValidator {
233
227
  };
234
228
  }
235
229
 
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
230
  // ==========================================================================
261
231
  // Schema 验证
262
232
  // ==========================================================================