@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 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
- 42plugin list # 查看已安装
123
- 42plugin list --type skill --json
124
- 42plugin uninstall <full_name> # 卸载
125
- 42plugin uninstall <full_name> --purge
126
- 42plugin version # 版本
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`(Windows: `%APPDATA%/42plugin/local.db`) |
146
- | 缓存 | `~/.42plugin/cache` |
147
- | 登录凭证 | `~/.42plugin/secrets.json`(权限 600 |
148
- | 项目链接 | `.claude/`(项目目录下) |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@42ailab/42plugin",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/api.ts CHANGED
@@ -601,6 +601,7 @@ class ApiClient {
601
601
  description: params.description,
602
602
  tags: params.tags,
603
603
  visibility: params.visibility,
604
+ doc_content: params.docContent,
604
605
  },
605
606
  });
606
607
 
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
- .action(() => {
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 };
@@ -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 author/kit/slug)')
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(` → ~/.42plugin/global/${downloadInfo.installPath}`));
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(`✓ 已安装 ${installed} 个插件到全局用户目录`));
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
  }
@@ -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
- let installations = await getInstallations(process.cwd());
19
-
20
- // 类型筛选
21
- if (options.type) {
22
- installations = installations.filter((i) => i.type === options.type);
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
- if (options.json) {
26
- console.log(JSON.stringify(installations, null, 2));
27
- return;
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
- if (installations.length === 0) {
31
- console.log(chalk.yellow('当前项目未安装任何插件'));
32
- console.log(chalk.gray('执行 42plugin install <name> 安装插件'));
33
- return;
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
- console.log(chalk.gray(`已安装 ${installations.length} 个插件:\n`));
87
+ if (options.json) {
88
+ console.log(JSON.stringify(installations, null, 2));
89
+ return;
90
+ }
37
91
 
38
- for (const item of installations) {
39
- const icon = getTypeIcon(item.type);
40
- const time = formatRelativeTime(item.installedAt);
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
- console.log(`${icon} ${chalk.cyan.bold(item.fullName)} ${chalk.gray(`v${item.version}`)}`);
43
- console.log(chalk.gray(` → ${item.linkPath}`));
98
+ console.log(chalk.cyan('📍 全局插件') + chalk.gray(' (~/.claude/) - 对所有项目生效\n'));
99
+ console.log(chalk.gray(`共 ${installations.length} 个插件:\n`));
44
100
 
45
- // 解析所属套包:fullName 格式为 author/kit/plugin
46
- const parts = item.fullName.split('/');
47
- if (parts.length >= 3) {
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
- console.log(chalk.gray(` 安装于 ${time}`));
53
- console.log();
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
- } catch (error) {
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
+ }
@@ -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('--purge', '同时清除缓存')
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}`));