@42ailab/42plugin 0.1.19 → 0.1.21
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 +1 -1
- package/src/api.ts +26 -4
- package/src/cli.ts +0 -2
- package/src/commands/index.ts +0 -1
- package/src/commands/install.ts +79 -31
- package/src/commands/publish.ts +1 -0
- package/src/commands/search.ts +84 -28
- package/src/commands/uninstall.ts +119 -34
- package/src/db.ts +103 -11
- package/src/types.ts +13 -1
- package/src/commands/completion.ts +0 -210
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
// 版本命令
|
package/src/commands/index.ts
CHANGED
|
@@ -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';
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
}
|
|
262
|
-
|
|
309
|
+
} else {
|
|
310
|
+
console.log(chalk.red(` ✗ ${item.plugin.fullName}: ${result.reason?.message || '未知错误'}`));
|
|
263
311
|
failed++;
|
|
264
312
|
}
|
|
265
313
|
}
|
package/src/commands/publish.ts
CHANGED
|
@@ -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', '仅验证,不实际发布')
|
package/src/commands/search.ts
CHANGED
|
@@ -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
|
|
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>', '
|
|
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
|
|
70
|
+
let allPlugins: SearchResult[] = [];
|
|
71
|
+
let allKits: KitSearchResult[] = [];
|
|
53
72
|
let currentPage = 1;
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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 <
|
|
126
|
+
while (displayedCount < total) {
|
|
88
127
|
// 非 --all 模式下询问用户
|
|
89
128
|
if (!options.all) {
|
|
90
|
-
const remaining =
|
|
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.
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 &&
|
|
176
|
+
if (options.interactive && (allPlugins.length > 0 || allKits.length > 0)) {
|
|
129
177
|
console.log();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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 (
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
import { Database } from 'bun:sqlite';
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
11
12
|
import crypto from 'crypto';
|
|
12
13
|
import * as tar from 'tar';
|
|
13
14
|
import cliProgress from 'cli-progress';
|
|
15
|
+
import { spawn } from 'child_process';
|
|
14
16
|
import { config } from './config';
|
|
15
17
|
import { formatBytes } from './utils';
|
|
16
18
|
import type { LocalProject, LocalCache, LocalInstallation, PluginType, PluginDownloadInfo } from './types';
|
|
17
19
|
|
|
20
|
+
// Windows 平台检测
|
|
21
|
+
const isWindows = process.platform === 'win32';
|
|
22
|
+
|
|
18
23
|
// 文件大小超过 1MB 时显示下载进度条
|
|
19
24
|
const PROGRESS_THRESHOLD = 1024 * 1024; // 1MB
|
|
20
25
|
|
|
@@ -386,8 +391,8 @@ export async function checkInstallConflict(
|
|
|
386
391
|
): Promise<string | null> {
|
|
387
392
|
const client = await getDb();
|
|
388
393
|
const absPath = path.resolve(projectPath);
|
|
389
|
-
//
|
|
390
|
-
const normalizedLinkPath = path.resolve(linkPath).replace(
|
|
394
|
+
// 规范化路径:去掉尾部斜杠以便比较(兼容 Unix 和 Windows)
|
|
395
|
+
const normalizedLinkPath = path.resolve(linkPath).replace(/[\/\\]+$/, '');
|
|
391
396
|
|
|
392
397
|
// 查询该项目下所有安装记录,在应用层比较路径
|
|
393
398
|
const rows = client.prepare(`
|
|
@@ -397,7 +402,7 @@ export async function checkInstallConflict(
|
|
|
397
402
|
`).all(absPath, newFullName) as { full_name: string; link_path: string }[];
|
|
398
403
|
|
|
399
404
|
for (const row of rows) {
|
|
400
|
-
const existingPath = row.link_path.replace(
|
|
405
|
+
const existingPath = row.link_path.replace(/[\/\\]+$/, '');
|
|
401
406
|
if (existingPath === normalizedLinkPath) {
|
|
402
407
|
return row.full_name;
|
|
403
408
|
}
|
|
@@ -659,12 +664,62 @@ export async function getDirectorySize(pathOrDir: string): Promise<number> {
|
|
|
659
664
|
return totalSize;
|
|
660
665
|
}
|
|
661
666
|
|
|
667
|
+
/**
|
|
668
|
+
* 递归复制目录或文件
|
|
669
|
+
*/
|
|
670
|
+
async function copyRecursive(src: string, dest: string): Promise<void> {
|
|
671
|
+
const stat = await fs.stat(src);
|
|
672
|
+
if (stat.isDirectory()) {
|
|
673
|
+
await fs.mkdir(dest, { recursive: true });
|
|
674
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
675
|
+
for (const entry of entries) {
|
|
676
|
+
const srcPath = path.join(src, entry.name);
|
|
677
|
+
const destPath = path.join(dest, entry.name);
|
|
678
|
+
if (entry.isDirectory()) {
|
|
679
|
+
await copyRecursive(srcPath, destPath);
|
|
680
|
+
} else {
|
|
681
|
+
await fs.copyFile(srcPath, destPath);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
await fs.copyFile(src, dest);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Windows 上使用 mklink /J 创建 Junction(目录链接)
|
|
691
|
+
* Junction 不需要管理员权限
|
|
692
|
+
*/
|
|
693
|
+
async function createJunction(sourcePath: string, targetPath: string): Promise<void> {
|
|
694
|
+
return new Promise((resolve, reject) => {
|
|
695
|
+
// mklink /J 创建目录 junction
|
|
696
|
+
const cmd = spawn('cmd', ['/c', 'mklink', '/J', targetPath, sourcePath], {
|
|
697
|
+
windowsHide: true,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
let stderr = '';
|
|
701
|
+
cmd.stderr.on('data', (data) => {
|
|
702
|
+
stderr += data.toString();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
cmd.on('close', (code) => {
|
|
706
|
+
if (code === 0) {
|
|
707
|
+
resolve();
|
|
708
|
+
} else {
|
|
709
|
+
reject(new Error(`创建 Junction 失败: ${stderr}`));
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
cmd.on('error', reject);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
662
717
|
export async function createLink(
|
|
663
718
|
sourcePath: string,
|
|
664
719
|
targetPath: string
|
|
665
720
|
): Promise<void> {
|
|
666
|
-
//
|
|
667
|
-
const normalizedTarget = targetPath.replace(
|
|
721
|
+
// 移除目标路径末尾的斜杠(兼容 Unix 和 Windows)
|
|
722
|
+
const normalizedTarget = targetPath.replace(/[\/\\]+$/, '');
|
|
668
723
|
const targetDir = path.dirname(normalizedTarget);
|
|
669
724
|
await fs.mkdir(targetDir, { recursive: true });
|
|
670
725
|
|
|
@@ -678,19 +733,56 @@ export async function createLink(
|
|
|
678
733
|
// 不存在,继续
|
|
679
734
|
}
|
|
680
735
|
|
|
681
|
-
//
|
|
736
|
+
// 检测源是文件还是目录
|
|
682
737
|
const sourceStat = await fs.stat(sourcePath);
|
|
683
|
-
const
|
|
684
|
-
|
|
738
|
+
const isDirectory = sourceStat.isDirectory();
|
|
739
|
+
|
|
740
|
+
// 尝试创建符号链接
|
|
741
|
+
try {
|
|
742
|
+
const linkType = isDirectory ? 'dir' : 'file';
|
|
743
|
+
await fs.symlink(sourcePath, normalizedTarget, linkType);
|
|
744
|
+
return;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
// 如果不是 Windows,直接抛出错误
|
|
747
|
+
if (!isWindows) {
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Windows 上符号链接失败(EPERM),尝试回退方案
|
|
752
|
+
const isEperm = (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
753
|
+
if (!isEperm) {
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 回退方案 1: 对于目录,尝试使用 Junction
|
|
758
|
+
if (isDirectory) {
|
|
759
|
+
try {
|
|
760
|
+
await createJunction(sourcePath, normalizedTarget);
|
|
761
|
+
return;
|
|
762
|
+
} catch {
|
|
763
|
+
// Junction 也失败,继续尝试复制
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 回退方案 2: 直接复制文件/目录
|
|
768
|
+
// 这是最后的保底方案,虽然会占用更多磁盘空间
|
|
769
|
+
console.warn(
|
|
770
|
+
`[Windows] 符号链接创建失败,回退到文件复制模式\n` +
|
|
771
|
+
` 提示: 启用 Windows 开发者模式可使用符号链接,节省磁盘空间`
|
|
772
|
+
);
|
|
773
|
+
await copyRecursive(sourcePath, normalizedTarget);
|
|
774
|
+
}
|
|
685
775
|
}
|
|
686
776
|
|
|
687
777
|
export async function removeLink(linkPath: string): Promise<void> {
|
|
778
|
+
// 去掉结尾斜杠,否则 lstat 会跟踪符号链接返回目标状态(兼容 Unix 和 Windows)
|
|
779
|
+
const normalizedPath = linkPath.replace(/[\/\\]+$/, '');
|
|
688
780
|
try {
|
|
689
|
-
const stat = await fs.lstat(
|
|
781
|
+
const stat = await fs.lstat(normalizedPath);
|
|
690
782
|
if (stat.isSymbolicLink()) {
|
|
691
|
-
await fs.unlink(
|
|
783
|
+
await fs.unlink(normalizedPath);
|
|
692
784
|
} else if (stat.isDirectory()) {
|
|
693
|
-
await fs.rm(
|
|
785
|
+
await fs.rm(normalizedPath, { recursive: true, force: true });
|
|
694
786
|
}
|
|
695
787
|
} catch {
|
|
696
788
|
// 不存在,忽略
|
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
|
-
|
|
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
|
-
}
|