team-skills 1.3.3 → 1.3.4
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/commands/init.js +3 -56
- package/src/commands/list.js +25 -50
- package/src/commands/setup.js +5 -80
- package/src/commands/uninstall.js +15 -29
- package/src/commands/update.js +15 -147
- package/src/lib/constants.js +14 -0
- package/src/lib/installers.js +120 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.4] - 2026-06-26
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
|
|
14
|
+
- 全局安装目录从 2 个扩展为 3 个:新增 `~/.cursor/skills/`(Cursor 全局 Skills 目录)
|
|
15
|
+
- `setup`、`update`、`uninstall`、`list` 四个命令统一使用循环模式处理 3 个全局目标(Agents / Cursor / Claude Code)
|
|
16
|
+
|
|
10
17
|
## [1.3.3] - 2026-06-26
|
|
11
18
|
|
|
12
19
|
### 变更
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { copyFileSync as fsCopyFile, existsSync, rmSync } from 'node:fs';
|
|
1
|
+
import { resolve } from 'node:path';
|
|
3
2
|
import { PACKAGE_ROOT } from '../lib/constants.js';
|
|
4
3
|
import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
|
|
5
|
-
import { copyRecursive, ensureDir } from '../lib/fs-utils.js';
|
|
6
4
|
import { detectIDE } from '../lib/detect-ide.js';
|
|
5
|
+
import { installSkillsProject } from '../lib/installers.js';
|
|
7
6
|
import * as log from '../lib/logger.js';
|
|
8
7
|
|
|
9
8
|
export function registerInit(program) {
|
|
@@ -23,65 +22,13 @@ function runInit(dir, opts) {
|
|
|
23
22
|
const exclude = withScore ? [] : ['team-score'];
|
|
24
23
|
const ides = detectIDE(dir, ide, { strict: true });
|
|
25
24
|
|
|
26
|
-
const tag = dryRun ? '[dry-run] ' : '';
|
|
27
|
-
let count = 0;
|
|
28
|
-
|
|
29
25
|
log.heading('初始化 team-skills 到项目');
|
|
30
26
|
log.info(`项目目录: ${dir}`);
|
|
31
27
|
log.info(`目标 IDE: ${ides.join(', ')}`);
|
|
32
28
|
|
|
33
29
|
const skills = discoverSkills(PACKAGE_ROOT, { exclude });
|
|
34
30
|
const rules = discoverSharedRules();
|
|
35
|
-
|
|
36
|
-
// Cursor: skills → .cursor/skills/
|
|
37
|
-
if (ides.includes('cursor')) {
|
|
38
|
-
const skillsDst = join(dir, '.cursor', 'skills');
|
|
39
|
-
log.heading(`复制 Skills → ${skillsDst}`);
|
|
40
|
-
|
|
41
|
-
if (!dryRun) ensureDir(skillsDst);
|
|
42
|
-
for (const skill of skills) {
|
|
43
|
-
const dest = join(skillsDst, skill.name);
|
|
44
|
-
if (!dryRun) {
|
|
45
|
-
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
46
|
-
copyRecursive(skill.path, dest);
|
|
47
|
-
}
|
|
48
|
-
log.success(`${tag}Skill: ${skill.name}`);
|
|
49
|
-
count++;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const rulesDst = join(skillsDst, '_team-rules');
|
|
53
|
-
if (!dryRun) ensureDir(rulesDst);
|
|
54
|
-
for (const r of rules) {
|
|
55
|
-
if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
|
|
56
|
-
log.success(`${tag}Rule: ${r.name}`);
|
|
57
|
-
count++;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Claude Code: skills → .claude/skills/ (same structure as Cursor)
|
|
62
|
-
if (ides.includes('claude')) {
|
|
63
|
-
const skillsDst = join(dir, '.claude', 'skills');
|
|
64
|
-
log.heading(`复制 Skills → ${skillsDst}`);
|
|
65
|
-
|
|
66
|
-
if (!dryRun) ensureDir(skillsDst);
|
|
67
|
-
for (const skill of skills) {
|
|
68
|
-
const dest = join(skillsDst, skill.name);
|
|
69
|
-
if (!dryRun) {
|
|
70
|
-
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
71
|
-
copyRecursive(skill.path, dest);
|
|
72
|
-
}
|
|
73
|
-
log.success(`${tag}Skill: ${skill.name}`);
|
|
74
|
-
count++;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const rulesDst = join(skillsDst, '_team-rules');
|
|
78
|
-
if (!dryRun) ensureDir(rulesDst);
|
|
79
|
-
for (const r of rules) {
|
|
80
|
-
if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
|
|
81
|
-
log.success(`${tag}Rule: ${r.name}`);
|
|
82
|
-
count++;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
31
|
+
const count = installSkillsProject(dir, ides, skills, rules, { dryRun });
|
|
85
32
|
|
|
86
33
|
log.done(`初始化完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
|
|
87
34
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { existsSync, readlinkSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_SKILLS_TARGET,
|
|
5
|
-
DEFAULT_CLAUDE_SKILLS_TARGET,
|
|
6
|
-
} from '../lib/constants.js';
|
|
3
|
+
import { DEFAULT_SKILLS_TARGET, resolveTargets } from '../lib/constants.js';
|
|
7
4
|
import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
|
|
8
5
|
import { isSymlink } from '../lib/fs-utils.js';
|
|
9
6
|
import * as log from '../lib/logger.js';
|
|
@@ -19,48 +16,29 @@ export function registerList(program) {
|
|
|
19
16
|
|
|
20
17
|
function runList(opts) {
|
|
21
18
|
const { target, json } = opts;
|
|
22
|
-
const results = { skills: [], rules: []
|
|
19
|
+
const results = { skills: [], rules: [] };
|
|
23
20
|
|
|
24
|
-
// Check symlink-based install
|
|
25
21
|
const skills = discoverSkills();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
results.skills.push({
|
|
29
|
-
name: skill.name,
|
|
30
|
-
status: getStatus(dest, skill.path),
|
|
31
|
-
path: dest,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Shared rules
|
|
36
|
-
for (const rule of discoverSharedRules()) {
|
|
37
|
-
const dest = join(target, '_team-rules', rule.name);
|
|
38
|
-
results.rules.push({
|
|
39
|
-
name: rule.name,
|
|
40
|
-
status: getStatus(dest, rule.path),
|
|
41
|
-
path: dest,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Claude Code Skills
|
|
46
|
-
const claudeSkillsTarget = DEFAULT_CLAUDE_SKILLS_TARGET;
|
|
47
|
-
for (const skill of skills) {
|
|
48
|
-
const dest = join(claudeSkillsTarget, skill.name);
|
|
49
|
-
results.skillCommands.push({
|
|
50
|
-
name: skill.name,
|
|
51
|
-
status: getStatus(dest, skill.path),
|
|
52
|
-
path: dest,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
22
|
+
const rules = discoverSharedRules();
|
|
23
|
+
const targets = resolveTargets(target);
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
25
|
+
for (const t of targets) {
|
|
26
|
+
for (const skill of skills) {
|
|
27
|
+
const dest = join(t.dir, skill.name);
|
|
28
|
+
results.skills.push({
|
|
29
|
+
name: `${t.label}/${skill.name}`,
|
|
30
|
+
status: getStatus(dest, skill.path),
|
|
31
|
+
path: dest,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const rule of rules) {
|
|
35
|
+
const dest = join(t.dir, '_team-rules', rule.name);
|
|
36
|
+
results.rules.push({
|
|
37
|
+
name: `${t.label}/${rule.name}`,
|
|
38
|
+
status: getStatus(dest, rule.path),
|
|
39
|
+
path: dest,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
64
42
|
}
|
|
65
43
|
|
|
66
44
|
if (json) {
|
|
@@ -68,17 +46,14 @@ function runList(opts) {
|
|
|
68
46
|
return;
|
|
69
47
|
}
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
for (const t of targets) {
|
|
50
|
+
log.heading(`${t.label} Skills`);
|
|
51
|
+
printTable(results.skills.filter(s => s.name.startsWith(`${t.label}/`)));
|
|
52
|
+
}
|
|
74
53
|
|
|
75
54
|
log.heading('共享规则');
|
|
76
55
|
printTable(results.rules);
|
|
77
56
|
|
|
78
|
-
log.heading('Claude Code Skills');
|
|
79
|
-
printTable(results.skillCommands);
|
|
80
|
-
|
|
81
|
-
// Summary
|
|
82
57
|
const installed = results.skills.filter(s => s.status === 'ok' || s.status === 'file').length;
|
|
83
58
|
const total = results.skills.length;
|
|
84
59
|
console.log(`\nSkills: ${installed}/${total} 已安装`);
|
package/src/commands/setup.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
PACKAGE_ROOT, DEFAULT_SKILLS_TARGET, DEFAULT_CLAUDE_SKILLS_TARGET,
|
|
4
|
-
} from '../lib/constants.js';
|
|
1
|
+
import { PACKAGE_ROOT, DEFAULT_SKILLS_TARGET, resolveTargets } from '../lib/constants.js';
|
|
5
2
|
import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
|
|
6
|
-
import {
|
|
3
|
+
import { installSkillsGlobal, verifyGlobalSymlinks } from '../lib/installers.js';
|
|
7
4
|
import * as log from '../lib/logger.js';
|
|
8
5
|
|
|
9
6
|
export function registerSetup(program) {
|
|
@@ -17,72 +14,14 @@ export function registerSetup(program) {
|
|
|
17
14
|
|
|
18
15
|
function runSetup(target, opts) {
|
|
19
16
|
const { dryRun } = opts;
|
|
20
|
-
const tag = dryRun ? '[dry-run] ' : '';
|
|
21
|
-
let count = 0;
|
|
22
|
-
|
|
23
|
-
// Skills → ~/.agents/skills/ (Cursor auto-discovers from here)
|
|
24
|
-
log.heading('安装 Skills → Cursor');
|
|
25
17
|
const skills = discoverSkills(PACKAGE_ROOT);
|
|
26
|
-
for (const skill of skills) {
|
|
27
|
-
const dest = join(target, skill.name);
|
|
28
|
-
const result = createSymlinkSafe(skill.path, dest, { force: true, dryRun });
|
|
29
|
-
logResult(`${tag}Skill: ${skill.name}`, result);
|
|
30
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Shared rules → ~/.agents/skills/_team-rules/
|
|
34
|
-
log.heading('安装共享规则');
|
|
35
18
|
const rules = discoverSharedRules();
|
|
36
|
-
const
|
|
37
|
-
if (!dryRun) ensureDir(rulesTarget);
|
|
38
|
-
for (const rule of rules) {
|
|
39
|
-
const dest = join(rulesTarget, rule.name);
|
|
40
|
-
const result = createSymlinkSafe(rule.path, dest, { force: true, dryRun });
|
|
41
|
-
logResult(`${tag}Rule: ${rule.name}`, result);
|
|
42
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Skills → ~/.claude/skills/ (Claude Code auto-discovers from here)
|
|
46
|
-
log.heading('安装 Skills → Claude Code');
|
|
47
|
-
const claudeSkillsTarget = DEFAULT_CLAUDE_SKILLS_TARGET;
|
|
48
|
-
if (!dryRun) ensureDir(claudeSkillsTarget);
|
|
49
|
-
for (const skill of skills) {
|
|
50
|
-
const dest = join(claudeSkillsTarget, skill.name);
|
|
51
|
-
const result = createSymlinkSafe(skill.path, dest, { force: true, dryRun });
|
|
52
|
-
logResult(`${tag}Skill: ${skill.name}`, result);
|
|
53
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
54
|
-
}
|
|
19
|
+
const targets = resolveTargets(target);
|
|
55
20
|
|
|
56
|
-
|
|
57
|
-
log.heading('安装 Claude Code 共享规则');
|
|
58
|
-
const claudeRulesTarget = join(claudeSkillsTarget, '_team-rules');
|
|
59
|
-
if (!dryRun) ensureDir(claudeRulesTarget);
|
|
60
|
-
for (const rule of rules) {
|
|
61
|
-
const dest = join(claudeRulesTarget, rule.name);
|
|
62
|
-
const result = createSymlinkSafe(rule.path, dest, { force: true, dryRun });
|
|
63
|
-
logResult(`${tag}Rule: ${rule.name}`, result);
|
|
64
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
65
|
-
}
|
|
21
|
+
const count = installSkillsGlobal(targets, skills, rules, { dryRun });
|
|
66
22
|
|
|
67
23
|
if (!dryRun) {
|
|
68
|
-
|
|
69
|
-
let errors = 0;
|
|
70
|
-
const verify = (label, dest) => {
|
|
71
|
-
if (isSymlink(dest)) {
|
|
72
|
-
log.success(label);
|
|
73
|
-
} else {
|
|
74
|
-
log.error(`${label} 未正确安装`);
|
|
75
|
-
errors++;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
for (const skill of skills) {
|
|
79
|
-
verify(`Cursor Skill: ${skill.name}`, join(target, skill.name));
|
|
80
|
-
verify(`Claude Skill: ${skill.name}`, join(claudeSkillsTarget, skill.name));
|
|
81
|
-
}
|
|
82
|
-
for (const rule of rules) {
|
|
83
|
-
verify(`Rule: ${rule.name}`, join(rulesTarget, rule.name));
|
|
84
|
-
verify(`Claude Rule: ${rule.name}`, join(claudeRulesTarget, rule.name));
|
|
85
|
-
}
|
|
24
|
+
const errors = verifyGlobalSymlinks(targets, skills, rules);
|
|
86
25
|
if (errors > 0) {
|
|
87
26
|
log.error(`有 ${errors} 个组件安装异常,请检查。`);
|
|
88
27
|
process.exit(1);
|
|
@@ -94,17 +33,3 @@ function runSetup(target, opts) {
|
|
|
94
33
|
console.log('\n后续可通过 team-skills setup 重新安装,或 team-skills uninstall 卸载。');
|
|
95
34
|
}
|
|
96
35
|
}
|
|
97
|
-
|
|
98
|
-
function logResult(label, result) {
|
|
99
|
-
switch (result) {
|
|
100
|
-
case 'created':
|
|
101
|
-
case 'dry-run':
|
|
102
|
-
log.success(label);
|
|
103
|
-
break;
|
|
104
|
-
case 'exists':
|
|
105
|
-
log.skip(`${label}(已存在,跳过)`);
|
|
106
|
-
break;
|
|
107
|
-
default:
|
|
108
|
-
log.info(label);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
PACKAGE_ROOT, DEFAULT_SKILLS_TARGET,
|
|
4
|
-
DEFAULT_CLAUDE_SKILLS_TARGET,
|
|
5
|
-
} from '../lib/constants.js';
|
|
2
|
+
import { DEFAULT_SKILLS_TARGET, resolveTargets } from '../lib/constants.js';
|
|
6
3
|
import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
|
|
7
4
|
import { removeSymlinkSafe, rmdirIfEmpty } from '../lib/fs-utils.js';
|
|
8
5
|
import * as log from '../lib/logger.js';
|
|
@@ -35,34 +32,23 @@ function runUninstall(target, opts) {
|
|
|
35
32
|
const { dryRun } = opts;
|
|
36
33
|
let removed = 0;
|
|
37
34
|
|
|
38
|
-
// Cursor Skills → ~/.agents/skills/
|
|
39
|
-
log.heading('移除 Cursor Skills');
|
|
40
35
|
const skills = discoverSkills();
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
const rules = discoverSharedRules();
|
|
37
|
+
const targets = resolveTargets(target);
|
|
38
|
+
|
|
39
|
+
for (const t of targets) {
|
|
40
|
+
log.heading(`移除 ${t.label} Skills`);
|
|
41
|
+
for (const skill of skills) {
|
|
42
|
+
if (remove(join(t.dir, skill.name), skill.path, `${t.label} Skill: ${skill.name}`, dryRun)) removed++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.heading(`移除 ${t.label} 共享规则`);
|
|
46
|
+
for (const rule of rules) {
|
|
47
|
+
if (remove(join(t.dir, '_team-rules', rule.name), rule.path, `${t.label} Rule: ${rule.name}`, dryRun)) removed++;
|
|
48
|
+
}
|
|
49
|
+
if (!dryRun) rmdirIfEmpty(join(t.dir, '_team-rules'));
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
// Claude Code Skills → ~/.claude/skills/
|
|
46
|
-
log.heading('移除 Claude Code Skills');
|
|
47
|
-
const claudeSkillsTarget = DEFAULT_CLAUDE_SKILLS_TARGET;
|
|
48
|
-
for (const skill of skills) {
|
|
49
|
-
if (remove(join(claudeSkillsTarget, skill.name), skill.path, `Claude Skill: ${skill.name}`, dryRun)) removed++;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Claude Code shared rules → ~/.claude/skills/_team-rules/
|
|
53
|
-
log.heading('移除 Claude Code 共享规则');
|
|
54
|
-
for (const rule of discoverSharedRules()) {
|
|
55
|
-
if (remove(join(claudeSkillsTarget, '_team-rules', rule.name), rule.path, `Claude Rule: ${rule.name}`, dryRun)) removed++;
|
|
56
|
-
}
|
|
57
|
-
if (!dryRun) rmdirIfEmpty(join(claudeSkillsTarget, '_team-rules'));
|
|
58
|
-
|
|
59
|
-
// Shared rules
|
|
60
|
-
log.heading('移除共享规则');
|
|
61
|
-
for (const rule of discoverSharedRules()) {
|
|
62
|
-
if (remove(join(target, '_team-rules', rule.name), rule.path, `Rule: ${rule.name}`, dryRun)) removed++;
|
|
63
|
-
}
|
|
64
|
-
if (!dryRun) rmdirIfEmpty(join(target, '_team-rules'));
|
|
65
|
-
|
|
66
52
|
log.done(`卸载完成${dryRun ? ' (dry-run)' : ''},共移除 ${removed} 个软链接。`);
|
|
67
53
|
if (!dryRun) console.log('本仓库源文件未受影响。');
|
|
68
54
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import {
|
|
5
|
-
PACKAGE_ROOT, DEFAULT_SKILLS_TARGET, DEFAULT_CLAUDE_SKILLS_TARGET,
|
|
6
|
-
} from '../lib/constants.js';
|
|
4
|
+
import { PACKAGE_ROOT, GLOBAL_TARGETS, PROJECT_IDE_DIRS } from '../lib/constants.js';
|
|
7
5
|
import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
|
|
8
|
-
import { copyRecursive, ensureDir, createSymlinkSafe, isSymlink } from '../lib/fs-utils.js';
|
|
9
6
|
import { detectIDE } from '../lib/detect-ide.js';
|
|
7
|
+
import {
|
|
8
|
+
installSkillsGlobal, verifyGlobalSymlinks,
|
|
9
|
+
installSkillsProject, cleanStaleSkills,
|
|
10
|
+
} from '../lib/installers.js';
|
|
10
11
|
import * as log from '../lib/logger.js';
|
|
11
12
|
|
|
12
13
|
export function registerUpdate(program) {
|
|
@@ -47,111 +48,37 @@ function upgradeSelf(dryRun) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function cleanStaleSkills(targetDir, currentNames, dryRun, exclude = []) {
|
|
51
|
-
if (!existsSync(targetDir)) return;
|
|
52
|
-
const existing = readdirSync(targetDir).filter(
|
|
53
|
-
name => !name.startsWith('_') && name !== 'CLAUDE.md' && name.startsWith('team-'),
|
|
54
|
-
);
|
|
55
|
-
for (const name of existing) {
|
|
56
|
-
if (!currentNames.has(name) && !exclude.includes(name)) {
|
|
57
|
-
const tag = dryRun ? '[dry-run] ' : '';
|
|
58
|
-
if (!dryRun) rmSync(join(targetDir, name), { recursive: true });
|
|
59
|
-
log.warn(`${tag}移除旧 Skill: ${name}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
51
|
function runUpdate(dir, opts) {
|
|
65
52
|
dir = resolve(dir);
|
|
66
53
|
const { ide, withScore, skipSelf, dryRun } = opts;
|
|
67
54
|
|
|
68
55
|
if (!skipSelf) upgradeSelf(dryRun);
|
|
69
56
|
|
|
70
|
-
const exclude = withScore ? [] : ['team-score'];
|
|
71
|
-
const tag = dryRun ? '[dry-run] ' : '';
|
|
72
|
-
let count = 0;
|
|
73
|
-
|
|
74
|
-
// ── Global: always include team-score ──
|
|
75
57
|
const allSkills = discoverSkills(PACKAGE_ROOT);
|
|
76
58
|
const rules = discoverSharedRules();
|
|
77
59
|
const allSkillNames = new Set(allSkills.map(s => s.name));
|
|
78
60
|
|
|
79
61
|
log.heading('更新全局 Skills');
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const cursorGlobal = DEFAULT_SKILLS_TARGET;
|
|
83
|
-
log.heading(`更新 Skills → ${cursorGlobal}`);
|
|
84
|
-
cleanStaleSkills(cursorGlobal, allSkillNames, dryRun);
|
|
85
|
-
if (!dryRun) ensureDir(cursorGlobal);
|
|
86
|
-
for (const skill of allSkills) {
|
|
87
|
-
const dest = join(cursorGlobal, skill.name);
|
|
88
|
-
const result = createSymlinkSafe(skill.path, dest, { force: true, dryRun });
|
|
89
|
-
logGlobalResult(`${tag}Skill: ${skill.name}`, result);
|
|
90
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
91
|
-
}
|
|
92
|
-
const cursorRulesDst = join(cursorGlobal, '_team-rules');
|
|
93
|
-
if (!dryRun) ensureDir(cursorRulesDst);
|
|
94
|
-
for (const r of rules) {
|
|
95
|
-
const dest = join(cursorRulesDst, r.name);
|
|
96
|
-
const result = createSymlinkSafe(r.path, dest, { force: true, dryRun });
|
|
97
|
-
logGlobalResult(`${tag}Rule: ${r.name}`, result);
|
|
98
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Claude Code global: ~/.claude/skills/
|
|
102
|
-
const claudeGlobal = DEFAULT_CLAUDE_SKILLS_TARGET;
|
|
103
|
-
log.heading(`更新 Skills → ${claudeGlobal}`);
|
|
104
|
-
cleanStaleSkills(claudeGlobal, allSkillNames, dryRun);
|
|
105
|
-
if (!dryRun) ensureDir(claudeGlobal);
|
|
106
|
-
for (const skill of allSkills) {
|
|
107
|
-
const dest = join(claudeGlobal, skill.name);
|
|
108
|
-
const result = createSymlinkSafe(skill.path, dest, { force: true, dryRun });
|
|
109
|
-
logGlobalResult(`${tag}Skill: ${skill.name}`, result);
|
|
110
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
111
|
-
}
|
|
112
|
-
const claudeRulesDst = join(claudeGlobal, '_team-rules');
|
|
113
|
-
if (!dryRun) ensureDir(claudeRulesDst);
|
|
114
|
-
for (const r of rules) {
|
|
115
|
-
const dest = join(claudeRulesDst, r.name);
|
|
116
|
-
const result = createSymlinkSafe(r.path, dest, { force: true, dryRun });
|
|
117
|
-
logGlobalResult(`${tag}Rule: ${r.name}`, result);
|
|
118
|
-
if (result === 'created' || result === 'dry-run') count++;
|
|
62
|
+
for (const t of GLOBAL_TARGETS) {
|
|
63
|
+
cleanStaleSkills(t.dir, allSkillNames, { dryRun });
|
|
119
64
|
}
|
|
65
|
+
let count = installSkillsGlobal(GLOBAL_TARGETS, allSkills, rules, { dryRun, verb: '更新' });
|
|
120
66
|
|
|
121
|
-
// Verify global symlinks
|
|
122
67
|
if (!dryRun) {
|
|
123
|
-
|
|
124
|
-
let errors = 0;
|
|
125
|
-
const verify = (label, dest) => {
|
|
126
|
-
if (isSymlink(dest)) {
|
|
127
|
-
log.success(label);
|
|
128
|
-
} else {
|
|
129
|
-
log.error(`${label} 未正确安装`);
|
|
130
|
-
errors++;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
for (const skill of allSkills) {
|
|
134
|
-
verify(`Cursor Skill: ${skill.name}`, join(cursorGlobal, skill.name));
|
|
135
|
-
verify(`Claude Skill: ${skill.name}`, join(claudeGlobal, skill.name));
|
|
136
|
-
}
|
|
137
|
-
for (const r of rules) {
|
|
138
|
-
verify(`Rule: ${r.name}`, join(cursorRulesDst, r.name));
|
|
139
|
-
verify(`Claude Rule: ${r.name}`, join(claudeRulesDst, r.name));
|
|
140
|
-
}
|
|
68
|
+
const errors = verifyGlobalSymlinks(GLOBAL_TARGETS, allSkills, rules);
|
|
141
69
|
if (errors > 0) {
|
|
142
70
|
log.error(`有 ${errors} 个全局组件安装异常,请检查。`);
|
|
143
71
|
}
|
|
144
72
|
}
|
|
145
73
|
|
|
146
|
-
// ── Project: respect --with-score ──
|
|
147
74
|
const ides = detectIDE(dir, ide);
|
|
148
|
-
|
|
149
75
|
if (ides.length === 0) {
|
|
150
76
|
log.info('当前项目未检测到 IDE 配置(.claude/ 或 .cursor/),跳过项目更新。');
|
|
151
77
|
log.done(`更新完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
|
|
152
78
|
return;
|
|
153
79
|
}
|
|
154
80
|
|
|
81
|
+
const exclude = withScore ? [] : ['team-score'];
|
|
155
82
|
const projectSkills = discoverSkills(PACKAGE_ROOT, { exclude });
|
|
156
83
|
const projectSkillNames = new Set(projectSkills.map(s => s.name));
|
|
157
84
|
|
|
@@ -159,71 +86,12 @@ function runUpdate(dir, opts) {
|
|
|
159
86
|
log.info(`项目目录: ${dir}`);
|
|
160
87
|
log.info(`目标 IDE: ${ides.join(', ')}`);
|
|
161
88
|
|
|
162
|
-
|
|
163
|
-
const skillsDst = join(dir,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
cleanStaleSkills(skillsDst, projectSkillNames, dryRun, exclude);
|
|
167
|
-
|
|
168
|
-
if (!dryRun) ensureDir(skillsDst);
|
|
169
|
-
for (const skill of projectSkills) {
|
|
170
|
-
const dest = join(skillsDst, skill.name);
|
|
171
|
-
if (!dryRun) {
|
|
172
|
-
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
173
|
-
copyRecursive(skill.path, dest);
|
|
174
|
-
}
|
|
175
|
-
log.success(`${tag}Skill: ${skill.name}`);
|
|
176
|
-
count++;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const rulesDst = join(skillsDst, '_team-rules');
|
|
180
|
-
if (!dryRun) ensureDir(rulesDst);
|
|
181
|
-
for (const r of rules) {
|
|
182
|
-
if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
|
|
183
|
-
log.success(`${tag}Rule: ${r.name}`);
|
|
184
|
-
count++;
|
|
185
|
-
}
|
|
89
|
+
for (const ideName of ides) {
|
|
90
|
+
const skillsDst = join(dir, PROJECT_IDE_DIRS[ideName], 'skills');
|
|
91
|
+
cleanStaleSkills(skillsDst, projectSkillNames, { dryRun, exclude });
|
|
186
92
|
}
|
|
187
93
|
|
|
188
|
-
|
|
189
|
-
const skillsDst = join(dir, '.claude', 'skills');
|
|
190
|
-
log.heading(`更新 Skills → ${skillsDst}`);
|
|
191
|
-
|
|
192
|
-
cleanStaleSkills(skillsDst, projectSkillNames, dryRun, exclude);
|
|
193
|
-
|
|
194
|
-
if (!dryRun) ensureDir(skillsDst);
|
|
195
|
-
for (const skill of projectSkills) {
|
|
196
|
-
const dest = join(skillsDst, skill.name);
|
|
197
|
-
if (!dryRun) {
|
|
198
|
-
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
199
|
-
copyRecursive(skill.path, dest);
|
|
200
|
-
}
|
|
201
|
-
log.success(`${tag}Skill: ${skill.name}`);
|
|
202
|
-
count++;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const rulesDst = join(skillsDst, '_team-rules');
|
|
206
|
-
if (!dryRun) ensureDir(rulesDst);
|
|
207
|
-
for (const r of rules) {
|
|
208
|
-
if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
|
|
209
|
-
log.success(`${tag}Rule: ${r.name}`);
|
|
210
|
-
count++;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
94
|
+
count += installSkillsProject(dir, ides, projectSkills, rules, { dryRun, verb: '更新' });
|
|
213
95
|
|
|
214
96
|
log.done(`更新完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
|
|
215
97
|
}
|
|
216
|
-
|
|
217
|
-
function logGlobalResult(label, result) {
|
|
218
|
-
switch (result) {
|
|
219
|
-
case 'created':
|
|
220
|
-
case 'dry-run':
|
|
221
|
-
log.success(label);
|
|
222
|
-
break;
|
|
223
|
-
case 'exists':
|
|
224
|
-
log.skip(`${label}(已存在,跳过)`);
|
|
225
|
-
break;
|
|
226
|
-
default:
|
|
227
|
-
log.info(label);
|
|
228
|
-
}
|
|
229
|
-
}
|
package/src/lib/constants.js
CHANGED
|
@@ -7,5 +7,19 @@ const __dirname = dirname(__filename);
|
|
|
7
7
|
|
|
8
8
|
export const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
9
9
|
export const DEFAULT_SKILLS_TARGET = join(homedir(), '.agents', 'skills');
|
|
10
|
+
export const DEFAULT_CURSOR_SKILLS_TARGET = join(homedir(), '.cursor', 'skills');
|
|
10
11
|
export const DEFAULT_CLAUDE_SKILLS_TARGET = join(homedir(), '.claude', 'skills');
|
|
11
12
|
export const SKILLS_DIR = 'skills';
|
|
13
|
+
|
|
14
|
+
export const GLOBAL_TARGETS = [
|
|
15
|
+
{ label: 'Agents', dir: DEFAULT_SKILLS_TARGET },
|
|
16
|
+
{ label: 'Cursor', dir: DEFAULT_CURSOR_SKILLS_TARGET },
|
|
17
|
+
{ label: 'Claude Code', dir: DEFAULT_CLAUDE_SKILLS_TARGET },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const PROJECT_IDE_DIRS = { cursor: '.cursor', claude: '.claude' };
|
|
21
|
+
|
|
22
|
+
export function resolveTargets(customFirst) {
|
|
23
|
+
if (!customFirst || customFirst === DEFAULT_SKILLS_TARGET) return GLOBAL_TARGETS;
|
|
24
|
+
return GLOBAL_TARGETS.map((t, i) => (i === 0 ? { ...t, dir: customFirst } : t));
|
|
25
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, readdirSync, rmSync, copyFileSync } from 'node:fs';
|
|
3
|
+
import { createSymlinkSafe, ensureDir, isSymlink, copyRecursive } from './fs-utils.js';
|
|
4
|
+
import { PROJECT_IDE_DIRS } from './constants.js';
|
|
5
|
+
import * as log from './logger.js';
|
|
6
|
+
|
|
7
|
+
export function logInstallResult(label, result) {
|
|
8
|
+
switch (result) {
|
|
9
|
+
case 'created':
|
|
10
|
+
case 'dry-run':
|
|
11
|
+
log.success(label);
|
|
12
|
+
break;
|
|
13
|
+
case 'exists':
|
|
14
|
+
log.skip(`${label}(已存在,跳过)`);
|
|
15
|
+
break;
|
|
16
|
+
default:
|
|
17
|
+
log.info(label);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function installSkillsGlobal(targets, skills, rules, { dryRun, verb = '安装' }) {
|
|
22
|
+
const tag = dryRun ? '[dry-run] ' : '';
|
|
23
|
+
let count = 0;
|
|
24
|
+
|
|
25
|
+
for (const t of targets) {
|
|
26
|
+
log.heading(`${verb} → ${t.label}`);
|
|
27
|
+
if (!dryRun) ensureDir(t.dir);
|
|
28
|
+
|
|
29
|
+
for (const skill of skills) {
|
|
30
|
+
const dest = join(t.dir, skill.name);
|
|
31
|
+
const result = createSymlinkSafe(skill.path, dest, { force: true, dryRun });
|
|
32
|
+
logInstallResult(`${tag}Skill: ${skill.name}`, result);
|
|
33
|
+
if (result === 'created' || result === 'dry-run') count++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rulesDir = join(t.dir, '_team-rules');
|
|
37
|
+
if (!dryRun) ensureDir(rulesDir);
|
|
38
|
+
for (const rule of rules) {
|
|
39
|
+
const dest = join(rulesDir, rule.name);
|
|
40
|
+
const result = createSymlinkSafe(rule.path, dest, { force: true, dryRun });
|
|
41
|
+
logInstallResult(`${tag}Rule: ${rule.name}`, result);
|
|
42
|
+
if (result === 'created' || result === 'dry-run') count++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function verifyGlobalSymlinks(targets, skills, rules) {
|
|
50
|
+
log.heading('验证安装');
|
|
51
|
+
let errors = 0;
|
|
52
|
+
|
|
53
|
+
const verify = (label, dest) => {
|
|
54
|
+
if (isSymlink(dest)) {
|
|
55
|
+
log.success(label);
|
|
56
|
+
} else {
|
|
57
|
+
log.error(`${label} 未正确安装`);
|
|
58
|
+
errors++;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const t of targets) {
|
|
63
|
+
for (const skill of skills) {
|
|
64
|
+
verify(`${t.label} Skill: ${skill.name}`, join(t.dir, skill.name));
|
|
65
|
+
}
|
|
66
|
+
for (const rule of rules) {
|
|
67
|
+
verify(`${t.label} Rule: ${rule.name}`, join(t.dir, '_team-rules', rule.name));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return errors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function installSkillsProject(projectDir, ides, skills, rules, { dryRun, verb = '复制' }) {
|
|
75
|
+
const tag = dryRun ? '[dry-run] ' : '';
|
|
76
|
+
let count = 0;
|
|
77
|
+
|
|
78
|
+
for (const ideName of ides) {
|
|
79
|
+
const ideSubdir = PROJECT_IDE_DIRS[ideName];
|
|
80
|
+
if (!ideSubdir) continue;
|
|
81
|
+
|
|
82
|
+
const skillsDst = join(projectDir, ideSubdir, 'skills');
|
|
83
|
+
log.heading(`${verb} → ${skillsDst}`);
|
|
84
|
+
|
|
85
|
+
if (!dryRun) ensureDir(skillsDst);
|
|
86
|
+
for (const skill of skills) {
|
|
87
|
+
const dest = join(skillsDst, skill.name);
|
|
88
|
+
if (!dryRun) {
|
|
89
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
90
|
+
copyRecursive(skill.path, dest);
|
|
91
|
+
}
|
|
92
|
+
log.success(`${tag}Skill: ${skill.name}`);
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rulesDst = join(skillsDst, '_team-rules');
|
|
97
|
+
if (!dryRun) ensureDir(rulesDst);
|
|
98
|
+
for (const r of rules) {
|
|
99
|
+
if (!dryRun) copyFileSync(r.path, join(rulesDst, r.name));
|
|
100
|
+
log.success(`${tag}Rule: ${r.name}`);
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function cleanStaleSkills(targetDir, currentNames, { dryRun, exclude = [] }) {
|
|
109
|
+
if (!existsSync(targetDir)) return;
|
|
110
|
+
const existing = readdirSync(targetDir).filter(
|
|
111
|
+
name => !name.startsWith('_') && name !== 'CLAUDE.md' && name.startsWith('team-'),
|
|
112
|
+
);
|
|
113
|
+
for (const name of existing) {
|
|
114
|
+
if (!currentNames.has(name) && !exclude.includes(name)) {
|
|
115
|
+
const tag = dryRun ? '[dry-run] ' : '';
|
|
116
|
+
if (!dryRun) rmSync(join(targetDir, name), { recursive: true });
|
|
117
|
+
log.warn(`${tag}移除旧 Skill: ${name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|