@42ailab/42plugin 0.1.20 → 0.1.21

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/db.ts +100 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@42ailab/42plugin",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "活水插件",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/db.ts CHANGED
@@ -8,13 +8,18 @@
8
8
  import { Database } from 'bun:sqlite';
9
9
  import fs from 'fs/promises';
10
10
  import path from 'path';
11
+ import os from 'os';
11
12
  import crypto from 'crypto';
12
13
  import * as tar from 'tar';
13
14
  import cliProgress from 'cli-progress';
15
+ import { spawn } from 'child_process';
14
16
  import { config } from './config';
15
17
  import { formatBytes } from './utils';
16
18
  import type { LocalProject, LocalCache, LocalInstallation, PluginType, PluginDownloadInfo } from './types';
17
19
 
20
+ // Windows 平台检测
21
+ const isWindows = process.platform === 'win32';
22
+
18
23
  // 文件大小超过 1MB 时显示下载进度条
19
24
  const PROGRESS_THRESHOLD = 1024 * 1024; // 1MB
20
25
 
@@ -386,8 +391,8 @@ export async function checkInstallConflict(
386
391
  ): Promise<string | null> {
387
392
  const client = await getDb();
388
393
  const absPath = path.resolve(projectPath);
389
- // 规范化路径:去掉尾部斜杠以便比较
390
- const normalizedLinkPath = path.resolve(linkPath).replace(/\/+$/, '');
394
+ // 规范化路径:去掉尾部斜杠以便比较(兼容 Unix 和 Windows)
395
+ const normalizedLinkPath = path.resolve(linkPath).replace(/[\/\\]+$/, '');
391
396
 
392
397
  // 查询该项目下所有安装记录,在应用层比较路径
393
398
  const rows = client.prepare(`
@@ -397,7 +402,7 @@ export async function checkInstallConflict(
397
402
  `).all(absPath, newFullName) as { full_name: string; link_path: string }[];
398
403
 
399
404
  for (const row of rows) {
400
- const existingPath = row.link_path.replace(/\/+$/, '');
405
+ const existingPath = row.link_path.replace(/[\/\\]+$/, '');
401
406
  if (existingPath === normalizedLinkPath) {
402
407
  return row.full_name;
403
408
  }
@@ -659,12 +664,62 @@ export async function getDirectorySize(pathOrDir: string): Promise<number> {
659
664
  return totalSize;
660
665
  }
661
666
 
667
+ /**
668
+ * 递归复制目录或文件
669
+ */
670
+ async function copyRecursive(src: string, dest: string): Promise<void> {
671
+ const stat = await fs.stat(src);
672
+ if (stat.isDirectory()) {
673
+ await fs.mkdir(dest, { recursive: true });
674
+ const entries = await fs.readdir(src, { withFileTypes: true });
675
+ for (const entry of entries) {
676
+ const srcPath = path.join(src, entry.name);
677
+ const destPath = path.join(dest, entry.name);
678
+ if (entry.isDirectory()) {
679
+ await copyRecursive(srcPath, destPath);
680
+ } else {
681
+ await fs.copyFile(srcPath, destPath);
682
+ }
683
+ }
684
+ } else {
685
+ await fs.copyFile(src, dest);
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Windows 上使用 mklink /J 创建 Junction(目录链接)
691
+ * Junction 不需要管理员权限
692
+ */
693
+ async function createJunction(sourcePath: string, targetPath: string): Promise<void> {
694
+ return new Promise((resolve, reject) => {
695
+ // mklink /J 创建目录 junction
696
+ const cmd = spawn('cmd', ['/c', 'mklink', '/J', targetPath, sourcePath], {
697
+ windowsHide: true,
698
+ });
699
+
700
+ let stderr = '';
701
+ cmd.stderr.on('data', (data) => {
702
+ stderr += data.toString();
703
+ });
704
+
705
+ cmd.on('close', (code) => {
706
+ if (code === 0) {
707
+ resolve();
708
+ } else {
709
+ reject(new Error(`创建 Junction 失败: ${stderr}`));
710
+ }
711
+ });
712
+
713
+ cmd.on('error', reject);
714
+ });
715
+ }
716
+
662
717
  export async function createLink(
663
718
  sourcePath: string,
664
719
  targetPath: string
665
720
  ): Promise<void> {
666
- // 移除目标路径末尾的斜杠
667
- const normalizedTarget = targetPath.replace(/\/+$/, '');
721
+ // 移除目标路径末尾的斜杠(兼容 Unix 和 Windows)
722
+ const normalizedTarget = targetPath.replace(/[\/\\]+$/, '');
668
723
  const targetDir = path.dirname(normalizedTarget);
669
724
  await fs.mkdir(targetDir, { recursive: true });
670
725
 
@@ -678,15 +733,50 @@ export async function createLink(
678
733
  // 不存在,继续
679
734
  }
680
735
 
681
- // 检测源是文件还是目录,创建对应类型的符号链接
736
+ // 检测源是文件还是目录
682
737
  const sourceStat = await fs.stat(sourcePath);
683
- const linkType = sourceStat.isDirectory() ? 'dir' : 'file';
684
- await fs.symlink(sourcePath, normalizedTarget, linkType);
738
+ const isDirectory = sourceStat.isDirectory();
739
+
740
+ // 尝试创建符号链接
741
+ try {
742
+ const linkType = isDirectory ? 'dir' : 'file';
743
+ await fs.symlink(sourcePath, normalizedTarget, linkType);
744
+ return;
745
+ } catch (error) {
746
+ // 如果不是 Windows,直接抛出错误
747
+ if (!isWindows) {
748
+ throw error;
749
+ }
750
+
751
+ // Windows 上符号链接失败(EPERM),尝试回退方案
752
+ const isEperm = (error as NodeJS.ErrnoException).code === 'EPERM';
753
+ if (!isEperm) {
754
+ throw error;
755
+ }
756
+
757
+ // 回退方案 1: 对于目录,尝试使用 Junction
758
+ if (isDirectory) {
759
+ try {
760
+ await createJunction(sourcePath, normalizedTarget);
761
+ return;
762
+ } catch {
763
+ // Junction 也失败,继续尝试复制
764
+ }
765
+ }
766
+
767
+ // 回退方案 2: 直接复制文件/目录
768
+ // 这是最后的保底方案,虽然会占用更多磁盘空间
769
+ console.warn(
770
+ `[Windows] 符号链接创建失败,回退到文件复制模式\n` +
771
+ ` 提示: 启用 Windows 开发者模式可使用符号链接,节省磁盘空间`
772
+ );
773
+ await copyRecursive(sourcePath, normalizedTarget);
774
+ }
685
775
  }
686
776
 
687
777
  export async function removeLink(linkPath: string): Promise<void> {
688
- // 去掉结尾斜杠,否则 lstat 会跟踪符号链接返回目标状态
689
- const normalizedPath = linkPath.replace(/\/+$/, '');
778
+ // 去掉结尾斜杠,否则 lstat 会跟踪符号链接返回目标状态(兼容 Unix 和 Windows)
779
+ const normalizedPath = linkPath.replace(/[\/\\]+$/, '');
690
780
  try {
691
781
  const stat = await fs.lstat(normalizedPath);
692
782
  if (stat.isSymbolicLink()) {