@42ailab/42plugin 0.1.18 → 0.1.20

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.18",
3
+ "version": "0.1.20",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/api.ts CHANGED
@@ -261,18 +261,20 @@ class ApiClient {
261
261
  // ==========================================================================
262
262
 
263
263
  /**
264
- * 搜索插件
264
+ * 搜索插件和套包
265
265
  * 使用 CLI 专用端点 /cli/v1/search
266
- * 响应直接返回 camelCase,只返回插件不返回套包
266
+ * 响应直接返回 camelCase
267
267
  */
268
268
  async search(params: {
269
269
  q: string;
270
+ searchType?: 'plugin' | 'kit' | 'all';
270
271
  pluginType?: string;
271
272
  page?: number;
272
273
  perPage?: number;
273
274
  }): Promise<SearchResponse> {
274
275
  const searchParams = new URLSearchParams();
275
276
  searchParams.set('q', params.q);
277
+ if (params.searchType) searchParams.set('search_type', params.searchType);
276
278
  if (params.pluginType) searchParams.set('type', params.pluginType);
277
279
  if (params.page) searchParams.set('page', String(params.page));
278
280
  if (params.perPage) searchParams.set('per_page', String(params.perPage));
@@ -302,7 +304,7 @@ class ApiClient {
302
304
 
303
305
  // CLI 专用端点直接返回 camelCase 格式
304
306
  interface CliSearchResponse {
305
- data: Array<{
307
+ plugins: Array<{
306
308
  fullName: string;
307
309
  name: string;
308
310
  title: string | null;
@@ -312,9 +314,19 @@ class ApiClient {
312
314
  downloads: number;
313
315
  description: string | null;
314
316
  }>;
317
+ kits: Array<{
318
+ fullName: string;
319
+ name: string;
320
+ author: string;
321
+ pluginCount: number;
322
+ downloads: number;
323
+ description: string | null;
324
+ }>;
315
325
  pagination: {
316
326
  page: number;
317
327
  perPage: number;
328
+ totalPlugins: number;
329
+ totalKits: number;
318
330
  total: number;
319
331
  totalPages: number;
320
332
  };
@@ -323,7 +335,7 @@ class ApiClient {
323
335
  const apiData = raw as CliSearchResponse;
324
336
 
325
337
  return {
326
- data: apiData.data.map((p) => ({
338
+ plugins: apiData.plugins.map((p) => ({
327
339
  fullName: p.fullName,
328
340
  name: p.name,
329
341
  title: p.title,
@@ -336,9 +348,19 @@ class ApiClient {
336
348
  downloads: p.downloads,
337
349
  icon: null,
338
350
  })),
351
+ kits: apiData.kits.map((k) => ({
352
+ fullName: k.fullName,
353
+ name: k.name,
354
+ author: k.author,
355
+ pluginCount: k.pluginCount,
356
+ downloads: k.downloads,
357
+ description: k.description,
358
+ })),
339
359
  pagination: {
340
360
  page: apiData.pagination.page,
341
361
  perPage: apiData.pagination.perPage,
362
+ totalPlugins: apiData.pagination.totalPlugins,
363
+ totalKits: apiData.pagination.totalKits,
342
364
  total: apiData.pagination.total,
343
365
  totalPages: apiData.pagination.totalPages,
344
366
  },
package/src/cli.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  searchCommand,
10
10
  listCommand,
11
11
  uninstallCommand,
12
- completionCommand,
13
12
  setupCommand,
14
13
  publishCommand,
15
14
  checkCommand,
@@ -53,7 +52,6 @@ program.addCommand(listCommand);
53
52
  program.addCommand(uninstallCommand);
54
53
  program.addCommand(publishCommand);
55
54
  program.addCommand(checkCommand);
56
- program.addCommand(completionCommand);
57
55
  program.addCommand(setupCommand);
58
56
 
59
57
  // 版本命令
@@ -7,7 +7,6 @@ export { installCommand } from './install';
7
7
  export { searchCommand } from './search';
8
8
  export { listCommand } from './list';
9
9
  export { uninstallCommand } from './uninstall';
10
- export { completionCommand } from './completion';
11
10
  export { setupCommand } from './setup';
12
11
  export { publishCommand } from './publish';
13
12
  export { checkCommand } from './check';
@@ -192,43 +192,74 @@ async function installKit(
192
192
  }
193
193
 
194
194
  spinner.succeed(`${kitDownload.kit.name} - ${plugins.length} 个插件`);
195
+
196
+ // 超过 42 个插件时提示确认
197
+ if (plugins.length > 42) {
198
+ console.log();
199
+ console.log(chalk.yellow(`⚠ 该套包包含 ${plugins.length} 个插件,数量较多`));
200
+ const shouldContinue = await confirm({
201
+ message: '确定要继续安装吗?',
202
+ default: true,
203
+ });
204
+ if (!shouldContinue) {
205
+ console.log(chalk.gray('已取消安装'));
206
+ return;
207
+ }
208
+ }
209
+
195
210
  console.log();
196
211
 
197
212
  // 获取项目
198
213
  const projectPath = options.global ? config.globalDir : process.cwd();
199
214
  const project = await getOrCreateProject(projectPath);
200
215
 
201
- // 安装每个插件
202
- let installed = 0;
203
- let failed = 0;
204
-
205
- for (const item of plugins) {
206
- const pluginSpinner = ora(` ${item.plugin.fullName}...`).start();
207
-
208
- try {
216
+ // 第一阶段:检查所有冲突(并行)
217
+ const conflictCheckSpinner = ora('检查安装冲突...').start();
218
+ const conflictResults = await Promise.all(
219
+ plugins.map(async (item) => {
209
220
  const downloadInfo = item.download;
210
221
  const linkPath = path.join(projectPath, downloadInfo.installPath);
211
-
212
- // 检查是否有同名冲突
213
222
  const conflictPlugin = await checkInstallConflict(projectPath, linkPath, downloadInfo.fullName);
214
- if (conflictPlugin) {
215
- pluginSpinner.stop();
216
- console.log(chalk.yellow(` ⚠ 路径冲突: ${downloadInfo.installPath}`));
217
- console.log(chalk.gray(` 已安装: ${conflictPlugin}`));
218
-
219
- const shouldReplace = await confirm({
220
- message: `是否替换 ${conflictPlugin}?`,
221
- default: false,
222
- });
223
-
224
- if (!shouldReplace) {
225
- console.log(chalk.gray(' 已跳过'));
226
- continue;
227
- }
228
- // 删除冲突的旧安装记录
229
- await removeInstallation(projectPath, conflictPlugin);
230
- pluginSpinner.start(` ${item.plugin.fullName}...`);
223
+ return { item, linkPath, conflictPlugin };
224
+ })
225
+ );
226
+ conflictCheckSpinner.stop();
227
+
228
+ // 第二阶段:处理冲突(需要用户交互,只能串行)
229
+ const toInstall: typeof conflictResults = [];
230
+ for (const result of conflictResults) {
231
+ if (result.conflictPlugin) {
232
+ console.log(chalk.yellow(`⚠ 路径冲突: ${result.item.download.installPath}`));
233
+ console.log(chalk.gray(` 已安装: ${result.conflictPlugin}`));
234
+ console.log(chalk.gray(` 新插件: ${result.item.plugin.fullName}`));
235
+
236
+ const shouldReplace = await confirm({
237
+ message: `是否替换 ${result.conflictPlugin}?`,
238
+ default: false,
239
+ });
240
+
241
+ if (shouldReplace) {
242
+ await removeInstallation(projectPath, result.conflictPlugin);
243
+ toInstall.push(result);
244
+ } else {
245
+ console.log(chalk.gray(' 已跳过'));
231
246
  }
247
+ } else {
248
+ toInstall.push(result);
249
+ }
250
+ }
251
+
252
+ if (toInstall.length === 0) {
253
+ console.log(chalk.yellow('没有需要安装的插件'));
254
+ return;
255
+ }
256
+
257
+ // 第三阶段:并行安装
258
+ const installSpinner = ora(`安装 ${toInstall.length} 个插件...`).start();
259
+
260
+ const results = await Promise.allSettled(
261
+ toInstall.map(async ({ item, linkPath }) => {
262
+ const downloadInfo = item.download;
232
263
 
233
264
  const { cachePath, fromCache } = await resolveCachePath(
234
265
  downloadInfo,
@@ -253,13 +284,30 @@ async function installKit(
253
284
  api.recordInstall(downloadInfo.fullName).catch(() => {});
254
285
  }
255
286
 
256
- pluginSpinner.succeed(
257
- ` ${getTypeIcon(downloadInfo.type)} ${downloadInfo.fullName}` +
287
+ return { downloadInfo, fromCache };
288
+ })
289
+ );
290
+
291
+ installSpinner.stop();
292
+
293
+ // 显示结果
294
+ let installed = 0;
295
+ let failed = 0;
296
+
297
+ for (let i = 0; i < results.length; i++) {
298
+ const result = results[i];
299
+ const item = toInstall[i].item;
300
+
301
+ if (result.status === 'fulfilled') {
302
+ const { downloadInfo, fromCache } = result.value;
303
+ console.log(
304
+ chalk.green(' ✓ ') +
305
+ `${getTypeIcon(downloadInfo.type)} ${downloadInfo.fullName}` +
258
306
  (fromCache ? chalk.gray(' (cached)') : '')
259
307
  );
260
308
  installed++;
261
- } catch (error) {
262
- pluginSpinner.fail(` ${item.plugin.fullName}: ${(error as Error).message}`);
309
+ } else {
310
+ console.log(chalk.red(` ${item.plugin.fullName}: ${result.reason?.message || '未知错误'}`));
263
311
  failed++;
264
312
  }
265
313
  }
@@ -10,6 +10,7 @@ import { ValidationError, UploadError, AuthRequiredError } from '../errors';
10
10
  import { getTypeIcon } from '../utils';
11
11
 
12
12
  export const publishCommand = new Command('publish')
13
+ .alias('pub')
13
14
  .description('发布插件到 42plugin')
14
15
  .argument('[path]', '插件路径(文件或目录)', '.')
15
16
  .option('--dry-run', '仅验证,不实际发布')
@@ -1,5 +1,5 @@
1
1
  /**
2
- * search 命令 - 搜索插件
2
+ * search 命令 - 搜索插件和套包
3
3
  */
4
4
 
5
5
  import { Command } from 'commander';
@@ -8,11 +8,11 @@ import ora from 'ora';
8
8
  import { checkbox, confirm } from '@inquirer/prompts';
9
9
  import { api } from '../api';
10
10
  import { getTypeIcon, getTypeLabel } from '../utils';
11
- import type { SearchResult } from '../types';
11
+ import type { SearchResult, KitSearchResult } from '../types';
12
12
 
13
13
  const DEFAULT_PAGE_SIZE = 7;
14
14
 
15
- function displayResults(items: SearchResult[], startIndex: number = 0): void {
15
+ function displayPluginResults(items: SearchResult[]): void {
16
16
  for (const item of items) {
17
17
  const icon = getTypeIcon(item.type);
18
18
  const typeLabel = getTypeLabel(item.type);
@@ -35,10 +35,28 @@ function displayResults(items: SearchResult[], startIndex: number = 0): void {
35
35
  }
36
36
  }
37
37
 
38
+ function displayKitResults(items: KitSearchResult[]): void {
39
+ for (const item of items) {
40
+ console.log(`📦 ${chalk.magenta.bold(item.fullName)} ${chalk.gray('[套包]')}`);
41
+
42
+ if (item.description) {
43
+ const desc = item.description.length > 80
44
+ ? item.description.slice(0, 80) + '...'
45
+ : item.description;
46
+ console.log(chalk.gray(` ${desc}`));
47
+ }
48
+
49
+ console.log(chalk.gray(` ${item.pluginCount} 个插件 · ${item.downloads} 下载`));
50
+ console.log();
51
+ }
52
+ }
53
+
38
54
  export const searchCommand = new Command('search')
39
- .description('搜索插件')
55
+ .description('搜索插件和套包')
40
56
  .argument('<keyword>', '搜索关键词')
41
- .option('-t, --type <type>', '筛选类型 (skill, agent, command, hook, mcp)')
57
+ .option('-t, --type <type>', '筛选插件类型 (skill, agent, command, hook, mcp)')
58
+ .option('-k, --kit', '只搜索套包')
59
+ .option('-p, --plugin', '只搜索插件')
42
60
  .option('-l, --limit <n>', '每页数量', String(DEFAULT_PAGE_SIZE))
43
61
  .option('-a, --all', '显示所有结果')
44
62
  .option('--json', '输出 JSON 格式')
@@ -49,14 +67,19 @@ export const searchCommand = new Command('search')
49
67
 
50
68
  try {
51
69
  const pageSize = parseInt(options.limit) || DEFAULT_PAGE_SIZE;
52
- let allResults: SearchResult[] = [];
70
+ let allPlugins: SearchResult[] = [];
71
+ let allKits: KitSearchResult[] = [];
53
72
  let currentPage = 1;
54
- let totalResults = 0;
55
- let displayedCount = 0;
73
+
74
+ // 确定搜索类型
75
+ let searchType: 'plugin' | 'kit' | 'all' = 'all';
76
+ if (options.kit) searchType = 'kit';
77
+ else if (options.plugin || options.type) searchType = 'plugin';
56
78
 
57
79
  // 首次搜索
58
80
  const result = await api.search({
59
81
  q: keyword,
82
+ searchType,
60
83
  pluginType: options.type,
61
84
  perPage: pageSize,
62
85
  page: currentPage,
@@ -69,25 +92,41 @@ export const searchCommand = new Command('search')
69
92
  return;
70
93
  }
71
94
 
72
- if (result.data.length === 0) {
73
- console.log(chalk.yellow('未找到匹配的插件'));
95
+ const totalPlugins = result.pagination.totalPlugins;
96
+ const totalKits = result.pagination.totalKits;
97
+ const total = result.pagination.total;
98
+
99
+ if (total === 0) {
100
+ console.log(chalk.yellow('未找到匹配的结果'));
74
101
  return;
75
102
  }
76
103
 
77
- totalResults = result.pagination.total;
78
- allResults = result.data;
104
+ // 显示统计
105
+ const stats: string[] = [];
106
+ if (totalPlugins > 0) stats.push(`${totalPlugins} 个插件`);
107
+ if (totalKits > 0) stats.push(`${totalKits} 个套包`);
108
+ console.log(chalk.gray(`找到 ${stats.join(',')}:\n`));
109
+
110
+ allPlugins = result.plugins;
111
+ allKits = result.kits;
112
+
113
+ // 先显示套包
114
+ if (allKits.length > 0) {
115
+ displayKitResults(allKits);
116
+ }
79
117
 
80
- console.log(chalk.gray(`找到 ${totalResults} 个结果:\n`));
118
+ // 再显示插件
119
+ if (allPlugins.length > 0) {
120
+ displayPluginResults(allPlugins);
121
+ }
81
122
 
82
- // 显示首批结果
83
- displayResults(allResults);
84
- displayedCount = allResults.length;
123
+ let displayedCount = allPlugins.length + allKits.length;
85
124
 
86
125
  // 如果有更多结果,根据模式决定是否继续
87
- while (displayedCount < totalResults) {
126
+ while (displayedCount < total) {
88
127
  // 非 --all 模式下询问用户
89
128
  if (!options.all) {
90
- const remaining = totalResults - displayedCount;
129
+ const remaining = total - displayedCount;
91
130
 
92
131
  const loadMore = await confirm({
93
132
  message: `还有 ${remaining} 个结果,是否继续查看?`,
@@ -104,6 +143,7 @@ export const searchCommand = new Command('search')
104
143
 
105
144
  const moreResult = await api.search({
106
145
  q: keyword,
146
+ searchType,
107
147
  pluginType: options.type,
108
148
  perPage: pageSize,
109
149
  page: currentPage,
@@ -111,7 +151,7 @@ export const searchCommand = new Command('search')
111
151
 
112
152
  moreSpinner.stop();
113
153
 
114
- if (moreResult.data.length === 0) {
154
+ if (moreResult.plugins.length === 0 && moreResult.kits.length === 0) {
115
155
  break;
116
156
  }
117
157
 
@@ -119,21 +159,37 @@ export const searchCommand = new Command('search')
119
159
  console.log();
120
160
  }
121
161
 
122
- displayResults(moreResult.data);
123
- allResults = [...allResults, ...moreResult.data];
124
- displayedCount += moreResult.data.length;
162
+ if (moreResult.kits.length > 0) {
163
+ displayKitResults(moreResult.kits);
164
+ allKits = [...allKits, ...moreResult.kits];
165
+ }
166
+
167
+ if (moreResult.plugins.length > 0) {
168
+ displayPluginResults(moreResult.plugins);
169
+ allPlugins = [...allPlugins, ...moreResult.plugins];
170
+ }
171
+
172
+ displayedCount += moreResult.plugins.length + moreResult.kits.length;
125
173
  }
126
174
 
127
175
  // 交互式选择安装
128
- if (options.interactive && allResults.length > 0) {
176
+ if (options.interactive && (allPlugins.length > 0 || allKits.length > 0)) {
129
177
  console.log();
130
- const choices = allResults.map((item) => ({
131
- name: `${getTypeIcon(item.type)} ${item.fullName} - ${item.title || item.slogan || item.description?.slice(0, 30) || ''}`,
132
- value: item.fullName,
133
- }));
178
+
179
+ // 构建选项列表(套包在前,插件在后)
180
+ const choices = [
181
+ ...allKits.map((item) => ({
182
+ name: `📦 ${item.fullName} - ${item.description?.slice(0, 30) || `${item.pluginCount} 个插件`}`,
183
+ value: item.fullName,
184
+ })),
185
+ ...allPlugins.map((item) => ({
186
+ name: `${getTypeIcon(item.type)} ${item.fullName} - ${item.title || item.slogan || item.description?.slice(0, 30) || ''}`,
187
+ value: item.fullName,
188
+ })),
189
+ ];
134
190
 
135
191
  const selected = await checkbox({
136
- message: '选择要安装的插件 (空格选择,回车确认):',
192
+ message: '选择要安装的插件/套包 (空格选择,回车确认):',
137
193
  choices,
138
194
  });
139
195
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * uninstall 命令 - 卸载插件
2
+ * uninstall 命令 - 卸载插件或套包
3
3
  */
4
4
 
5
5
  import { Command } from 'commander';
@@ -8,55 +8,140 @@ import ora from 'ora';
8
8
  import { api } from '../api';
9
9
  import { getInstallations, removeInstallation, removeLink, removeCache } from '../db';
10
10
  import { parseTarget } from '../utils';
11
+ import { TargetType } from '../types';
11
12
 
12
13
  export const uninstallCommand = new Command('uninstall')
13
14
  .alias('rm')
14
- .description('卸载插件')
15
- .argument('<target>', '插件名 (author/name)')
15
+ .description('卸载插件或套包')
16
+ .argument('<target>', '插件名 (author/name) 或套包名 (author/kit/slug)')
16
17
  .option('--purge', '同时清除缓存')
17
18
  .action(async (target, options) => {
18
- // token 已在全局 hook 中注入
19
- const spinner = ora(`卸载 ${target}...`).start();
20
-
21
19
  try {
22
20
  const parsed = parseTarget(target);
23
21
  const projectPath = process.cwd();
24
22
 
25
- // 查找安装记录
26
- const installations = await getInstallations(projectPath);
27
- const installation = installations.find((i) => i.fullName === parsed.fullName);
28
-
29
- if (!installation) {
30
- spinner.fail(`未找到已安装的插件: ${target}`);
31
- process.exit(1);
23
+ if (parsed.type === TargetType.Kit) {
24
+ // 卸载套包:批量卸载所有来自该套包的插件
25
+ await uninstallKit(projectPath, parsed.fullName, options.purge);
26
+ } else {
27
+ // 卸载单个插件
28
+ await uninstallPlugin(projectPath, parsed.fullName, options.purge);
32
29
  }
30
+ } catch (error) {
31
+ console.error(chalk.red((error as Error).message));
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ /**
37
+ * 卸载单个插件
38
+ */
39
+ async function uninstallPlugin(
40
+ projectPath: string,
41
+ fullName: string,
42
+ purge: boolean
43
+ ): Promise<void> {
44
+ const spinner = ora(`卸载 ${fullName}...`).start();
45
+
46
+ // 查找安装记录
47
+ const installations = await getInstallations(projectPath);
48
+ const installation = installations.find((i) => i.fullName === fullName);
49
+
50
+ if (!installation) {
51
+ spinner.fail(`未找到已安装的插件: ${fullName}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ // 移除链接
56
+ await removeLink(installation.linkPath);
57
+
58
+ // 移除安装记录
59
+ await removeInstallation(projectPath, fullName);
60
+
61
+ // 同步卸载记录(静默)
62
+ if (api.isAuthenticated()) {
63
+ const parts = fullName.split('/');
64
+ const author = parts[0];
65
+ const name = parts.slice(1).join('/');
66
+ api.removeInstallRecord(author, name).catch(() => {});
67
+ }
68
+
69
+ // 清除缓存
70
+ if (purge) {
71
+ const removed = await removeCache(fullName, installation.version);
72
+ if (removed) {
73
+ spinner.succeed(`已卸载 ${fullName}(含缓存)`);
74
+ } else {
75
+ spinner.succeed(`已卸载 ${fullName}`);
76
+ console.log(chalk.gray(' 缓存已不存在'));
77
+ }
78
+ } else {
79
+ spinner.succeed(`已卸载 ${fullName}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 卸载套包(批量卸载所有来自该套包的插件)
85
+ */
86
+ async function uninstallKit(
87
+ projectPath: string,
88
+ kitFullName: string,
89
+ purge: boolean
90
+ ): Promise<void> {
91
+ const spinner = ora(`查找套包 ${kitFullName} 的插件...`).start();
92
+
93
+ // 查找所有来自该套包的安装记录
94
+ const installations = await getInstallations(projectPath);
95
+ const kitInstallations = installations.filter((i) => i.sourceKit === kitFullName);
96
+
97
+ if (kitInstallations.length === 0) {
98
+ spinner.fail(`未找到来自套包 ${kitFullName} 的已安装插件`);
99
+ process.exit(1);
100
+ }
101
+
102
+ spinner.text = `卸载 ${kitInstallations.length} 个插件...`;
33
103
 
104
+ // 批量卸载
105
+ let successCount = 0;
106
+ let failCount = 0;
107
+
108
+ for (const installation of kitInstallations) {
109
+ try {
34
110
  // 移除链接
35
111
  await removeLink(installation.linkPath);
36
112
 
37
113
  // 移除安装记录
38
- await removeInstallation(projectPath, parsed.fullName);
39
-
40
- // 同步卸载记录(静默)
41
- if (api.isAuthenticated()) {
42
- api.removeInstallRecord(parsed.author, parsed.name).catch(() => {});
43
- }
114
+ await removeInstallation(projectPath, installation.fullName);
44
115
 
45
116
  // 清除缓存
46
- if (options.purge) {
47
- const removed = await removeCache(parsed.fullName, installation.version);
48
- if (removed) {
49
- spinner.succeed(`已卸载 ${target}(含缓存)`);
50
- } else {
51
- spinner.succeed(`已卸载 ${target}`);
52
- console.log(chalk.gray(' 缓存已不存在'));
53
- }
54
- } else {
55
- spinner.succeed(`已卸载 ${target}`);
117
+ if (purge) {
118
+ await removeCache(installation.fullName, installation.version);
56
119
  }
57
- } catch (error) {
58
- spinner.fail('卸载失败');
59
- console.error(chalk.red((error as Error).message));
60
- process.exit(1);
120
+
121
+ successCount++;
122
+ } catch {
123
+ failCount++;
61
124
  }
62
- });
125
+ }
126
+
127
+ // 同步卸载记录(静默)
128
+ if (api.isAuthenticated()) {
129
+ const parts = kitFullName.split('/');
130
+ const author = parts[0];
131
+ const slug = parts[2];
132
+ api.removeInstallRecord(author, `kit/${slug}`).catch(() => {});
133
+ }
134
+
135
+ spinner.stop();
136
+
137
+ if (failCount === 0) {
138
+ console.log(chalk.green(`✓ 已卸载套包 ${kitFullName}`));
139
+ console.log(chalk.gray(` 共 ${successCount} 个插件`));
140
+ if (purge) {
141
+ console.log(chalk.gray(' 缓存已清除'));
142
+ }
143
+ } else {
144
+ console.log(chalk.yellow(`⚠ 部分卸载完成`));
145
+ console.log(chalk.gray(` 成功: ${successCount}, 失败: ${failCount}`));
146
+ }
147
+ }
package/src/db.ts CHANGED
@@ -467,7 +467,8 @@ export async function downloadAndExtract(
467
467
  url: string,
468
468
  expectedChecksum: string,
469
469
  fullName: string,
470
- version: string
470
+ version: string,
471
+ pluginType: string
471
472
  ): Promise<string> {
472
473
  const parts = fullName.split('/');
473
474
  const targetDir = path.join(config.cacheDir, ...parts, version);
@@ -550,7 +551,7 @@ export async function downloadAndExtract(
550
551
  await fs.unlink(tempFile).catch(() => {});
551
552
 
552
553
  // 返回最终路径(如果只有一个子目录则进入)
553
- return resolveFinalPath(targetDir);
554
+ return resolveFinalPath(targetDir, pluginType);
554
555
  } catch (error) {
555
556
  await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
556
557
  throw error;
@@ -559,8 +560,9 @@ export async function downloadAndExtract(
559
560
 
560
561
  /**
561
562
  * 修正旧缓存路径(如果是目录但应该是文件)
563
+ * @param pluginType 插件类型:skill 需要目录,agent/command 需要文件
562
564
  */
563
- async function correctCachePath(cachePath: string): Promise<string> {
565
+ async function correctCachePath(cachePath: string, pluginType: string): Promise<string> {
564
566
  try {
565
567
  const stat = await fs.stat(cachePath);
566
568
  if (!stat.isDirectory()) {
@@ -577,11 +579,16 @@ async function correctCachePath(cachePath: string): Promise<string> {
577
579
  // 如果只有一个子目录(且没有其他文件),递归进入
578
580
  if (dirs.length === 1 && nonMetadataFiles.length === 0) {
579
581
  const subDir = path.join(cachePath, dirs[0].name);
580
- return correctCachePath(subDir);
582
+ return correctCachePath(subDir, pluginType);
581
583
  }
582
584
 
583
- // 如果只有一个主文件(排除 metadata.json),返回该文件路径
585
+ // Skill 类型:保持目录结构(包含 SKILL.md)
586
+ // Agent/Command 类型:返回 .md 文件路径
584
587
  if (nonMetadataFiles.length === 1 && dirs.length === 0) {
588
+ if (pluginType === 'skill') {
589
+ // Skill 需要目录,不返回文件
590
+ return cachePath;
591
+ }
585
592
  return path.join(cachePath, nonMetadataFiles[0].name);
586
593
  }
587
594
 
@@ -591,7 +598,11 @@ async function correctCachePath(cachePath: string): Promise<string> {
591
598
  }
592
599
  }
593
600
 
594
- async function resolveFinalPath(dir: string): Promise<string> {
601
+ /**
602
+ * 解析最终路径
603
+ * @param pluginType 插件类型:skill 需要目录,agent/command 需要文件
604
+ */
605
+ async function resolveFinalPath(dir: string, pluginType: string): Promise<string> {
595
606
  const entries = await fs.readdir(dir, { withFileTypes: true });
596
607
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
597
608
  const files = entries.filter((e) => e.isFile() && !e.name.startsWith('.'));
@@ -603,13 +614,17 @@ async function resolveFinalPath(dir: string): Promise<string> {
603
614
  const subEntries = await fs.readdir(subDir);
604
615
  if (subEntries.length > 0) {
605
616
  // 递归查找,处理 agent/command 类型的 name/name.md 结构
606
- return resolveFinalPath(subDir);
617
+ return resolveFinalPath(subDir, pluginType);
607
618
  }
608
619
  }
609
620
 
610
- // 如果只有一个主文件(排除 metadata.json),返回该文件路径
611
- // 这是单文件插件(如 agent/command)的情况
621
+ // Skill 类型:保持目录结构(包含 SKILL.md)
622
+ // Agent/Command 类型:返回 .md 文件路径
612
623
  if (nonMetadataFiles.length === 1 && dirs.length === 0) {
624
+ if (pluginType === 'skill') {
625
+ // Skill 需要目录,不返回文件
626
+ return dir;
627
+ }
613
628
  return path.join(dir, nonMetadataFiles[0].name);
614
629
  }
615
630
 
@@ -670,12 +685,14 @@ export async function createLink(
670
685
  }
671
686
 
672
687
  export async function removeLink(linkPath: string): Promise<void> {
688
+ // 去掉结尾斜杠,否则 lstat 会跟踪符号链接返回目标状态
689
+ const normalizedPath = linkPath.replace(/\/+$/, '');
673
690
  try {
674
- const stat = await fs.lstat(linkPath);
691
+ const stat = await fs.lstat(normalizedPath);
675
692
  if (stat.isSymbolicLink()) {
676
- await fs.unlink(linkPath);
693
+ await fs.unlink(normalizedPath);
677
694
  } else if (stat.isDirectory()) {
678
- await fs.rm(linkPath, { recursive: true, force: true });
695
+ await fs.rm(normalizedPath, { recursive: true, force: true });
679
696
  }
680
697
  } catch {
681
698
  // 不存在,忽略
@@ -701,8 +718,8 @@ export async function resolveCachePath(
701
718
  // 验证缓存文件是否仍然存在
702
719
  try {
703
720
  await fs.access(cached.cachePath);
704
- // 修正旧缓存的路径(可能是目录而应该是文件)
705
- const correctedPath = await correctCachePath(cached.cachePath);
721
+ // 修正旧缓存的路径(根据类型:skill 需要目录,agent/command 需要文件)
722
+ const correctedPath = await correctCachePath(cached.cachePath, downloadInfo.type);
706
723
  // 如果路径被修正,更新缓存记录
707
724
  if (correctedPath !== cached.cachePath) {
708
725
  await setCache({
@@ -726,7 +743,8 @@ export async function resolveCachePath(
726
743
  downloadInfo.downloadUrl,
727
744
  downloadInfo.checksum,
728
745
  downloadInfo.fullName,
729
- downloadInfo.version
746
+ downloadInfo.version,
747
+ downloadInfo.type
730
748
  );
731
749
 
732
750
  // 更新缓存记录
package/src/types.ts CHANGED
@@ -95,11 +95,23 @@ export interface SearchResult {
95
95
  icon: string | null;
96
96
  }
97
97
 
98
+ export interface KitSearchResult {
99
+ fullName: string;
100
+ name: string;
101
+ author: string;
102
+ pluginCount: number;
103
+ downloads: number;
104
+ description: string | null;
105
+ }
106
+
98
107
  export interface SearchResponse {
99
- data: SearchResult[];
108
+ plugins: SearchResult[];
109
+ kits: KitSearchResult[];
100
110
  pagination: {
101
111
  page: number;
102
112
  perPage: number;
113
+ totalPlugins: number;
114
+ totalKits: number;
103
115
  total: number;
104
116
  totalPages: number;
105
117
  };
@@ -1,210 +0,0 @@
1
- /**
2
- * completion 命令 - 生成 shell 自动补全脚本
3
- */
4
-
5
- import { Command } from 'commander';
6
- import chalk from 'chalk';
7
-
8
- export const completionCommand = new Command('completion')
9
- .description('生成 shell 自动补全脚本')
10
- .argument('<shell>', 'shell 类型 (bash, zsh, fish)')
11
- .action((shell: string) => {
12
- switch (shell.toLowerCase()) {
13
- case 'bash':
14
- console.log(generateBashCompletion());
15
- break;
16
- case 'zsh':
17
- console.log(generateZshCompletion());
18
- break;
19
- case 'fish':
20
- console.log(generateFishCompletion());
21
- break;
22
- default:
23
- console.error(chalk.red(`不支持的 shell: ${shell}`));
24
- console.log(chalk.gray('支持的 shell: bash, zsh, fish'));
25
- process.exit(1);
26
- }
27
- });
28
-
29
- function generateBashCompletion(): string {
30
- return `# 42plugin bash completion
31
- # 添加到 ~/.bashrc:
32
- # eval "$(42plugin completion bash)"
33
- # 或:
34
- # 42plugin completion bash >> ~/.bashrc
35
- #
36
- # 注意: 动态补全插件名需要安装 jq (可选)
37
- # 未安装 jq 时仍可使用命令和选项补全
38
-
39
- _42plugin_completions() {
40
- local cur="\${COMP_WORDS[COMP_CWORD]}"
41
- local prev="\${COMP_WORDS[COMP_CWORD-1]}"
42
-
43
- case "\${prev}" in
44
- 42plugin)
45
- COMPREPLY=( $(compgen -W "auth search install list uninstall version completion" -- "\${cur}") )
46
- return 0
47
- ;;
48
- install|uninstall)
49
- # 动态补全插件名 (需要 jq)
50
- if command -v jq &>/dev/null; then
51
- if [[ "\${cur}" == */* ]]; then
52
- local plugins=$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null)
53
- COMPREPLY=( $(compgen -W "\${plugins}" -- "\${cur}") )
54
- else
55
- local authors=$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null | cut -d'/' -f1 | sort -u)
56
- COMPREPLY=( $(compgen -W "\${authors}" -- "\${cur}") )
57
- fi
58
- fi
59
- return 0
60
- ;;
61
- search)
62
- # 类型补全
63
- if [[ "\${cur}" == -* ]]; then
64
- COMPREPLY=( $(compgen -W "-t --type -l --limit --json -i --interactive" -- "\${cur}") )
65
- fi
66
- return 0
67
- ;;
68
- -t|--type)
69
- COMPREPLY=( $(compgen -W "skill agent command hook mcp" -- "\${cur}") )
70
- return 0
71
- ;;
72
- auth)
73
- COMPREPLY=( $(compgen -W "--status --logout" -- "\${cur}") )
74
- return 0
75
- ;;
76
- list|ls)
77
- if [[ "\${cur}" == -* ]]; then
78
- COMPREPLY=( $(compgen -W "-t --type --json" -- "\${cur}") )
79
- fi
80
- return 0
81
- ;;
82
- completion)
83
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
84
- return 0
85
- ;;
86
- esac
87
- }
88
-
89
- complete -F _42plugin_completions 42plugin
90
- `;
91
- }
92
-
93
- function generateZshCompletion(): string {
94
- return `#compdef 42plugin
95
- # 42plugin zsh completion
96
- # 添加到 ~/.zshrc:
97
- # eval "$(42plugin completion zsh)"
98
- # 或:
99
- # 42plugin completion zsh >> ~/.zshrc
100
- #
101
- # 注意: 动态补全插件名需要安装 jq (可选)
102
-
103
- _42plugin() {
104
- local -a commands
105
- commands=(
106
- 'auth:登录/登出/查看状态'
107
- 'search:搜索插件'
108
- 'install:安装插件或套包'
109
- 'list:查看已安装插件'
110
- 'uninstall:卸载插件'
111
- 'version:显示版本'
112
- 'completion:生成补全脚本'
113
- )
114
-
115
- _arguments -C \\
116
- '1: :->command' \\
117
- '*: :->args'
118
-
119
- case $state in
120
- command)
121
- _describe 'command' commands
122
- ;;
123
- args)
124
- case $words[2] in
125
- install|uninstall)
126
- # 插件名补全 (需要 jq)
127
- if (( $+commands[jq] )); then
128
- local plugins
129
- plugins=(\${(f)"$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null)"})
130
- _describe 'plugin' plugins
131
- fi
132
- ;;
133
- search)
134
- _arguments \\
135
- '-t[筛选类型]:type:(skill agent command hook mcp)' \\
136
- '--type[筛选类型]:type:(skill agent command hook mcp)' \\
137
- '-l[结果数量]:limit:' \\
138
- '--limit[结果数量]:limit:' \\
139
- '--json[JSON 输出]' \\
140
- '-i[交互式安装]' \\
141
- '--interactive[交互式安装]'
142
- ;;
143
- auth)
144
- _arguments \\
145
- '--status[查看登录状态]' \\
146
- '--logout[登出]'
147
- ;;
148
- list)
149
- _arguments \\
150
- '-t[筛选类型]:type:(skill agent command hook mcp)' \\
151
- '--type[筛选类型]:type:(skill agent command hook mcp)' \\
152
- '--json[JSON 输出]'
153
- ;;
154
- completion)
155
- _describe 'shell' '(bash zsh fish)'
156
- ;;
157
- esac
158
- ;;
159
- esac
160
- }
161
-
162
- _42plugin
163
- `;
164
- }
165
-
166
- function generateFishCompletion(): string {
167
- return `# 42plugin fish completion
168
- # 添加到 ~/.config/fish/config.fish:
169
- # 42plugin completion fish | source
170
- # 或:
171
- # 42plugin completion fish > ~/.config/fish/completions/42plugin.fish
172
-
173
- complete -c 42plugin -f
174
-
175
- # Commands
176
- complete -c 42plugin -n '__fish_use_subcommand' -a auth -d '登录/登出/查看状态'
177
- complete -c 42plugin -n '__fish_use_subcommand' -a search -d '搜索插件'
178
- complete -c 42plugin -n '__fish_use_subcommand' -a install -d '安装插件或套包'
179
- complete -c 42plugin -n '__fish_use_subcommand' -a list -d '查看已安装插件'
180
- complete -c 42plugin -n '__fish_use_subcommand' -a uninstall -d '卸载插件'
181
- complete -c 42plugin -n '__fish_use_subcommand' -a version -d '显示版本'
182
- complete -c 42plugin -n '__fish_use_subcommand' -a completion -d '生成补全脚本'
183
-
184
- # auth options
185
- complete -c 42plugin -n '__fish_seen_subcommand_from auth' -l status -d '查看登录状态'
186
- complete -c 42plugin -n '__fish_seen_subcommand_from auth' -l logout -d '登出'
187
-
188
- # search options
189
- complete -c 42plugin -n '__fish_seen_subcommand_from search' -s t -l type -d '筛选类型' -a 'skill agent command hook mcp'
190
- complete -c 42plugin -n '__fish_seen_subcommand_from search' -s l -l limit -d '结果数量'
191
- complete -c 42plugin -n '__fish_seen_subcommand_from search' -l json -d 'JSON 输出'
192
- complete -c 42plugin -n '__fish_seen_subcommand_from search' -s i -l interactive -d '交互式安装'
193
-
194
- # list options
195
- complete -c 42plugin -n '__fish_seen_subcommand_from list' -s t -l type -d '筛选类型' -a 'skill agent command hook mcp'
196
- complete -c 42plugin -n '__fish_seen_subcommand_from list' -l json -d 'JSON 输出'
197
-
198
- # install options
199
- complete -c 42plugin -n '__fish_seen_subcommand_from install' -s g -l global -d '安装到全局目录'
200
- complete -c 42plugin -n '__fish_seen_subcommand_from install' -s f -l force -d '强制重新下载'
201
- complete -c 42plugin -n '__fish_seen_subcommand_from install' -l no-cache -d '跳过缓存检查'
202
- complete -c 42plugin -n '__fish_seen_subcommand_from install' -l optional -d '安装套包时包含可选插件'
203
-
204
- # uninstall options
205
- complete -c 42plugin -n '__fish_seen_subcommand_from uninstall' -l purge -d '同时清除缓存'
206
-
207
- # completion argument
208
- complete -c 42plugin -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell 类型'
209
- `;
210
- }