@42ailab/42plugin 0.1.9 → 0.1.11

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.9",
3
+ "version": "0.1.11",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -51,11 +51,13 @@
51
51
  "open": "^10.1.0",
52
52
  "ora": "^8.1.1",
53
53
  "pg": "^8.12.0",
54
+ "prompts": "^2.4.2",
54
55
  "tar": "^7.4.3"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/bun": "latest",
58
59
  "@types/cli-progress": "^3.11.6",
60
+ "@types/prompts": "^2.4.9",
59
61
  "@types/tar": "^6.1.13",
60
62
  "typescript": "^5.7.0"
61
63
  }
package/src/cli.ts CHANGED
@@ -24,7 +24,9 @@ const program = new Command();
24
24
  program
25
25
  .name('42plugin')
26
26
  .description('活水插件 - AI 插件管理工具')
27
- .version(version);
27
+ .version(version, '-v, --version', '显示版本号')
28
+ .option('--verbose', '详细输出')
29
+ .option('--no-color', '禁用颜色输出');
28
30
 
29
31
  // 注册命令
30
32
  program.addCommand(authCommand);
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 套包管理服务
3
+ *
4
+ * 管理用户的套包(Kit),包括列出、创建、选择等操作
5
+ */
6
+ import prompts from 'prompts';
7
+ import chalk from 'chalk';
8
+ import { api } from './api';
9
+
10
+ export interface Kit {
11
+ id: string;
12
+ name: string;
13
+ title: string | null;
14
+ description: string | null;
15
+ }
16
+
17
+ /**
18
+ * 获取用户的套包列表
19
+ */
20
+ export async function listUserKits(): Promise<Kit[]> {
21
+ return api.listUserKits();
22
+ }
23
+
24
+ /**
25
+ * 创建默认套包
26
+ *
27
+ * 当用户没有套包时,自动创建名为 "main" 的套包
28
+ */
29
+ export async function createDefaultKit(): Promise<Kit> {
30
+ return api.createKit({
31
+ name: 'main',
32
+ title: 'Main',
33
+ description: '默认插件套包',
34
+ });
35
+ }
36
+
37
+ /**
38
+ * 按名称获取套包
39
+ */
40
+ export async function getKitByName(name: string): Promise<Kit | null> {
41
+ try {
42
+ return await api.getKit(name);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 确定目标套包
50
+ *
51
+ * 优先级:
52
+ * 1. 命令行 --kit 参数指定
53
+ * 2. 用户选择(如有多个套包)
54
+ * 3. 自动创建 main 套包(如用户无套包)
55
+ *
56
+ * @param kitOption 命令行指定的套包名称
57
+ * @param skipPrompt 是否跳过交互式选择
58
+ */
59
+ export async function resolveTargetKit(kitOption?: string, skipPrompt?: boolean): Promise<Kit> {
60
+ // 1. 明确指定套包
61
+ if (kitOption) {
62
+ const kit = await getKitByName(kitOption);
63
+ if (!kit) {
64
+ throw new Error(`套包 "${kitOption}" 不存在`);
65
+ }
66
+ return kit;
67
+ }
68
+
69
+ // 2. 获取用户套包列表
70
+ const kits = await listUserKits();
71
+
72
+ // 3. 无套包 → 自动创建 main
73
+ if (kits.length === 0) {
74
+ console.log(chalk.yellow('您还没有套包,正在创建默认套包 "main"...'));
75
+ const newKit = await createDefaultKit();
76
+ console.log(chalk.green(`✓ 已创建套包: ${newKit.name}`));
77
+ return newKit;
78
+ }
79
+
80
+ // 4. 仅一个套包 → 直接使用
81
+ if (kits.length === 1) {
82
+ return kits[0];
83
+ }
84
+
85
+ // 5. 跳过交互 → 使用第一个套包
86
+ if (skipPrompt) {
87
+ return kits[0];
88
+ }
89
+
90
+ // 6. 多个套包 → 交互选择
91
+ const response = await prompts({
92
+ type: 'select',
93
+ name: 'kit',
94
+ message: '选择目标套包:',
95
+ choices: kits.map(k => ({
96
+ title: formatKitChoice(k),
97
+ value: k,
98
+ })),
99
+ });
100
+
101
+ if (!response.kit) {
102
+ throw new Error('未选择套包');
103
+ }
104
+
105
+ return response.kit;
106
+ }
107
+
108
+ /**
109
+ * 格式化套包选择项
110
+ */
111
+ function formatKitChoice(kit: Kit): string {
112
+ const desc = kit.title || kit.description;
113
+ if (desc) {
114
+ return `${kit.name} - ${desc}`;
115
+ }
116
+ return kit.name;
117
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * 插件清单解析服务
3
+ *
4
+ * 从插件目录读取 SKILL.md / AGENT.md / COMMAND.md / HOOK.md / MCP.md
5
+ * 解析 frontmatter 获取插件元数据
6
+ */
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import matter from 'gray-matter';
10
+
11
+ export type CapabilityType = 'skill' | 'agent' | 'command' | 'hook' | 'mcp';
12
+
13
+ export interface PluginManifest {
14
+ name: string; // 必填,插件名称
15
+ title: string; // 必填,显示标题
16
+ description: string; // 必填,描述
17
+ type: CapabilityType; // 插件类型
18
+ version: string; // 版本号
19
+ tags?: string[]; // 标签
20
+ author?: string; // 作者(可选,默认用当前用户)
21
+ filePath: string; // 清单文件路径
22
+ content: string; // 文件内容(不含 frontmatter)
23
+ }
24
+
25
+ /** 类型文件映射 */
26
+ const TYPE_FILES: Record<string, CapabilityType> = {
27
+ 'SKILL.md': 'skill',
28
+ 'AGENT.md': 'agent',
29
+ 'COMMAND.md': 'command',
30
+ 'HOOK.md': 'hook',
31
+ 'MCP.md': 'mcp',
32
+ };
33
+
34
+ /**
35
+ * 从目录读取插件清单
36
+ *
37
+ * 扫描顺序:SKILL.md → AGENT.md → COMMAND.md → HOOK.md → MCP.md
38
+ * 返回第一个找到的清单
39
+ */
40
+ export async function readManifest(pluginPath: string): Promise<PluginManifest> {
41
+ const resolvedPath = path.resolve(pluginPath);
42
+
43
+ // 检查目录是否存在
44
+ try {
45
+ const stat = await fs.stat(resolvedPath);
46
+ if (!stat.isDirectory()) {
47
+ throw new Error(`路径不是目录: ${resolvedPath}`);
48
+ }
49
+ } catch (error) {
50
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
51
+ throw new Error(`目录不存在: ${resolvedPath}`);
52
+ }
53
+ throw error;
54
+ }
55
+
56
+ // 扫描清单文件
57
+ for (const [filename, type] of Object.entries(TYPE_FILES)) {
58
+ const filePath = path.join(resolvedPath, filename);
59
+
60
+ try {
61
+ const content = await fs.readFile(filePath, 'utf-8');
62
+ const { data: frontmatter, content: body } = matter(content);
63
+
64
+ // 验证必填字段
65
+ const name = frontmatter.name || path.basename(resolvedPath);
66
+ const title = frontmatter.title || frontmatter.name || name;
67
+ const description = frontmatter.description || '';
68
+
69
+ if (!description) {
70
+ throw new Error(`${filename} 缺少 description 字段`);
71
+ }
72
+
73
+ return {
74
+ name: sanitizeName(name),
75
+ title,
76
+ description,
77
+ type,
78
+ version: frontmatter.version || '1.0.0',
79
+ tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : undefined,
80
+ author: frontmatter.author,
81
+ filePath,
82
+ content: body.trim(),
83
+ };
84
+ } catch (error) {
85
+ // 文件不存在,继续检查下一个
86
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
87
+ continue;
88
+ }
89
+ // 其他错误(如解析错误)直接抛出
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ // 未找到任何清单文件
95
+ throw new Error(
96
+ `未找到插件清单文件。请在目录中创建以下文件之一:\n` +
97
+ ` - SKILL.md (技能插件)\n` +
98
+ ` - AGENT.md (代理插件)\n` +
99
+ ` - COMMAND.md (命令插件)\n` +
100
+ ` - HOOK.md (钩子插件)\n` +
101
+ ` - MCP.md (MCP 插件)`
102
+ );
103
+ }
104
+
105
+ /**
106
+ * 清理插件名称
107
+ * - 转小写
108
+ * - 空格替换为连字符
109
+ * - 移除非法字符
110
+ */
111
+ function sanitizeName(name: string): string {
112
+ return name
113
+ .toLowerCase()
114
+ .replace(/\s+/g, '-')
115
+ .replace(/[^a-z0-9-_]/g, '')
116
+ .replace(/-+/g, '-')
117
+ .replace(/^-|-$/g, '');
118
+ }
119
+
120
+ /**
121
+ * 验证清单完整性
122
+ */
123
+ export function validateManifest(manifest: PluginManifest): { valid: boolean; errors: string[] } {
124
+ const errors: string[] = [];
125
+
126
+ if (!manifest.name || manifest.name.length < 2) {
127
+ errors.push('name 字段必须至少 2 个字符');
128
+ }
129
+
130
+ if (!manifest.title || manifest.title.length < 2) {
131
+ errors.push('title 字段必须至少 2 个字符');
132
+ }
133
+
134
+ if (!manifest.description || manifest.description.length < 10) {
135
+ errors.push('description 字段必须至少 10 个字符');
136
+ }
137
+
138
+ if (manifest.name && !/^[a-z0-9-_]+$/.test(manifest.name)) {
139
+ errors.push('name 只能包含小写字母、数字、连字符和下划线');
140
+ }
141
+
142
+ return {
143
+ valid: errors.length === 0,
144
+ errors,
145
+ };
146
+ }