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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-skills",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "AI Agent Skills framework — Spec-Driven development with directed-graph rollback and quality gates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,8 @@
1
- import { join, resolve } from 'node:path';
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
  }
@@ -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: [], skillCommands: [] };
19
+ const results = { skills: [], rules: [] };
23
20
 
24
- // Check symlink-based install
25
21
  const skills = discoverSkills();
26
- for (const skill of skills) {
27
- const dest = join(target, skill.name);
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
- // Claude Code shared rules
57
- for (const rule of discoverSharedRules()) {
58
- const claudeRuleDest = join(claudeSkillsTarget, '_team-rules', rule.name);
59
- results.rules.push({
60
- name: `Claude/${rule.name}`,
61
- status: getStatus(claudeRuleDest, rule.path),
62
- path: claudeRuleDest,
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
- // Pretty print
72
- log.heading('Agent Skills');
73
- printTable(results.skills);
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} 已安装`);
@@ -1,9 +1,6 @@
1
- import { join } from 'node:path';
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 { createSymlinkSafe, ensureDir, isSymlink } from '../lib/fs-utils.js';
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 rulesTarget = join(target, '_team-rules');
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
- // Shared rules ~/.claude/skills/_team-rules/
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
- log.heading('验证安装');
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
- for (const skill of skills) {
42
- if (remove(join(target, skill.name), skill.path, `Skill: ${skill.name}`, dryRun)) removed++;
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
  }
@@ -1,12 +1,13 @@
1
1
  import { join, resolve } from 'node:path';
2
- import { existsSync, copyFileSync as fsCopyFile, rmSync, readdirSync, readFileSync } from 'node:fs';
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
- // Cursor global: ~/.agents/skills/
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
- log.heading('验证全局安装');
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
- if (ides.includes('cursor')) {
163
- const skillsDst = join(dir, '.cursor', 'skills');
164
- log.heading(`更新 Skills ${skillsDst}`);
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
- if (ides.includes('claude')) {
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
- }
@@ -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
+ }