@42ailab/42plugin 0.1.22 → 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.22",
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
+ }
@@ -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}`));
package/src/config.ts CHANGED
@@ -12,13 +12,54 @@ const dataDir =
12
12
  ? path.join(process.env.APPDATA || os.homedir(), '42plugin')
13
13
  : path.join(os.homedir(), '.42plugin'));
14
14
 
15
+ /**
16
+ * 平台配置 - 支持多种 AI 编程助手
17
+ * 未来可扩展支持 Codex、Gemini 等
18
+ */
19
+ export interface PlatformConfig {
20
+ id: string;
21
+ name: string;
22
+ globalDir: string; // 全局安装目录(用户级)
23
+ projectDir: string; // 项目安装目录名
24
+ }
25
+
26
+ const homeDir = os.homedir();
27
+
28
+ export const platforms: Record<string, PlatformConfig> = {
29
+ claude: {
30
+ id: 'claude',
31
+ name: 'Claude Code',
32
+ globalDir: path.join(homeDir, '.claude'),
33
+ projectDir: '.claude',
34
+ },
35
+ // 预留:未来扩展
36
+ // codex: {
37
+ // id: 'codex',
38
+ // name: 'OpenAI Codex CLI',
39
+ // globalDir: path.join(homeDir, '.codex'),
40
+ // projectDir: '.codex',
41
+ // },
42
+ // gemini: {
43
+ // id: 'gemini',
44
+ // name: 'Google Gemini',
45
+ // globalDir: path.join(homeDir, '.gemini'),
46
+ // projectDir: '.gemini',
47
+ // },
48
+ };
49
+
50
+ // 当前默认平台
51
+ export const currentPlatform: PlatformConfig = platforms.claude;
52
+
15
53
  export const config = {
16
54
  // 目录
17
55
  dataDir,
18
56
  cacheDir: path.join(dataDir, 'cache'),
19
- globalDir: path.join(dataDir, 'global'),
57
+ globalDir: currentPlatform.globalDir, // 全局安装目录:~/.claude
20
58
  dbPath: path.join(dataDir, 'local.db'),
21
59
 
60
+ // 平台
61
+ platform: currentPlatform,
62
+
22
63
  // API
23
64
  apiBaseUrl: process.env.API_BASE_URL || 'https://api.42plugin.com',
24
65
  webBaseUrl: process.env.WEB_BASE_URL || 'https://42plugin.com',
@@ -34,3 +75,22 @@ export const config = {
34
75
  export const getDataDir = () => config.dataDir;
35
76
  export const getCacheDir = () => config.cacheDir;
36
77
  export const getDbPath = () => config.dbPath;
78
+
79
+ /**
80
+ * 获取全局目录路径
81
+ * @param platformId 平台 ID(默认当前平台)
82
+ */
83
+ export const getGlobalDir = (platformId?: string): string => {
84
+ if (platformId && platforms[platformId]) {
85
+ return platforms[platformId].globalDir;
86
+ }
87
+ return config.globalDir;
88
+ };
89
+
90
+ /**
91
+ * 判断路径是否为全局目录
92
+ */
93
+ export const isGlobalDir = (dirPath: string): boolean => {
94
+ const resolved = path.resolve(dirPath);
95
+ return Object.values(platforms).some((p) => path.resolve(p.globalDir) === resolved);
96
+ };
package/src/db.ts CHANGED
@@ -367,6 +367,37 @@ export async function getInstallations(projectPath: string): Promise<LocalInstal
367
367
  }));
368
368
  }
369
369
 
370
+ export interface InstallationWithProject extends LocalInstallation {
371
+ projectPath: string;
372
+ projectName: string;
373
+ }
374
+
375
+ export async function getAllInstallations(): Promise<InstallationWithProject[]> {
376
+ const client = await getDb();
377
+
378
+ const rows = client.prepare(`
379
+ SELECT i.*, p.path as project_path, p.name as project_name
380
+ FROM installations i
381
+ JOIN projects p ON i.project_id = p.id
382
+ ORDER BY p.path, i.installed_at DESC
383
+ `).all() as Record<string, unknown>[];
384
+
385
+ return rows.map((row) => ({
386
+ id: row.id as string,
387
+ projectId: row.project_id as string,
388
+ fullName: row.full_name as string,
389
+ type: row.type as PluginType,
390
+ version: row.version as string,
391
+ cachePath: row.cache_path as string,
392
+ linkPath: row.link_path as string,
393
+ source: row.source as 'direct' | 'kit',
394
+ sourceKit: row.source_kit as string | undefined,
395
+ installedAt: row.installed_at as string,
396
+ projectPath: row.project_path as string,
397
+ projectName: row.project_name as string,
398
+ }));
399
+ }
400
+
370
401
  export async function removeInstallation(projectPath: string, fullName: string): Promise<boolean> {
371
402
  const client = await getDb();
372
403
  const absPath = path.resolve(projectPath);
@@ -9,6 +9,8 @@
9
9
  * 5. 确认发布
10
10
  */
11
11
 
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
12
14
  import ora from 'ora';
13
15
  import chalk from 'chalk';
14
16
  import { confirm } from '@inquirer/prompts';
@@ -24,6 +26,7 @@ import type {
24
26
  PublishResult,
25
27
  PackageResult,
26
28
  ValidationResult,
29
+ PluginType,
27
30
  } from '../types';
28
31
 
29
32
  export class Publisher {
@@ -143,6 +146,9 @@ export class Publisher {
143
146
  const uploadResult = await this.uploader.upload(metadata, pkg, versionDecision.version);
144
147
  spinner.succeed('上传完成');
145
148
 
149
+ // 读取文档内容
150
+ const docContent = await this.readDocContent(metadata.sourcePath, metadata.type);
151
+
146
152
  // Phase 5: 确认发布
147
153
  spinner.start('确认发布...');
148
154
  const result = await api.confirmPublish({
@@ -157,6 +163,7 @@ export class Publisher {
157
163
  description: metadata.description,
158
164
  tags: metadata.tags,
159
165
  visibility: options.visibility || 'self',
166
+ docContent,
160
167
  });
161
168
  spinner.succeed('发布成功');
162
169
 
@@ -234,4 +241,82 @@ export class Publisher {
234
241
  const session = await api.getSession();
235
242
  return session.user.username || session.user.name || 'unknown';
236
243
  }
244
+
245
+ /**
246
+ * 读取插件文档内容
247
+ * 优先级:类型对应文档 > README.md > 任意 .md 文件
248
+ */
249
+ private async readDocContent(sourcePath: string, pluginType: PluginType): Promise<string | undefined> {
250
+ const MAX_DOC_SIZE = 100 * 1024; // 100KB 限制(与 Pipeline 一致)
251
+
252
+ const stat = await fs.stat(sourcePath).catch(() => null);
253
+
254
+ // 单文件插件:文件本身就是文档
255
+ if (stat?.isFile() && sourcePath.endsWith('.md')) {
256
+ const content = await fs.readFile(sourcePath, 'utf-8');
257
+ if (content.length > MAX_DOC_SIZE) {
258
+ return content.slice(0, MAX_DOC_SIZE);
259
+ }
260
+ return content;
261
+ }
262
+
263
+ if (!stat?.isDirectory()) return undefined;
264
+
265
+ // 根据类型定义优先查找的文档名
266
+ const typeDocNames: Record<PluginType, string[]> = {
267
+ skill: ['SKILL.md', 'skill.md'],
268
+ agent: ['AGENT.md', 'agent.md'],
269
+ command: ['COMMAND.md', 'command.md'],
270
+ hook: ['HOOK.md', 'hook.md'],
271
+ mcp: ['MCP.md', 'mcp.md'],
272
+ };
273
+
274
+ const fallbackNames = ['README.md', 'readme.md'];
275
+
276
+ // 1. 优先匹配类型对应的文档
277
+ const primaryDocs = typeDocNames[pluginType] || [];
278
+ for (const name of primaryDocs) {
279
+ const filePath = path.join(sourcePath, name);
280
+ try {
281
+ const content = await fs.readFile(filePath, 'utf-8');
282
+ if (content.length > MAX_DOC_SIZE) {
283
+ return content.slice(0, MAX_DOC_SIZE);
284
+ }
285
+ return content;
286
+ } catch {
287
+ // 继续尝试下一个
288
+ }
289
+ }
290
+
291
+ // 2. Fallback 到 README
292
+ for (const name of fallbackNames) {
293
+ const filePath = path.join(sourcePath, name);
294
+ try {
295
+ const content = await fs.readFile(filePath, 'utf-8');
296
+ if (content.length > MAX_DOC_SIZE) {
297
+ return content.slice(0, MAX_DOC_SIZE);
298
+ }
299
+ return content;
300
+ } catch {
301
+ // 继续尝试下一个
302
+ }
303
+ }
304
+
305
+ // 3. 任意 .md 文件(最后手段)
306
+ try {
307
+ const entries = await fs.readdir(sourcePath, { withFileTypes: true });
308
+ const mdFile = entries.find(e => e.isFile() && e.name.endsWith('.md'));
309
+ if (mdFile) {
310
+ const content = await fs.readFile(path.join(sourcePath, mdFile.name), 'utf-8');
311
+ if (content.length > MAX_DOC_SIZE) {
312
+ return content.slice(0, MAX_DOC_SIZE);
313
+ }
314
+ return content;
315
+ }
316
+ } catch {
317
+ // 忽略错误
318
+ }
319
+
320
+ return undefined;
321
+ }
237
322
  }
package/src/types.ts CHANGED
@@ -340,6 +340,7 @@ export interface PublishConfirmRequest {
340
340
  description?: string;
341
341
  tags?: string[];
342
342
  visibility: 'self' | 'public';
343
+ docContent?: string;
343
344
  }
344
345
 
345
346
  export interface PublishConfirmResponse {
@@ -0,0 +1,343 @@
1
+ /**
2
+ * 版本更新检查模块
3
+ *
4
+ * 功能:
5
+ * - 启动时后台检查 npm registry 获取最新版本
6
+ * - 24 小时内只检查一次,每天最多提示一次
7
+ * - 3 秒超时,静默失败不影响正常使用
8
+ * - 检测安装方式(Homebrew/npm)显示对应升级命令
9
+ * - 支持 NO_UPDATE_CHECK=1 环境变量禁用
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import chalk from 'chalk';
15
+ import { config } from './config';
16
+
17
+ // ============================================================================
18
+ // 配置
19
+ // ============================================================================
20
+
21
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 小时
22
+ const NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 小时内只提示一次
23
+ const FETCH_TIMEOUT_MS = 3000; // 3 秒超时
24
+ const CACHE_FILE = path.join(config.dataDir, '.update-check');
25
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@42ailab/42plugin/latest';
26
+
27
+ // ============================================================================
28
+ // 类型
29
+ // ============================================================================
30
+
31
+ interface UpdateCache {
32
+ lastCheck: number; // 上次检查时间戳
33
+ latestVersion: string | null; // 最新版本
34
+ lastNotified: number; // 上次提示时间戳
35
+ }
36
+
37
+ type InstallSource = 'homebrew' | 'npm';
38
+
39
+ const DEFAULT_CACHE: UpdateCache = { lastCheck: 0, latestVersion: null, lastNotified: 0 };
40
+
41
+ // ============================================================================
42
+ // 缓存管理
43
+ // ============================================================================
44
+
45
+ function readCache(): UpdateCache {
46
+ try {
47
+ if (fs.existsSync(CACHE_FILE)) {
48
+ const content = fs.readFileSync(CACHE_FILE, 'utf-8');
49
+ const parsed = JSON.parse(content);
50
+ // 基本校验:确保必需字段存在
51
+ if (
52
+ typeof parsed.lastCheck === 'number' &&
53
+ typeof parsed.lastNotified === 'number'
54
+ ) {
55
+ return {
56
+ lastCheck: parsed.lastCheck,
57
+ latestVersion: parsed.latestVersion ?? null,
58
+ lastNotified: parsed.lastNotified,
59
+ };
60
+ }
61
+ }
62
+ } catch {
63
+ // 缓存读取/解析失败,返回默认值
64
+ // 可选:在 debug 模式下记录警告
65
+ if (config.debug) {
66
+ console.error(chalk.gray('[update-checker] 缓存读取失败,使用默认值'));
67
+ }
68
+ }
69
+ return { ...DEFAULT_CACHE };
70
+ }
71
+
72
+ function writeCache(cache: UpdateCache): void {
73
+ try {
74
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
75
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache));
76
+ } catch {
77
+ // 缓存写入失败,忽略
78
+ }
79
+ }
80
+
81
+ // ============================================================================
82
+ // 版本比较
83
+ // ============================================================================
84
+
85
+ /**
86
+ * 解析版本号,处理预发布版本
87
+ * 例如:'1.2.3-beta.1' -> { parts: [1, 2, 3], prerelease: 'beta.1' }
88
+ */
89
+ function parseVersion(v: string): { parts: number[]; prerelease: string | null } {
90
+ const cleaned = v.replace(/^v/, '');
91
+ const [main, prerelease] = cleaned.split('-', 2);
92
+ const parts = main.split('.').map((part) => {
93
+ const num = parseInt(part, 10);
94
+ return isNaN(num) ? 0 : num;
95
+ });
96
+ return { parts, prerelease: prerelease || null };
97
+ }
98
+
99
+ /**
100
+ * 比较两个版本号
101
+ * @returns -1 表示 a < b, 0 表示 a = b, 1 表示 a > b
102
+ *
103
+ * 规则:
104
+ * - 主版本号按数字比较
105
+ * - 预发布版本 < 正式版本 (1.0.0-beta < 1.0.0)
106
+ * - 预发布版本之间按字符串比较
107
+ */
108
+ function compareVersions(a: string, b: string): number {
109
+ const va = parseVersion(a);
110
+ const vb = parseVersion(b);
111
+
112
+ // 比较主版本号(动态长度)
113
+ const maxLen = Math.max(va.parts.length, vb.parts.length);
114
+ for (let i = 0; i < maxLen; i++) {
115
+ const na = va.parts[i] || 0;
116
+ const nb = vb.parts[i] || 0;
117
+ if (na < nb) return -1;
118
+ if (na > nb) return 1;
119
+ }
120
+
121
+ // 主版本号相同,比较预发布标识
122
+ // 有预发布标识 < 无预发布标识
123
+ if (va.prerelease && !vb.prerelease) return -1;
124
+ if (!va.prerelease && vb.prerelease) return 1;
125
+
126
+ // 都有预发布标识,按字符串比较
127
+ if (va.prerelease && vb.prerelease) {
128
+ return va.prerelease.localeCompare(vb.prerelease);
129
+ }
130
+
131
+ return 0;
132
+ }
133
+
134
+ // ============================================================================
135
+ // 安装方式检测
136
+ // ============================================================================
137
+
138
+ /**
139
+ * 检测 CLI 的安装来源
140
+ */
141
+ function detectInstallSource(): InstallSource {
142
+ const execPath = process.execPath;
143
+
144
+ // Homebrew 路径特征
145
+ // macOS ARM: /opt/homebrew/...
146
+ // macOS Intel: /usr/local/Cellar/...
147
+ // Linux: /home/linuxbrew/.linuxbrew/...
148
+ if (
149
+ execPath.includes('/opt/homebrew') ||
150
+ execPath.includes('/usr/local/Cellar') ||
151
+ execPath.includes('/linuxbrew')
152
+ ) {
153
+ return 'homebrew';
154
+ }
155
+
156
+ return 'npm';
157
+ }
158
+
159
+ /**
160
+ * 获取升级命令
161
+ */
162
+ function getUpgradeCommand(source: InstallSource): string {
163
+ switch (source) {
164
+ case 'homebrew':
165
+ return 'brew upgrade 42plugin';
166
+ case 'npm':
167
+ default:
168
+ // 使用 bun,性能更好
169
+ return 'bun update -g @42ailab/42plugin';
170
+ }
171
+ }
172
+
173
+ // ============================================================================
174
+ // 网络请求
175
+ // ============================================================================
176
+
177
+ /**
178
+ * 从 npm registry 获取最新版本
179
+ */
180
+ async function fetchLatestVersion(): Promise<string | null> {
181
+ try {
182
+ const response = await fetch(NPM_REGISTRY_URL, {
183
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
184
+ headers: {
185
+ Accept: 'application/json',
186
+ },
187
+ });
188
+
189
+ if (response.ok) {
190
+ const data = (await response.json()) as { version?: string };
191
+ return data.version || null;
192
+ }
193
+ } catch {
194
+ // 网络错误、超时等,静默失败
195
+ }
196
+ return null;
197
+ }
198
+
199
+ // ============================================================================
200
+ // 提示显示
201
+ // ============================================================================
202
+
203
+ /**
204
+ * 显示更新提示(简洁单行)
205
+ */
206
+ function showUpdateNotice(currentVersion: string, latestVersion: string): void {
207
+ const source = detectInstallSource();
208
+ const command = getUpgradeCommand(source);
209
+
210
+ console.log();
211
+ console.log(
212
+ chalk.yellow(`✨ 新版本 ${latestVersion} 可用`) +
213
+ chalk.gray(` (当前 ${currentVersion}) `) +
214
+ chalk.cyan(`升级: ${command}`)
215
+ );
216
+ }
217
+
218
+ /**
219
+ * 判断是否需要显示提示并显示
220
+ * @returns 是否显示了提示
221
+ */
222
+ function maybeShowNotice(
223
+ currentVersion: string,
224
+ latestVersion: string,
225
+ lastNotified: number,
226
+ now: number
227
+ ): boolean {
228
+ if (compareVersions(currentVersion, latestVersion) < 0) {
229
+ const needsNotify = now - lastNotified >= NOTIFY_INTERVAL_MS;
230
+ if (needsNotify) {
231
+ showUpdateNotice(currentVersion, latestVersion);
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
237
+
238
+ // ============================================================================
239
+ // 主入口
240
+ // ============================================================================
241
+
242
+ /**
243
+ * 检查更新(非阻塞)
244
+ *
245
+ * 在命令执行后调用,后台检查并在需要时显示提示
246
+ *
247
+ * @param currentVersion 当前版本号
248
+ */
249
+ export function checkForUpdates(currentVersion: string): void {
250
+ // 1. 检查是否禁用
251
+ if (process.env.NO_UPDATE_CHECK === '1') {
252
+ return;
253
+ }
254
+
255
+ // 2. 读取缓存
256
+ const cache = readCache();
257
+ const now = Date.now();
258
+
259
+ // 3. 判断是否需要检查
260
+ const needsCheck = now - cache.lastCheck >= CHECK_INTERVAL_MS;
261
+
262
+ if (needsCheck) {
263
+ // 后台检查,使用立即执行的异步函数
264
+ // 注意:这里不使用 setImmediate,因为 CLI 可能在回调执行前退出
265
+ // 改用 Promise,让 Node.js 事件循环保持活跃直到完成
266
+ (async () => {
267
+ try {
268
+ const latestVersion = await fetchLatestVersion();
269
+
270
+ // 重新读取缓存以避免竞态条件
271
+ const freshCache = readCache();
272
+ const freshNow = Date.now();
273
+
274
+ // 更新缓存
275
+ const newCache: UpdateCache = {
276
+ lastCheck: freshNow,
277
+ latestVersion: latestVersion ?? freshCache.latestVersion,
278
+ lastNotified: freshCache.lastNotified,
279
+ };
280
+
281
+ // 判断是否需要提示
282
+ if (latestVersion) {
283
+ const didNotify = maybeShowNotice(
284
+ currentVersion,
285
+ latestVersion,
286
+ freshCache.lastNotified,
287
+ freshNow
288
+ );
289
+ if (didNotify) {
290
+ newCache.lastNotified = freshNow;
291
+ }
292
+ }
293
+
294
+ writeCache(newCache);
295
+ } catch {
296
+ // 静默失败
297
+ }
298
+ })();
299
+ } else {
300
+ // 使用缓存的版本信息判断
301
+ if (cache.latestVersion) {
302
+ const didNotify = maybeShowNotice(
303
+ currentVersion,
304
+ cache.latestVersion,
305
+ cache.lastNotified,
306
+ now
307
+ );
308
+ if (didNotify) {
309
+ writeCache({ ...cache, lastNotified: now });
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 主动检查更新(阻塞,用于 version --check)
317
+ *
318
+ * @param currentVersion 当前版本号
319
+ * @returns 是否有新版本
320
+ */
321
+ export async function checkForUpdatesSync(currentVersion: string): Promise<boolean> {
322
+ const latestVersion = await fetchLatestVersion();
323
+
324
+ if (!latestVersion) {
325
+ console.log(chalk.gray('无法获取最新版本信息'));
326
+ return false;
327
+ }
328
+
329
+ // 更新缓存
330
+ writeCache({
331
+ lastCheck: Date.now(),
332
+ latestVersion,
333
+ lastNotified: Date.now(),
334
+ });
335
+
336
+ if (compareVersions(currentVersion, latestVersion) < 0) {
337
+ showUpdateNotice(currentVersion, latestVersion);
338
+ return true;
339
+ } else {
340
+ console.log(chalk.green('✓ 已是最新版本'));
341
+ return false;
342
+ }
343
+ }