@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.
@@ -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
+ }
@@ -1,100 +1,104 @@
1
+ /**
2
+ * search 命令 - 搜索插件
3
+ */
4
+
1
5
  import { Command } from 'commander';
2
6
  import chalk from 'chalk';
3
- import { api } from '../services/api';
4
-
5
- interface SearchOptions {
6
- type?: string;
7
- limit?: string;
8
- json?: boolean;
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('<query>', '搜索关键词')
14
- .option('--type <type>', '筛选类型: skill | agent | command | hook | list')
15
- .option('--limit <n>', '结果数量限制', '20')
16
- .option('--json', 'JSON 格式输出')
17
- .action(async (query: string, options: SearchOptions) => {
18
- await search(query, options);
19
- });
20
-
21
- async function search(query: string, options: SearchOptions) {
22
- try {
23
- // Map CLI type options to API type parameter
24
- // API supports: capability | list | all
25
- // CLI supports: skill | agent | command | hook | list
26
- const capabilityTypes = ['skill', 'agent', 'command', 'hook'];
27
- const apiType = options.type === 'list' ? 'list'
28
- : capabilityTypes.includes(options.type || '') ? 'capability'
29
- : undefined;
30
-
31
- const results = await api.search(query, {
32
- type: apiType,
33
- per_page: Number(options.limit) || 20,
34
- });
35
-
36
- // Filter capabilities by specific type if requested
37
- if (options.type && capabilityTypes.includes(options.type)) {
38
- results.capabilities = results.capabilities.filter(cap => cap.type === options.type);
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);
39
26
  }
40
27
 
41
- if (options.json) {
42
- console.log(JSON.stringify(results, null, 2));
43
- return;
44
- }
28
+ const spinner = ora('搜索中...').start();
45
29
 
46
- // Display capability results
47
- if (results.capabilities.length > 0) {
48
- console.log();
49
- console.log(chalk.bold('插件:'));
50
- console.log();
51
-
52
- for (const cap of results.capabilities) {
53
- const typeColor = getTypeColor(cap.type);
54
- // Use short_name (author/slug) for display, making it easy to copy for install
55
- const displayName = cap.short_name;
56
- console.log(` ${typeColor(cap.type.padEnd(7))} ${chalk.cyan(displayName)}`);
57
- if (cap.description) {
58
- console.log(` ${chalk.gray(truncate(cap.description, 60))}`);
59
- }
60
- console.log(` ${chalk.gray(`下载: ${cap.downloads}`)} ${chalk.dim(`安装: 42plugin install ${displayName}`)}`);
61
- console.log();
30
+ try {
31
+ const result = await api.search({
32
+ q: keyword,
33
+ pluginType: options.type,
34
+ perPage: parseInt(options.limit) || 20,
35
+ });
36
+
37
+ spinner.stop();
38
+
39
+ if (options.json) {
40
+ console.log(JSON.stringify(result, null, 2));
41
+ return;
62
42
  }
63
- }
64
43
 
65
- // Display list results
66
- if (results.lists.length > 0) {
67
- console.log();
68
- console.log(chalk.bold('列表:'));
69
- console.log();
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);
70
56
 
71
- for (const list of results.lists) {
72
- console.log(` ${chalk.magenta('list')} ${chalk.cyan(list.full_name)}`);
73
- console.log(` ${list.name}`);
74
- console.log(` ${chalk.gray(`${list.capability_count} 个插件 · 下载: ${list.downloads}`)}`);
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} 下载`));
75
71
  console.log();
76
72
  }
77
- }
78
73
 
79
- if (results.capabilities.length === 0 && results.lists.length === 0) {
80
- console.log(chalk.yellow('未找到结果'));
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);
81
103
  }
82
- } catch (error) {
83
- console.error(chalk.red(`搜索失败: ${(error as Error).message}`));
84
- process.exit(1);
85
- }
86
- }
87
-
88
- function getTypeColor(type: string): (text: string) => string {
89
- const colors: Record<string, (text: string) => string> = {
90
- skill: chalk.blue,
91
- agent: chalk.green,
92
- command: chalk.yellow,
93
- hook: chalk.magenta,
94
- };
95
- return colors[type] || chalk.white;
96
- }
97
-
98
- function truncate(str: string, len: number): string {
99
- return str.length > len ? str.slice(0, len - 3) + '...' : str;
100
- }
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 { getProject, removeProjectPlugin } from '../services/project';
6
- import { removeFromCache } from '../services/cache';
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('<plugin>', '插件标识符')
15
- .option('--purge', '同时删除缓存')
16
- .action(async (plugin: string, options: UninstallOptions) => {
17
- await uninstall(plugin, options);
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
- async function uninstall(plugin: string, options: UninstallOptions) {
21
- const spinner = ora(`卸载 ${plugin}...`).start();
24
+ const spinner = ora(`卸载 ${target}...`).start();
22
25
 
23
- try {
24
- const projectPath = process.cwd();
25
- const project = await getProject(projectPath);
26
+ try {
27
+ const parsed = parseTarget(target);
28
+ const projectPath = process.cwd();
26
29
 
27
- if (!project) {
28
- spinner.fail('当前目录不是 42plugin 项目');
29
- return;
30
- }
30
+ // 查找安装记录
31
+ const installations = await getInstallations(projectPath);
32
+ const installation = installations.find((i) => i.fullName === parsed.fullName);
31
33
 
32
- // Get install record
33
- const record = await removeProjectPlugin(project.id, plugin);
34
+ if (!installation) {
35
+ spinner.fail(`未找到已安装的插件: ${target}`);
36
+ process.exit(1);
37
+ }
34
38
 
35
- if (!record) {
36
- spinner.fail(`未找到已安装的插件: ${plugin}`);
37
- return;
38
- }
39
+ // 移除链接
40
+ await removeLink(installation.linkPath);
39
41
 
40
- // Remove link (normalize path by removing trailing slash)
41
- try {
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
- // If --purge specified, also remove from cache
49
- if (options.purge) {
50
- await removeFromCache(plugin, record.version);
51
- }
45
+ // 同步卸载记录(静默)
46
+ if (api.isAuthenticated()) {
47
+ api.removeInstallRecord(parsed.author, parsed.name).catch(() => {});
48
+ }
52
49
 
53
- spinner.succeed(`${chalk.green('已卸载')} ${plugin}`);
54
- } catch (error) {
55
- spinner.fail(`卸载失败: ${(error as Error).message}`);
56
- process.exit(1);
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
- return dataDir;
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 function getCacheDir(): string {
25
- const cacheDir = path.join(getDataDir(), 'cache');
26
- if (!fs.existsSync(cacheDir)) {
27
- fs.mkdirSync(cacheDir, { recursive: true });
28
- }
29
- return cacheDir;
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
- export function getSecretsPath(): string {
37
- return path.join(getDataDir(), 'secrets.json');
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
- export const config = {
41
- apiBase: process.env.API_BASE_URL || '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;