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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,114 +1,128 @@
1
+ /**
2
+ * auth 命令 - 认证管理
3
+ */
4
+
1
5
  import { Command } from 'commander';
2
- import open from 'open';
3
- import ora from 'ora';
4
6
  import chalk from 'chalk';
5
- import { api } from '../services/api';
6
- import { saveSecrets, loadSecrets, clearSecrets } from '../services/auth';
7
+ import ora from 'ora';
8
+ import open from 'open';
9
+ import { api } from '../api';
10
+ import { getSessionToken, saveSessionToken, clearSessionToken } from '../db';
7
11
 
8
12
  export const authCommand = new Command('auth')
9
- .description('登录 / 授权账户')
10
- .option('--status', '查看当前登录状态')
11
- .option('--logout', '登出并清除本地凭证')
13
+ .description('登录/登出 42plugin')
14
+ .option('--status', '查看登录状态')
15
+ .option('--logout', '登出')
12
16
  .action(async (options) => {
13
- if (options.status) {
14
- await showStatus();
15
- return;
16
- }
17
-
18
17
  if (options.logout) {
19
18
  await logout();
20
- return;
19
+ } else if (options.status) {
20
+ await status();
21
+ } else {
22
+ await login();
21
23
  }
22
-
23
- await login();
24
24
  });
25
25
 
26
- async function login() {
27
- const spinner = ora('正在初始化登录...').start();
26
+ async function login(): Promise<void> {
27
+ // 检查是否已登录
28
+ const existingToken = await getSessionToken();
29
+ if (existingToken) {
30
+ api.setSessionToken(existingToken);
31
+ try {
32
+ const session = await api.getSession();
33
+ console.log(chalk.yellow(`已登录为 ${session.user.name || session.user.email}`));
34
+ console.log(chalk.gray('如需切换账号,请先执行 42plugin auth --logout'));
35
+ return;
36
+ } catch {
37
+ // Token 无效,继续登录流程
38
+ }
39
+ }
40
+
41
+ const spinner = ora('正在请求设备授权码...').start();
28
42
 
29
43
  try {
30
- // 1. Get authorization code
31
- const { code, auth_url, expires_at } = await api.startAuth();
44
+ const deviceCode = await api.requestDeviceCode();
32
45
  spinner.stop();
33
46
 
34
47
  console.log();
35
- console.log(chalk.cyan('正在打开浏览器进行授权...'));
48
+ console.log(chalk.cyan('请在浏览器中完成登录:'));
36
49
  console.log();
37
- console.log('如果浏览器没有自动打开,请访问:');
38
- console.log(chalk.underline(auth_url));
50
+ console.log(` ${chalk.bold('验证码')}: ${chalk.yellow.bold(deviceCode.userCode)}`);
51
+ console.log(` ${chalk.bold('链接')}: ${deviceCode.verificationUri}`);
39
52
  console.log();
40
53
 
41
- // 2. Open browser
42
- await open(auth_url);
54
+ // 尝试打开浏览器
55
+ if (deviceCode.verificationUriComplete) {
56
+ await open(deviceCode.verificationUriComplete).catch(() => {});
57
+ }
43
58
 
44
- // 3. Poll for authorization completion
45
- spinner.start('等待浏览器授权...');
59
+ // 轮询等待授权
60
+ const pollSpinner = ora('等待授权...').start();
61
+ const interval = (deviceCode.interval || 5) * 1000;
62
+ const expiresAt = Date.now() + deviceCode.expiresIn * 1000;
46
63
 
47
- const pollInterval = 2000; // 2 seconds
48
- const expiresTime = new Date(expires_at).getTime();
64
+ while (Date.now() < expiresAt) {
65
+ await new Promise((r) => setTimeout(r, interval));
49
66
 
50
- while (Date.now() < expiresTime) {
51
- const result = await api.pollAuth(code);
67
+ const tokenResponse = await api.pollDeviceToken(deviceCode.deviceCode);
52
68
 
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
- });
69
+ if (tokenResponse.accessToken && tokenResponse.user) {
70
+ pollSpinner.succeed('授权成功!');
60
71
 
61
- // Update API client token
62
- api.setToken(result.access_token!);
72
+ // 保存 token
73
+ await saveSessionToken(tokenResponse.accessToken);
74
+ api.setSessionToken(tokenResponse.accessToken);
63
75
 
64
- spinner.succeed(chalk.green('登录成功!'));
65
76
  console.log();
66
- console.log(`欢迎,${chalk.cyan(result.user!.display_name || result.user!.username)}!`);
77
+ console.log(chalk.green(`欢迎,${tokenResponse.user.name || tokenResponse.user.username || tokenResponse.user.email}!`));
67
78
  return;
68
79
  }
69
80
 
70
- await sleep(pollInterval);
81
+ if (tokenResponse.error === 'expired_token') {
82
+ pollSpinner.fail('授权码已过期,请重新执行 42plugin auth');
83
+ process.exit(1);
84
+ }
85
+
86
+ if (tokenResponse.error === 'access_denied') {
87
+ pollSpinner.fail('授权被拒绝');
88
+ process.exit(1);
89
+ }
90
+
91
+ // authorization_pending 继续等待
71
92
  }
72
93
 
73
- spinner.fail('授权超时,请重试');
94
+ pollSpinner.fail('授权超时,请重试');
95
+ process.exit(1);
74
96
  } catch (error) {
75
- spinner.fail(`登录失败: ${(error as Error).message}`);
97
+ spinner.fail('登录失败');
98
+ console.error(chalk.red((error as Error).message));
76
99
  process.exit(1);
77
100
  }
78
101
  }
79
102
 
80
- async function showStatus() {
81
- const secrets = await loadSecrets();
103
+ async function logout(): Promise<void> {
104
+ await clearSessionToken();
105
+ api.setSessionToken(null);
106
+ console.log(chalk.green('已登出'));
107
+ }
82
108
 
83
- if (!secrets) {
109
+ async function status(): Promise<void> {
110
+ const token = await getSessionToken();
111
+
112
+ if (!token) {
84
113
  console.log(chalk.yellow('未登录'));
85
- console.log('使用 42plugin auth 登录');
114
+ console.log(chalk.gray('执行 42plugin auth 登录'));
86
115
  return;
87
116
  }
88
117
 
118
+ api.setSessionToken(token);
119
+
89
120
  try {
90
- const user = await api.getMe();
121
+ const session = await api.getSession();
91
122
  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
- }
123
+ console.log(` 用户: ${session.user.name || session.user.email}`);
100
124
  } catch {
101
- console.log(chalk.yellow('Token 已过期,请重新登录'));
102
- console.log('使用 42plugin auth 重新登录');
125
+ console.log(chalk.yellow('登录已过期'));
126
+ console.log(chalk.gray('请执行 42plugin auth 重新登录'));
103
127
  }
104
128
  }
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,118 @@
1
+ /**
2
+ * check 命令 - 检查插件格式(发布前检查)
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { PluginValidator } from '../validators/plugin-validator';
8
+ import type { ValidationResult } from '../types';
9
+
10
+ export const checkCommand = new Command('check')
11
+ .description('检查插件格式(发布前检查)')
12
+ .argument('[path]', '插件路径(文件或目录)', '.')
13
+ .option('--json', 'JSON 格式输出')
14
+ .option('-q, --quiet', '仅显示错误')
15
+ .action(async (pluginPath: string, options: { json?: boolean; quiet?: boolean }) => {
16
+ const validator = new PluginValidator();
17
+
18
+ try {
19
+ const result = await validator.validateFull(pluginPath);
20
+
21
+ // JSON 输出
22
+ if (options.json) {
23
+ console.log(JSON.stringify(result, null, 2));
24
+ process.exit(result.valid ? 0 : 1);
25
+ }
26
+
27
+ // 常规输出
28
+ displayResult(result, pluginPath, options.quiet);
29
+
30
+ process.exit(result.valid ? 0 : 1);
31
+ } catch (error) {
32
+ if (options.json) {
33
+ console.log(
34
+ JSON.stringify(
35
+ {
36
+ valid: false,
37
+ errors: [{ code: 'E000', message: (error as Error).message }],
38
+ warnings: [],
39
+ },
40
+ null,
41
+ 2
42
+ )
43
+ );
44
+ } else {
45
+ console.error(chalk.red((error as Error).message));
46
+ }
47
+ process.exit(1);
48
+ }
49
+ });
50
+
51
+ /**
52
+ * 显示检查结果
53
+ */
54
+ function displayResult(result: ValidationResult, pluginPath: string, quiet?: boolean): void {
55
+ console.log();
56
+ console.log(chalk.bold(`📋 检查: ${pluginPath}`));
57
+ console.log();
58
+
59
+ // 元信息(非静默模式)
60
+ if (!quiet) {
61
+ const { metadata } = result;
62
+ console.log(` 名称: ${metadata.name || chalk.gray('(未指定)')}`);
63
+ console.log(` 类型: ${metadata.type}`);
64
+ console.log(` 路径: ${metadata.sourcePath}`);
65
+
66
+ if (metadata.description) {
67
+ const desc =
68
+ metadata.description.length > 60
69
+ ? metadata.description.slice(0, 60) + '...'
70
+ : metadata.description;
71
+ console.log(` 描述: ${desc}`);
72
+ }
73
+
74
+ if (metadata.tags && metadata.tags.length > 0) {
75
+ console.log(` 标签: ${metadata.tags.join(', ')}`);
76
+ }
77
+ }
78
+
79
+ // 错误
80
+ if (result.errors.length > 0) {
81
+ console.log();
82
+ console.log(chalk.red.bold(`❌ 错误 (${result.errors.length}):`));
83
+ for (const error of result.errors) {
84
+ console.log(chalk.red(` [${error.code}] ${error.message}`));
85
+ if (error.suggestion) {
86
+ console.log(chalk.gray(` 💡 ${error.suggestion}`));
87
+ }
88
+ }
89
+ }
90
+
91
+ // 警告(非静默模式)
92
+ if (result.warnings.length > 0 && !quiet) {
93
+ console.log();
94
+ console.log(chalk.yellow.bold(`⚠️ 警告 (${result.warnings.length}):`));
95
+ for (const warning of result.warnings) {
96
+ console.log(chalk.yellow(` [${warning.code}] ${warning.message}`));
97
+ if (warning.suggestion) {
98
+ console.log(chalk.gray(` 💡 ${warning.suggestion}`));
99
+ }
100
+ }
101
+ }
102
+
103
+ // 总结
104
+ console.log();
105
+ if (result.valid) {
106
+ if (result.warnings.length > 0) {
107
+ console.log(chalk.green(`✅ 检查通过 (${result.warnings.length} 个警告)`));
108
+ } else {
109
+ console.log(chalk.green('✅ 检查通过'));
110
+ }
111
+ console.log();
112
+ console.log(chalk.gray('可以执行 42plugin publish 发布插件'));
113
+ } else {
114
+ console.log(chalk.red('❌ 检查失败'));
115
+ console.log();
116
+ console.log(chalk.gray('请修复上述错误后重试'));
117
+ }
118
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * completion 命令 - 生成 shell 自动补全脚本
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+
8
+ export const completionCommand = new Command('completion')
9
+ .description('生成 shell 自动补全脚本')
10
+ .argument('<shell>', 'shell 类型 (bash, zsh, fish)')
11
+ .action((shell: string) => {
12
+ switch (shell.toLowerCase()) {
13
+ case 'bash':
14
+ console.log(generateBashCompletion());
15
+ break;
16
+ case 'zsh':
17
+ console.log(generateZshCompletion());
18
+ break;
19
+ case 'fish':
20
+ console.log(generateFishCompletion());
21
+ break;
22
+ default:
23
+ console.error(chalk.red(`不支持的 shell: ${shell}`));
24
+ console.log(chalk.gray('支持的 shell: bash, zsh, fish'));
25
+ process.exit(1);
26
+ }
27
+ });
28
+
29
+ function generateBashCompletion(): string {
30
+ return `# 42plugin bash completion
31
+ # 添加到 ~/.bashrc:
32
+ # eval "$(42plugin completion bash)"
33
+ # 或:
34
+ # 42plugin completion bash >> ~/.bashrc
35
+ #
36
+ # 注意: 动态补全插件名需要安装 jq (可选)
37
+ # 未安装 jq 时仍可使用命令和选项补全
38
+
39
+ _42plugin_completions() {
40
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
41
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
42
+
43
+ case "\${prev}" in
44
+ 42plugin)
45
+ COMPREPLY=( $(compgen -W "auth search install list uninstall version completion" -- "\${cur}") )
46
+ return 0
47
+ ;;
48
+ install|uninstall)
49
+ # 动态补全插件名 (需要 jq)
50
+ if command -v jq &>/dev/null; then
51
+ if [[ "\${cur}" == */* ]]; then
52
+ local plugins=$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null)
53
+ COMPREPLY=( $(compgen -W "\${plugins}" -- "\${cur}") )
54
+ else
55
+ local authors=$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null | cut -d'/' -f1 | sort -u)
56
+ COMPREPLY=( $(compgen -W "\${authors}" -- "\${cur}") )
57
+ fi
58
+ fi
59
+ return 0
60
+ ;;
61
+ search)
62
+ # 类型补全
63
+ if [[ "\${cur}" == -* ]]; then
64
+ COMPREPLY=( $(compgen -W "-t --type -l --limit --json -i --interactive" -- "\${cur}") )
65
+ fi
66
+ return 0
67
+ ;;
68
+ -t|--type)
69
+ COMPREPLY=( $(compgen -W "skill agent command hook mcp" -- "\${cur}") )
70
+ return 0
71
+ ;;
72
+ auth)
73
+ COMPREPLY=( $(compgen -W "--status --logout" -- "\${cur}") )
74
+ return 0
75
+ ;;
76
+ list|ls)
77
+ if [[ "\${cur}" == -* ]]; then
78
+ COMPREPLY=( $(compgen -W "-t --type --json" -- "\${cur}") )
79
+ fi
80
+ return 0
81
+ ;;
82
+ completion)
83
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
84
+ return 0
85
+ ;;
86
+ esac
87
+ }
88
+
89
+ complete -F _42plugin_completions 42plugin
90
+ `;
91
+ }
92
+
93
+ function generateZshCompletion(): string {
94
+ return `#compdef 42plugin
95
+ # 42plugin zsh completion
96
+ # 添加到 ~/.zshrc:
97
+ # eval "$(42plugin completion zsh)"
98
+ # 或:
99
+ # 42plugin completion zsh >> ~/.zshrc
100
+ #
101
+ # 注意: 动态补全插件名需要安装 jq (可选)
102
+
103
+ _42plugin() {
104
+ local -a commands
105
+ commands=(
106
+ 'auth:登录/登出/查看状态'
107
+ 'search:搜索插件'
108
+ 'install:安装插件或套包'
109
+ 'list:查看已安装插件'
110
+ 'uninstall:卸载插件'
111
+ 'version:显示版本'
112
+ 'completion:生成补全脚本'
113
+ )
114
+
115
+ _arguments -C \\
116
+ '1: :->command' \\
117
+ '*: :->args'
118
+
119
+ case $state in
120
+ command)
121
+ _describe 'command' commands
122
+ ;;
123
+ args)
124
+ case $words[2] in
125
+ install|uninstall)
126
+ # 插件名补全 (需要 jq)
127
+ if (( $+commands[jq] )); then
128
+ local plugins
129
+ plugins=(\${(f)"$(42plugin list --json 2>/dev/null | jq -r '.[].fullName' 2>/dev/null)"})
130
+ _describe 'plugin' plugins
131
+ fi
132
+ ;;
133
+ search)
134
+ _arguments \\
135
+ '-t[筛选类型]:type:(skill agent command hook mcp)' \\
136
+ '--type[筛选类型]:type:(skill agent command hook mcp)' \\
137
+ '-l[结果数量]:limit:' \\
138
+ '--limit[结果数量]:limit:' \\
139
+ '--json[JSON 输出]' \\
140
+ '-i[交互式安装]' \\
141
+ '--interactive[交互式安装]'
142
+ ;;
143
+ auth)
144
+ _arguments \\
145
+ '--status[查看登录状态]' \\
146
+ '--logout[登出]'
147
+ ;;
148
+ list)
149
+ _arguments \\
150
+ '-t[筛选类型]:type:(skill agent command hook mcp)' \\
151
+ '--type[筛选类型]:type:(skill agent command hook mcp)' \\
152
+ '--json[JSON 输出]'
153
+ ;;
154
+ completion)
155
+ _describe 'shell' '(bash zsh fish)'
156
+ ;;
157
+ esac
158
+ ;;
159
+ esac
160
+ }
161
+
162
+ _42plugin
163
+ `;
164
+ }
165
+
166
+ function generateFishCompletion(): string {
167
+ return `# 42plugin fish completion
168
+ # 添加到 ~/.config/fish/config.fish:
169
+ # 42plugin completion fish | source
170
+ # 或:
171
+ # 42plugin completion fish > ~/.config/fish/completions/42plugin.fish
172
+
173
+ complete -c 42plugin -f
174
+
175
+ # Commands
176
+ complete -c 42plugin -n '__fish_use_subcommand' -a auth -d '登录/登出/查看状态'
177
+ complete -c 42plugin -n '__fish_use_subcommand' -a search -d '搜索插件'
178
+ complete -c 42plugin -n '__fish_use_subcommand' -a install -d '安装插件或套包'
179
+ complete -c 42plugin -n '__fish_use_subcommand' -a list -d '查看已安装插件'
180
+ complete -c 42plugin -n '__fish_use_subcommand' -a uninstall -d '卸载插件'
181
+ complete -c 42plugin -n '__fish_use_subcommand' -a version -d '显示版本'
182
+ complete -c 42plugin -n '__fish_use_subcommand' -a completion -d '生成补全脚本'
183
+
184
+ # auth options
185
+ complete -c 42plugin -n '__fish_seen_subcommand_from auth' -l status -d '查看登录状态'
186
+ complete -c 42plugin -n '__fish_seen_subcommand_from auth' -l logout -d '登出'
187
+
188
+ # search options
189
+ complete -c 42plugin -n '__fish_seen_subcommand_from search' -s t -l type -d '筛选类型' -a 'skill agent command hook mcp'
190
+ complete -c 42plugin -n '__fish_seen_subcommand_from search' -s l -l limit -d '结果数量'
191
+ complete -c 42plugin -n '__fish_seen_subcommand_from search' -l json -d 'JSON 输出'
192
+ complete -c 42plugin -n '__fish_seen_subcommand_from search' -s i -l interactive -d '交互式安装'
193
+
194
+ # list options
195
+ complete -c 42plugin -n '__fish_seen_subcommand_from list' -s t -l type -d '筛选类型' -a 'skill agent command hook mcp'
196
+ complete -c 42plugin -n '__fish_seen_subcommand_from list' -l json -d 'JSON 输出'
197
+
198
+ # install options
199
+ complete -c 42plugin -n '__fish_seen_subcommand_from install' -s g -l global -d '安装到全局目录'
200
+ complete -c 42plugin -n '__fish_seen_subcommand_from install' -s f -l force -d '强制重新下载'
201
+ complete -c 42plugin -n '__fish_seen_subcommand_from install' -l no-cache -d '跳过缓存检查'
202
+ complete -c 42plugin -n '__fish_seen_subcommand_from install' -l optional -d '安装套包时包含可选插件'
203
+
204
+ # uninstall options
205
+ complete -c 42plugin -n '__fish_seen_subcommand_from uninstall' -l purge -d '同时清除缓存'
206
+
207
+ # completion argument
208
+ complete -c 42plugin -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell 类型'
209
+ `;
210
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 命令导出
3
+ */
4
+
5
+ export { authCommand } from './auth';
6
+ export { installCommand } from './install';
7
+ export { searchCommand } from './search';
8
+ export { listCommand } from './list';
9
+ export { uninstallCommand } from './uninstall';
10
+ export { completionCommand } from './completion';
11
+ export { setupCommand } from './setup';
12
+ export { publishCommand } from './publish';
13
+ export { checkCommand } from './check';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 安装辅助函数 - 用于从其他命令调用安装功能
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import path from 'path';
8
+ import { api, QuotaExceededError } from '../api';
9
+ import {
10
+ getOrCreateProject,
11
+ addInstallation,
12
+ createLink,
13
+ resolveCachePath,
14
+ } from '../db';
15
+ import { parseTarget, getTypeIcon } from '../utils';
16
+ import { TargetType } from '../types';
17
+
18
+ /**
19
+ * 安装单个插件(简化接口,用于交互式安装)
20
+ * 注意:调用方应提前初始化 API token
21
+ */
22
+ export async function installPlugin(fullName: string): Promise<void> {
23
+ const parsed = parseTarget(fullName);
24
+ const spinner = ora(`安装 ${fullName}...`).start();
25
+
26
+ try {
27
+ const pluginDownload = await api.getPluginDownload(parsed.author, parsed.name);
28
+ const downloadInfo = pluginDownload.download;
29
+
30
+ // 解析缓存
31
+ const { cachePath, fromCache } = await resolveCachePath(downloadInfo, false, true);
32
+
33
+ // 链接到项目
34
+ const projectPath = process.cwd();
35
+ const project = await getOrCreateProject(projectPath);
36
+ const linkPath = path.join(projectPath, downloadInfo.installPath);
37
+
38
+ await createLink(cachePath, linkPath);
39
+
40
+ await addInstallation({
41
+ projectId: project.id,
42
+ fullName: downloadInfo.fullName,
43
+ type: downloadInfo.type,
44
+ version: downloadInfo.version,
45
+ cachePath,
46
+ linkPath,
47
+ source: 'direct',
48
+ });
49
+
50
+ // 同步安装记录(静默)
51
+ if (api.isAuthenticated()) {
52
+ api.recordInstall(downloadInfo.fullName).catch(() => {});
53
+ }
54
+
55
+ spinner.succeed(
56
+ `${getTypeIcon(downloadInfo.type)} ${downloadInfo.fullName} v${downloadInfo.version}` +
57
+ (fromCache ? chalk.gray(' (from cache)') : '')
58
+ );
59
+ console.log(chalk.gray(` → ${downloadInfo.installPath}`));
60
+ } catch (error) {
61
+ spinner.fail(`安装失败: ${fullName}`);
62
+ if (error instanceof QuotaExceededError) {
63
+ console.error(chalk.red(error.message));
64
+ if (error.isGuestQuota()) {
65
+ console.log(chalk.yellow('提示: 登录后可获得更高配额'));
66
+ }
67
+ } else {
68
+ console.error(chalk.red((error as Error).message));
69
+ }
70
+ }
71
+ }