@42ailab/42plugin 0.1.0-beta.1 → 0.1.2

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.
@@ -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 { api } from '../services/api';
6
- import { downloadAndExtract } from '../services/download';
7
- import { getOrCreateProject, addProjectPlugin } from '../services/project';
8
- import { getCachedPlugin, cachePlugin } from '../services/cache';
9
- import { createLink } from '../services/link';
10
- import { parseTarget, TargetType } from '../utils/target';
11
-
12
- interface DownloadInfo {
13
- type: string;
14
- install_path: string;
15
- full_name: string;
16
- version: string;
17
- }
18
-
19
- interface InstallOptions {
20
- global?: boolean;
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('--optional', '仅安装列表中的可选项')
31
- .option('--force', '强制重新安装')
32
- .option('--no-cache', '不使用缓存')
33
- .action(async (target: string, options: InstallOptions) => {
34
- await install(target, options);
35
- });
36
-
37
- async function install(target: string, options: InstallOptions) {
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
- async function installCapability(
73
- fullName: string,
74
- options: InstallOptions,
75
- spinner: Ora
76
- ) {
77
- spinner.text = `获取 ${fullName} 的安装信息...`;
78
-
79
- // Get download info
80
- const downloadInfo = await api.getCapabilityDownload(fullName);
81
-
82
- // Check permissions
83
- if (downloadInfo.requires_auth && !api.isAuthenticated()) {
84
- spinner.fail('此插件需要登录才能安装');
85
- console.log('使用 42plugin auth 登录');
86
- process.exit(1);
87
- }
88
-
89
- // Check cache
90
- if (!options.force && options.noCache !== true) {
91
- const cached = await getCachedPlugin(fullName, downloadInfo.version);
92
- if (cached && cached.checksum === downloadInfo.checksum) {
93
- spinner.text = '使用缓存...';
94
- await linkToProject(cached.cache_path, downloadInfo, options);
95
- spinner.succeed(`${chalk.green('已安装')} ${fullName}`);
96
- return;
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
- // Link to project
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
- slug: string,
127
- options: InstallOptions,
128
- spinner: Ora
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
- // Check cache
144
- if (!options.force && options.noCache !== true) {
145
- const cached = await getCachedPlugin(downloadInfo.full_name, downloadInfo.version);
146
- if (cached && cached.checksum === downloadInfo.checksum) {
147
- spinner.text = '使用缓存...';
148
- await linkToProject(cached.cache_path, downloadInfo, options);
149
- spinner.succeed(`${chalk.green('已安装')} ${displayName}`);
150
- return;
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
- // Download
155
- spinner.text = `下载 ${displayName}...`;
156
- const cachePath = await downloadAndExtract(
157
- downloadInfo.download_url,
158
- downloadInfo.checksum,
159
- downloadInfo.full_name,
160
- downloadInfo.version
161
- );
162
-
163
- // Update cache record
164
- await cachePlugin({
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 installList(
234
- fullName: string,
235
- options: InstallOptions,
236
- spinner: Ora
237
- ) {
238
- // Parse list identifier
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
- // Install each capability
257
- let installed = 0;
258
- for (const cap of capabilities) {
259
- try {
260
- spinner.text = `安装 ${cap.name}...`;
261
-
262
- const cachePath = await downloadAndExtract(
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
- spinner.succeed(`${chalk.green('已安装')} ${installed}/${capabilities.length} 个插件`);
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
- async function linkToProject(
292
- cachePath: string,
293
- info: { type: string; install_path: string; full_name: string; version: string },
294
- options: InstallOptions,
295
- extra?: { source?: string; source_list?: string }
296
- ) {
297
- if (options.global) {
298
- // Global install doesn't link to project
299
- return;
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
  }
@@ -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 { getProject, getProjectPlugins } from '../services/project';
5
- import type { ProjectPlugin } from '../types/db';
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: ListOptions) => {
17
- await list(options);
18
- });
19
-
20
- async function list(options: ListOptions) {
21
- const projectPath = process.cwd();
22
- const project = await getProject(projectPath);
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
- if (options.json) {
37
- console.log(JSON.stringify(plugins, null, 2));
38
- return;
39
- }
25
+ if (options.json) {
26
+ console.log(JSON.stringify(installations, null, 2));
27
+ return;
28
+ }
40
29
 
41
- if (plugins.length === 0) {
42
- console.log(chalk.yellow('当前项目没有安装任何插件'));
43
- return;
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
- console.log();
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
- // Group by type
54
- const grouped = groupBy(plugins, 'type');
38
+ for (const item of installations) {
39
+ const icon = getTypeIcon(item.type);
40
+ const time = formatRelativeTime(item.installedAt);
55
41
 
56
- for (const [type, items] of Object.entries(grouped)) {
57
- console.log(chalk.bold(`${type}:`));
42
+ console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
43
+ console.log(chalk.gray(` → ${item.linkPath}`));
58
44
 
59
- for (const plugin of items) {
60
- const relativePath = path.relative(projectPath, plugin.link_path);
61
- console.log(` ${chalk.cyan(plugin.full_name)}`);
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
- if (plugin.source === 'list') {
66
- console.log(` 来源: ${chalk.gray(plugin.source_list)}`);
49
+ console.log(chalk.gray(` 安装于 ${time}`));
50
+ console.log();
67
51
  }
68
-
69
- console.log();
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
+ });