@42ailab/42plugin 0.1.21 → 0.1.23
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 +38 -12
- package/package.json +1 -1
- package/src/api.ts +1 -0
- package/src/cli.ts +13 -1
- package/src/commands/install.ts +59 -12
- package/src/commands/list.ts +151 -34
- package/src/commands/publish.ts +87 -0
- package/src/commands/uninstall.ts +33 -6
- package/src/config.ts +61 -1
- package/src/db.ts +31 -0
- package/src/services/publisher.ts +86 -1
- package/src/types.ts +2 -0
- package/src/update-checker.ts +343 -0
- package/src/validators/plugin-validator.ts +21 -51
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ bun run build
|
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
选项:
|
|
112
|
-
- `-g, --global
|
|
112
|
+
- `-g, --global`:安装到全局目录 (~/.42plugin/global/)
|
|
113
113
|
- `-k, --kit`:安装该插件所属的整个套包
|
|
114
114
|
- `--optional`:安装套包时包含可选项
|
|
115
115
|
- `--force`:强制重新下载
|
|
@@ -119,11 +119,19 @@ bun run build
|
|
|
119
119
|
### 管理
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
|
|
123
|
-
42plugin list
|
|
124
|
-
42plugin
|
|
125
|
-
42plugin
|
|
126
|
-
42plugin
|
|
122
|
+
# 查看插件
|
|
123
|
+
42plugin list # 当前项目已安装的插件
|
|
124
|
+
42plugin list --global # 全局安装的插件 (~/.claude/)
|
|
125
|
+
42plugin list --all # 所有项目 + 全局的插件
|
|
126
|
+
42plugin list --type skill --json # 筛选类型,输出 JSON
|
|
127
|
+
|
|
128
|
+
# 卸载插件
|
|
129
|
+
42plugin uninstall <full_name> # 从当前项目卸载
|
|
130
|
+
42plugin uninstall <full_name> --global # 从全局目录卸载
|
|
131
|
+
42plugin uninstall <full_name> --purge # 卸载并清除下载缓存
|
|
132
|
+
|
|
133
|
+
# 版本
|
|
134
|
+
42plugin version
|
|
127
135
|
```
|
|
128
136
|
|
|
129
137
|
### 发布(开发中)
|
|
@@ -140,12 +148,30 @@ bun run build
|
|
|
140
148
|
|
|
141
149
|
## 数据存储
|
|
142
150
|
|
|
143
|
-
| 类型 | 路径 |
|
|
144
|
-
|
|
145
|
-
| 数据库 | `~/.42plugin/local.db
|
|
146
|
-
|
|
|
147
|
-
| 登录凭证 | `~/.42plugin/secrets.json
|
|
148
|
-
|
|
|
151
|
+
| 类型 | 路径 | 说明 |
|
|
152
|
+
|------|------|------|
|
|
153
|
+
| 数据库 | `~/.42plugin/local.db` | 本地 SQLite(Windows: `%APPDATA%/42plugin/`) |
|
|
154
|
+
| 下载缓存 | `~/.42plugin/cache/` | 插件原始文件缓存 |
|
|
155
|
+
| 登录凭证 | `~/.42plugin/secrets.json` | 权限 600 |
|
|
156
|
+
| 全局安装 | `~/.claude/` | 对所有项目生效,Claude Code 可识别 |
|
|
157
|
+
| 项目安装 | `./.claude/` | 仅对当前项目生效 |
|
|
158
|
+
|
|
159
|
+
### 全局 vs 项目安装
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
~/.claude/ # 全局目录,对所有项目生效
|
|
163
|
+
├── skills/
|
|
164
|
+
├── agents/
|
|
165
|
+
└── ...
|
|
166
|
+
|
|
167
|
+
/path/to/project/.claude/ # 项目目录,仅对该项目生效
|
|
168
|
+
├── skills/
|
|
169
|
+
├── agents/
|
|
170
|
+
└── ...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- **全局安装** (`--global`):插件安装到 `~/.claude/`,所有项目都能使用
|
|
174
|
+
- **项目安装**(默认):插件安装到当前项目的 `.claude/`,仅当前项目可用
|
|
149
175
|
|
|
150
176
|
## 本地数据库表(SQLite)
|
|
151
177
|
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
checkCommand,
|
|
15
15
|
} from './commands';
|
|
16
16
|
import { setupAuthHook } from './auth-middleware';
|
|
17
|
+
import { checkForUpdates, checkForUpdatesSync } from './update-checker';
|
|
17
18
|
|
|
18
19
|
// 版本号处理:
|
|
19
20
|
// - Homebrew(编译后):构建时通过 --define __VERSION__ 注入
|
|
@@ -58,11 +59,22 @@ program.addCommand(setupCommand);
|
|
|
58
59
|
program
|
|
59
60
|
.command('version')
|
|
60
61
|
.description('显示版本信息')
|
|
61
|
-
.
|
|
62
|
+
.option('--check', '检查是否有新版本')
|
|
63
|
+
.action(async (options: { check?: boolean }) => {
|
|
62
64
|
console.log(`42plugin v${VERSION}`);
|
|
65
|
+
if (options.check) {
|
|
66
|
+
console.log();
|
|
67
|
+
await checkForUpdatesSync(VERSION);
|
|
68
|
+
}
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
// 注册全局认证 Hook
|
|
66
72
|
setupAuthHook(program);
|
|
67
73
|
|
|
74
|
+
// 注册版本更新检查 Hook(命令执行后触发)
|
|
75
|
+
program.hook('postAction', () => {
|
|
76
|
+
// 非阻塞检查,不影响命令执行
|
|
77
|
+
checkForUpdates(VERSION);
|
|
78
|
+
});
|
|
79
|
+
|
|
68
80
|
export { program };
|
package/src/commands/install.ts
CHANGED
|
@@ -8,7 +8,7 @@ import ora from 'ora';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { confirm } from '@inquirer/prompts';
|
|
11
|
-
import { api, QuotaExceededError } from '../api';
|
|
11
|
+
import { api, ApiError, QuotaExceededError } from '../api';
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
13
|
import {
|
|
14
14
|
getOrCreateProject,
|
|
@@ -17,19 +17,63 @@ import {
|
|
|
17
17
|
resolveCachePath,
|
|
18
18
|
checkInstallConflict,
|
|
19
19
|
removeInstallation,
|
|
20
|
+
getSessionToken,
|
|
20
21
|
} from '../db';
|
|
21
22
|
import { config } from '../config';
|
|
22
23
|
import { parseTarget, getInstallPath, formatBytes, getTypeIcon } from '../utils';
|
|
23
24
|
import { TargetType, type PluginDownloadInfo } from '../types';
|
|
24
25
|
|
|
25
26
|
export const installCommand = new Command('install')
|
|
26
|
-
.description('
|
|
27
|
-
.argument('<target>', '安装目标 (author/name
|
|
28
|
-
.option('-g, --global',
|
|
27
|
+
.description('安装插件或套包\n\n示例:\n 42plugin install alice/smart-reviewer 安装插件\n 42plugin install alice/kit/dev-tools 安装套包\n 42plugin install like 安装您的收藏')
|
|
28
|
+
.argument('<target>', '安装目标 (author/name, author/kit/slug, 或 like)')
|
|
29
|
+
.option('-g, --global', `安装到全局目录,对所有项目生效 (~/.claude/)`)
|
|
29
30
|
.option('-f, --force', '强制重新下载')
|
|
30
31
|
.option('--no-cache', '跳过缓存检查')
|
|
31
32
|
.option('--optional', '安装套包时包含可选插件')
|
|
32
33
|
.action(async (target, options) => {
|
|
34
|
+
// 保留名称检测和扩展(虚拟收藏 Kit)
|
|
35
|
+
const RESERVED_KIT_NAMES = ['like', 'liked', 'starred', 'favorites', 'favourite', 'favourites'];
|
|
36
|
+
const normalizedTarget = target.trim().toLowerCase();
|
|
37
|
+
|
|
38
|
+
// 检测是否为单段保留名称(简化格式)
|
|
39
|
+
if (!normalizedTarget.includes('/') && RESERVED_KIT_NAMES.includes(normalizedTarget)) {
|
|
40
|
+
// 需要登录才能使用收藏功能
|
|
41
|
+
const sessionToken = await getSessionToken();
|
|
42
|
+
if (!sessionToken) {
|
|
43
|
+
console.error(chalk.red('\n使用收藏功能需要先登录'));
|
|
44
|
+
console.log(chalk.gray('执行 42plugin auth 登录'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 验证登录状态并获取 username(token 已在全局 hook 中注入)
|
|
49
|
+
let username: string | null = null;
|
|
50
|
+
try {
|
|
51
|
+
const session = await api.getSession();
|
|
52
|
+
const emailPrefix = session.user.email?.split('@')[0]?.trim();
|
|
53
|
+
username = session.user.username || session.user.name || (emailPrefix || null);
|
|
54
|
+
|
|
55
|
+
if (!username || username.trim() === '') {
|
|
56
|
+
console.error(chalk.red('\n无法获取用户名,请设置用户名后重试'));
|
|
57
|
+
console.log(chalk.gray('访问 42plugin.com 设置用户名'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof ApiError && error.statusCode === 401) {
|
|
62
|
+
console.error(chalk.red('\n登录已过期,请重新登录'));
|
|
63
|
+
console.log(chalk.gray('执行 42plugin auth 登录'));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(chalk.red('\n验证登录状态失败'));
|
|
66
|
+
console.log(chalk.gray(`错误: ${(error as Error).message}`));
|
|
67
|
+
console.log(chalk.gray('请检查网络连接或稍后重试'));
|
|
68
|
+
}
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 扩展为完整格式
|
|
73
|
+
target = `${username}/kit/${normalizedTarget}`;
|
|
74
|
+
console.log(chalk.gray(`\n安装您的收藏: ${username}/${normalizedTarget}\n`));
|
|
75
|
+
}
|
|
76
|
+
|
|
33
77
|
// token 已在全局 hook 中注入
|
|
34
78
|
try {
|
|
35
79
|
// 检测是否在 home 目录
|
|
@@ -38,18 +82,19 @@ export const installCommand = new Command('install')
|
|
|
38
82
|
const isHomeDir = currentPath === homePath;
|
|
39
83
|
|
|
40
84
|
if (isHomeDir && !options.global) {
|
|
85
|
+
console.log(chalk.yellow('\n⚠ 您当前在用户根目录'));
|
|
86
|
+
console.log(chalk.gray(' • 全局安装:插件对所有项目生效,存放在 ~/.claude/'));
|
|
87
|
+
console.log(chalk.gray(' • 项目安装:插件仅对当前项目生效,存放在 ./.claude/\n'));
|
|
88
|
+
|
|
41
89
|
const answer = await confirm({
|
|
42
|
-
message: '
|
|
90
|
+
message: '是否安装到全局目录?',
|
|
43
91
|
default: true,
|
|
44
92
|
});
|
|
45
93
|
|
|
46
94
|
if (answer) {
|
|
47
95
|
options.global = true;
|
|
48
|
-
// 确保全局目录存在
|
|
49
|
-
await fs.mkdir(config.globalDir, { recursive: true });
|
|
50
|
-
console.log(chalk.gray('将安装到全局目录: ~/.42plugin/global/'));
|
|
51
96
|
} else {
|
|
52
|
-
console.log(chalk.yellow('提示:
|
|
97
|
+
console.log(chalk.yellow('提示: 请进入具体项目目录后再执行安装命令'));
|
|
53
98
|
return;
|
|
54
99
|
}
|
|
55
100
|
}
|
|
@@ -163,8 +208,9 @@ async function installPlugin(
|
|
|
163
208
|
);
|
|
164
209
|
|
|
165
210
|
if (options.global) {
|
|
166
|
-
console.log(chalk.green('✓
|
|
167
|
-
console.log(chalk.gray(` → ~/.
|
|
211
|
+
console.log(chalk.green('✓ 已全局安装,对所有项目生效'));
|
|
212
|
+
console.log(chalk.gray(` → ~/.claude/${downloadInfo.installPath}`));
|
|
213
|
+
console.log(chalk.gray(' 提示: 使用 42plugin list --global 查看全局插件'));
|
|
168
214
|
} else {
|
|
169
215
|
console.log(chalk.green(`✓ 已安装到当前项目: ${path.basename(projectPath)}`));
|
|
170
216
|
console.log(chalk.gray(` → ${downloadInfo.installPath}`));
|
|
@@ -316,7 +362,8 @@ async function installKit(
|
|
|
316
362
|
console.log();
|
|
317
363
|
if (failed === 0) {
|
|
318
364
|
if (options.global) {
|
|
319
|
-
console.log(chalk.green(`✓
|
|
365
|
+
console.log(chalk.green(`✓ 已全局安装 ${installed} 个插件,对所有项目生效`));
|
|
366
|
+
console.log(chalk.gray(' 提示: 使用 42plugin list --global 查看全局插件'));
|
|
320
367
|
} else {
|
|
321
368
|
console.log(chalk.green(`✓ 已安装 ${installed} 个插件到当前项目`));
|
|
322
369
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -4,56 +4,173 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { getInstallations } from '../db';
|
|
7
|
+
import { getInstallations, getAllInstallations } from '../db';
|
|
8
|
+
import { config, isGlobalDir } from '../config';
|
|
8
9
|
import { getTypeIcon, formatRelativeTime } from '../utils';
|
|
9
10
|
import type { PluginType } from '../types';
|
|
10
11
|
|
|
11
12
|
export const listCommand = new Command('list')
|
|
12
13
|
.alias('ls')
|
|
13
|
-
.description('
|
|
14
|
+
.description('查看已安装的插件')
|
|
15
|
+
.option('-g, --global', '显示全局安装的插件 (~/.claude/)')
|
|
16
|
+
.option('-a, --all', '显示所有项目的插件(包括全局)')
|
|
14
17
|
.option('-t, --type <type>', '筛选类型 (skill, agent, command, hook, mcp)')
|
|
15
18
|
.option('--json', '输出 JSON 格式')
|
|
16
19
|
.action(async (options) => {
|
|
17
20
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
if (options.all) {
|
|
22
|
+
await listAllProjects(options);
|
|
23
|
+
} else if (options.global) {
|
|
24
|
+
await listGlobalPlugins(options);
|
|
25
|
+
} else {
|
|
26
|
+
await listCurrentProject(options);
|
|
23
27
|
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(chalk.red((error as Error).message));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
34
|
+
async function listCurrentProject(options: { type?: string; json?: boolean }): Promise<void> {
|
|
35
|
+
const projectPath = process.cwd();
|
|
36
|
+
let installations = await getInstallations(projectPath);
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// 类型筛选
|
|
39
|
+
if (options.type) {
|
|
40
|
+
installations = installations.filter((i) => i.type === options.type);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify(installations, null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (installations.length === 0) {
|
|
49
|
+
console.log(chalk.yellow('当前项目未安装任何插件'));
|
|
50
|
+
console.log(chalk.gray('执行 42plugin install <name> 安装插件'));
|
|
51
|
+
console.log(chalk.gray('执行 42plugin list --global 查看全局插件'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.gray(`当前项目已安装 ${installations.length} 个插件:\n`));
|
|
56
|
+
|
|
57
|
+
for (const item of installations) {
|
|
58
|
+
const icon = getTypeIcon(item.type);
|
|
59
|
+
const time = formatRelativeTime(item.installedAt);
|
|
60
|
+
|
|
61
|
+
console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
|
|
62
|
+
console.log(chalk.gray(` → ${item.linkPath}`));
|
|
63
|
+
|
|
64
|
+
// 解析所属套包:fullName 格式为 author/kit/plugin
|
|
65
|
+
const parts = item.fullName.split('/');
|
|
66
|
+
if (parts.length >= 3) {
|
|
67
|
+
const kitName = `${parts[0]}/${parts[1]}`;
|
|
68
|
+
console.log(chalk.gray(` 所属套包: ${kitName}`));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(chalk.gray(` 安装于 ${time}`));
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 列出全局安装的插件
|
|
78
|
+
*/
|
|
79
|
+
async function listGlobalPlugins(options: { type?: string; json?: boolean }): Promise<void> {
|
|
80
|
+
let installations = await getInstallations(config.globalDir);
|
|
81
|
+
|
|
82
|
+
// 类型筛选
|
|
83
|
+
if (options.type) {
|
|
84
|
+
installations = installations.filter((i) => i.type === options.type);
|
|
85
|
+
}
|
|
35
86
|
|
|
36
|
-
|
|
87
|
+
if (options.json) {
|
|
88
|
+
console.log(JSON.stringify(installations, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
37
91
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
92
|
+
if (installations.length === 0) {
|
|
93
|
+
console.log(chalk.yellow('未安装任何全局插件'));
|
|
94
|
+
console.log(chalk.gray('执行 42plugin install <name> --global 安装全局插件'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
41
97
|
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
console.log(chalk.cyan('📍 全局插件') + chalk.gray(' (~/.claude/) - 对所有项目生效\n'));
|
|
99
|
+
console.log(chalk.gray(`共 ${installations.length} 个插件:\n`));
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const kitName = `${parts[0]}/${parts[1]}`;
|
|
49
|
-
console.log(chalk.gray(` 所属套包: ${kitName}`));
|
|
50
|
-
}
|
|
101
|
+
for (const item of installations) {
|
|
102
|
+
const icon = getTypeIcon(item.type);
|
|
103
|
+
const time = formatRelativeTime(item.installedAt);
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
|
|
106
|
+
|
|
107
|
+
// 解析所属套包:fullName 格式为 author/kit/plugin
|
|
108
|
+
const parts = item.fullName.split('/');
|
|
109
|
+
if (parts.length >= 3) {
|
|
110
|
+
const kitName = `${parts[0]}/${parts[1]}`;
|
|
111
|
+
console.log(chalk.gray(` 所属套包: ${kitName}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(chalk.gray(` 安装于 ${time}`));
|
|
115
|
+
console.log();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function listAllProjects(options: { type?: string; json?: boolean }): Promise<void> {
|
|
120
|
+
let installations = await getAllInstallations();
|
|
121
|
+
|
|
122
|
+
// 类型筛选
|
|
123
|
+
if (options.type) {
|
|
124
|
+
installations = installations.filter((i) => i.type === options.type);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(JSON.stringify(installations, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (installations.length === 0) {
|
|
133
|
+
console.log(chalk.yellow('未安装任何插件'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 按项目分组,区分全局和项目
|
|
138
|
+
const globalPlugins: typeof installations = [];
|
|
139
|
+
const byProject = new Map<string, typeof installations>();
|
|
140
|
+
|
|
141
|
+
for (const item of installations) {
|
|
142
|
+
if (isGlobalDir(item.projectPath)) {
|
|
143
|
+
globalPlugins.push(item);
|
|
144
|
+
} else {
|
|
145
|
+
const key = item.projectPath;
|
|
146
|
+
if (!byProject.has(key)) {
|
|
147
|
+
byProject.set(key, []);
|
|
54
148
|
}
|
|
55
|
-
|
|
56
|
-
console.error(chalk.red((error as Error).message));
|
|
57
|
-
process.exit(1);
|
|
149
|
+
byProject.get(key)!.push(item);
|
|
58
150
|
}
|
|
59
|
-
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const projectCount = byProject.size + (globalPlugins.length > 0 ? 1 : 0);
|
|
154
|
+
console.log(chalk.gray(`共 ${installations.length} 个插件,分布在 ${projectCount} 个位置:\n`));
|
|
155
|
+
|
|
156
|
+
// 先显示全局插件
|
|
157
|
+
if (globalPlugins.length > 0) {
|
|
158
|
+
console.log(chalk.cyan.bold(`🌐 全局`) + chalk.gray(' (~/.claude/) - 对所有项目生效'));
|
|
159
|
+
for (const item of globalPlugins) {
|
|
160
|
+
const icon = getTypeIcon(item.type);
|
|
161
|
+
console.log(` ${icon} ${chalk.cyan(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
|
|
162
|
+
}
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 再显示各项目
|
|
167
|
+
for (const [projectPath, items] of byProject) {
|
|
168
|
+
console.log(chalk.bold(`📁 ${projectPath}`));
|
|
169
|
+
|
|
170
|
+
for (const item of items) {
|
|
171
|
+
const icon = getTypeIcon(item.type);
|
|
172
|
+
console.log(` ${icon} ${chalk.cyan(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
|
|
173
|
+
}
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/commands/publish.ts
CHANGED
|
@@ -4,10 +4,73 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import { select } from '@inquirer/prompts';
|
|
7
10
|
import { api } from '../api';
|
|
8
11
|
import { Publisher } from '../services/publisher';
|
|
9
12
|
import { ValidationError, UploadError, AuthRequiredError } from '../errors';
|
|
10
13
|
import { getTypeIcon } from '../utils';
|
|
14
|
+
import type { PluginType } from '../types';
|
|
15
|
+
|
|
16
|
+
const VALID_TYPES: PluginType[] = ['skill', 'agent', 'command', 'hook', 'mcp'];
|
|
17
|
+
|
|
18
|
+
const TYPE_DESCRIPTIONS: Record<PluginType, string> = {
|
|
19
|
+
skill: 'skill(技能) - 包含 SKILL.md 的目录,为 Claude 提供特定能力',
|
|
20
|
+
agent: 'agent(代理) - 单个 .md 文件,定义 AI 角色和行为',
|
|
21
|
+
command: 'command(命令) - 单个 .md 文件,定义可执行的斜杠命令',
|
|
22
|
+
hook: 'hook(钩子) - 包含 hooks.json 的目录,响应事件触发',
|
|
23
|
+
mcp: 'mcp(MCP 服务) - 包含 mcp.json 的目录,Model Context Protocol 服务',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从路径和文件特征推断建议类型(用于设置默认选项)
|
|
28
|
+
*/
|
|
29
|
+
async function suggestType(pluginPath: string): Promise<PluginType> {
|
|
30
|
+
const absPath = path.resolve(pluginPath);
|
|
31
|
+
const normalizedPath = absPath.replace(/\\/g, '/').toLowerCase();
|
|
32
|
+
|
|
33
|
+
// 1. 从路径推断(Claude Code 目录约定)
|
|
34
|
+
if (normalizedPath.includes('/.claude/agents/') || normalizedPath.includes('.claude/agents/')) {
|
|
35
|
+
return 'agent';
|
|
36
|
+
}
|
|
37
|
+
if (normalizedPath.includes('/.claude/commands/') || normalizedPath.includes('.claude/commands/')) {
|
|
38
|
+
return 'command';
|
|
39
|
+
}
|
|
40
|
+
if (normalizedPath.includes('/.claude/skills/') || normalizedPath.includes('.claude/skills/')) {
|
|
41
|
+
return 'skill';
|
|
42
|
+
}
|
|
43
|
+
if (normalizedPath.includes('/.claude/hooks/') || normalizedPath.includes('.claude/hooks/')) {
|
|
44
|
+
return 'hook';
|
|
45
|
+
}
|
|
46
|
+
if (normalizedPath.includes('/.claude/mcp/') || normalizedPath.includes('.claude/mcp/')) {
|
|
47
|
+
return 'mcp';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. 检查是否是目录,如果是则检查特征文件
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.stat(absPath);
|
|
53
|
+
if (stat.isDirectory()) {
|
|
54
|
+
const files = await fs.readdir(absPath);
|
|
55
|
+
const lowerFiles = files.map((f) => f.toLowerCase());
|
|
56
|
+
|
|
57
|
+
if (lowerFiles.includes('mcp.json') || lowerFiles.includes('.mcp.json')) {
|
|
58
|
+
return 'mcp';
|
|
59
|
+
}
|
|
60
|
+
if (lowerFiles.includes('hooks.json') || lowerFiles.includes('hook.json')) {
|
|
61
|
+
return 'hook';
|
|
62
|
+
}
|
|
63
|
+
if (lowerFiles.includes('skill.md')) {
|
|
64
|
+
return 'skill';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// 路径不存在,忽略
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. 默认返回 skill
|
|
72
|
+
return 'skill';
|
|
73
|
+
}
|
|
11
74
|
|
|
12
75
|
export const publishCommand = new Command('publish')
|
|
13
76
|
.alias('pub')
|
|
@@ -17,8 +80,31 @@ export const publishCommand = new Command('publish')
|
|
|
17
80
|
.option('-f, --force', '强制发布(即使内容未变化)')
|
|
18
81
|
.option('--public', '公开发布(默认仅自己可见)')
|
|
19
82
|
.option('-n, --name <name>', '覆盖插件名称')
|
|
83
|
+
.option('-t, --type <type>', '指定插件类型 (skill|agent|command|hook|mcp)')
|
|
20
84
|
.action(async (pluginPath, options) => {
|
|
21
85
|
try {
|
|
86
|
+
// 验证 --type 参数
|
|
87
|
+
let selectedType: PluginType | undefined = options.type;
|
|
88
|
+
if (selectedType && !VALID_TYPES.includes(selectedType)) {
|
|
89
|
+
console.error(chalk.red(`无效的类型: ${selectedType}`));
|
|
90
|
+
console.log(chalk.gray(`有效类型: ${VALID_TYPES.join(', ')}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 如果没有指定类型,让用户交互式选择(智能默认)
|
|
95
|
+
if (!selectedType) {
|
|
96
|
+
const suggested = await suggestType(pluginPath);
|
|
97
|
+
selectedType = await select({
|
|
98
|
+
message: '请选择插件类型:',
|
|
99
|
+
choices: VALID_TYPES.map((t) => ({
|
|
100
|
+
name: TYPE_DESCRIPTIONS[t],
|
|
101
|
+
value: t,
|
|
102
|
+
})),
|
|
103
|
+
default: suggested,
|
|
104
|
+
});
|
|
105
|
+
console.log();
|
|
106
|
+
}
|
|
107
|
+
|
|
22
108
|
// 认证检查已在全局 hook 中完成
|
|
23
109
|
// 执行发布
|
|
24
110
|
const publisher = new Publisher();
|
|
@@ -28,6 +114,7 @@ export const publishCommand = new Command('publish')
|
|
|
28
114
|
force: options.force,
|
|
29
115
|
visibility: options.public ? 'public' : 'self',
|
|
30
116
|
name: options.name,
|
|
117
|
+
type: selectedType,
|
|
31
118
|
});
|
|
32
119
|
|
|
33
120
|
// 显示结果
|
|
@@ -6,6 +6,7 @@ import { Command } from 'commander';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { api } from '../api';
|
|
9
|
+
import { config } from '../config';
|
|
9
10
|
import { getInstallations, removeInstallation, removeLink, removeCache } from '../db';
|
|
10
11
|
import { parseTarget } from '../utils';
|
|
11
12
|
import { TargetType } from '../types';
|
|
@@ -14,18 +15,24 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
14
15
|
.alias('rm')
|
|
15
16
|
.description('卸载插件或套包')
|
|
16
17
|
.argument('<target>', '插件名 (author/name) 或套包名 (author/kit/slug)')
|
|
17
|
-
.option('--
|
|
18
|
+
.option('-g, --global', '从全局目录卸载 (~/.claude/)')
|
|
19
|
+
.option('--purge', '同时清除下载缓存')
|
|
18
20
|
.action(async (target, options) => {
|
|
19
21
|
try {
|
|
20
22
|
const parsed = parseTarget(target);
|
|
21
|
-
const projectPath = process.cwd();
|
|
23
|
+
const projectPath = options.global ? config.globalDir : process.cwd();
|
|
24
|
+
|
|
25
|
+
// 显示操作范围提示
|
|
26
|
+
if (options.global) {
|
|
27
|
+
console.log(chalk.gray('从全局目录卸载 (~/.claude/)\n'));
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
if (parsed.type === TargetType.Kit) {
|
|
24
31
|
// 卸载套包:批量卸载所有来自该套包的插件
|
|
25
|
-
await uninstallKit(projectPath, parsed.fullName, options.purge);
|
|
32
|
+
await uninstallKit(projectPath, parsed.fullName, options.purge, options.global);
|
|
26
33
|
} else {
|
|
27
34
|
// 卸载单个插件
|
|
28
|
-
await uninstallPlugin(projectPath, parsed.fullName, options.purge);
|
|
35
|
+
await uninstallPlugin(projectPath, parsed.fullName, options.purge, options.global);
|
|
29
36
|
}
|
|
30
37
|
} catch (error) {
|
|
31
38
|
console.error(chalk.red((error as Error).message));
|
|
@@ -39,7 +46,8 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
39
46
|
async function uninstallPlugin(
|
|
40
47
|
projectPath: string,
|
|
41
48
|
fullName: string,
|
|
42
|
-
purge: boolean
|
|
49
|
+
purge: boolean,
|
|
50
|
+
isGlobal: boolean
|
|
43
51
|
): Promise<void> {
|
|
44
52
|
const spinner = ora(`卸载 ${fullName}...`).start();
|
|
45
53
|
|
|
@@ -49,6 +57,11 @@ async function uninstallPlugin(
|
|
|
49
57
|
|
|
50
58
|
if (!installation) {
|
|
51
59
|
spinner.fail(`未找到已安装的插件: ${fullName}`);
|
|
60
|
+
if (isGlobal) {
|
|
61
|
+
console.log(chalk.gray('提示: 该插件可能安装在某个项目目录下,请进入项目目录后重试'));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.gray('提示: 使用 42plugin uninstall --global 从全局目录卸载'));
|
|
64
|
+
}
|
|
52
65
|
process.exit(1);
|
|
53
66
|
}
|
|
54
67
|
|
|
@@ -78,6 +91,11 @@ async function uninstallPlugin(
|
|
|
78
91
|
} else {
|
|
79
92
|
spinner.succeed(`已卸载 ${fullName}`);
|
|
80
93
|
}
|
|
94
|
+
|
|
95
|
+
// 显示位置提示
|
|
96
|
+
if (isGlobal) {
|
|
97
|
+
console.log(chalk.gray(' 已从全局目录移除'));
|
|
98
|
+
}
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/**
|
|
@@ -86,7 +104,8 @@ async function uninstallPlugin(
|
|
|
86
104
|
async function uninstallKit(
|
|
87
105
|
projectPath: string,
|
|
88
106
|
kitFullName: string,
|
|
89
|
-
purge: boolean
|
|
107
|
+
purge: boolean,
|
|
108
|
+
isGlobal: boolean
|
|
90
109
|
): Promise<void> {
|
|
91
110
|
const spinner = ora(`查找套包 ${kitFullName} 的插件...`).start();
|
|
92
111
|
|
|
@@ -96,6 +115,11 @@ async function uninstallKit(
|
|
|
96
115
|
|
|
97
116
|
if (kitInstallations.length === 0) {
|
|
98
117
|
spinner.fail(`未找到来自套包 ${kitFullName} 的已安装插件`);
|
|
118
|
+
if (isGlobal) {
|
|
119
|
+
console.log(chalk.gray('提示: 该套包可能安装在某个项目目录下,请进入项目目录后重试'));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.gray('提示: 使用 42plugin uninstall --global 从全局目录卸载'));
|
|
122
|
+
}
|
|
99
123
|
process.exit(1);
|
|
100
124
|
}
|
|
101
125
|
|
|
@@ -140,6 +164,9 @@ async function uninstallKit(
|
|
|
140
164
|
if (purge) {
|
|
141
165
|
console.log(chalk.gray(' 缓存已清除'));
|
|
142
166
|
}
|
|
167
|
+
if (isGlobal) {
|
|
168
|
+
console.log(chalk.gray(' 已从全局目录移除'));
|
|
169
|
+
}
|
|
143
170
|
} else {
|
|
144
171
|
console.log(chalk.yellow(`⚠ 部分卸载完成`));
|
|
145
172
|
console.log(chalk.gray(` 成功: ${successCount}, 失败: ${failCount}`));
|