@42ailab/42plugin 0.1.0-beta.0 → 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.
- package/README.md +211 -68
- package/package.json +13 -8
- package/src/api.ts +447 -0
- package/src/cli.ts +33 -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 -72
- 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* publish 命令 - 发布插件到 42plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { api } from '../api';
|
|
8
|
+
import { config } from '../config';
|
|
9
|
+
import { getSessionToken } from '../db';
|
|
10
|
+
import { Publisher } from '../services/publisher';
|
|
11
|
+
import { ValidationError, UploadError, AuthRequiredError } from '../errors';
|
|
12
|
+
import { getTypeIcon } from '../utils';
|
|
13
|
+
|
|
14
|
+
export const publishCommand = new Command('publish')
|
|
15
|
+
.description('发布插件到 42plugin')
|
|
16
|
+
.argument('[path]', '插件路径(文件或目录)', '.')
|
|
17
|
+
.option('--dry-run', '仅验证,不实际发布')
|
|
18
|
+
.option('-f, --force', '强制发布(即使内容未变化)')
|
|
19
|
+
.option('--public', '公开发布(默认仅自己可见)')
|
|
20
|
+
.option('-n, --name <name>', '覆盖插件名称')
|
|
21
|
+
.action(async (pluginPath, options) => {
|
|
22
|
+
try {
|
|
23
|
+
// 开发模式跳过认证检查
|
|
24
|
+
if (!config.devSkipAuth) {
|
|
25
|
+
// 检查登录状态
|
|
26
|
+
const token = await getSessionToken();
|
|
27
|
+
if (!token) {
|
|
28
|
+
throw new AuthRequiredError();
|
|
29
|
+
}
|
|
30
|
+
api.setSessionToken(token);
|
|
31
|
+
|
|
32
|
+
// 验证 session 有效性
|
|
33
|
+
try {
|
|
34
|
+
const session = await api.getSession();
|
|
35
|
+
if (!session.user.name) {
|
|
36
|
+
console.log(chalk.yellow('提示: 建议先设置用户名'));
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
throw new AuthRequiredError('登录已过期,请重新登录: 42plugin auth');
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
console.log(chalk.cyan('[DEV] 跳过认证检查'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 执行发布
|
|
46
|
+
const publisher = new Publisher();
|
|
47
|
+
const result = await publisher.publish({
|
|
48
|
+
path: pluginPath,
|
|
49
|
+
dryRun: options.dryRun,
|
|
50
|
+
force: options.force,
|
|
51
|
+
visibility: options.public ? 'public' : 'self',
|
|
52
|
+
name: options.name,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 显示结果
|
|
56
|
+
console.log();
|
|
57
|
+
if (result.action === 'unchanged') {
|
|
58
|
+
console.log(chalk.yellow('插件内容未变化,跳过发布'));
|
|
59
|
+
} else if (!options.dryRun) {
|
|
60
|
+
console.log(chalk.green('✓ 发布成功!'));
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(` ${getTypeIcon(result.type)} ${chalk.bold(result.fullName)}`);
|
|
63
|
+
console.log(` 版本: v${result.version}`);
|
|
64
|
+
console.log(` 类型: ${result.type}`);
|
|
65
|
+
console.log(` 状态: ${result.action === 'created' ? '新建' : '更新'}`);
|
|
66
|
+
|
|
67
|
+
if (!options.public) {
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(chalk.gray('当前仅自己可见,使用 --public 参数公开发布'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(chalk.gray(`安装: 42plugin install ${result.fullName}`));
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
handleError(error as Error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 错误处理
|
|
83
|
+
*/
|
|
84
|
+
function handleError(error: Error): void {
|
|
85
|
+
if (error instanceof AuthRequiredError) {
|
|
86
|
+
console.error(chalk.red(error.message));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (error instanceof ValidationError) {
|
|
91
|
+
console.error(chalk.red(`验证失败: ${error.message}`));
|
|
92
|
+
|
|
93
|
+
// 提供具体建议
|
|
94
|
+
if (error.code === 'PATH_NOT_FOUND') {
|
|
95
|
+
console.log(chalk.yellow('\n请检查路径是否正确'));
|
|
96
|
+
} else if (error.code === 'UNSUPPORTED_FILE_TYPE') {
|
|
97
|
+
console.log(chalk.yellow('\n支持的文件类型: .md, .yaml, .yml'));
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (error instanceof UploadError) {
|
|
103
|
+
console.error(chalk.red(`上传失败: ${error.message}`));
|
|
104
|
+
if (error.statusCode === 413) {
|
|
105
|
+
console.log(chalk.yellow('\n文件过大,请压缩后重试'));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 通用错误处理
|
|
111
|
+
console.error(chalk.red(error.message));
|
|
112
|
+
|
|
113
|
+
// 常见错误提示
|
|
114
|
+
if (error.message.includes('ENOENT')) {
|
|
115
|
+
console.log(chalk.yellow('\n文件或目录不存在'));
|
|
116
|
+
} else if (error.message.includes('network') || error.message.includes('ECONNREFUSED')) {
|
|
117
|
+
console.log(chalk.yellow('\n网络连接失败,请检查网络'));
|
|
118
|
+
} else if (error.message.includes('401') || error.message.includes('unauthorized')) {
|
|
119
|
+
console.log(chalk.yellow('\n认证失败,请重新登录: 42plugin auth'));
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/commands/search.ts
CHANGED
|
@@ -1,87 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search 命令 - 搜索插件
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
6
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { checkbox } from '@inquirer/prompts';
|
|
9
|
+
import { api } from '../api';
|
|
10
|
+
import { getSessionToken } from '../db';
|
|
11
|
+
import { getTypeIcon, getTypeLabel, parseTarget } from '../utils';
|
|
12
|
+
import type { PluginType, SearchResult } from '../types';
|
|
10
13
|
|
|
11
14
|
export const searchCommand = new Command('search')
|
|
12
15
|
.description('搜索插件')
|
|
13
|
-
.argument('<
|
|
14
|
-
.option('--type <type>', '
|
|
15
|
-
.option('--limit <n>', '
|
|
16
|
-
.option('--json', 'JSON
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
.argument('<keyword>', '搜索关键词')
|
|
17
|
+
.option('-t, --type <type>', '筛选类型 (skill, agent, command, hook, mcp)')
|
|
18
|
+
.option('-l, --limit <n>', '结果数量', '20')
|
|
19
|
+
.option('--json', '输出 JSON 格式')
|
|
20
|
+
.option('-i, --interactive', '交互式选择安装')
|
|
21
|
+
.action(async (keyword, options) => {
|
|
22
|
+
// 初始化 API token
|
|
23
|
+
const token = await getSessionToken();
|
|
24
|
+
if (token) {
|
|
25
|
+
api.setSessionToken(token);
|
|
26
|
+
}
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const results = await api.search(query, {
|
|
24
|
-
type: options.type,
|
|
25
|
-
per_page: Number(options.limit) || 20,
|
|
26
|
-
});
|
|
28
|
+
const spinner = ora('搜索中...').start();
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
try {
|
|
31
|
+
const result = await api.search({
|
|
32
|
+
q: keyword,
|
|
33
|
+
pluginType: options.type,
|
|
34
|
+
perPage: parseInt(options.limit) || 20,
|
|
35
|
+
});
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const cap of results.capabilities) {
|
|
40
|
-
const typeColor = getTypeColor(cap.type);
|
|
41
|
-
// Use short_name (author/slug) for display, making it easy to copy for install
|
|
42
|
-
const displayName = cap.short_name;
|
|
43
|
-
console.log(` ${typeColor(cap.type.padEnd(7))} ${chalk.cyan(displayName)}`);
|
|
44
|
-
if (cap.description) {
|
|
45
|
-
console.log(` ${chalk.gray(truncate(cap.description, 60))}`);
|
|
46
|
-
}
|
|
47
|
-
console.log(` ${chalk.gray(`下载: ${cap.downloads}`)} ${chalk.dim(`安装: 42plugin install ${displayName}`)}`);
|
|
48
|
-
console.log();
|
|
37
|
+
spinner.stop();
|
|
38
|
+
|
|
39
|
+
if (options.json) {
|
|
40
|
+
console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
return;
|
|
49
42
|
}
|
|
50
|
-
}
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
if (result.data.length === 0) {
|
|
45
|
+
console.log(chalk.yellow('未找到匹配的插件'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.gray(`找到 ${result.pagination.total} 个结果,显示第 1-${result.data.length} 个:\n`)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
for (const item of result.data) {
|
|
54
|
+
const icon = getTypeIcon(item.type);
|
|
55
|
+
const typeLabel = getTypeLabel(item.type);
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`[${typeLabel}]`)}`);
|
|
58
|
+
|
|
59
|
+
if (item.title || item.slogan) {
|
|
60
|
+
console.log(` ${item.title || item.slogan}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (item.description) {
|
|
64
|
+
const desc = item.description.length > 80
|
|
65
|
+
? item.description.slice(0, 80) + '...'
|
|
66
|
+
: item.description;
|
|
67
|
+
console.log(chalk.gray(` ${desc}`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.gray(` v${item.version} · ${item.downloads} 下载`));
|
|
62
71
|
console.log();
|
|
63
72
|
}
|
|
64
|
-
}
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
// 交互式选择安装
|
|
75
|
+
if (options.interactive && result.data.length > 0) {
|
|
76
|
+
console.log();
|
|
77
|
+
const choices = result.data.map((item) => ({
|
|
78
|
+
name: `${getTypeIcon(item.type)} ${item.fullName} - ${item.title || item.slogan || item.description?.slice(0, 30) || ''}`,
|
|
79
|
+
value: item.fullName,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const selected = await checkbox({
|
|
83
|
+
message: '选择要安装的插件 (空格选择,回车确认):',
|
|
84
|
+
choices,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (selected.length > 0) {
|
|
88
|
+
console.log();
|
|
89
|
+
// 动态导入 install 命令以避免循环依赖
|
|
90
|
+
const { installPlugin } = await import('./install-helper');
|
|
91
|
+
for (const fullName of selected) {
|
|
92
|
+
await installPlugin(fullName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// 提示
|
|
97
|
+
console.log(chalk.gray(`安装: 42plugin install <name>`));
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
spinner.fail('搜索失败');
|
|
101
|
+
console.error(chalk.red((error as Error).message));
|
|
102
|
+
process.exit(1);
|
|
68
103
|
}
|
|
69
|
-
}
|
|
70
|
-
console.error(chalk.red(`搜索失败: ${(error as Error).message}`));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getTypeColor(type: string): (text: string) => string {
|
|
76
|
-
const colors: Record<string, (text: string) => string> = {
|
|
77
|
-
skill: chalk.blue,
|
|
78
|
-
agent: chalk.green,
|
|
79
|
-
command: chalk.yellow,
|
|
80
|
-
hook: chalk.magenta,
|
|
81
|
-
};
|
|
82
|
-
return colors[type] || chalk.white;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function truncate(str: string, len: number): string {
|
|
86
|
-
return str.length > len ? str.slice(0, len - 3) + '...' : str;
|
|
87
|
-
}
|
|
104
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup 命令 - 自动配置 CLI 环境
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
11
|
+
|
|
12
|
+
export const setupCommand = new Command('setup')
|
|
13
|
+
.description('配置 CLI 环境(Shell 补全等)')
|
|
14
|
+
.option('--completion', '仅配置 Shell 补全')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
console.log(chalk.cyan.bold('\n42plugin CLI 环境配置\n'));
|
|
17
|
+
|
|
18
|
+
if (options.completion) {
|
|
19
|
+
await setupCompletion();
|
|
20
|
+
} else {
|
|
21
|
+
// 交互式菜单
|
|
22
|
+
const choice = await select({
|
|
23
|
+
message: '选择要配置的功能:',
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: 'Shell 自动补全', value: 'completion' },
|
|
26
|
+
{ name: '退出', value: 'exit' },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (choice === 'completion') {
|
|
31
|
+
await setupCompletion();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function setupCompletion(): Promise<void> {
|
|
37
|
+
// 检测当前 Shell
|
|
38
|
+
const shell = detectShell();
|
|
39
|
+
|
|
40
|
+
if (!shell) {
|
|
41
|
+
console.log(chalk.yellow('无法检测当前 Shell 类型'));
|
|
42
|
+
console.log(chalk.gray('请手动运行: 42plugin completion <bash|zsh|fish>'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(chalk.gray(`检测到 Shell: ${shell}`));
|
|
47
|
+
|
|
48
|
+
// 获取配置文件路径
|
|
49
|
+
const configFile = getShellConfigFile(shell);
|
|
50
|
+
if (!configFile) {
|
|
51
|
+
console.log(chalk.yellow(`不支持的 Shell: ${shell}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 检查是否已配置
|
|
56
|
+
const alreadyConfigured = await checkAlreadyConfigured(configFile);
|
|
57
|
+
if (alreadyConfigured) {
|
|
58
|
+
console.log(chalk.green('✓ Shell 补全已配置'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 确认安装
|
|
63
|
+
const shouldInstall = await confirm({
|
|
64
|
+
message: `是否将补全脚本添加到 ${configFile}?`,
|
|
65
|
+
default: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!shouldInstall) {
|
|
69
|
+
console.log(chalk.gray('已取消'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 生成并写入补全脚本
|
|
74
|
+
try {
|
|
75
|
+
await installCompletion(shell, configFile);
|
|
76
|
+
console.log(chalk.green(`\n✓ 补全脚本已添加到 ${configFile}`));
|
|
77
|
+
console.log(chalk.yellow(`\n请运行以下命令使配置生效:`));
|
|
78
|
+
console.log(chalk.cyan(` source ${configFile}`));
|
|
79
|
+
console.log(chalk.gray(`\n或重新打开终端`));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(chalk.red(`配置失败: ${(error as Error).message}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectShell(): string | null {
|
|
86
|
+
const shell = process.env.SHELL || '';
|
|
87
|
+
if (shell.includes('zsh')) return 'zsh';
|
|
88
|
+
if (shell.includes('bash')) return 'bash';
|
|
89
|
+
if (shell.includes('fish')) return 'fish';
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getShellConfigFile(shell: string): string | null {
|
|
94
|
+
const home = os.homedir();
|
|
95
|
+
switch (shell) {
|
|
96
|
+
case 'zsh':
|
|
97
|
+
return path.join(home, '.zshrc');
|
|
98
|
+
case 'bash':
|
|
99
|
+
// macOS 使用 .bash_profile,Linux 使用 .bashrc
|
|
100
|
+
return process.platform === 'darwin'
|
|
101
|
+
? path.join(home, '.bash_profile')
|
|
102
|
+
: path.join(home, '.bashrc');
|
|
103
|
+
case 'fish':
|
|
104
|
+
return path.join(home, '.config', 'fish', 'config.fish');
|
|
105
|
+
default:
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function checkAlreadyConfigured(configFile: string): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(configFile, 'utf-8');
|
|
113
|
+
return content.includes('42plugin completion');
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function installCompletion(shell: string, configFile: string): Promise<void> {
|
|
120
|
+
// 确保配置文件目录存在
|
|
121
|
+
await fs.mkdir(path.dirname(configFile), { recursive: true });
|
|
122
|
+
|
|
123
|
+
// 生成补全初始化脚本
|
|
124
|
+
const initScript = getCompletionInitScript(shell);
|
|
125
|
+
|
|
126
|
+
// 追加到配置文件
|
|
127
|
+
let content = '';
|
|
128
|
+
try {
|
|
129
|
+
content = await fs.readFile(configFile, 'utf-8');
|
|
130
|
+
} catch {
|
|
131
|
+
// 文件不存在,创建新文件
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const newContent = content + '\n' + initScript + '\n';
|
|
135
|
+
await fs.writeFile(configFile, newContent);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getCompletionInitScript(shell: string): string {
|
|
139
|
+
switch (shell) {
|
|
140
|
+
case 'bash':
|
|
141
|
+
return `
|
|
142
|
+
# 42plugin CLI 补全
|
|
143
|
+
eval "$(42plugin completion bash)"
|
|
144
|
+
`;
|
|
145
|
+
case 'zsh':
|
|
146
|
+
return `
|
|
147
|
+
# 42plugin CLI 补全
|
|
148
|
+
eval "$(42plugin completion zsh)"
|
|
149
|
+
`;
|
|
150
|
+
case 'fish':
|
|
151
|
+
return `
|
|
152
|
+
# 42plugin CLI 补全
|
|
153
|
+
42plugin completion fish | source
|
|
154
|
+
`;
|
|
155
|
+
default:
|
|
156
|
+
return '';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,58 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uninstall 命令 - 卸载插件
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
6
|
import chalk from 'chalk';
|
|
4
7
|
import ora from 'ora';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
interface UninstallOptions {
|
|
9
|
-
purge?: boolean;
|
|
10
|
-
}
|
|
8
|
+
import { api } from '../api';
|
|
9
|
+
import { getSessionToken, getInstallations, removeInstallation, removeLink, removeCache } from '../db';
|
|
10
|
+
import { parseTarget } from '../utils';
|
|
11
11
|
|
|
12
12
|
export const uninstallCommand = new Command('uninstall')
|
|
13
|
+
.alias('rm')
|
|
13
14
|
.description('卸载插件')
|
|
14
|
-
.argument('<
|
|
15
|
-
.option('--purge', '
|
|
16
|
-
.action(async (
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
.argument('<target>', '插件名 (author/name)')
|
|
16
|
+
.option('--purge', '同时清除缓存')
|
|
17
|
+
.action(async (target, options) => {
|
|
18
|
+
// 初始化 API token
|
|
19
|
+
const token = await getSessionToken();
|
|
20
|
+
if (token) {
|
|
21
|
+
api.setSessionToken(token);
|
|
22
|
+
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
const spinner = ora(`卸载 ${plugin}...`).start();
|
|
24
|
+
const spinner = ora(`卸载 ${target}...`).start();
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
try {
|
|
27
|
+
const parsed = parseTarget(target);
|
|
28
|
+
const projectPath = process.cwd();
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
30
|
+
// 查找安装记录
|
|
31
|
+
const installations = await getInstallations(projectPath);
|
|
32
|
+
const installation = installations.find((i) => i.fullName === parsed.fullName);
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
if (!installation) {
|
|
35
|
+
spinner.fail(`未找到已安装的插件: ${target}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
+
// 移除链接
|
|
40
|
+
await removeLink(installation.linkPath);
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const linkPath = record.link_path.replace(/\/+$/, '');
|
|
43
|
-
await fs.rm(linkPath, { recursive: true, force: true });
|
|
44
|
-
} catch {
|
|
45
|
-
// Ignore deletion errors
|
|
46
|
-
}
|
|
42
|
+
// 移除安装记录
|
|
43
|
+
await removeInstallation(projectPath, parsed.fullName);
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
// 同步卸载记录(静默)
|
|
46
|
+
if (api.isAuthenticated()) {
|
|
47
|
+
api.removeInstallRecord(parsed.author, parsed.name).catch(() => {});
|
|
48
|
+
}
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
50
|
+
// 清除缓存
|
|
51
|
+
if (options.purge) {
|
|
52
|
+
const removed = await removeCache(parsed.fullName, installation.version);
|
|
53
|
+
if (removed) {
|
|
54
|
+
spinner.succeed(`已卸载 ${target}(含缓存)`);
|
|
55
|
+
} else {
|
|
56
|
+
spinner.succeed(`已卸载 ${target}`);
|
|
57
|
+
console.log(chalk.gray(' 缓存已不存在'));
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
spinner.succeed(`已卸载 ${target}`);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
spinner.fail('卸载失败');
|
|
64
|
+
console.error(chalk.red((error as Error).message));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -1,44 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 配置
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import path from 'path';
|
|
2
6
|
import os from 'os';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
|
|
5
|
-
let dataDir: string | null = null;
|
|
6
|
-
|
|
7
|
-
export function getDataDir(): string {
|
|
8
|
-
if (dataDir) return dataDir;
|
|
9
|
-
|
|
10
|
-
if (process.platform === 'win32') {
|
|
11
|
-
dataDir = path.join(process.env.APPDATA || os.homedir(), '42plugin');
|
|
12
|
-
} else {
|
|
13
|
-
dataDir = path.join(os.homedir(), '.42plugin');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Ensure directory exists
|
|
17
|
-
if (!fs.existsSync(dataDir)) {
|
|
18
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
19
|
-
}
|
|
20
7
|
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
// 数据目录
|
|
9
|
+
const dataDir =
|
|
10
|
+
process.platform === 'win32'
|
|
11
|
+
? path.join(process.env.APPDATA || os.homedir(), '42plugin')
|
|
12
|
+
: path.join(os.homedir(), '.42plugin');
|
|
23
13
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getDbPath(): string {
|
|
33
|
-
return path.join(getDataDir(), 'local.db');
|
|
34
|
-
}
|
|
14
|
+
export const config = {
|
|
15
|
+
// 目录
|
|
16
|
+
dataDir,
|
|
17
|
+
cacheDir: path.join(dataDir, 'cache'),
|
|
18
|
+
globalDir: path.join(dataDir, 'global'),
|
|
19
|
+
dbPath: path.join(dataDir, 'local.db'),
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
// API
|
|
22
|
+
apiBaseUrl: process.env.API_BASE_URL || 'https://api.42plugin.com',
|
|
23
|
+
webBaseUrl: process.env.WEB_BASE_URL || 'https://42plugin.com',
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
apiBase: process.env.API_BASE || 'https://api.42plugin.com',
|
|
42
|
-
cdnBase: process.env.CDN_BASE || 'https://cdn.42plugin.com',
|
|
25
|
+
// 调试
|
|
43
26
|
debug: process.env.DEBUG === 'true',
|
|
27
|
+
|
|
28
|
+
// 开发模式跳过认证
|
|
29
|
+
devSkipAuth: process.env.DEV_SKIP_AUTH === 'true',
|
|
44
30
|
};
|
|
31
|
+
|
|
32
|
+
// 兼容旧 API
|
|
33
|
+
export const getDataDir = () => config.dataDir;
|
|
34
|
+
export const getCacheDir = () => config.cacheDir;
|
|
35
|
+
export const getDbPath = () => config.dbPath;
|