@42ailab/42plugin 0.1.0-beta.0
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 +104 -0
- package/package.json +58 -0
- package/src/cli.ts +27 -0
- package/src/commands/auth.ts +114 -0
- package/src/commands/install.ts +323 -0
- package/src/commands/list.ts +80 -0
- package/src/commands/search.ts +87 -0
- package/src/commands/uninstall.ts +58 -0
- package/src/commands/version.ts +20 -0
- package/src/config.ts +44 -0
- package/src/db/client.ts +180 -0
- package/src/index.ts +35 -0
- package/src/services/api.ts +128 -0
- package/src/services/auth.ts +46 -0
- package/src/services/cache.ts +101 -0
- package/src/services/download.ts +148 -0
- package/src/services/link.ts +86 -0
- package/src/services/project.ts +179 -0
- package/src/types/api.ts +115 -0
- package/src/types/db.ts +31 -0
- package/src/utils/errors.ts +40 -0
- package/src/utils/platform.ts +6 -0
- package/src/utils/target.ts +114 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# 42plugin CLI
|
|
2
|
+
|
|
3
|
+
Claude Code 插件管理器,支持搜索、安装、管理来自 42plugin 平台的能力、插件与插件列表。
|
|
4
|
+
|
|
5
|
+
## 前提条件
|
|
6
|
+
- 需要安装 [Bun](https://bun.sh)(用于运行与构建)。
|
|
7
|
+
- Node.js/npm 非必需;依赖通过 `bun install` 处理。
|
|
8
|
+
|
|
9
|
+
## 安装依赖
|
|
10
|
+
```bash
|
|
11
|
+
bun install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 本地开发与运行
|
|
15
|
+
- 直接运行入口(便于调试):
|
|
16
|
+
```bash
|
|
17
|
+
bun run src/index.ts <command> [options]
|
|
18
|
+
```
|
|
19
|
+
- 或使用脚本(等同于上方):
|
|
20
|
+
```bash
|
|
21
|
+
bun run dev -- <command> [options]
|
|
22
|
+
```
|
|
23
|
+
- 构建跨平台可执行文件:
|
|
24
|
+
```bash
|
|
25
|
+
bun run build
|
|
26
|
+
# 输出位于 dist/ 下,如 dist/42plugin-darwin-arm64、dist/42plugin-linux-x64 等
|
|
27
|
+
```
|
|
28
|
+
构建后二进制即可直接执行,例如 `./dist/42plugin-darwin-arm64 search claude`。
|
|
29
|
+
|
|
30
|
+
## CLI 使用
|
|
31
|
+
### 登录
|
|
32
|
+
- 浏览器授权登录:`42plugin auth`
|
|
33
|
+
- 查看状态:`42plugin auth --status`
|
|
34
|
+
- 登出并清理凭证:`42plugin auth --logout`
|
|
35
|
+
|
|
36
|
+
### 搜索插件/能力/列表
|
|
37
|
+
```bash
|
|
38
|
+
42plugin search <关键词> [--type skill|agent|command|hook|list] [--limit 50] [--json]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 安装
|
|
42
|
+
支持的安装目标格式:
|
|
43
|
+
- 能力:`owner/repo:plugin:type:name`
|
|
44
|
+
- 插件(带插件名):`owner/repo:plugin`
|
|
45
|
+
- 简化插件:`owner/plugin`
|
|
46
|
+
- 列表:`owner/list/<slug>`
|
|
47
|
+
|
|
48
|
+
常用示例:
|
|
49
|
+
```bash
|
|
50
|
+
42plugin install user/repo:cool-plugin
|
|
51
|
+
42plugin install user/repo:cool-plugin:command:deploy
|
|
52
|
+
42plugin install user/list/tools # 安装列表(默认只装必选项)
|
|
53
|
+
42plugin install user/list/tools --optional # 包含列表中的可选项
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
选项:
|
|
57
|
+
- `-g, --global`:仅下载到缓存,不链接到当前项目。
|
|
58
|
+
- `--force`:忽略缓存,强制重新下载。
|
|
59
|
+
- `--no-cache`:跳过缓存命中检查。
|
|
60
|
+
- `--optional`:安装列表时包含可选能力(默认只安装必选)。
|
|
61
|
+
|
|
62
|
+
安装会将包解压到本地缓存,再在当前工作目录创建链接到具体安装路径(例如 `.claude/` 下),同时在本地数据库记录。请在项目根目录执行。
|
|
63
|
+
|
|
64
|
+
### 查看已安装
|
|
65
|
+
```bash
|
|
66
|
+
42plugin list [--type <type>] [--json]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 卸载
|
|
70
|
+
```bash
|
|
71
|
+
42plugin uninstall <full_name> [--purge]
|
|
72
|
+
```
|
|
73
|
+
`--purge` 会同时删除缓存内容。
|
|
74
|
+
|
|
75
|
+
### 查看版本
|
|
76
|
+
```bash
|
|
77
|
+
42plugin version
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 数据存储位置
|
|
81
|
+
- 配置与数据库:`~/.42plugin/local.db`(Windows 为 `%APPDATA%/42plugin/local.db`)
|
|
82
|
+
- 缓存:`~/.42plugin/cache`
|
|
83
|
+
- 登录凭证:`~/.42plugin/secrets.json`(权限 600)
|
|
84
|
+
|
|
85
|
+
## 环境变量
|
|
86
|
+
- `API_BASE`:替换默认的 API 地址(默认 `https://api.42plugin.com`)。
|
|
87
|
+
- `CDN_BASE`:替换默认的 CDN 地址(默认 `https://cdn.42plugin.com`)。
|
|
88
|
+
- `DEBUG=true`:输出调试信息与原始错误。
|
|
89
|
+
- `CLI_DB_DRIVER=postgres`:让 CLI 使用 Postgres 而非默认的本地 SQLite(`~/.42plugin/local.db`)。配合 `CLI_DATABASE_URL` 或 `LOCAL_DATABASE_URL` 指定连接串,例如 `postgresql://postgres:password@localhost:5432/42plugin_cli_dev`。
|
|
90
|
+
|
|
91
|
+
## 项目结构速览
|
|
92
|
+
- `src/cli.ts`:命令定义与统一入口。
|
|
93
|
+
- `src/commands/*`:各子命令实现(auth、install、search、list、uninstall、version)。
|
|
94
|
+
- `src/services/*`:API、缓存、下载、项目记录、链接创建等核心逻辑。
|
|
95
|
+
- `src/db/client.ts`:本地 SQLite 初始化与连接(基于 @libsql/client)。
|
|
96
|
+
- `tests/`:命令与服务的测试用例。
|
|
97
|
+
|
|
98
|
+
## 常用开发命令
|
|
99
|
+
```bash
|
|
100
|
+
bun test # 运行测试
|
|
101
|
+
bun run lint # ESLint
|
|
102
|
+
bun run format # Prettier
|
|
103
|
+
bun run typecheck # TypeScript 类型检查
|
|
104
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@42ailab/42plugin",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Claude Code 插件管理器",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"42plugin": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun run src/index.ts",
|
|
16
|
+
"build": "bun run scripts/build.ts",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"lint": "eslint src/",
|
|
19
|
+
"format": "prettier --write src/",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"plugin",
|
|
26
|
+
"cli",
|
|
27
|
+
"ai",
|
|
28
|
+
"anthropic"
|
|
29
|
+
],
|
|
30
|
+
"author": "42ailab <y@42ailab.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/42ailab/42plugin.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/42ailab/42plugin/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/42ailab/42plugin#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@libsql/client": "^0.14.0",
|
|
45
|
+
"chalk": "^5.4.1",
|
|
46
|
+
"commander": "^13.0.0",
|
|
47
|
+
"nanoid": "^5.0.9",
|
|
48
|
+
"open": "^10.1.0",
|
|
49
|
+
"ora": "^8.1.1",
|
|
50
|
+
"pg": "^8.12.0",
|
|
51
|
+
"tar": "^7.4.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "latest",
|
|
55
|
+
"@types/tar": "^6.1.13",
|
|
56
|
+
"typescript": "^5.7.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { authCommand } from './commands/auth';
|
|
3
|
+
import { installCommand } from './commands/install';
|
|
4
|
+
import { searchCommand } from './commands/search';
|
|
5
|
+
import { listCommand } from './commands/list';
|
|
6
|
+
import { uninstallCommand } from './commands/uninstall';
|
|
7
|
+
import { versionCommand } from './commands/version';
|
|
8
|
+
|
|
9
|
+
export const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('42plugin')
|
|
13
|
+
.description('Claude Code 插件管理器')
|
|
14
|
+
.version('0.1.0', '-v, --version', '显示版本号')
|
|
15
|
+
.option('--verbose', '详细输出')
|
|
16
|
+
.option('--no-color', '禁用颜色输出');
|
|
17
|
+
|
|
18
|
+
// Register commands
|
|
19
|
+
program.addCommand(authCommand);
|
|
20
|
+
program.addCommand(installCommand);
|
|
21
|
+
program.addCommand(searchCommand);
|
|
22
|
+
program.addCommand(listCommand);
|
|
23
|
+
program.addCommand(uninstallCommand);
|
|
24
|
+
program.addCommand(versionCommand);
|
|
25
|
+
|
|
26
|
+
// Default help
|
|
27
|
+
program.showHelpAfterError();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { api } from '../services/api';
|
|
6
|
+
import { saveSecrets, loadSecrets, clearSecrets } from '../services/auth';
|
|
7
|
+
|
|
8
|
+
export const authCommand = new Command('auth')
|
|
9
|
+
.description('登录 / 授权账户')
|
|
10
|
+
.option('--status', '查看当前登录状态')
|
|
11
|
+
.option('--logout', '登出并清除本地凭证')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
if (options.status) {
|
|
14
|
+
await showStatus();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (options.logout) {
|
|
19
|
+
await logout();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await login();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function login() {
|
|
27
|
+
const spinner = ora('正在初始化登录...').start();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 1. Get authorization code
|
|
31
|
+
const { code, auth_url, expires_at } = await api.startAuth();
|
|
32
|
+
spinner.stop();
|
|
33
|
+
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.cyan('正在打开浏览器进行授权...'));
|
|
36
|
+
console.log();
|
|
37
|
+
console.log('如果浏览器没有自动打开,请访问:');
|
|
38
|
+
console.log(chalk.underline(auth_url));
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
// 2. Open browser
|
|
42
|
+
await open(auth_url);
|
|
43
|
+
|
|
44
|
+
// 3. Poll for authorization completion
|
|
45
|
+
spinner.start('等待浏览器授权...');
|
|
46
|
+
|
|
47
|
+
const pollInterval = 2000; // 2 seconds
|
|
48
|
+
const expiresTime = new Date(expires_at).getTime();
|
|
49
|
+
|
|
50
|
+
while (Date.now() < expiresTime) {
|
|
51
|
+
const result = await api.pollAuth(code);
|
|
52
|
+
|
|
53
|
+
if (result.status === 'completed') {
|
|
54
|
+
// 4. Save credentials
|
|
55
|
+
await saveSecrets({
|
|
56
|
+
access_token: result.access_token!,
|
|
57
|
+
refresh_token: result.refresh_token!,
|
|
58
|
+
created_at: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Update API client token
|
|
62
|
+
api.setToken(result.access_token!);
|
|
63
|
+
|
|
64
|
+
spinner.succeed(chalk.green('登录成功!'));
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(`欢迎,${chalk.cyan(result.user!.display_name || result.user!.username)}!`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await sleep(pollInterval);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
spinner.fail('授权超时,请重试');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
spinner.fail(`登录失败: ${(error as Error).message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function showStatus() {
|
|
81
|
+
const secrets = await loadSecrets();
|
|
82
|
+
|
|
83
|
+
if (!secrets) {
|
|
84
|
+
console.log(chalk.yellow('未登录'));
|
|
85
|
+
console.log('使用 42plugin auth 登录');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const user = await api.getMe();
|
|
91
|
+
console.log(chalk.green('已登录'));
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(`用户名: ${chalk.cyan(user.username)}`);
|
|
94
|
+
console.log(`显示名: ${user.display_name || '-'}`);
|
|
95
|
+
console.log(`角色: ${user.role}`);
|
|
96
|
+
|
|
97
|
+
if (user.role === 'vip' && user.vip_expires_at) {
|
|
98
|
+
console.log(`VIP 到期: ${new Date(user.vip_expires_at).toLocaleDateString()}`);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
console.log(chalk.yellow('Token 已过期,请重新登录'));
|
|
102
|
+
console.log('使用 42plugin auth 重新登录');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function logout() {
|
|
107
|
+
await clearSecrets();
|
|
108
|
+
api.resetToken();
|
|
109
|
+
console.log(chalk.green('已登出'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sleep(ms: number) {
|
|
113
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora, { type Ora } from 'ora';
|
|
4
|
+
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
|
+
}
|
|
25
|
+
|
|
26
|
+
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);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
spinner.fail(`安装失败: ${(error as Error).message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
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;
|
|
97
|
+
}
|
|
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
|
+
});
|
|
117
|
+
|
|
118
|
+
// Link to project
|
|
119
|
+
await linkToProject(cachePath, downloadInfo, options);
|
|
120
|
+
|
|
121
|
+
spinner.succeed(`${chalk.green('已安装')} ${fullName}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function installCapabilityBySlug(
|
|
125
|
+
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
|
+
}
|
|
142
|
+
|
|
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;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
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;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
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
|
+
}
|
|
232
|
+
|
|
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
|
+
}
|
|
255
|
+
|
|
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}`));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
spinner.succeed(`${chalk.green('已安装')} ${installed}/${capabilities.length} 个能力`);
|
|
289
|
+
}
|
|
290
|
+
|
|
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;
|
|
300
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
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
|
+
}
|
|
11
|
+
|
|
12
|
+
export const listCommand = new Command('list')
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.json) {
|
|
37
|
+
console.log(JSON.stringify(plugins, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (plugins.length === 0) {
|
|
42
|
+
console.log(chalk.yellow('当前项目没有安装任何插件'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
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();
|
|
52
|
+
|
|
53
|
+
// Group by type
|
|
54
|
+
const grouped = groupBy(plugins, 'type');
|
|
55
|
+
|
|
56
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
57
|
+
console.log(chalk.bold(`${type}:`));
|
|
58
|
+
|
|
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)}`);
|
|
64
|
+
|
|
65
|
+
if (plugin.source === 'list') {
|
|
66
|
+
console.log(` 来源: ${chalk.gray(plugin.source_list)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log();
|
|
70
|
+
}
|
|
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
|
+
}
|