@boses/skillink 1.0.5 → 1.1.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/README.md CHANGED
@@ -1,51 +1,50 @@
1
1
  # Skillink 🚀
2
2
 
3
- **Skillink** 是一个为 AI 时代打造的技能管理工具。它允许你在一个统一的目录(`.agents/skills`)中编写 AI 技能(Skills),并利用符号链接(Symlink/Junction)技术,即时同步到各种 AI 工具(如 Cursor、Windsurf、VSCode、Gemini)的配置目录中。
3
+ [English](./README.md) | [简体中文](./README.zh-CN.md)
4
4
 
5
- > **核心理念:一次编写,处处生效。**
5
+ **Skillink** is a skill linker for AI tools.
6
+ Write skills in one place (`.agents/skills`) and sync to multiple tool directories with symlinks/junctions.
6
7
 
7
- ## 特性
8
+ > Core idea: **Write once, use everywhere.**
8
9
 
9
- - **🎯 极简架构**:基于 Node.js 20+ 和 TypeScript 5.x,性能卓越。
10
- - **🔗 零克隆开销**:采用符号链接技术,目标目录的文件只是源文件的引用。修改源文件,AI 工具立即感知,无需等待同步。
11
- - **🛠️ 极致 DX**:
12
- - **交互式初始化**:一键引导配置。
13
- - **自动探测与创建**:自动管理 AI 工具的配置目录。
14
- - **实时监视**:支持 `--watch` 模式,动态响应技能的增删。
15
- - **🛡️ 安全可靠**:仅操作符号链接,不轻易改动或删除用户的原始文件。
10
+ ## Features
16
11
 
17
- ## 📦 安装
12
+ - Minimal architecture with Node.js 20+ and TypeScript
13
+ - Symlink-based sync (no copy, instant effect)
14
+ - Interactive `init` flow
15
+ - `sync --watch` for real-time skill folder changes
16
+ - Safe clean behavior (only removes links under source boundary)
17
+ - CLI localization via config (`en` / `zh-CN`)
18
18
 
19
- 推荐作为开发依赖安装到项目中:
19
+ ## Install
20
+
21
+ Install as a dev dependency:
20
22
 
21
23
  ```bash
22
- # 使用 pnpm
24
+ # pnpm
23
25
  pnpm add -D @boses/skillink
24
26
 
25
- # 使用 npm
27
+ # npm
26
28
  npm install -D @boses/skillink
27
29
 
28
- # 使用 yarn
30
+ # yarn
29
31
  yarn add -D @boses/skillink
30
32
  ```
31
33
 
32
- ## 🚀 快速开始
33
-
34
- ### 1. 初始化项目
34
+ ## Quick Start
35
35
 
36
- 在项目根目录下运行:
36
+ ### 1) Initialize
37
37
 
38
38
  ```bash
39
39
  npx skillink init
40
40
  ```
41
41
 
42
- 按照交互提示选择你正在使用的 AI 工具。该命令会自动:
43
- - 创建 `.agents/skills` 目录并添加一个示例技能。
44
- - 生成 `skillink.config.ts` 配置文件。
42
+ The first step in `init` asks language (`English / 简体中文`), then it creates:
45
43
 
46
- ### 2. 编写技能
44
+ - `.agents/skills` (with an example skill)
45
+ - `skillink.config.ts`
47
46
 
48
- `.agents/skills` 目录下创建子文件夹,并在其中编写 `SKILL.md`:
47
+ ### 2) Write skills
49
48
 
50
49
  ```text
51
50
  .agents/skills/
@@ -53,54 +52,60 @@ npx skillink init
53
52
  └── SKILL.md
54
53
  ```
55
54
 
56
- ### 3. 同步到工具
55
+ ### 3) Sync
57
56
 
58
57
  ```bash
59
58
  npx skillink sync
60
59
  ```
61
60
 
62
- 想要在开发时自动同步新增的技能?运行:
61
+ Watch mode:
63
62
 
64
63
  ```bash
65
64
  npx skillink sync --watch
66
65
  ```
67
66
 
68
- ## 🛠️ 命令详解
67
+ ## Commands
69
68
 
70
- | 命令 | 描述 |
71
- | :--- | :--- |
72
- | `init` | 初始化项目环境,生成 `.agents/skills` 和配置文件。 |
73
- | `sync` | 将技能同步到所有配置的目标工具中。支持 `-w, --watch` 模式。 |
74
- | `status` | 检查并显示当前所有技能与目标工具的同步状态。 |
75
- | `clean` | 移除所有由 Skillink 创建的符号链接,恢复环境。 |
76
- | `check` | 检查是否有新版本可用。 |
69
+ | Command | Description |
70
+ | :------- | :----------------------------------------------------------------------------------- |
71
+ | `init` | Initialize project and create config. |
72
+ | `sync` | Sync skills to all enabled targets (`--watch` supported). |
73
+ | `status` | Show detailed sync status. |
74
+ | `clean` | Remove generated symlinks from configured targets. |
75
+ | `check` | Check updates by semantic versions from npm `versions` (no `latest` tag dependency). |
77
76
 
78
- ## ⚙️ 配置说明 (`skillink.config.ts`)
77
+ ## Configuration (`skillink.config.ts`)
79
78
 
80
79
  ```typescript
81
80
  import { defineConfig } from '@boses/skillink';
82
81
 
83
82
  export default defineConfig({
84
- // 技能源目录
83
+ // CLI locale: 'en' | 'zh-CN' (default: 'en')
84
+ locale: 'en',
85
+ // Skills source directory
85
86
  source: '.agents/skills',
86
- // 同步目标列表
87
+ // Sync targets
87
88
  targets: [
88
89
  {
89
90
  name: 'cursor',
90
91
  path: '.cursor/rules',
91
- // 是否启用该目标(默认为 true)
92
- // 设置为 false 后,sync 和 status 命令将忽略此目标
93
92
  enabled: true,
94
93
  },
95
94
  {
96
95
  name: 'gemini',
97
- path: '.gemini/modules',
96
+ path: '.gemini/skills',
98
97
  enabled: true,
99
- }
98
+ },
100
99
  ],
101
100
  });
102
101
  ```
103
102
 
104
- ## 📄 许可证
103
+ ## Git Recommendation
104
+
105
+ - Commit: `skillink.config.ts`, `.agents/skills/**`
106
+ - Avoid committing generated link targets (for example: `.cursor/rules`, `.gemini/skills`)
107
+ - `init` will remind you to add target directories to `.gitignore`
108
+
109
+ ## License
105
110
 
106
- MIT
111
+ MIT
@@ -0,0 +1,111 @@
1
+ # Skillink 🚀
2
+
3
+ [English](./README.md) | [简体中文](./README.zh-CN.md)
4
+
5
+ **Skillink** 是一个 AI Skills 链接工具。
6
+ 你可以在统一目录(`.agents/skills`)编写技能,并通过符号链接(Symlink/Junction)同步到多个 AI 工具目录。
7
+
8
+ > 核心理念:**一次编写,处处生效。**
9
+
10
+ ## 特性
11
+
12
+ - Node.js 20+ + TypeScript 的简洁架构
13
+ - 基于符号链接同步,零拷贝、即时生效
14
+ - 交互式 `init` 初始化流程
15
+ - `sync --watch` 支持实时监听技能目录变化
16
+ - 安全清理策略(仅清理位于 source 边界内的链接)
17
+ - 支持 CLI 国际化输出(`en` / `zh-CN`)
18
+
19
+ ## 安装
20
+
21
+ 推荐安装为开发依赖:
22
+
23
+ ```bash
24
+ # pnpm
25
+ pnpm add -D @boses/skillink
26
+
27
+ # npm
28
+ npm install -D @boses/skillink
29
+
30
+ # yarn
31
+ yarn add -D @boses/skillink
32
+ ```
33
+
34
+ ## 快速开始
35
+
36
+ ### 1)初始化
37
+
38
+ ```bash
39
+ npx skillink init
40
+ ```
41
+
42
+ `init` 第一步会先询问语言(`English / 简体中文`),然后自动创建:
43
+
44
+ - `.agents/skills`(包含示例技能)
45
+ - `skillink.config.ts`
46
+
47
+ ### 2)编写技能
48
+
49
+ ```text
50
+ .agents/skills/
51
+ └── react-expert/
52
+ └── SKILL.md
53
+ ```
54
+
55
+ ### 3)同步
56
+
57
+ ```bash
58
+ npx skillink sync
59
+ ```
60
+
61
+ 监听模式:
62
+
63
+ ```bash
64
+ npx skillink sync --watch
65
+ ```
66
+
67
+ ## 命令
68
+
69
+ | 命令 | 说明 |
70
+ | :------- | :----------------------------------------------------------------- |
71
+ | `init` | 初始化项目并生成配置。 |
72
+ | `sync` | 同步技能到所有启用目标(支持 `--watch`)。 |
73
+ | `status` | 显示详细同步状态。 |
74
+ | `clean` | 清理配置目标中的已生成符号链接。 |
75
+ | `check` | 基于 npm `versions` 的语义化版本检查更新(不依赖 `latest` 标签)。 |
76
+
77
+ ## 配置说明(`skillink.config.ts`)
78
+
79
+ ```typescript
80
+ import { defineConfig } from '@boses/skillink';
81
+
82
+ export default defineConfig({
83
+ // CLI 语言: 'en' | 'zh-CN'(默认: 'en')
84
+ locale: 'en',
85
+ // 技能源目录
86
+ source: '.agents/skills',
87
+ // 同步目标
88
+ targets: [
89
+ {
90
+ name: 'cursor',
91
+ path: '.cursor/rules',
92
+ enabled: true,
93
+ },
94
+ {
95
+ name: 'gemini',
96
+ path: '.gemini/skills',
97
+ enabled: true,
98
+ },
99
+ ],
100
+ });
101
+ ```
102
+
103
+ ## Git 建议
104
+
105
+ - 推荐提交:`skillink.config.ts`、`.agents/skills/**`
106
+ - 不建议提交:链接产物目录(如 `.cursor/rules`、`.gemini/skills`)
107
+ - `init` 完成后会提示将目标目录加入 `.gitignore`
108
+
109
+ ## 许可证
110
+
111
+ MIT
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';const require = createRequire(import.meta.url);
3
- import "../chunk-WG3NDRGG.js";
3
+ import "../chunk-2UL2JQ4R.js";
4
4
  import "../chunk-AGAXTALJ.js";
@@ -9,7 +9,7 @@ var require_package = __commonJS({
9
9
  "package.json"(exports, module) {
10
10
  module.exports = {
11
11
  name: "@boses/skillink",
12
- version: "1.0.5",
12
+ version: "1.1.0",
13
13
  description: "\u7EDF\u4E00 AI Skills \u7BA1\u7406\u5DE5\u5177 - \u50CF pnpm \u4E00\u6837\u94FE\u63A5\u5230\u5404 AI \u5DE5\u5177\u76EE\u5F55",
14
14
  type: "module",
15
15
  main: "dist/index.js",
@@ -35,7 +35,8 @@ var require_package = __commonJS({
35
35
  start: "node dist/bin/skillink.js",
36
36
  lint: "tsc --noEmit",
37
37
  check: "eslint",
38
- format: "prettier --write src"
38
+ test: "vitest run",
39
+ format: "prettier --write ."
39
40
  },
40
41
  keywords: [
41
42
  "ai",
@@ -71,7 +72,8 @@ var require_package = __commonJS({
71
72
  prettier: "^3.8.1",
72
73
  tsup: "^8.5.1",
73
74
  typescript: "^5.9.3",
74
- "typescript-eslint": "^8.54.0"
75
+ "typescript-eslint": "^8.54.0",
76
+ vitest: "^4.0.18"
75
77
  },
76
78
  engines: {
77
79
  node: ">=20.0.0"
@@ -87,7 +89,7 @@ import { cac } from "cac";
87
89
  import path from "path";
88
90
  import fs2 from "fs/promises";
89
91
  import { existsSync as existsSync2 } from "fs";
90
- import { checkbox, confirm } from "@inquirer/prompts";
92
+ import { checkbox, confirm, select } from "@inquirer/prompts";
91
93
 
92
94
  // src/utils/fs.ts
93
95
  import fs from "fs/promises";
@@ -105,18 +107,40 @@ function isSymlink(p) {
105
107
  }
106
108
  async function createSymlink(target, path5) {
107
109
  const type = process.platform === "win32" ? "junction" : "dir";
108
- if (existsSync(path5)) {
109
- if (isSymlink(path5)) {
110
+ try {
111
+ const stats = await fs.lstat(path5);
112
+ if (stats.isSymbolicLink()) {
110
113
  await fs.unlink(path5);
111
114
  } else {
112
115
  throw new Error(`\u8DEF\u5F84 ${path5} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F\u7B26\u53F7\u94FE\u63A5\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406\u3002`);
113
116
  }
117
+ } catch (error) {
118
+ if (error.code !== "ENOENT") {
119
+ throw error;
120
+ }
114
121
  }
115
122
  await fs.symlink(target, path5, type);
116
123
  }
117
124
 
125
+ // src/utils/locale.ts
126
+ function resolveLocale(locale) {
127
+ return locale === "zh-CN" ? "zh-CN" : "en";
128
+ }
129
+ function isChineseLocale(locale) {
130
+ return locale === "zh-CN";
131
+ }
132
+
118
133
  // src/commands/init.ts
119
- var TEMPLATE_SKILL = `---
134
+ var TEMPLATE_SKILL_EN = `---
135
+ name: Example Skill
136
+ description: This is an example skill generated by Skillink.
137
+ ---
138
+
139
+ # Usage
140
+
141
+ Enable this skill to use it.
142
+ `;
143
+ var TEMPLATE_SKILL_ZH = `---
120
144
  name: \u793A\u4F8B\u6280\u80FD
121
145
  description: \u8FD9\u662F\u4E00\u4E2A\u7531 Skillink \u751F\u6210\u7684\u6F14\u793A\u6280\u80FD\u3002
122
146
  ---
@@ -129,47 +153,88 @@ var DEFAULT_TARGETS = [
129
153
  { name: "Cursor", value: "cursor", path: ".cursor/rules" },
130
154
  { name: "Windsurf", value: "windsurf", path: ".windsurf/rules" },
131
155
  { name: "VSCode", value: "vscode", path: ".vscode/skills" },
132
- { name: "Gemini", value: "gemini", path: ".gemini/modules" }
156
+ { name: "Gemini", value: "gemini", path: ".gemini/skills" }
133
157
  ];
158
+ function getInitText(locale) {
159
+ if (isChineseLocale(locale)) {
160
+ return {
161
+ title: "\u2728 Skillink \u521D\u59CB\u5316",
162
+ createSkillsDir: (skillsDir) => `\u662F\u5426\u5728 ${skillsDir} \u521B\u5EFA\u6280\u80FD\u76EE\u5F55\uFF1F`,
163
+ exampleSkillCreated: "\u2705 \u5DF2\u521B\u5EFA\u793A\u4F8B\u6280\u80FD\u3002",
164
+ skillsDirExists: "\u2139\uFE0F \u6280\u80FD\u76EE\u5F55\u5DF2\u5B58\u5728\u3002",
165
+ selectTargets: "\u9009\u62E9\u8981\u540C\u6B65\u7684 AI \u5DE5\u5177\uFF1A",
166
+ noTargets: "\u26A0\uFE0F \u672A\u9009\u62E9\u4EFB\u4F55\u76EE\u6807\u3002\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u76EE\u6807\u5217\u8868\u5C06\u4E3A\u7A7A\u3002",
167
+ overwriteConfig: "\u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u3002\u662F\u5426\u8986\u76D6\uFF1F",
168
+ initCancelled: "\u274C \u521D\u59CB\u5316\u5DF2\u53D6\u6D88\u3002",
169
+ configCreated: "\u2705 \u5DF2\u521B\u5EFA skillink.config.ts",
170
+ gitAdvice: (paths) => `\u{1F4A1} Git \u5EFA\u8BAE\uFF1A\u8BF7\u5C06\u76EE\u6807\u76EE\u5F55\uFF08${paths}\uFF09\u52A0\u5165 .gitignore\uFF0C\u53EA\u63D0\u4EA4 .agents/skills \u4E0E\u914D\u7F6E\u6587\u4EF6\u3002`,
171
+ nextStep: '\n\u{1F449} \u8FD0\u884C "npx skillink sync" \u5F00\u59CB\u540C\u6B65\uFF01'
172
+ };
173
+ }
174
+ return {
175
+ title: "\u2728 Skillink Initialization",
176
+ createSkillsDir: (skillsDir) => `Create skills directory at ${skillsDir}?`,
177
+ exampleSkillCreated: "\u2705 Example skill created.",
178
+ skillsDirExists: "\u2139\uFE0F Skills directory already exists.",
179
+ selectTargets: "Select AI tools to sync:",
180
+ noTargets: "\u26A0\uFE0F No targets selected. The config will contain an empty targets list.",
181
+ overwriteConfig: "Configuration file already exists. Overwrite?",
182
+ initCancelled: "\u274C Initialization cancelled.",
183
+ configCreated: "\u2705 Created skillink.config.ts",
184
+ gitAdvice: (paths) => `\u{1F4A1} Git tip: Add target directories (${paths}) to .gitignore. Commit only .agents/skills and config files.`,
185
+ nextStep: '\n\u{1F449} Run "npx skillink sync" to start syncing!'
186
+ };
187
+ }
134
188
  async function initCommand(cwd = process.cwd()) {
135
- console.log("\u2728 Skillink \u521D\u59CB\u5316");
189
+ const locale = await select({
190
+ message: "Select language / \u9009\u62E9\u8BED\u8A00",
191
+ choices: [
192
+ { name: "English", value: "en" },
193
+ { name: "\u7B80\u4F53\u4E2D\u6587", value: "zh-CN" }
194
+ ],
195
+ default: "en"
196
+ });
197
+ const text = getInitText(locale);
198
+ const templateSkill = isChineseLocale(locale) ? TEMPLATE_SKILL_ZH : TEMPLATE_SKILL_EN;
199
+ console.log(text.title);
136
200
  const skillsDir = path.join(cwd, ".agents", "skills");
137
201
  const configFile = path.join(cwd, "skillink.config.ts");
138
202
  if (!existsSync2(skillsDir)) {
139
203
  const create = await confirm({
140
- message: `\u662F\u5426\u5728 ${skillsDir} \u521B\u5EFA\u6280\u80FD\u76EE\u5F55\uFF1F`,
204
+ message: text.createSkillsDir(skillsDir),
141
205
  default: true
142
206
  });
143
207
  if (create) {
144
208
  await ensureDir(skillsDir);
145
209
  const exampleDir = path.join(skillsDir, "example-skill");
146
210
  await ensureDir(exampleDir);
147
- await fs2.writeFile(path.join(exampleDir, "SKILL.md"), TEMPLATE_SKILL);
148
- console.log("\u2705 \u5DF2\u521B\u5EFA\u793A\u4F8B\u6280\u80FD\u3002");
211
+ await fs2.writeFile(path.join(exampleDir, "SKILL.md"), templateSkill);
212
+ console.log(text.exampleSkillCreated);
149
213
  }
150
214
  } else {
151
- console.log("\u2139\uFE0F \u6280\u80FD\u76EE\u5F55\u5DF2\u5B58\u5728\u3002");
215
+ console.log(text.skillsDirExists);
152
216
  }
153
217
  const selectedTargets = await checkbox({
154
- message: "\u9009\u62E9\u8981\u540C\u6B65\u7684 AI \u5DE5\u5177\uFF1A",
218
+ message: text.selectTargets,
155
219
  choices: DEFAULT_TARGETS.map((t) => ({ name: t.name, value: t }))
156
220
  });
157
221
  if (selectedTargets.length === 0) {
158
- console.log("\u26A0\uFE0F \u672A\u9009\u62E9\u4EFB\u4F55\u76EE\u6807\u3002\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684\u76EE\u6807\u5217\u8868\u5C06\u4E3A\u7A7A\u3002");
222
+ console.log(text.noTargets);
159
223
  }
160
224
  if (existsSync2(configFile)) {
161
225
  const overwrite = await confirm({
162
- message: "\u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u3002\u662F\u5426\u8986\u76D6\uFF1F",
226
+ message: text.overwriteConfig,
163
227
  default: false
164
228
  });
165
229
  if (!overwrite) {
166
- console.log("\u274C \u521D\u59CB\u5316\u5DF2\u53D6\u6D88\u3002");
230
+ console.log(text.initCancelled);
167
231
  return;
168
232
  }
169
233
  }
170
234
  const configContent = `import { defineConfig } from '@boses/skillink';
171
235
 
172
236
  export default defineConfig({
237
+ locale: '${locale}',
173
238
  source: '.agents/skills',
174
239
  targets: [
175
240
  ${selectedTargets.map(
@@ -183,8 +248,12 @@ ${selectedTargets.map(
183
248
  });
184
249
  `;
185
250
  await fs2.writeFile(configFile, configContent);
186
- console.log("\u2705 \u5DF2\u521B\u5EFA skillink.config.ts");
187
- console.log('\n\u{1F449} \u8FD0\u884C "npx skillink sync" \u5F00\u59CB\u540C\u6B65\uFF01');
251
+ console.log(text.configCreated);
252
+ if (selectedTargets.length > 0) {
253
+ const targetPaths = selectedTargets.map((t) => t.path).join(", ");
254
+ console.log(text.gitAdvice(targetPaths));
255
+ }
256
+ console.log(text.nextStep);
188
257
  }
189
258
 
190
259
  // src/commands/sync.ts
@@ -334,7 +403,11 @@ var Linker = class {
334
403
  * 清理所有由 Skillink 创建的符号链接
335
404
  */
336
405
  async cleanAll() {
337
- const targets = this.config.targets.filter((t) => t.enabled !== false);
406
+ const targets = this.config.targets;
407
+ const absSourceDir = path2.resolve(
408
+ this.root,
409
+ this.config.source || ".agents/skills"
410
+ );
338
411
  for (const target of targets) {
339
412
  const targetDir = path2.resolve(this.root, target.path);
340
413
  if (!existsSync3(targetDir)) continue;
@@ -345,11 +418,8 @@ var Linker = class {
345
418
  try {
346
419
  const linkTarget = await fs3.readlink(itemPath);
347
420
  const absLinkTarget = path2.resolve(targetDir, linkTarget);
348
- const absSourceDir = path2.resolve(
349
- this.root,
350
- this.config.source || ".agents/skills"
351
- );
352
- if (absLinkTarget.startsWith(absSourceDir)) {
421
+ const relative = path2.relative(absSourceDir, absLinkTarget);
422
+ if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
353
423
  await fs3.unlink(itemPath);
354
424
  console.log(`\u5DF2\u4ECE ${target.name} \u79FB\u9664 ${item.name}`);
355
425
  }
@@ -372,13 +442,23 @@ var Linker = class {
372
442
  import pc from "picocolors";
373
443
  async function syncCommand(options) {
374
444
  const cwd = options.cwd || process.cwd();
445
+ const fallbackLocale = resolveLocale();
446
+ const fallbackChinese = isChineseLocale(fallbackLocale);
375
447
  const config = await loadConfig(cwd);
376
448
  if (!config) {
377
- console.error(pc.red('\u274C \u672A\u627E\u5230\u914D\u7F6E\u3002\u8BF7\u5148\u8FD0\u884C "skillink init"\u3002'));
449
+ console.error(
450
+ pc.red(
451
+ fallbackChinese ? '\u274C \u672A\u627E\u5230\u914D\u7F6E\u3002\u8BF7\u5148\u8FD0\u884C "skillink init"\u3002' : '\u274C Configuration not found. Run "skillink init" first.'
452
+ )
453
+ );
378
454
  process.exit(1);
379
455
  }
456
+ const locale = resolveLocale(config.locale);
457
+ const isChinese = isChineseLocale(locale);
380
458
  const linker = new Linker(cwd, config);
381
- console.log(pc.cyan("\u{1F504} \u6B63\u5728\u540C\u6B65\u6280\u80FD..."));
459
+ console.log(
460
+ pc.cyan(isChinese ? "\u{1F504} \u6B63\u5728\u540C\u6B65\u6280\u80FD..." : "\u{1F504} Syncing skills...")
461
+ );
382
462
  const results = await linker.sync();
383
463
  let changes = 0;
384
464
  results.forEach((r) => {
@@ -388,33 +468,67 @@ async function syncCommand(options) {
388
468
  );
389
469
  changes++;
390
470
  } else if (r.status === "failed") {
391
- console.error(pc.red(`\u274C ${r.skill} -> ${r.target}: ${r.message}`));
471
+ console.error(
472
+ pc.red(
473
+ isChinese ? `\u274C ${r.skill} -> ${r.target}: ${r.message}` : `\u274C ${r.skill} -> ${r.target}: ${r.message}`
474
+ )
475
+ );
392
476
  }
393
477
  });
394
478
  if (changes === 0) {
395
- console.log(pc.gray("\u65E0\u9700\u66F4\u6539\u3002\u6240\u6709\u6280\u80FD\u5DF2\u540C\u6B65\u3002"));
479
+ console.log(
480
+ pc.gray(
481
+ isChinese ? "\u65E0\u9700\u66F4\u6539\u3002\u6240\u6709\u6280\u80FD\u5DF2\u540C\u6B65\u3002" : "No changes needed. All skills are already synced."
482
+ )
483
+ );
396
484
  } else {
397
- console.log(pc.green(`\u2705 \u5DF2\u540C\u6B65 ${changes} \u5904\u53D8\u66F4\u3002`));
485
+ console.log(
486
+ pc.green(
487
+ isChinese ? `\u2705 \u5DF2\u540C\u6B65 ${changes} \u5904\u53D8\u66F4\u3002` : `\u2705 Synced ${changes} change(s).`
488
+ )
489
+ );
398
490
  }
399
491
  if (options.watch) {
400
- console.log(pc.cyan("\n\u{1F440} \u6B63\u5728\u76D1\u89C6\u53D8\u66F4... \u6309 Ctrl+C \u505C\u6B62\u3002"));
492
+ console.log(
493
+ pc.cyan(
494
+ isChinese ? "\n\u{1F440} \u6B63\u5728\u76D1\u89C6\u53D8\u66F4... \u6309 Ctrl+C \u505C\u6B62\u3002" : "\n\u{1F440} Watching for changes... Press Ctrl+C to stop."
495
+ )
496
+ );
401
497
  const sourceDir = path3.resolve(cwd, config.source || ".agents/skills");
402
498
  const watcher = chokidar.watch(sourceDir, {
403
499
  ignoreInitial: true,
404
- depth: 0,
500
+ depth: 1,
405
501
  awaitWriteFinish: {
406
502
  stabilityThreshold: 100,
407
503
  pollInterval: 100
408
504
  }
409
505
  });
410
506
  watcher.on("all", async (event, filePath) => {
507
+ if (path3.dirname(filePath) !== sourceDir) return;
411
508
  const fileName = path3.basename(filePath);
412
- if (event === "addDir") {
413
- console.log(pc.green(`+ \u68C0\u6D4B\u5230\u65B0\u6280\u80FD: ${fileName}`));
414
- await linker.syncSkillToAll(fileName);
415
- } else if (event === "unlinkDir") {
416
- console.log(pc.red(`- \u6280\u80FD\u5DF2\u79FB\u9664: ${fileName}`));
417
- await linker.removeSkillFromAll(fileName);
509
+ if (!fileName || fileName.startsWith(".")) return;
510
+ try {
511
+ if (event === "addDir") {
512
+ console.log(
513
+ pc.green(
514
+ isChinese ? `+ \u68C0\u6D4B\u5230\u65B0\u6280\u80FD: ${fileName}` : `+ New skill detected: ${fileName}`
515
+ )
516
+ );
517
+ await linker.syncSkillToAll(fileName);
518
+ } else if (event === "unlinkDir") {
519
+ console.log(
520
+ pc.red(
521
+ isChinese ? `- \u6280\u80FD\u5DF2\u79FB\u9664: ${fileName}` : `- Skill removed: ${fileName}`
522
+ )
523
+ );
524
+ await linker.removeSkillFromAll(fileName);
525
+ }
526
+ } catch (error) {
527
+ console.error(
528
+ pc.red(
529
+ isChinese ? `\u274C \u5904\u7406\u76D1\u89C6\u4E8B\u4EF6\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}` : `\u274C Failed to process watch event: ${error instanceof Error ? error.message : String(error)}`
530
+ )
531
+ );
418
532
  }
419
533
  });
420
534
  }
@@ -440,56 +554,110 @@ import { existsSync as existsSync4 } from "fs";
440
554
  async function statusCommand(options) {
441
555
  const cwd = options.cwd || process.cwd();
442
556
  const config = await loadConfig(cwd);
557
+ const locale = resolveLocale(config?.locale);
558
+ const isChinese = isChineseLocale(locale);
443
559
  if (!config) {
444
- logger.error('\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\u3002\u8BF7\u5148\u8FD0\u884C "skillink init"\u3002');
560
+ logger.error(
561
+ isChinese ? '\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\u3002\u8BF7\u5148\u8FD0\u884C "skillink init"\u3002' : 'Configuration file not found. Run "skillink init" first.'
562
+ );
445
563
  return;
446
564
  }
447
- logger.title("Skillink \u540C\u6B65\u72B6\u6001");
565
+ logger.title(isChinese ? "Skillink \u540C\u6B65\u72B6\u6001" : "Skillink Sync Status");
448
566
  logger.newline();
449
567
  const sourcePath = path4.resolve(cwd, config.source || ".agents/skills");
450
- logger.info(`\u6E90\u76EE\u5F55: ${sourcePath}`);
568
+ logger.info(
569
+ isChinese ? `\u6E90\u76EE\u5F55: ${sourcePath}` : `Source directory: ${sourcePath}`
570
+ );
451
571
  if (!existsSync4(sourcePath)) {
452
- logger.error("\u6E90\u76EE\u5F55\u4E0D\u5B58\u5728\uFF01");
572
+ logger.error(
573
+ isChinese ? "\u6E90\u76EE\u5F55\u4E0D\u5B58\u5728\uFF01" : "Source directory does not exist!"
574
+ );
453
575
  return;
454
576
  }
455
577
  const skills = await fs4.readdir(sourcePath, { withFileTypes: true });
456
578
  const validSkills = skills.filter((s) => s.isDirectory() && !s.name.startsWith(".")).map((s) => s.name);
457
- logger.gray(`\u627E\u5230 ${validSkills.length} \u4E2A\u6280\u80FD\u3002`);
579
+ logger.gray(
580
+ isChinese ? `\u627E\u5230 ${validSkills.length} \u4E2A\u6280\u80FD\u3002` : `Found ${validSkills.length} skill(s).`
581
+ );
458
582
  logger.newline();
459
- logger.info("\u76EE\u6807\u5DE5\u5177:");
583
+ logger.info(isChinese ? "\u76EE\u6807\u5DE5\u5177:" : "Targets:");
460
584
  for (const target of config.targets) {
461
585
  if (target.enabled === false) continue;
462
586
  const targetDir = path4.resolve(cwd, target.path);
463
587
  console.log(`${pc3.bold(target.name)} [${targetDir}]`);
464
588
  if (!existsSync4(targetDir)) {
465
- console.log(pc3.red(" - \u76EE\u5F55\u7F3A\u5931\uFF08\u8FD0\u884C sync \u547D\u4EE4\u4EE5\u521B\u5EFA\uFF09"));
589
+ console.log(
590
+ pc3.red(
591
+ isChinese ? " - \u76EE\u5F55\u7F3A\u5931\uFF08\u8FD0\u884C sync \u547D\u4EE4\u4EE5\u521B\u5EFA\uFF09" : " - Missing directory (run sync to create it)"
592
+ )
593
+ );
466
594
  continue;
467
595
  }
468
596
  let syncedCount = 0;
469
597
  let missingCount = 0;
470
598
  let brokenCount = 0;
599
+ let mismatchedCount = 0;
600
+ let occupiedCount = 0;
471
601
  for (const skill of validSkills) {
472
602
  const linkPath = path4.join(targetDir, skill);
473
- if (!existsSync4(linkPath)) {
603
+ const sourceSkillPath = path4.resolve(sourcePath, skill);
604
+ try {
605
+ const stats = await fs4.lstat(linkPath);
606
+ if (!stats.isSymbolicLink()) {
607
+ occupiedCount++;
608
+ continue;
609
+ }
474
610
  try {
475
- const stats = await fs4.lstat(linkPath);
476
- if (stats.isSymbolicLink()) {
477
- brokenCount++;
611
+ const currentTarget = await fs4.readlink(linkPath);
612
+ const absCurrentTarget = path4.resolve(targetDir, currentTarget);
613
+ if (absCurrentTarget === sourceSkillPath) {
614
+ syncedCount++;
478
615
  } else {
479
- missingCount++;
616
+ mismatchedCount++;
480
617
  }
481
618
  } catch {
619
+ brokenCount++;
620
+ }
621
+ } catch (error) {
622
+ if (error.code === "ENOENT") {
482
623
  missingCount++;
624
+ continue;
483
625
  }
484
- } else {
485
- syncedCount++;
626
+ missingCount++;
486
627
  }
487
628
  }
488
- if (missingCount > 0) console.log(pc3.yellow(` - ${missingCount} \u4E2A\u7F3A\u5931`));
489
- if (brokenCount > 0) console.log(pc3.red(` - ${brokenCount} \u4E2A\u5931\u6548\u94FE\u63A5`));
490
- if (syncedCount > 0) console.log(pc3.green(` - ${syncedCount} \u4E2A\u5DF2\u540C\u6B65`));
491
- if (missingCount === 0 && brokenCount === 0)
492
- console.log(pc3.green(" \u72B6\u6001\u826F\u597D\uFF01"));
629
+ if (missingCount > 0)
630
+ console.log(
631
+ pc3.yellow(
632
+ isChinese ? ` - ${missingCount} \u4E2A\u7F3A\u5931` : ` - ${missingCount} missing`
633
+ )
634
+ );
635
+ if (brokenCount > 0)
636
+ console.log(
637
+ pc3.red(
638
+ isChinese ? ` - ${brokenCount} \u4E2A\u5931\u6548\u94FE\u63A5` : ` - ${brokenCount} broken link(s)`
639
+ )
640
+ );
641
+ if (mismatchedCount > 0)
642
+ console.log(
643
+ pc3.red(
644
+ isChinese ? ` - ${mismatchedCount} \u4E2A\u9519\u8BEF\u6307\u5411\u94FE\u63A5` : ` - ${mismatchedCount} mismatched link(s)`
645
+ )
646
+ );
647
+ if (occupiedCount > 0)
648
+ console.log(
649
+ pc3.yellow(
650
+ isChinese ? ` - ${occupiedCount} \u4E2A\u540C\u540D\u5360\u4F4D\uFF08\u975E\u94FE\u63A5\uFF09` : ` - ${occupiedCount} occupied by non-link`
651
+ )
652
+ );
653
+ if (syncedCount > 0)
654
+ console.log(
655
+ pc3.green(
656
+ isChinese ? ` - ${syncedCount} \u4E2A\u5DF2\u540C\u6B65` : ` - ${syncedCount} synced`
657
+ )
658
+ );
659
+ if (missingCount === 0 && brokenCount === 0 && mismatchedCount === 0 && occupiedCount === 0)
660
+ console.log(pc3.green(isChinese ? " \u72B6\u6001\u826F\u597D\uFF01" : " Healthy!"));
493
661
  console.log("");
494
662
  }
495
663
  }
@@ -499,19 +667,21 @@ import { confirm as confirm2 } from "@inquirer/prompts";
499
667
  async function cleanCommand(options = {}) {
500
668
  const cwd = options.cwd || process.cwd();
501
669
  const config = await loadConfig(cwd);
670
+ const locale = resolveLocale(config?.locale);
671
+ const isChinese = isChineseLocale(locale);
502
672
  if (!config) {
503
- logger.error("\u672A\u627E\u5230\u914D\u7F6E\u3002");
673
+ logger.error(isChinese ? "\u672A\u627E\u5230\u914D\u7F6E\u3002" : "Configuration not found.");
504
674
  return;
505
675
  }
506
676
  const answer = await confirm2({
507
- message: "\u786E\u5B9A\u8981\u79FB\u9664\u6240\u6709\u5DF2\u540C\u6B65\u7684\u6280\u80FD\u94FE\u63A5\u5417\uFF1F",
677
+ message: isChinese ? "\u786E\u5B9A\u8981\u79FB\u9664\u6240\u6709\u5DF2\u540C\u6B65\u7684\u6280\u80FD\u94FE\u63A5\u5417\uFF1F" : "Remove all synced skill links?",
508
678
  default: false
509
679
  });
510
680
  if (!answer) return;
511
681
  const linker = new Linker(cwd, config);
512
- logger.info("\u6B63\u5728\u6E05\u7406\u94FE\u63A5...");
682
+ logger.info(isChinese ? "\u6B63\u5728\u6E05\u7406\u94FE\u63A5..." : "Cleaning links...");
513
683
  await linker.cleanAll();
514
- logger.success("\u6E05\u7406\u5B8C\u6210\u3002");
684
+ logger.success(isChinese ? "\u6E05\u7406\u5B8C\u6210\u3002" : "Clean completed.");
515
685
  }
516
686
 
517
687
  // src/commands/check.ts
@@ -522,11 +692,21 @@ import semver from "semver";
522
692
  var pkg = require_package();
523
693
  var currentVersion = pkg.version;
524
694
  var pkgName = pkg.name;
695
+ function resolveLatestSemverVersion(versions) {
696
+ const validStableVersions = versions.filter(
697
+ (version) => semver.valid(version) && semver.prerelease(version) === null
698
+ );
699
+ const sorted = semver.rsort(validStableVersions);
700
+ if (sorted.length === 0) {
701
+ throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684\u7A33\u5B9A\u8BED\u4E49\u5316\u7248\u672C");
702
+ }
703
+ return sorted[0];
704
+ }
525
705
  async function checkUpdate() {
526
706
  const controller = new AbortController();
527
707
  const timeoutId = setTimeout(() => controller.abort(), 3e3);
528
708
  try {
529
- const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, {
709
+ const res = await fetch(`https://registry.npmjs.org/${pkg.name}`, {
530
710
  signal: controller.signal
531
711
  });
532
712
  clearTimeout(timeoutId);
@@ -534,12 +714,14 @@ async function checkUpdate() {
534
714
  throw new Error(`\u8BF7\u6C42\u5931\u8D25: ${res.status} ${res.statusText}`);
535
715
  }
536
716
  const data = await res.json();
537
- const latestVersion = data.version;
717
+ const latestVersion = resolveLatestSemverVersion(
718
+ Object.keys(data.versions ?? {})
719
+ );
538
720
  return {
539
721
  latest: latestVersion,
540
722
  current: currentVersion,
541
723
  hasUpdate: semver.gt(latestVersion, currentVersion),
542
- name: pkg.name
724
+ name: pkgName
543
725
  };
544
726
  } catch (error) {
545
727
  clearTimeout(timeoutId);
@@ -549,7 +731,12 @@ async function checkUpdate() {
549
731
 
550
732
  // src/commands/check.ts
551
733
  async function checkCommand() {
552
- logger.info("\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...");
734
+ const config = await loadConfig();
735
+ const locale = resolveLocale(config?.locale);
736
+ const isChinese = isChineseLocale(locale);
737
+ logger.info(
738
+ isChinese ? "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\uFF08\u8BED\u4E49\u5316\u7248\u672C\u6BD4\u8F83\uFF09..." : "Checking updates (semantic version comparison)..."
739
+ );
553
740
  try {
554
741
  const info = await checkUpdate();
555
742
  if (info.hasUpdate) {
@@ -562,7 +749,7 @@ async function checkCommand() {
562
749
  );
563
750
  console.log(
564
751
  pc4.yellow(
565
- ` \u2502 \u53D1\u73B0\u65B0\u7248\u672C ${pc4.green(info.latest)} (\u5F53\u524D ${pc4.gray(info.current)}) \u2502`
752
+ isChinese ? ` \u2502 \u53D1\u73B0\u65B0\u7248\u672C ${pc4.green(info.latest)} (\u5F53\u524D ${pc4.gray(info.current)}) \u2502` : ` \u2502 New version ${pc4.green(info.latest)} (current ${pc4.gray(info.current)}) \u2502`
566
753
  )
567
754
  );
568
755
  console.log(
@@ -570,7 +757,7 @@ async function checkCommand() {
570
757
  );
571
758
  console.log(
572
759
  pc4.yellow(
573
- ` \u2502 \u8BF7\u8FD0\u884C ${pc4.cyan(`npm i -g ${info.name}`)} \u66F4\u65B0 \u2502`
760
+ isChinese ? ` \u2502 \u8BF7\u8FD0\u884C ${pc4.cyan(`npm install -D ${info.name}@${info.latest}`)} \u66F4\u65B0 \u2502` : ` \u2502 Run ${pc4.cyan(`npm install -D ${info.name}@${info.latest}`)} to update \u2502`
574
761
  )
575
762
  );
576
763
  console.log(
@@ -581,11 +768,13 @@ async function checkCommand() {
581
768
  );
582
769
  console.log();
583
770
  } else {
584
- logger.success(`\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C (${pc4.green(info.current)})`);
771
+ logger.success(
772
+ isChinese ? `\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7A33\u5B9A\u7248\u672C (${pc4.green(info.current)})` : `You are on the latest stable version (${pc4.green(info.current)})`
773
+ );
585
774
  }
586
775
  } catch (error) {
587
776
  logger.error(
588
- `\u68C0\u67E5\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
777
+ isChinese ? `\u68C0\u67E5\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}` : `Update check failed: ${error instanceof Error ? error.message : String(error)}`
589
778
  );
590
779
  }
591
780
  }
@@ -593,11 +782,14 @@ async function checkCommand() {
593
782
  // src/cli.ts
594
783
  var cli = cac("skillink");
595
784
  cli.version(currentVersion);
596
- cli.command("init", "\u521D\u59CB\u5316 Skillink \u914D\u7F6E").action(() => initCommand());
597
- cli.command("sync", "\u5C06\u6280\u80FD\u540C\u6B65\u5230\u76EE\u6807\u5DE5\u5177").option("-w, --watch", "\u76D1\u89C6\u6587\u4EF6\u53D8\u66F4").action((options) => syncCommand(options));
598
- cli.command("status", "\u663E\u793A\u540C\u6B65\u72B6\u6001").action(() => statusCommand({}));
599
- cli.command("clean", "\u79FB\u9664\u6240\u6709\u751F\u6210\u7684\u7B26\u53F7\u94FE\u63A5").action(() => cleanCommand());
600
- cli.command("check", "\u68C0\u67E5\u7248\u672C\u66F4\u65B0").action(() => checkCommand());
785
+ cli.command(
786
+ "init",
787
+ "Initialize Skillink configuration / \u521D\u59CB\u5316 Skillink \u914D\u7F6E"
788
+ ).action(() => initCommand());
789
+ cli.command("sync", "Sync skills to configured targets / \u540C\u6B65\u6280\u80FD\u5230\u76EE\u6807\u5DE5\u5177").option("-w, --watch", "Watch file changes / \u76D1\u89C6\u6587\u4EF6\u53D8\u66F4").action((options) => syncCommand(options));
790
+ cli.command("status", "Show sync status / \u663E\u793A\u540C\u6B65\u72B6\u6001").action(() => statusCommand({}));
791
+ cli.command("clean", "Remove generated symlinks / \u79FB\u9664\u751F\u6210\u7684\u7B26\u53F7\u94FE\u63A5").action(() => cleanCommand());
792
+ cli.command("check", "Check package updates / \u68C0\u67E5\u7248\u672C\u66F4\u65B0").action(() => checkCommand());
601
793
  cli.help();
602
794
  try {
603
795
  cli.parse();
package/dist/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { createRequire } from 'module';const require = createRequire(import.meta.url);
2
- import "./chunk-WG3NDRGG.js";
2
+ import "./chunk-2UL2JQ4R.js";
3
3
  import "./chunk-AGAXTALJ.js";
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ type Locale = 'en' | 'zh-CN';
1
2
  interface SyncTarget {
2
3
  /** 目标名称(如 'Cursor', 'VSCode') */
3
4
  name: string;
@@ -9,10 +10,10 @@ interface SyncTarget {
9
10
  interface SkillinkConfig {
10
11
  /** 技能源目录(默认为 .agents/skills) */
11
12
  source?: string;
13
+ /** CLI 输出语言(默认 en) */
14
+ locale?: Locale;
12
15
  /** 同步目标列表 */
13
16
  targets: SyncTarget[];
14
- /** 忽略的文件模式(暂未使用) */
15
- ignore?: string[];
16
17
  }
17
18
  interface Skill {
18
19
  /** 技能名称(文件夹名) */
@@ -43,4 +44,4 @@ declare function loadConfig(cwd?: string): Promise<SkillinkConfig | null>;
43
44
  */
44
45
  declare function defineConfig(config: SkillinkConfig): SkillinkConfig;
45
46
 
46
- export { type Skill, type SkillinkConfig, type SyncResult, type SyncTarget, defineConfig, loadConfig };
47
+ export { type Locale, type Skill, type SkillinkConfig, type SyncResult, type SyncTarget, defineConfig, loadConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boses/skillink",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "统一 AI Skills 管理工具 - 像 pnpm 一样链接到各 AI 工具目录",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,7 +53,8 @@
53
53
  "prettier": "^3.8.1",
54
54
  "tsup": "^8.5.1",
55
55
  "typescript": "^5.9.3",
56
- "typescript-eslint": "^8.54.0"
56
+ "typescript-eslint": "^8.54.0",
57
+ "vitest": "^4.0.18"
57
58
  },
58
59
  "engines": {
59
60
  "node": ">=20.0.0"
@@ -64,6 +65,7 @@
64
65
  "start": "node dist/bin/skillink.js",
65
66
  "lint": "tsc --noEmit",
66
67
  "check": "eslint",
67
- "format": "prettier --write src"
68
+ "test": "vitest run",
69
+ "format": "prettier --write ."
68
70
  }
69
71
  }