joyskills-cli 0.2.9 → 0.3.0
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 +10 -3
- package/src/agents.js +198 -0
- package/src/commands/check.js +69 -0
- package/src/commands/install.js +106 -22
- package/src/commands/list.js +66 -124
- package/src/commands/remove.js +12 -2
- package/src/commands/sync.js +10 -4
- package/src/commands/update.js +114 -0
- package/src/commands/upgrade.js +7 -4
- package/src/index.js +4 -0
- package/src/installer.js +190 -0
- package/src/skill-loader.js +207 -0
- package/src/version-checker.js +129 -0
package/src/commands/list.js
CHANGED
|
@@ -1,149 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LockfileManager } from '../lockfile.js';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import * as os from 'os';
|
|
1
|
+
import { SkillLoader } from '../skill-loader.js';
|
|
6
2
|
import chalk from 'chalk';
|
|
7
3
|
|
|
8
|
-
/** Scan all standard skill directories and return unique skill names */
|
|
9
|
-
function scanAllSkillDirs(projectRoot) {
|
|
10
|
-
const dirs = [
|
|
11
|
-
path.join(projectRoot, '.agent', 'skills'),
|
|
12
|
-
path.join(projectRoot, '.claude', 'skills'),
|
|
13
|
-
path.join(os.homedir(), '.agent', 'skills'),
|
|
14
|
-
path.join(os.homedir(), '.claude', 'skills'),
|
|
15
|
-
path.join(projectRoot, 'skills'), // legacy
|
|
16
|
-
];
|
|
17
|
-
const seen = new Set();
|
|
18
|
-
const skills = [];
|
|
19
|
-
for (const dir of dirs) {
|
|
20
|
-
if (!fs.existsSync(dir)) continue;
|
|
21
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
-
if (entry.isDirectory() && !seen.has(entry.name)) {
|
|
23
|
-
seen.add(entry.name);
|
|
24
|
-
const skillMd = path.join(dir, entry.name, 'SKILL.md');
|
|
25
|
-
let description = '', version = '';
|
|
26
|
-
if (fs.existsSync(skillMd)) {
|
|
27
|
-
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
28
|
-
description = (content.match(/description:\s*(.+)/) || [])[1]?.trim() || '';
|
|
29
|
-
version = (content.match(/version:\s*(.+)/) || [])[1]?.trim() || '';
|
|
30
|
-
}
|
|
31
|
-
skills.push({ name: entry.name, dir, description, version });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return skills;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
4
|
export function listCommand(program) {
|
|
39
5
|
program
|
|
40
6
|
.command('list')
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.option('-
|
|
7
|
+
.alias('ls')
|
|
8
|
+
.description('List installed skills')
|
|
9
|
+
.option('-a, --agent <agent>', 'Filter by agent (joycode, claude-code, cursor, etc.)')
|
|
10
|
+
.option('-g, --global', 'Show only global (user-level) skills')
|
|
11
|
+
.option('-p, --project', 'Show only project-level skills')
|
|
44
12
|
.option('-s, --search <query>', 'Search skills by name or description')
|
|
45
|
-
.option('
|
|
46
|
-
.option('-l, --local', 'Show only local skills')
|
|
47
|
-
.option('-r, --registry <name>', 'Show skills from specific registry')
|
|
13
|
+
.option('--stats', 'Show statistics')
|
|
48
14
|
.action(async (options) => {
|
|
49
15
|
const projectRoot = process.cwd();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
console.log(chalk.blue('Available Skills:'));
|
|
53
|
-
console.log(chalk.gray('================='));
|
|
16
|
+
const loader = new SkillLoader(projectRoot);
|
|
54
17
|
|
|
55
18
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(chalk.blue('\nInstalled Skills:'));
|
|
65
|
-
let filtered = localSkills;
|
|
66
|
-
if (options.search) {
|
|
67
|
-
const q = options.search.toLowerCase();
|
|
68
|
-
filtered = filtered.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
|
|
69
|
-
}
|
|
70
|
-
if (filtered.length > 0) {
|
|
71
|
-
filtered.forEach(skill => {
|
|
72
|
-
const lock = installedSkills[skill.name];
|
|
73
|
-
const ver = lock ? chalk.gray(`v${lock.version}`) : (skill.version ? chalk.gray(`v${skill.version}`) : '');
|
|
74
|
-
const src = lock ? chalk.gray(`[${lock.source}]`) : '';
|
|
75
|
-
console.log(` ✅ ${chalk.bold(skill.name)} ${ver} ${src}`);
|
|
76
|
-
if (skill.description) console.log(` ${chalk.gray(skill.description)}`);
|
|
77
|
-
});
|
|
78
|
-
} else {
|
|
79
|
-
console.log(' No matching skills found.');
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
console.log(chalk.yellow('\n No skills installed.'));
|
|
83
|
-
console.log(chalk.gray(' Run: joySkills install <skill>'));
|
|
84
|
-
}
|
|
19
|
+
// 加载 skills
|
|
20
|
+
let skills = [];
|
|
21
|
+
if (options.global) {
|
|
22
|
+
skills = loader.loadGlobalSkills(options.agent);
|
|
23
|
+
} else if (options.project) {
|
|
24
|
+
skills = loader.loadProjectSkills(options.agent);
|
|
25
|
+
} else {
|
|
26
|
+
skills = loader.loadAllSkills(options.agent);
|
|
85
27
|
}
|
|
86
28
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
29
|
+
// 搜索过滤
|
|
30
|
+
if (options.search) {
|
|
31
|
+
const q = options.search.toLowerCase();
|
|
32
|
+
skills = skills.filter(s =>
|
|
33
|
+
s.name.toLowerCase().includes(q) ||
|
|
34
|
+
s.description.toLowerCase().includes(q)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
93
37
|
|
|
94
|
-
|
|
38
|
+
// 统计模式
|
|
39
|
+
if (options.stats) {
|
|
40
|
+
const stats = loader.getStats(options.agent);
|
|
41
|
+
console.log(chalk.blue('\nSkill Statistics:'));
|
|
42
|
+
console.log(chalk.gray('=================='));
|
|
43
|
+
console.log(` Total: ${stats.total}`);
|
|
44
|
+
console.log(` Project-level: ${stats.project}`);
|
|
45
|
+
console.log(` Global: ${stats.global}`);
|
|
46
|
+
console.log(` Agents: ${stats.agents.join(', ')}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
95
49
|
|
|
96
|
-
|
|
50
|
+
// 显示 skills
|
|
51
|
+
console.log(chalk.blue('\nInstalled Skills:'));
|
|
52
|
+
console.log(chalk.gray('=================='));
|
|
97
53
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
54
|
+
if (skills.length === 0) {
|
|
55
|
+
console.log(chalk.yellow(' No skills found.'));
|
|
56
|
+
console.log(chalk.gray(' Run: joySkills install <skill>'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
102
59
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
60
|
+
// 按 scope 分组显示
|
|
61
|
+
const projectSkills = skills.filter(s => s.scope === 'project');
|
|
62
|
+
const globalSkills = skills.filter(s => s.scope === 'global');
|
|
106
63
|
|
|
107
|
-
|
|
108
|
-
|
|
64
|
+
if (projectSkills.length > 0 && !options.global) {
|
|
65
|
+
console.log(chalk.cyan('\n 📁 Project-level:'));
|
|
66
|
+
projectSkills.forEach(skill => {
|
|
67
|
+
const scopeLabel = skill.agent === 'joycode' ? '' : chalk.gray(`[${skill.agent}]`);
|
|
68
|
+
console.log(` ✅ ${chalk.bold(skill.name)} ${chalk.gray(`v${skill.version}`)} ${scopeLabel}`);
|
|
69
|
+
if (skill.description) {
|
|
70
|
+
console.log(` ${chalk.gray(skill.description)}`);
|
|
109
71
|
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
110
74
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const recommendedVersion = recommended ? recommended.version : 'N/A';
|
|
119
|
-
|
|
120
|
-
console.log(` ${isInstalled} ${skill.name || skill.id} (${skill.id})`);
|
|
121
|
-
if (options.allVersions) {
|
|
122
|
-
skill.versions.forEach(v => {
|
|
123
|
-
const marker = v.recommended ? '⭐' : ' ';
|
|
124
|
-
const state = v.state === 'approved' ? '✓' : v.state;
|
|
125
|
-
console.log(` ${marker} v${v.version} [${state}]`);
|
|
126
|
-
});
|
|
127
|
-
} else {
|
|
128
|
-
console.log(` Recommended: v${recommendedVersion}`);
|
|
129
|
-
if (installedVersion) {
|
|
130
|
-
console.log(` Installed: v${installedVersion}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (skill.description) {
|
|
134
|
-
console.log(` ${skill.description}`);
|
|
135
|
-
}
|
|
136
|
-
console.log('');
|
|
137
|
-
});
|
|
138
|
-
} else {
|
|
139
|
-
console.log(' No matching registry skills found.');
|
|
75
|
+
if (globalSkills.length > 0 && !options.project) {
|
|
76
|
+
console.log(chalk.cyan('\n 🏠 Global (user-level):'));
|
|
77
|
+
globalSkills.forEach(skill => {
|
|
78
|
+
const scopeLabel = skill.agent === 'joycode' ? '' : chalk.gray(`[${skill.agent}]`);
|
|
79
|
+
console.log(` ✅ ${chalk.bold(skill.name)} ${chalk.gray(`v${skill.version}`)} ${scopeLabel}`);
|
|
80
|
+
if (skill.description) {
|
|
81
|
+
console.log(` ${chalk.gray(skill.description)}`);
|
|
140
82
|
}
|
|
141
|
-
}
|
|
142
|
-
console.error('Error loading registry:', error.message);
|
|
143
|
-
}
|
|
83
|
+
});
|
|
144
84
|
}
|
|
85
|
+
|
|
86
|
+
console.log(chalk.gray(`\n Total: ${skills.length} skill(s)`));
|
|
145
87
|
} catch (error) {
|
|
146
|
-
console.error('Error listing skills:', error);
|
|
88
|
+
console.error(chalk.red('Error listing skills:'), error.message);
|
|
147
89
|
}
|
|
148
90
|
});
|
|
149
91
|
}
|
package/src/commands/remove.js
CHANGED
|
@@ -5,12 +5,21 @@ import * as os from 'os';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
7
|
function findSkillOnDisk(skillName, projectRoot) {
|
|
8
|
+
// v2.0: Support all agent directories, joycode first
|
|
8
9
|
const dirs = [
|
|
10
|
+
// Project-level
|
|
11
|
+
path.join(projectRoot, '.joycode', 'skills'),
|
|
9
12
|
path.join(projectRoot, '.agent', 'skills'),
|
|
10
13
|
path.join(projectRoot, '.claude', 'skills'),
|
|
14
|
+
path.join(projectRoot, '.cursor', 'skills'),
|
|
15
|
+
path.join(projectRoot, '.qoder', 'skills'),
|
|
16
|
+
path.join(projectRoot, 'skills'),
|
|
17
|
+
// Global
|
|
18
|
+
path.join(os.homedir(), '.joycode', 'skills'),
|
|
11
19
|
path.join(os.homedir(), '.agent', 'skills'),
|
|
12
20
|
path.join(os.homedir(), '.claude', 'skills'),
|
|
13
|
-
path.join(
|
|
21
|
+
path.join(os.homedir(), '.cursor', 'skills'),
|
|
22
|
+
path.join(os.homedir(), '.qoder', 'skills'),
|
|
14
23
|
];
|
|
15
24
|
for (const d of dirs) {
|
|
16
25
|
const p = path.join(d, skillName);
|
|
@@ -23,7 +32,8 @@ export function removeCommand(program) {
|
|
|
23
32
|
program
|
|
24
33
|
.command('remove <skill>')
|
|
25
34
|
.description('Remove a skill')
|
|
26
|
-
.
|
|
35
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
36
|
+
.action(async (skillName, options) => {
|
|
27
37
|
const projectRoot = process.cwd();
|
|
28
38
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
29
39
|
|
package/src/commands/sync.js
CHANGED
|
@@ -20,12 +20,18 @@ export function syncCommand(program) {
|
|
|
20
20
|
|
|
21
21
|
console.log(chalk.blue('🔍 Scanning skills...'));
|
|
22
22
|
|
|
23
|
-
// Scan
|
|
23
|
+
// Scan project-level skill directories only (v2.0)
|
|
24
|
+
// AGENTS.md should only reference project-level skills
|
|
24
25
|
const skillDirs = [
|
|
26
|
+
// JoyCode (default, highest priority)
|
|
27
|
+
{ path: path.join(projectRoot, '.joycode', 'skills'), label: 'joycode' },
|
|
28
|
+
// Universal (multi-agent compatible)
|
|
25
29
|
{ path: path.join(projectRoot, '.agent', 'skills'), label: 'universal' },
|
|
26
|
-
|
|
27
|
-
{ path: path.join(
|
|
28
|
-
|
|
30
|
+
// Claude Code
|
|
31
|
+
{ path: path.join(projectRoot, '.claude', 'skills'), label: 'claude' },
|
|
32
|
+
// Other agents
|
|
33
|
+
{ path: path.join(projectRoot, '.cursor', 'skills'), label: 'cursor' },
|
|
34
|
+
{ path: path.join(projectRoot, '.qoder', 'skills'), label: 'qoder' },
|
|
29
35
|
// legacy
|
|
30
36
|
{ path: path.join(projectRoot, 'skills'), label: 'legacy' },
|
|
31
37
|
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command - 更新技能
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { getUpdatableSkills } from '../version-checker.js';
|
|
7
|
+
import { SkillLoader } from '../skill-loader.js';
|
|
8
|
+
import simpleGit from 'simple-git';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { confirm } from '@inquirer/prompts';
|
|
11
|
+
|
|
12
|
+
export function updateCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('update [skills...]')
|
|
15
|
+
.description('Update installed skills to latest versions')
|
|
16
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
17
|
+
.option('-a, --all', 'Update all skills')
|
|
18
|
+
.action(async (skillNames, options) => {
|
|
19
|
+
try {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
|
|
22
|
+
console.log(chalk.blue('Updating skills...'));
|
|
23
|
+
console.log(chalk.gray('==================\n'));
|
|
24
|
+
|
|
25
|
+
// 获取要更新的 skills
|
|
26
|
+
let skillsToUpdate = [];
|
|
27
|
+
|
|
28
|
+
if (options.all || skillNames.length === 0) {
|
|
29
|
+
// 更新所有有更新的 skills
|
|
30
|
+
const updatable = await getUpdatableSkills(projectRoot);
|
|
31
|
+
skillsToUpdate = updatable;
|
|
32
|
+
} else {
|
|
33
|
+
// 更新指定的 skills
|
|
34
|
+
const loader = new SkillLoader(projectRoot);
|
|
35
|
+
for (const name of skillNames) {
|
|
36
|
+
const skill = loader.getSkill(name);
|
|
37
|
+
if (skill) {
|
|
38
|
+
const git = simpleGit(skill.path);
|
|
39
|
+
const isRepo = await git.checkIsRepo();
|
|
40
|
+
if (isRepo) {
|
|
41
|
+
skillsToUpdate.push({
|
|
42
|
+
name: skill.name,
|
|
43
|
+
path: skill.path,
|
|
44
|
+
currentVersion: skill.version,
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
console.log(chalk.yellow(`⚠️ ${name} is not a git repository, skipping`));
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.red(`❌ Skill not found: ${name}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (skillsToUpdate.length === 0) {
|
|
56
|
+
console.log(chalk.green('✅ No skills to update.'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 显示将要更新的 skills
|
|
61
|
+
console.log(chalk.cyan('Skills to update:'));
|
|
62
|
+
for (const skill of skillsToUpdate) {
|
|
63
|
+
console.log(` 📦 ${skill.name} (v${skill.currentVersion})`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
|
|
67
|
+
// 确认
|
|
68
|
+
if (!options.yes) {
|
|
69
|
+
const shouldUpdate = await confirm({
|
|
70
|
+
message: `Update ${skillsToUpdate.length} skill(s)?`,
|
|
71
|
+
default: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!shouldUpdate) {
|
|
75
|
+
console.log(chalk.gray('Update cancelled.'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 执行更新
|
|
81
|
+
console.log(chalk.blue('\nUpdating...\n'));
|
|
82
|
+
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const skill of skillsToUpdate) {
|
|
85
|
+
try {
|
|
86
|
+
const git = simpleGit(skill.path);
|
|
87
|
+
|
|
88
|
+
// 拉取最新代码
|
|
89
|
+
await git.pull('origin', 'main', ['--depth', '1']);
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green(`✅ ${skill.name} updated successfully`));
|
|
92
|
+
results.push({ name: skill.name, success: true });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.log(chalk.red(`❌ ${skill.name} failed: ${e.message}`));
|
|
95
|
+
results.push({ name: skill.name, success: false, error: e.message });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 总结
|
|
100
|
+
const successCount = results.filter(r => r.success).length;
|
|
101
|
+
const failCount = results.length - successCount;
|
|
102
|
+
|
|
103
|
+
console.log(chalk.gray('\n=================='));
|
|
104
|
+
console.log(chalk.green(`✅ Updated: ${successCount}`));
|
|
105
|
+
if (failCount > 0) {
|
|
106
|
+
console.log(chalk.red(`❌ Failed: ${failCount}`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(chalk.red('❌ Failed to update:'), error.message);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -269,9 +269,12 @@ async function upgradeFromGitHub(skill, skillsDir) {
|
|
|
269
269
|
const git = simpleGit(cachePath);
|
|
270
270
|
await git.pull();
|
|
271
271
|
|
|
272
|
-
// Find skill in cache
|
|
273
|
-
const
|
|
274
|
-
|
|
272
|
+
// Find skill in cache (support nested directories)
|
|
273
|
+
const { findSkills } = await import('../commands/install.js');
|
|
274
|
+
const skills = await findSkills(cachePath);
|
|
275
|
+
const skillInfo = skills.find(s => s.name === skill.name);
|
|
276
|
+
|
|
277
|
+
if (!skillInfo) {
|
|
275
278
|
throw new Error(`Skill not found in repository: ${skill.name}`);
|
|
276
279
|
}
|
|
277
280
|
|
|
@@ -282,7 +285,7 @@ async function upgradeFromGitHub(skill, skillsDir) {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
// Copy new version
|
|
285
|
-
copyRecursive(
|
|
288
|
+
copyRecursive(skillInfo.path, targetPath);
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
async function upgradeFromRegistry(skill, skillsDir) {
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { syncCommand } from './commands/sync.js';
|
|
|
14
14
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
15
15
|
import { readCommand } from './commands/read.js';
|
|
16
16
|
import { manageCommand } from './commands/manage.js';
|
|
17
|
+
import { checkCommand } from './commands/check.js';
|
|
18
|
+
import { updateCommand } from './commands/update.js';
|
|
17
19
|
|
|
18
20
|
const program = new Command();
|
|
19
21
|
|
|
@@ -33,6 +35,8 @@ syncCommand(program);
|
|
|
33
35
|
upgradeCommand(program);
|
|
34
36
|
readCommand(program);
|
|
35
37
|
manageCommand(program);
|
|
38
|
+
checkCommand(program);
|
|
39
|
+
updateCommand(program);
|
|
36
40
|
|
|
37
41
|
// Parse arguments
|
|
38
42
|
program.parse();
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer - Skill 安装器
|
|
3
|
+
* 支持 Symlink 和 Copy 两种模式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 安装方式枚举
|
|
12
|
+
*/
|
|
13
|
+
export const InstallMethod = {
|
|
14
|
+
SYMLINK: 'symlink', // 符号链接(默认,单点更新)
|
|
15
|
+
COPY: 'copy', // 复制文件
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 检查是否支持符号链接
|
|
20
|
+
*/
|
|
21
|
+
export function supportsSymlink(targetDir) {
|
|
22
|
+
try {
|
|
23
|
+
// 尝试创建一个测试符号链接
|
|
24
|
+
const testLink = path.join(targetDir, '.symlink-test');
|
|
25
|
+
const testTarget = path.join(targetDir, '.symlink-target');
|
|
26
|
+
|
|
27
|
+
// 清理旧的测试文件
|
|
28
|
+
if (fs.existsSync(testLink)) fs.unlinkSync(testLink);
|
|
29
|
+
if (fs.existsSync(testTarget)) fs.unlinkSync(testTarget);
|
|
30
|
+
|
|
31
|
+
// 创建测试目标文件
|
|
32
|
+
fs.writeFileSync(testTarget, 'test');
|
|
33
|
+
|
|
34
|
+
// 尝试创建符号链接
|
|
35
|
+
fs.symlinkSync(testTarget, testLink);
|
|
36
|
+
|
|
37
|
+
// 清理
|
|
38
|
+
fs.unlinkSync(testLink);
|
|
39
|
+
fs.unlinkSync(testTarget);
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 复制目录(递归)
|
|
49
|
+
*/
|
|
50
|
+
export function copyRecursive(src, dest) {
|
|
51
|
+
// 如果目标已存在,先删除
|
|
52
|
+
if (fs.existsSync(dest)) {
|
|
53
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stat = fs.statSync(src);
|
|
57
|
+
|
|
58
|
+
if (stat.isDirectory()) {
|
|
59
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
60
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const srcPath = path.join(src, entry.name);
|
|
64
|
+
const destPath = path.join(dest, entry.name);
|
|
65
|
+
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
copyRecursive(srcPath, destPath);
|
|
68
|
+
} else {
|
|
69
|
+
fs.copyFileSync(srcPath, destPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
fs.copyFileSync(src, dest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 创建符号链接(跨平台)
|
|
79
|
+
*/
|
|
80
|
+
export function createSymlink(target, linkPath) {
|
|
81
|
+
// 确保目标存在
|
|
82
|
+
if (!fs.existsSync(target)) {
|
|
83
|
+
throw new Error(`Target does not exist: ${target}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 如果链接已存在,先删除
|
|
87
|
+
if (fs.existsSync(linkPath)) {
|
|
88
|
+
const stat = fs.lstatSync(linkPath);
|
|
89
|
+
if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) {
|
|
90
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 确保父目录存在
|
|
95
|
+
const parentDir = path.dirname(linkPath);
|
|
96
|
+
if (!fs.existsSync(parentDir)) {
|
|
97
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 创建符号链接
|
|
101
|
+
const isDir = fs.statSync(target).isDirectory();
|
|
102
|
+
fs.symlinkSync(target, linkPath, isDir ? 'dir' : 'file');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 安装 Skill
|
|
107
|
+
*/
|
|
108
|
+
export function installSkill(sourcePath, targetPath, method = InstallMethod.SYMLINK) {
|
|
109
|
+
// 确保源存在
|
|
110
|
+
if (!fs.existsSync(sourcePath)) {
|
|
111
|
+
throw new Error(`Source skill not found: ${sourcePath}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 确保父目录存在
|
|
115
|
+
const parentDir = path.dirname(targetPath);
|
|
116
|
+
if (!fs.existsSync(parentDir)) {
|
|
117
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (method === InstallMethod.SYMLINK) {
|
|
121
|
+
// 检查是否支持符号链接
|
|
122
|
+
if (!supportsSymlink(parentDir)) {
|
|
123
|
+
console.log('Symlink not supported, falling back to copy mode...');
|
|
124
|
+
copyRecursive(sourcePath, targetPath);
|
|
125
|
+
} else {
|
|
126
|
+
createSymlink(sourcePath, targetPath);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Copy 模式
|
|
130
|
+
copyRecursive(sourcePath, targetPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 批量安装 Skills
|
|
136
|
+
*/
|
|
137
|
+
export function installSkills(skills, targetDir, method = InstallMethod.SYMLINK) {
|
|
138
|
+
const results = [];
|
|
139
|
+
|
|
140
|
+
for (const skill of skills) {
|
|
141
|
+
try {
|
|
142
|
+
const targetPath = path.join(targetDir, skill.name);
|
|
143
|
+
installSkill(skill.path, targetPath, method);
|
|
144
|
+
results.push({
|
|
145
|
+
name: skill.name,
|
|
146
|
+
success: true,
|
|
147
|
+
method: fs.lstatSync(targetPath).isSymbolicLink() ? 'symlink' : 'copy',
|
|
148
|
+
});
|
|
149
|
+
} catch (e) {
|
|
150
|
+
results.push({
|
|
151
|
+
name: skill.name,
|
|
152
|
+
success: false,
|
|
153
|
+
error: e.message,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 移除 Skill
|
|
163
|
+
*/
|
|
164
|
+
export function removeSkill(skillPath) {
|
|
165
|
+
if (!fs.existsSync(skillPath)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 获取安装信息
|
|
175
|
+
*/
|
|
176
|
+
export function getInstallInfo(skillPath) {
|
|
177
|
+
if (!fs.existsSync(skillPath)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const stat = fs.lstatSync(skillPath);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
exists: true,
|
|
185
|
+
isSymlink: stat.isSymbolicLink(),
|
|
186
|
+
target: stat.isSymbolicLink() ? fs.readlinkSync(skillPath) : null,
|
|
187
|
+
size: stat.size,
|
|
188
|
+
modified: stat.mtime,
|
|
189
|
+
};
|
|
190
|
+
}
|