team-skills 1.3.2 → 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,28 @@
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
+
17
+ ## [1.3.3] - 2026-06-26
18
+
19
+ ### 变更
20
+
21
+ - `update` 命令新增全局安装阶段:同步更新 `~/.agents/skills/` 和 `~/.claude/skills/`(含 team-score)
22
+ - `setup` 全局安装始终包含 team-score,移除 `--with-score` 参数
23
+ - `setup`、`init`、`update` 统一为默认覆盖模式,移除 `--force` 参数
24
+ - `init` 覆盖行为与 `update` 对齐:先清理再复制(`rmSync` + `copyRecursive`)
25
+
26
+ ### 修复
27
+
28
+ - `update` 不再误删项目中已安装的 team-score(`cleanStaleSkills` 跳过被排除的 skill)
29
+ - `setup` 验证阶段补全 Claude Code 共享规则检查(之前仅验证 Cursor 路径)
30
+ - `update` 全局安装新增 symlink 验证阶段
31
+
10
32
  ## [1.3.2] - 2026-06-26
11
33
 
12
34
  ### 新增
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-skills",
3
- "version": "1.3.2",
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 } 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,59 +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) copyRecursive(skill.path, dest);
45
- log.success(`${tag}Skill: ${skill.name}`);
46
- count++;
47
- }
48
-
49
- const rulesDst = join(skillsDst, '_team-rules');
50
- if (!dryRun) ensureDir(rulesDst);
51
- for (const r of rules) {
52
- if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
53
- log.success(`${tag}Rule: ${r.name}`);
54
- count++;
55
- }
56
- }
57
-
58
- // Claude Code: skills → .claude/skills/ (same structure as Cursor)
59
- if (ides.includes('claude')) {
60
- const skillsDst = join(dir, '.claude', 'skills');
61
- log.heading(`复制 Skills → ${skillsDst}`);
62
-
63
- if (!dryRun) ensureDir(skillsDst);
64
- for (const skill of skills) {
65
- const dest = join(skillsDst, skill.name);
66
- if (!dryRun) copyRecursive(skill.path, dest);
67
- log.success(`${tag}Skill: ${skill.name}`);
68
- count++;
69
- }
70
-
71
- const rulesDst = join(skillsDst, '_team-rules');
72
- if (!dryRun) ensureDir(rulesDst);
73
- for (const r of rules) {
74
- if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
75
- log.success(`${tag}Rule: ${r.name}`);
76
- count++;
77
- }
78
- }
31
+ const count = installSkillsProject(dir, ides, skills, rules, { dryRun });
79
32
 
80
33
  log.done(`初始化完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
81
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) {
@@ -11,78 +8,20 @@ export function registerSetup(program) {
11
8
  .command('setup')
12
9
  .description('Install skills via symlinks to global directories')
13
10
  .argument('[target]', 'Target skills directory', DEFAULT_SKILLS_TARGET)
14
- .option('--with-score', 'Include team-score skill (hidden by default)', false)
15
- .option('--force', 'Overwrite existing symlinks', false)
16
11
  .option('--dry-run', 'Show what would be done without doing it', false)
17
12
  .action(runSetup);
18
13
  }
19
14
 
20
15
  function runSetup(target, opts) {
21
- const { withScore, force, dryRun } = opts;
22
- const tag = dryRun ? '[dry-run] ' : '';
23
- const exclude = withScore ? [] : ['team-score'];
24
- let count = 0;
25
-
26
- // Skills → ~/.agents/skills/ (Cursor auto-discovers from here)
27
- log.heading('安装 Skills → Cursor');
28
- const skills = discoverSkills(PACKAGE_ROOT, { exclude });
29
- for (const skill of skills) {
30
- const dest = join(target, skill.name);
31
- const result = createSymlinkSafe(skill.path, dest, { force, dryRun });
32
- logResult(`${tag}Skill: ${skill.name}`, result, dest);
33
- if (result === 'created' || result === 'dry-run') count++;
34
- }
35
-
36
- // Shared rules → ~/.agents/skills/_team-rules/
37
- log.heading('安装共享规则');
16
+ const { dryRun } = opts;
17
+ const skills = discoverSkills(PACKAGE_ROOT);
38
18
  const rules = discoverSharedRules();
39
- const rulesTarget = join(target, '_team-rules');
40
- if (!dryRun) ensureDir(rulesTarget);
41
- for (const rule of rules) {
42
- const dest = join(rulesTarget, rule.name);
43
- const result = createSymlinkSafe(rule.path, dest, { force, dryRun });
44
- logResult(`${tag}Rule: ${rule.name}`, result, dest);
45
- if (result === 'created' || result === 'dry-run') count++;
46
- }
19
+ const targets = resolveTargets(target);
47
20
 
48
- // Skills ~/.claude/skills/ (Claude Code auto-discovers from here)
49
- log.heading('安装 Skills → Claude Code');
50
- const claudeSkillsTarget = DEFAULT_CLAUDE_SKILLS_TARGET;
51
- if (!dryRun) ensureDir(claudeSkillsTarget);
52
- for (const skill of skills) {
53
- const dest = join(claudeSkillsTarget, skill.name);
54
- const result = createSymlinkSafe(skill.path, dest, { force, dryRun });
55
- logResult(`${tag}Skill: ${skill.name}`, result, dest);
56
- if (result === 'created' || result === 'dry-run') count++;
57
- }
58
-
59
- // Shared rules → ~/.claude/skills/_team-rules/
60
- log.heading('安装 Claude Code 共享规则');
61
- const claudeRulesTarget = join(claudeSkillsTarget, '_team-rules');
62
- if (!dryRun) ensureDir(claudeRulesTarget);
63
- for (const rule of rules) {
64
- const dest = join(claudeRulesTarget, rule.name);
65
- const result = createSymlinkSafe(rule.path, dest, { force, dryRun });
66
- logResult(`${tag}Rule: ${rule.name}`, result, dest);
67
- if (result === 'created' || result === 'dry-run') count++;
68
- }
21
+ const count = installSkillsGlobal(targets, skills, rules, { dryRun });
69
22
 
70
23
  if (!dryRun) {
71
- log.heading('验证安装');
72
- let errors = 0;
73
- const verify = (label, dest) => {
74
- if (isSymlink(dest)) {
75
- log.success(label);
76
- } else {
77
- log.error(`${label} 未正确安装`);
78
- errors++;
79
- }
80
- };
81
- for (const skill of skills) {
82
- verify(`Cursor Skill: ${skill.name}`, join(target, skill.name));
83
- verify(`Claude Skill: ${skill.name}`, join(claudeSkillsTarget, skill.name));
84
- }
85
- for (const rule of rules) verify(`Rule: ${rule.name}`, join(rulesTarget, rule.name));
24
+ const errors = verifyGlobalSymlinks(targets, skills, rules);
86
25
  if (errors > 0) {
87
26
  log.error(`有 ${errors} 个组件安装异常,请检查。`);
88
27
  process.exit(1);
@@ -94,20 +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, dest) {
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
- case 'conflict':
108
- log.warn(`${label}(已存在非 symlink,使用 --force 覆盖)`);
109
- break;
110
- default:
111
- log.info(label);
112
- }
113
- }
@@ -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,20 +1,23 @@
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 { PACKAGE_ROOT } from '../lib/constants.js';
4
+ import { PACKAGE_ROOT, GLOBAL_TARGETS, PROJECT_IDE_DIRS } from '../lib/constants.js';
5
5
  import { discoverSkills, discoverSharedRules } from '../lib/inventory.js';
6
- import { copyRecursive, ensureDir } from '../lib/fs-utils.js';
7
6
  import { detectIDE } from '../lib/detect-ide.js';
7
+ import {
8
+ installSkillsGlobal, verifyGlobalSymlinks,
9
+ installSkillsProject, cleanStaleSkills,
10
+ } from '../lib/installers.js';
8
11
  import * as log from '../lib/logger.js';
9
12
 
10
13
  export function registerUpdate(program) {
11
14
  program
12
15
  .command('update')
13
- .description('Upgrade team-skills package and update project installations')
16
+ .description('Upgrade team-skills package and update global & project installations')
14
17
  .argument('[dir]', 'Project directory', '.')
15
18
  .option('--ide <type>', 'Force IDE type: claude, cursor, or both')
16
- .option('--with-score', 'Include team-score skill (hidden by default)', false)
17
- .option('--skip-self', 'Skip self-upgrade, only update project', false)
19
+ .option('--with-score', 'Include team-score skill in project (hidden by default)', false)
20
+ .option('--skip-self', 'Skip self-upgrade, only update installations', false)
18
21
  .option('--dry-run', 'Show what would change', false)
19
22
  .action(runUpdate);
20
23
  }
@@ -45,98 +48,50 @@ function upgradeSelf(dryRun) {
45
48
  }
46
49
  }
47
50
 
48
- function cleanStaleSkills(targetDir, currentNames, dryRun) {
49
- if (!existsSync(targetDir)) return;
50
- const existing = readdirSync(targetDir).filter(
51
- name => !name.startsWith('_') && name !== 'CLAUDE.md' && name.startsWith('team-'),
52
- );
53
- for (const name of existing) {
54
- if (!currentNames.has(name)) {
55
- const tag = dryRun ? '[dry-run] ' : '';
56
- if (!dryRun) rmSync(join(targetDir, name), { recursive: true });
57
- log.warn(`${tag}移除旧 Skill: ${name}`);
58
- }
59
- }
60
- }
61
-
62
51
  function runUpdate(dir, opts) {
63
52
  dir = resolve(dir);
64
53
  const { ide, withScore, skipSelf, dryRun } = opts;
65
54
 
66
55
  if (!skipSelf) upgradeSelf(dryRun);
67
56
 
68
- const exclude = withScore ? [] : ['team-score'];
69
- const ides = detectIDE(dir, ide);
57
+ const allSkills = discoverSkills(PACKAGE_ROOT);
58
+ const rules = discoverSharedRules();
59
+ const allSkillNames = new Set(allSkills.map(s => s.name));
70
60
 
61
+ log.heading('更新全局 Skills');
62
+ for (const t of GLOBAL_TARGETS) {
63
+ cleanStaleSkills(t.dir, allSkillNames, { dryRun });
64
+ }
65
+ let count = installSkillsGlobal(GLOBAL_TARGETS, allSkills, rules, { dryRun, verb: '更新' });
66
+
67
+ if (!dryRun) {
68
+ const errors = verifyGlobalSymlinks(GLOBAL_TARGETS, allSkills, rules);
69
+ if (errors > 0) {
70
+ log.error(`有 ${errors} 个全局组件安装异常,请检查。`);
71
+ }
72
+ }
73
+
74
+ const ides = detectIDE(dir, ide);
71
75
  if (ides.length === 0) {
72
76
  log.info('当前项目未检测到 IDE 配置(.claude/ 或 .cursor/),跳过项目更新。');
77
+ log.done(`更新完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
73
78
  return;
74
79
  }
75
80
 
76
- const tag = dryRun ? '[dry-run] ' : '';
77
- let count = 0;
81
+ const exclude = withScore ? [] : ['team-score'];
82
+ const projectSkills = discoverSkills(PACKAGE_ROOT, { exclude });
83
+ const projectSkillNames = new Set(projectSkills.map(s => s.name));
78
84
 
79
85
  log.heading('更新项目中的 team-skills');
80
86
  log.info(`项目目录: ${dir}`);
81
87
  log.info(`目标 IDE: ${ides.join(', ')}`);
82
88
 
83
- const skills = discoverSkills(PACKAGE_ROOT, { exclude });
84
- const rules = discoverSharedRules();
85
- const currentSkillNames = new Set(skills.map(s => s.name));
86
-
87
- // Cursor: skills → .cursor/skills/
88
- if (ides.includes('cursor')) {
89
- const skillsDst = join(dir, '.cursor', 'skills');
90
- log.heading(`更新 Skills → ${skillsDst}`);
91
-
92
- cleanStaleSkills(skillsDst, currentSkillNames, dryRun);
93
-
94
- if (!dryRun) ensureDir(skillsDst);
95
- for (const skill of skills) {
96
- const dest = join(skillsDst, skill.name);
97
- if (!dryRun) {
98
- if (existsSync(dest)) rmSync(dest, { recursive: true });
99
- copyRecursive(skill.path, dest);
100
- }
101
- log.success(`${tag}Skill: ${skill.name}`);
102
- count++;
103
- }
104
-
105
- const rulesDst = join(skillsDst, '_team-rules');
106
- if (!dryRun) ensureDir(rulesDst);
107
- for (const r of rules) {
108
- if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
109
- log.success(`${tag}Rule: ${r.name}`);
110
- count++;
111
- }
89
+ for (const ideName of ides) {
90
+ const skillsDst = join(dir, PROJECT_IDE_DIRS[ideName], 'skills');
91
+ cleanStaleSkills(skillsDst, projectSkillNames, { dryRun, exclude });
112
92
  }
113
93
 
114
- // Claude Code: skills .claude/skills/ (same structure as Cursor)
115
- if (ides.includes('claude')) {
116
- const skillsDst = join(dir, '.claude', 'skills');
117
- log.heading(`更新 Skills → ${skillsDst}`);
118
-
119
- cleanStaleSkills(skillsDst, currentSkillNames, dryRun);
120
-
121
- if (!dryRun) ensureDir(skillsDst);
122
- for (const skill of skills) {
123
- const dest = join(skillsDst, skill.name);
124
- if (!dryRun) {
125
- if (existsSync(dest)) rmSync(dest, { recursive: true });
126
- copyRecursive(skill.path, dest);
127
- }
128
- log.success(`${tag}Skill: ${skill.name}`);
129
- count++;
130
- }
131
-
132
- const rulesDst = join(skillsDst, '_team-rules');
133
- if (!dryRun) ensureDir(rulesDst);
134
- for (const r of rules) {
135
- if (!dryRun) fsCopyFile(r.path, join(rulesDst, r.name));
136
- log.success(`${tag}Rule: ${r.name}`);
137
- count++;
138
- }
139
- }
94
+ count += installSkillsProject(dir, ides, projectSkills, rules, { dryRun, verb: '更新' });
140
95
 
141
96
  log.done(`更新完成${dryRun ? ' (dry-run)' : ''}!共 ${count} 个组件。`);
142
97
  }
@@ -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
+ }