@42ailab/42plugin 0.1.24 → 0.1.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@42ailab/42plugin",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -20,7 +20,7 @@ import {
20
20
  getSessionToken,
21
21
  } from '../db';
22
22
  import { config } from '../config';
23
- import { parseTarget, getInstallPath, formatBytes, getTypeIcon } from '../utils';
23
+ import { parseTarget, getInstallPath, formatBytes, getTypeIcon, normalizeInstallPath } from '../utils';
24
24
  import { TargetType, type PluginDownloadInfo } from '../types';
25
25
 
26
26
  export const installCommand = new Command('install')
@@ -152,7 +152,8 @@ async function installPlugin(
152
152
 
153
153
  // 确定安装路径
154
154
  const projectPath = options.global ? config.globalDir : process.cwd();
155
- const linkPath = path.join(projectPath, downloadInfo.installPath);
155
+ const normalizedPath = normalizeInstallPath(downloadInfo.installPath, !!options.global);
156
+ const linkPath = path.join(projectPath, normalizedPath);
156
157
 
157
158
  // 检查是否有同名冲突(不同插件安装到同一路径)
158
159
  const conflictPlugin = await checkInstallConflict(projectPath, linkPath, downloadInfo.fullName);
@@ -209,11 +210,11 @@ async function installPlugin(
209
210
 
210
211
  if (options.global) {
211
212
  console.log(chalk.green('✓ 已全局安装,对所有项目生效'));
212
- console.log(chalk.gray(` → ~/.claude/${downloadInfo.installPath}`));
213
+ console.log(chalk.gray(` → ~/.claude/${normalizedPath}`));
213
214
  console.log(chalk.gray(' 提示: 使用 42plugin list --global 查看全局插件'));
214
215
  } else {
215
216
  console.log(chalk.green(`✓ 已安装到当前项目: ${path.basename(projectPath)}`));
216
- console.log(chalk.gray(` → ${downloadInfo.installPath}`));
217
+ console.log(chalk.gray(` → ${normalizedPath}`));
217
218
  }
218
219
  } catch (error) {
219
220
  spinner.fail('安装失败');
@@ -260,11 +261,13 @@ async function installKit(
260
261
  const project = await getOrCreateProject(projectPath);
261
262
 
262
263
  // 第一阶段:检查所有冲突(并行)
264
+ const isGlobal = !!options.global;
263
265
  const conflictCheckSpinner = ora('检查安装冲突...').start();
264
266
  const conflictResults = await Promise.all(
265
267
  plugins.map(async (item) => {
266
268
  const downloadInfo = item.download;
267
- const linkPath = path.join(projectPath, downloadInfo.installPath);
269
+ const normalizedPath = normalizeInstallPath(downloadInfo.installPath, isGlobal);
270
+ const linkPath = path.join(projectPath, normalizedPath);
268
271
  const conflictPlugin = await checkInstallConflict(projectPath, linkPath, downloadInfo.fullName);
269
272
  return { item, linkPath, conflictPlugin };
270
273
  })
package/src/db.ts CHANGED
@@ -581,7 +581,43 @@ export async function downloadAndExtract(
581
581
  }
582
582
 
583
583
  // 解压
584
- await tar.extract({ file: tempFile, cwd: targetDir, strip: 0 });
584
+ if (isWindows) {
585
+ // Windows: 使用 tar.Parser 手动提取,解决 Bun 编译的 tar 包无法正确写入文件的问题
586
+ const fileBuffer = await fs.readFile(tempFile);
587
+ const { Parser } = await import('tar');
588
+ const parser = new Parser();
589
+ const extractPromises: Promise<void>[] = [];
590
+
591
+ parser.on('entry', (entry) => {
592
+ const entryPath = path.join(targetDir, entry.path);
593
+ if (entry.type === 'Directory') {
594
+ extractPromises.push(fs.mkdir(entryPath, { recursive: true }));
595
+ } else if (entry.type === 'File') {
596
+ const chunks: Buffer[] = [];
597
+ entry.on('data', (chunk: Buffer) => chunks.push(chunk));
598
+ entry.on('end', () => {
599
+ extractPromises.push(
600
+ (async () => {
601
+ await fs.mkdir(path.dirname(entryPath), { recursive: true });
602
+ await fs.writeFile(entryPath, Buffer.concat(chunks));
603
+ })()
604
+ );
605
+ });
606
+ }
607
+ entry.resume();
608
+ });
609
+
610
+ await new Promise<void>((resolve, reject) => {
611
+ parser.on('end', resolve);
612
+ parser.on('error', reject);
613
+ parser.write(fileBuffer);
614
+ parser.end();
615
+ });
616
+
617
+ await Promise.all(extractPromises);
618
+ } else {
619
+ await tar.extract({ file: tempFile, cwd: targetDir, strip: 0 });
620
+ }
585
621
 
586
622
  // 清理
587
623
  await fs.unlink(tempFile).catch(() => {});
@@ -796,7 +832,6 @@ export async function createLink(
796
832
  }
797
833
 
798
834
  // 回退方案 2: 直接复制文件/目录
799
- // 这是最后的保底方案,虽然会占用更多磁盘空间
800
835
  console.warn(
801
836
  `[Windows] 符号链接创建失败,回退到文件复制模式\n` +
802
837
  ` 提示: 启用 Windows 开发者模式可使用符号链接,节省磁盘空间`
package/src/utils.ts CHANGED
@@ -69,6 +69,28 @@ export function parseTarget(target: string): ParsedTarget {
69
69
  // 安装路径
70
70
  // ============================================================================
71
71
 
72
+ /**
73
+ * 规范化安装路径(用于全局安装)
74
+ *
75
+ * API 返回的 installPath 包含 .claude/ 前缀(如 .claude/skills/xxx/)
76
+ * 全局安装目录本身就是 ~/.claude,所以需要去掉前缀避免路径重复
77
+ *
78
+ * @param installPath API 返回的安装路径
79
+ * @param isGlobal 是否全局安装
80
+ * @returns 规范化后的路径
81
+ */
82
+ export function normalizeInstallPath(installPath: string, isGlobal: boolean): string {
83
+ if (!isGlobal) {
84
+ return installPath;
85
+ }
86
+ // 全局安装时去掉 .claude/ 前缀
87
+ const prefix = '.claude/';
88
+ if (installPath.startsWith(prefix)) {
89
+ return installPath.slice(prefix.length);
90
+ }
91
+ return installPath;
92
+ }
93
+
72
94
  /**
73
95
  * 获取插件安装路径(.claude/ 目录下)
74
96
  */