@42ailab/42plugin 0.1.0-beta.1 → 0.1.5
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 +39 -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
package/src/commands/install.ts
CHANGED
|
@@ -1,323 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install 命令 - 安装插件/套包
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ora, { type Ora } from 'ora';
|
|
4
6
|
import chalk from 'chalk';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
optional?: boolean;
|
|
22
|
-
force?: boolean;
|
|
23
|
-
noCache?: boolean;
|
|
24
|
-
}
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { confirm } from '@inquirer/prompts';
|
|
11
|
+
import { api, QuotaExceededError } from '../api';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import {
|
|
14
|
+
getSessionToken,
|
|
15
|
+
getOrCreateProject,
|
|
16
|
+
addInstallation,
|
|
17
|
+
createLink,
|
|
18
|
+
resolveCachePath,
|
|
19
|
+
} from '../db';
|
|
20
|
+
import { config } from '../config';
|
|
21
|
+
import { parseTarget, getInstallPath, formatBytes, getTypeIcon } from '../utils';
|
|
22
|
+
import { TargetType, type PluginDownloadInfo } from '../types';
|
|
25
23
|
|
|
26
24
|
export const installCommand = new Command('install')
|
|
27
|
-
.description('
|
|
28
|
-
.argument('<target>', '
|
|
29
|
-
.option('-g, --global', '
|
|
30
|
-
.option('--
|
|
31
|
-
.option('--
|
|
32
|
-
.option('--
|
|
33
|
-
.action(async (target
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const spinner = ora('解析安装目标...').start();
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// 1. Parse target
|
|
42
|
-
const parsed = parseTarget(target);
|
|
43
|
-
|
|
44
|
-
// 2. Handle based on type
|
|
45
|
-
switch (parsed.type) {
|
|
46
|
-
case TargetType.Capability:
|
|
47
|
-
await installCapability(parsed.fullName, options, spinner);
|
|
48
|
-
break;
|
|
49
|
-
|
|
50
|
-
case TargetType.CapabilitySlug:
|
|
51
|
-
await installCapabilityBySlug(parsed.author!, parsed.slug!, options, spinner);
|
|
52
|
-
break;
|
|
53
|
-
|
|
54
|
-
case TargetType.Plugin:
|
|
55
|
-
await installPlugin(parsed.fullName, options, spinner);
|
|
56
|
-
break;
|
|
57
|
-
|
|
58
|
-
case TargetType.List:
|
|
59
|
-
await installList(parsed.fullName, options, spinner);
|
|
60
|
-
break;
|
|
61
|
-
|
|
62
|
-
default:
|
|
63
|
-
spinner.fail(`无法识别的安装目标: ${target}`);
|
|
64
|
-
process.exit(1);
|
|
25
|
+
.description('安装插件或套包')
|
|
26
|
+
.argument('<target>', '安装目标 (author/name 或 author/kit/slug)')
|
|
27
|
+
.option('-g, --global', '仅下载到缓存,不链接到项目')
|
|
28
|
+
.option('-f, --force', '强制重新下载')
|
|
29
|
+
.option('--no-cache', '跳过缓存检查')
|
|
30
|
+
.option('--optional', '安装套包时包含可选插件')
|
|
31
|
+
.action(async (target, options) => {
|
|
32
|
+
// 初始化 API token
|
|
33
|
+
const token = await getSessionToken();
|
|
34
|
+
if (token) {
|
|
35
|
+
api.setSessionToken(token);
|
|
65
36
|
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
spinner.fail(`安装失败: ${(error as Error).message}`);
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
37
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
38
|
+
try {
|
|
39
|
+
// 检测是否在 home 目录
|
|
40
|
+
const currentPath = path.resolve(process.cwd());
|
|
41
|
+
const homePath = path.resolve(os.homedir());
|
|
42
|
+
const isHomeDir = currentPath === homePath;
|
|
43
|
+
|
|
44
|
+
if (isHomeDir && !options.global) {
|
|
45
|
+
const answer = await confirm({
|
|
46
|
+
message: '您当前在根目录,是否将插件安装到全局用户目录?',
|
|
47
|
+
default: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (answer) {
|
|
51
|
+
options.global = true;
|
|
52
|
+
// 确保全局目录存在
|
|
53
|
+
await fs.mkdir(config.globalDir, { recursive: true });
|
|
54
|
+
console.log(chalk.gray('将安装到全局目录: ~/.42plugin/global/'));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(chalk.yellow('提示: 建议在具体项目目录下执行安装命令'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 如果是全局安装,确保目录存在
|
|
62
|
+
if (options.global) {
|
|
63
|
+
await fs.mkdir(config.globalDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parsed = parseTarget(target);
|
|
67
|
+
|
|
68
|
+
if (parsed.type === TargetType.Kit) {
|
|
69
|
+
await installKit(parsed.author, parsed.name, options);
|
|
70
|
+
} else {
|
|
71
|
+
await installPlugin(parsed.author, parsed.name, options);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof QuotaExceededError) {
|
|
75
|
+
console.error(chalk.red(`\n${error.message}`));
|
|
76
|
+
if (error.isGuestQuota()) {
|
|
77
|
+
console.log(chalk.yellow('\n提示: 登录后可获得更高配额'));
|
|
78
|
+
console.log(chalk.gray('执行 42plugin auth 登录'));
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const message = (error as Error).message;
|
|
82
|
+
console.error(chalk.red(message));
|
|
83
|
+
|
|
84
|
+
// 为常见错误提供更友好的提示
|
|
85
|
+
if (message.includes('no downloadable content')) {
|
|
86
|
+
console.log(chalk.yellow('\n该插件暂无可下载内容,可能是作者尚未上传文件'));
|
|
87
|
+
} else if (message.includes('not found') || message.includes('404')) {
|
|
88
|
+
console.log(chalk.yellow('\n插件不存在,请检查名称是否正确'));
|
|
89
|
+
console.log(chalk.gray('使用 42plugin search <关键词> 搜索插件'));
|
|
90
|
+
} else if (message.includes('CAPTCHA') || message.includes('验证码')) {
|
|
91
|
+
console.log(chalk.yellow('\n需要验证码验证,请先登录'));
|
|
92
|
+
console.log(chalk.gray('执行 42plugin auth 登录'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
process.exit(1);
|
|
97
96
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Download
|
|
101
|
-
spinner.text = `下载 ${fullName}...`;
|
|
102
|
-
const cachePath = await downloadAndExtract(
|
|
103
|
-
downloadInfo.download_url,
|
|
104
|
-
downloadInfo.checksum,
|
|
105
|
-
fullName,
|
|
106
|
-
downloadInfo.version
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Update cache record
|
|
110
|
-
await cachePlugin({
|
|
111
|
-
full_name: fullName,
|
|
112
|
-
type: downloadInfo.type,
|
|
113
|
-
version: downloadInfo.version,
|
|
114
|
-
cache_path: cachePath,
|
|
115
|
-
checksum: downloadInfo.checksum,
|
|
116
97
|
});
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
await linkToProject(cachePath, downloadInfo, options);
|
|
120
|
-
|
|
121
|
-
spinner.succeed(`${chalk.green('已安装')} ${fullName}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function installCapabilityBySlug(
|
|
99
|
+
async function installPlugin(
|
|
125
100
|
author: string,
|
|
126
|
-
|
|
127
|
-
options:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const displayName = `${author}/${slug}`;
|
|
131
|
-
spinner.text = `获取 ${displayName} 的安装信息...`;
|
|
132
|
-
|
|
133
|
-
// Get download info by slug
|
|
134
|
-
const downloadInfo = await api.getCapabilityDownloadBySlug(author, slug);
|
|
135
|
-
|
|
136
|
-
// Check permissions
|
|
137
|
-
if (downloadInfo.requires_auth && !api.isAuthenticated()) {
|
|
138
|
-
spinner.fail('此插件需要登录才能安装');
|
|
139
|
-
console.log('使用 42plugin auth 登录');
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
101
|
+
name: string,
|
|
102
|
+
options: { global?: boolean; force?: boolean; cache?: boolean }
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const spinner = ora(`获取 ${author}/${name} 信息...`).start();
|
|
142
105
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
106
|
+
try {
|
|
107
|
+
const pluginDownload = await api.getPluginDownload(author, name);
|
|
108
|
+
const downloadInfo = pluginDownload.download;
|
|
109
|
+
|
|
110
|
+
spinner.text = `安装 ${downloadInfo.fullName}...`;
|
|
111
|
+
|
|
112
|
+
// 解析缓存
|
|
113
|
+
const { cachePath, fromCache } = await resolveCachePath(
|
|
114
|
+
downloadInfo,
|
|
115
|
+
options.force || false,
|
|
116
|
+
options.cache !== false
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 确定安装路径
|
|
120
|
+
const projectPath = options.global ? config.globalDir : process.cwd();
|
|
121
|
+
const project = await getOrCreateProject(projectPath);
|
|
122
|
+
const linkPath = path.join(projectPath, downloadInfo.installPath);
|
|
123
|
+
|
|
124
|
+
await createLink(cachePath, linkPath);
|
|
125
|
+
|
|
126
|
+
await addInstallation({
|
|
127
|
+
projectId: project.id,
|
|
128
|
+
fullName: downloadInfo.fullName,
|
|
129
|
+
type: downloadInfo.type,
|
|
130
|
+
version: downloadInfo.version,
|
|
131
|
+
cachePath,
|
|
132
|
+
linkPath,
|
|
133
|
+
source: 'direct',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 同步安装记录(静默)
|
|
137
|
+
if (api.isAuthenticated()) {
|
|
138
|
+
api.recordInstall(downloadInfo.fullName).catch(() => {});
|
|
151
139
|
}
|
|
152
|
-
}
|
|
153
140
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
full_name: downloadInfo.full_name,
|
|
166
|
-
type: downloadInfo.type,
|
|
167
|
-
version: downloadInfo.version,
|
|
168
|
-
cache_path: cachePath,
|
|
169
|
-
checksum: downloadInfo.checksum,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Link to project
|
|
173
|
-
await linkToProject(cachePath, downloadInfo, options);
|
|
174
|
-
|
|
175
|
-
spinner.succeed(`${chalk.green('已安装')} ${displayName}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function installPlugin(
|
|
179
|
-
fullName: string,
|
|
180
|
-
options: InstallOptions,
|
|
181
|
-
spinner: Ora
|
|
182
|
-
) {
|
|
183
|
-
spinner.text = `获取插件 ${fullName} 的信息...`;
|
|
184
|
-
|
|
185
|
-
// Get plugin details - for simple plugins, use the download endpoint
|
|
186
|
-
const [owner, plugin] = fullName.split('/');
|
|
187
|
-
|
|
188
|
-
// Try to get as a simple plugin first
|
|
189
|
-
const downloadInfo = await api.getPluginDownload(owner, plugin);
|
|
190
|
-
|
|
191
|
-
// Check permissions
|
|
192
|
-
if (downloadInfo.requires_auth && !api.isAuthenticated()) {
|
|
193
|
-
spinner.fail('此插件需要登录才能安装');
|
|
194
|
-
console.log('使用 42plugin auth 登录');
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Check cache
|
|
199
|
-
if (!options.force && options.noCache !== true) {
|
|
200
|
-
const cached = await getCachedPlugin(fullName, downloadInfo.version);
|
|
201
|
-
if (cached && cached.checksum === downloadInfo.checksum) {
|
|
202
|
-
spinner.text = '使用缓存...';
|
|
203
|
-
await linkToProject(cached.cache_path, downloadInfo, options);
|
|
204
|
-
spinner.succeed(`${chalk.green('已安装')} ${fullName}`);
|
|
205
|
-
return;
|
|
141
|
+
spinner.succeed(
|
|
142
|
+
`${getTypeIcon(downloadInfo.type)} ${downloadInfo.fullName} v${downloadInfo.version}` +
|
|
143
|
+
(fromCache ? chalk.gray(' (from cache)') : '')
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (options.global) {
|
|
147
|
+
console.log(chalk.green('✓ 已安装到全局用户目录'));
|
|
148
|
+
console.log(chalk.gray(` → ~/.42plugin/global/${downloadInfo.installPath}`));
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.green(`✓ 已安装到当前项目: ${path.basename(projectPath)}`));
|
|
151
|
+
console.log(chalk.gray(` → ${downloadInfo.installPath}`));
|
|
206
152
|
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
spinner.fail('安装失败');
|
|
155
|
+
throw error;
|
|
207
156
|
}
|
|
208
|
-
|
|
209
|
-
// Download
|
|
210
|
-
spinner.text = `下载 ${fullName}...`;
|
|
211
|
-
const cachePath = await downloadAndExtract(
|
|
212
|
-
downloadInfo.download_url,
|
|
213
|
-
downloadInfo.checksum,
|
|
214
|
-
fullName,
|
|
215
|
-
downloadInfo.version
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Update cache record
|
|
219
|
-
await cachePlugin({
|
|
220
|
-
full_name: fullName,
|
|
221
|
-
type: downloadInfo.type,
|
|
222
|
-
version: downloadInfo.version,
|
|
223
|
-
cache_path: cachePath,
|
|
224
|
-
checksum: downloadInfo.checksum,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Link to project
|
|
228
|
-
await linkToProject(cachePath, downloadInfo, options);
|
|
229
|
-
|
|
230
|
-
spinner.succeed(`${chalk.green('已安装')} ${fullName}`);
|
|
231
157
|
}
|
|
232
158
|
|
|
233
|
-
async function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
) {
|
|
238
|
-
|
|
239
|
-
const [username, , slugOrId] = fullName.split('/');
|
|
240
|
-
spinner.text = `获取列表 ${fullName}...`;
|
|
241
|
-
|
|
242
|
-
// Get list download info
|
|
243
|
-
const listDownload = await api.getListDownload(username, slugOrId);
|
|
244
|
-
|
|
245
|
-
console.log();
|
|
246
|
-
console.log(`列表: ${chalk.cyan(listDownload.list.name)}`);
|
|
247
|
-
console.log(`插件: ${listDownload.capabilities.length} 个`);
|
|
248
|
-
console.log();
|
|
249
|
-
|
|
250
|
-
// Filter optional items
|
|
251
|
-
let capabilities = listDownload.capabilities;
|
|
252
|
-
if (!options.optional) {
|
|
253
|
-
capabilities = capabilities.filter(c => c.required);
|
|
254
|
-
}
|
|
159
|
+
async function installKit(
|
|
160
|
+
username: string,
|
|
161
|
+
slug: string,
|
|
162
|
+
options: { global?: boolean; force?: boolean; cache?: boolean; optional?: boolean }
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const spinner = ora(`获取套包 ${username}/kit/${slug}...`).start();
|
|
255
165
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
cap.download_url,
|
|
264
|
-
cap.checksum,
|
|
265
|
-
cap.full_name,
|
|
266
|
-
cap.version
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
await cachePlugin({
|
|
270
|
-
full_name: cap.full_name,
|
|
271
|
-
type: cap.type,
|
|
272
|
-
version: cap.version,
|
|
273
|
-
cache_path: cachePath,
|
|
274
|
-
checksum: cap.checksum,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
await linkToProject(cachePath, cap as DownloadInfo, options, {
|
|
278
|
-
source: 'list',
|
|
279
|
-
source_list: fullName,
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
installed++;
|
|
283
|
-
} catch (error) {
|
|
284
|
-
console.log(chalk.yellow(` 跳过 ${cap.name}: ${(error as Error).message}`));
|
|
166
|
+
try {
|
|
167
|
+
const kitDownload = await api.getKitDownload(username, slug);
|
|
168
|
+
|
|
169
|
+
// 筛选插件
|
|
170
|
+
let plugins = kitDownload.plugins;
|
|
171
|
+
if (!options.optional) {
|
|
172
|
+
plugins = plugins.filter((p) => p.required);
|
|
285
173
|
}
|
|
286
|
-
}
|
|
287
174
|
|
|
288
|
-
|
|
289
|
-
|
|
175
|
+
spinner.succeed(`${kitDownload.kit.name} - ${plugins.length} 个插件`);
|
|
176
|
+
console.log();
|
|
177
|
+
|
|
178
|
+
// 获取项目
|
|
179
|
+
const projectPath = options.global ? config.globalDir : process.cwd();
|
|
180
|
+
const project = await getOrCreateProject(projectPath);
|
|
181
|
+
|
|
182
|
+
// 安装每个插件
|
|
183
|
+
let installed = 0;
|
|
184
|
+
let failed = 0;
|
|
185
|
+
|
|
186
|
+
for (const item of plugins) {
|
|
187
|
+
const pluginSpinner = ora(` ${item.plugin.fullName}...`).start();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const downloadInfo = item.download;
|
|
191
|
+
|
|
192
|
+
const { cachePath, fromCache } = await resolveCachePath(
|
|
193
|
+
downloadInfo,
|
|
194
|
+
options.force || false,
|
|
195
|
+
options.cache !== false
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const linkPath = path.join(projectPath, downloadInfo.installPath);
|
|
199
|
+
await createLink(cachePath, linkPath);
|
|
200
|
+
|
|
201
|
+
await addInstallation({
|
|
202
|
+
projectId: project.id,
|
|
203
|
+
fullName: downloadInfo.fullName,
|
|
204
|
+
type: downloadInfo.type,
|
|
205
|
+
version: downloadInfo.version,
|
|
206
|
+
cachePath,
|
|
207
|
+
linkPath,
|
|
208
|
+
source: 'kit',
|
|
209
|
+
sourceKit: `${username}/kit/${slug}`,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (api.isAuthenticated()) {
|
|
213
|
+
api.recordInstall(downloadInfo.fullName).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pluginSpinner.succeed(
|
|
217
|
+
` ${getTypeIcon(downloadInfo.type)} ${downloadInfo.fullName}` +
|
|
218
|
+
(fromCache ? chalk.gray(' (cached)') : '')
|
|
219
|
+
);
|
|
220
|
+
installed++;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
pluginSpinner.fail(` ${item.plugin.fullName}: ${(error as Error).message}`);
|
|
223
|
+
failed++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
290
226
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
227
|
+
// 汇总
|
|
228
|
+
console.log();
|
|
229
|
+
if (failed === 0) {
|
|
230
|
+
if (options.global) {
|
|
231
|
+
console.log(chalk.green(`✓ 已安装 ${installed} 个插件到全局用户目录`));
|
|
232
|
+
} else {
|
|
233
|
+
console.log(chalk.green(`✓ 已安装 ${installed} 个插件到当前项目`));
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.yellow(`已安装 ${installed} 个,${failed} 个失败`));
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
spinner.fail('安装失败');
|
|
240
|
+
throw error;
|
|
300
241
|
}
|
|
301
|
-
|
|
302
|
-
// Get or create project
|
|
303
|
-
const projectPath = process.cwd();
|
|
304
|
-
const project = await getOrCreateProject(projectPath);
|
|
305
|
-
|
|
306
|
-
// Target path
|
|
307
|
-
const targetPath = path.join(projectPath, info.install_path);
|
|
308
|
-
|
|
309
|
-
// Create link
|
|
310
|
-
await createLink(cachePath, targetPath);
|
|
311
|
-
|
|
312
|
-
// Record in database
|
|
313
|
-
await addProjectPlugin({
|
|
314
|
-
project_id: project.id,
|
|
315
|
-
full_name: info.full_name,
|
|
316
|
-
type: info.type,
|
|
317
|
-
version: info.version,
|
|
318
|
-
cache_path: cachePath,
|
|
319
|
-
link_path: targetPath,
|
|
320
|
-
source: extra?.source || 'direct',
|
|
321
|
-
source_list: extra?.source_list,
|
|
322
|
-
});
|
|
323
242
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,80 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list 命令 - 查看已安装插件
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
|
-
import path from 'path';
|
|
3
6
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
interface ListOptions {
|
|
8
|
-
type?: string;
|
|
9
|
-
json?: boolean;
|
|
10
|
-
}
|
|
7
|
+
import { getInstallations } from '../db';
|
|
8
|
+
import { getTypeIcon, formatRelativeTime } from '../utils';
|
|
9
|
+
import type { PluginType } from '../types';
|
|
11
10
|
|
|
12
11
|
export const listCommand = new Command('list')
|
|
12
|
+
.alias('ls')
|
|
13
13
|
.description('查看当前项目已安装的插件')
|
|
14
|
-
.option('--type <type>', '筛选类型')
|
|
15
|
-
.option('--json', 'JSON
|
|
16
|
-
.action(async (options
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!project) {
|
|
25
|
-
console.log(chalk.yellow('当前目录不是 42plugin 项目'));
|
|
26
|
-
console.log('使用 42plugin install <plugin> 安装第一个插件');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let plugins = await getProjectPlugins(project.id);
|
|
31
|
-
|
|
32
|
-
if (options.type) {
|
|
33
|
-
plugins = plugins.filter(p => p.type === options.type);
|
|
34
|
-
}
|
|
14
|
+
.option('-t, --type <type>', '筛选类型 (skill, agent, command, hook, mcp)')
|
|
15
|
+
.option('--json', '输出 JSON 格式')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
let installations = await getInstallations(process.cwd());
|
|
19
|
+
|
|
20
|
+
// 类型筛选
|
|
21
|
+
if (options.type) {
|
|
22
|
+
installations = installations.filter((i) => i.type === options.type);
|
|
23
|
+
}
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
if (options.json) {
|
|
26
|
+
console.log(JSON.stringify(installations, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
if (installations.length === 0) {
|
|
31
|
+
console.log(chalk.yellow('当前项目未安装任何插件'));
|
|
32
|
+
console.log(chalk.gray('执行 42plugin install <name> 安装插件'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
console.log(`项目: ${chalk.cyan(path.basename(projectPath))}`);
|
|
48
|
-
console.log(`路径: ${projectPath}`);
|
|
49
|
-
console.log();
|
|
50
|
-
console.log(`已安装 ${plugins.length} 个插件:`);
|
|
51
|
-
console.log();
|
|
36
|
+
console.log(chalk.gray(`已安装 ${installations.length} 个插件:\n`));
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
for (const item of installations) {
|
|
39
|
+
const icon = getTypeIcon(item.type);
|
|
40
|
+
const time = formatRelativeTime(item.installedAt);
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
|
|
43
|
+
console.log(chalk.gray(` → ${item.linkPath}`));
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.log(` 版本: ${plugin.version}`);
|
|
63
|
-
console.log(` 路径: ${chalk.gray(relativePath)}`);
|
|
45
|
+
if (item.source === 'kit' && item.sourceKit) {
|
|
46
|
+
console.log(chalk.gray(` 来自套包: ${item.sourceKit}`));
|
|
47
|
+
}
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
console.log(
|
|
49
|
+
console.log(chalk.gray(` 安装于 ${time}`));
|
|
50
|
+
console.log();
|
|
67
51
|
}
|
|
68
|
-
|
|
69
|
-
console.
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(chalk.red((error as Error).message));
|
|
54
|
+
process.exit(1);
|
|
70
55
|
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function groupBy<T>(arr: T[], key: keyof T): Record<string, T[]> {
|
|
75
|
-
return arr.reduce((acc, item) => {
|
|
76
|
-
const k = String(item[key]);
|
|
77
|
-
(acc[k] = acc[k] || []).push(item);
|
|
78
|
-
return acc;
|
|
79
|
-
}, {} as Record<string, T[]>);
|
|
80
|
-
}
|
|
56
|
+
});
|