@42ailab/42plugin 0.1.23 → 0.1.25
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 +1 -1
- package/src/commands/install.ts +8 -5
- package/src/db.ts +45 -2
- package/src/utils.ts +22 -0
package/package.json
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -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
|
|
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/${
|
|
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(` → ${
|
|
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
|
|
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
|
@@ -583,11 +583,22 @@ export async function downloadAndExtract(
|
|
|
583
583
|
// 解压
|
|
584
584
|
await tar.extract({ file: tempFile, cwd: targetDir, strip: 0 });
|
|
585
585
|
|
|
586
|
+
if (config.debug) {
|
|
587
|
+
const entries = await fs.readdir(targetDir);
|
|
588
|
+
console.log(`[DEBUG] 解压完成,目录内容: ${entries.join(', ')}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
586
591
|
// 清理
|
|
587
592
|
await fs.unlink(tempFile).catch(() => {});
|
|
588
593
|
|
|
589
594
|
// 返回最终路径(如果只有一个子目录则进入)
|
|
590
|
-
|
|
595
|
+
const finalPath = await resolveFinalPath(targetDir, pluginType);
|
|
596
|
+
if (config.debug) {
|
|
597
|
+
console.log(`[DEBUG] 最终缓存路径: ${finalPath}`);
|
|
598
|
+
const finalEntries = await fs.readdir(finalPath).catch(() => []);
|
|
599
|
+
console.log(`[DEBUG] 最终路径内容: ${finalEntries.join(', ') || '(空)'}`);
|
|
600
|
+
}
|
|
601
|
+
return finalPath;
|
|
591
602
|
} catch (error) {
|
|
592
603
|
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
|
|
593
604
|
throw error;
|
|
@@ -754,6 +765,17 @@ export async function createLink(
|
|
|
754
765
|
const targetDir = path.dirname(normalizedTarget);
|
|
755
766
|
await fs.mkdir(targetDir, { recursive: true });
|
|
756
767
|
|
|
768
|
+
if (config.debug) {
|
|
769
|
+
console.log(`[DEBUG] createLink: ${sourcePath} -> ${normalizedTarget}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 验证源路径存在
|
|
773
|
+
try {
|
|
774
|
+
await fs.access(sourcePath);
|
|
775
|
+
} catch {
|
|
776
|
+
throw new Error(`源路径不存在: ${sourcePath}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
757
779
|
// 移除已存在的链接
|
|
758
780
|
try {
|
|
759
781
|
const stat = await fs.lstat(normalizedTarget);
|
|
@@ -768,6 +790,11 @@ export async function createLink(
|
|
|
768
790
|
const sourceStat = await fs.stat(sourcePath);
|
|
769
791
|
const isDirectory = sourceStat.isDirectory();
|
|
770
792
|
|
|
793
|
+
if (config.debug && isDirectory) {
|
|
794
|
+
const entries = await fs.readdir(sourcePath);
|
|
795
|
+
console.log(`[DEBUG] 源目录内容 (${entries.length} 项): ${entries.join(', ')}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
771
798
|
// 尝试创建符号链接
|
|
772
799
|
try {
|
|
773
800
|
const linkType = isDirectory ? 'dir' : 'file';
|
|
@@ -788,9 +815,18 @@ export async function createLink(
|
|
|
788
815
|
// 回退方案 1: 对于目录,尝试使用 Junction
|
|
789
816
|
if (isDirectory) {
|
|
790
817
|
try {
|
|
818
|
+
if (config.debug) {
|
|
819
|
+
console.log(`[DEBUG] 尝试创建 Junction: ${normalizedTarget} -> ${sourcePath}`);
|
|
820
|
+
}
|
|
791
821
|
await createJunction(sourcePath, normalizedTarget);
|
|
822
|
+
if (config.debug) {
|
|
823
|
+
console.log(`[DEBUG] Junction 创建成功`);
|
|
824
|
+
}
|
|
792
825
|
return;
|
|
793
|
-
} catch {
|
|
826
|
+
} catch (junctionError) {
|
|
827
|
+
if (config.debug) {
|
|
828
|
+
console.log(`[DEBUG] Junction 创建失败: ${(junctionError as Error).message}`);
|
|
829
|
+
}
|
|
794
830
|
// Junction 也失败,继续尝试复制
|
|
795
831
|
}
|
|
796
832
|
}
|
|
@@ -801,7 +837,14 @@ export async function createLink(
|
|
|
801
837
|
`[Windows] 符号链接创建失败,回退到文件复制模式\n` +
|
|
802
838
|
` 提示: 启用 Windows 开发者模式可使用符号链接,节省磁盘空间`
|
|
803
839
|
);
|
|
840
|
+
if (config.debug) {
|
|
841
|
+
console.log(`[DEBUG] 开始复制: ${sourcePath} -> ${normalizedTarget}`);
|
|
842
|
+
}
|
|
804
843
|
await copyRecursive(sourcePath, normalizedTarget);
|
|
844
|
+
if (config.debug) {
|
|
845
|
+
const destEntries = await fs.readdir(normalizedTarget).catch(() => []);
|
|
846
|
+
console.log(`[DEBUG] 复制完成,目标目录内容: ${destEntries.join(', ') || '(空)'}`);
|
|
847
|
+
}
|
|
805
848
|
}
|
|
806
849
|
}
|
|
807
850
|
|
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
|
*/
|