@boses/skillink 0.0.0 → 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/LICENSE +21 -21
- package/README.md +55 -55
- package/README.zh-CN.md +111 -0
- package/dist/bin/skillink.js +2 -6
- package/dist/chunk-2UL2JQ4R.js +799 -0
- package/dist/chunk-AGAXTALJ.js +41 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +46 -1
- package/dist/index.js +8 -1
- package/package.json +14 -7
- package/dist/chunk-ZEFDUUIX.js +0 -2628
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
__commonJS,
|
|
4
|
+
loadConfig
|
|
5
|
+
} from "./chunk-AGAXTALJ.js";
|
|
6
|
+
|
|
7
|
+
// package.json
|
|
8
|
+
var require_package = __commonJS({
|
|
9
|
+
"package.json"(exports, module) {
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: "@boses/skillink",
|
|
12
|
+
version: "1.1.0",
|
|
13
|
+
description: "\u7EDF\u4E00 AI Skills \u7BA1\u7406\u5DE5\u5177 - \u50CF pnpm \u4E00\u6837\u94FE\u63A5\u5230\u5404 AI \u5DE5\u5177\u76EE\u5F55",
|
|
14
|
+
type: "module",
|
|
15
|
+
main: "dist/index.js",
|
|
16
|
+
module: "dist/index.js",
|
|
17
|
+
types: "dist/index.d.ts",
|
|
18
|
+
exports: {
|
|
19
|
+
".": {
|
|
20
|
+
import: "./dist/index.js",
|
|
21
|
+
types: "./dist/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
bin: {
|
|
25
|
+
skillink: "./dist/bin/skillink.js"
|
|
26
|
+
},
|
|
27
|
+
files: [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
scripts: {
|
|
33
|
+
build: "tsup",
|
|
34
|
+
dev: "tsup --watch",
|
|
35
|
+
start: "node dist/bin/skillink.js",
|
|
36
|
+
lint: "tsc --noEmit",
|
|
37
|
+
check: "eslint",
|
|
38
|
+
test: "vitest run",
|
|
39
|
+
format: "prettier --write ."
|
|
40
|
+
},
|
|
41
|
+
keywords: [
|
|
42
|
+
"ai",
|
|
43
|
+
"skills",
|
|
44
|
+
"cursor",
|
|
45
|
+
"gemini",
|
|
46
|
+
"cli",
|
|
47
|
+
"sync",
|
|
48
|
+
"symlink"
|
|
49
|
+
],
|
|
50
|
+
author: "",
|
|
51
|
+
license: "MIT",
|
|
52
|
+
repository: {
|
|
53
|
+
type: "git",
|
|
54
|
+
url: "https://github.com/bosens-China/skillink"
|
|
55
|
+
},
|
|
56
|
+
packageManager: "pnpm@10.18.2",
|
|
57
|
+
dependencies: {
|
|
58
|
+
"@inquirer/prompts": "^8.2.0",
|
|
59
|
+
cac: "^6.7.14",
|
|
60
|
+
chokidar: "^5.0.0",
|
|
61
|
+
jiti: "^2.6.1",
|
|
62
|
+
picocolors: "^1.1.1",
|
|
63
|
+
semver: "^7.7.4"
|
|
64
|
+
},
|
|
65
|
+
devDependencies: {
|
|
66
|
+
"@eslint/js": "^10.0.1",
|
|
67
|
+
"@eslint/markdown": "^7.5.1",
|
|
68
|
+
"@types/node": "^25.2.1",
|
|
69
|
+
"@types/semver": "^7.7.1",
|
|
70
|
+
eslint: "^10.0.0",
|
|
71
|
+
globals: "^17.3.0",
|
|
72
|
+
prettier: "^3.8.1",
|
|
73
|
+
tsup: "^8.5.1",
|
|
74
|
+
typescript: "^5.9.3",
|
|
75
|
+
"typescript-eslint": "^8.54.0",
|
|
76
|
+
vitest: "^4.0.18"
|
|
77
|
+
},
|
|
78
|
+
engines: {
|
|
79
|
+
node: ">=20.0.0"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/cli.ts
|
|
86
|
+
import { cac } from "cac";
|
|
87
|
+
|
|
88
|
+
// src/commands/init.ts
|
|
89
|
+
import path from "path";
|
|
90
|
+
import fs2 from "fs/promises";
|
|
91
|
+
import { existsSync as existsSync2 } from "fs";
|
|
92
|
+
import { checkbox, confirm, select } from "@inquirer/prompts";
|
|
93
|
+
|
|
94
|
+
// src/utils/fs.ts
|
|
95
|
+
import fs from "fs/promises";
|
|
96
|
+
import { existsSync, lstatSync } from "fs";
|
|
97
|
+
async function ensureDir(dir) {
|
|
98
|
+
await fs.mkdir(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
function isSymlink(p) {
|
|
101
|
+
try {
|
|
102
|
+
const stats = lstatSync(p);
|
|
103
|
+
return stats.isSymbolicLink();
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function createSymlink(target, path5) {
|
|
109
|
+
const type = process.platform === "win32" ? "junction" : "dir";
|
|
110
|
+
try {
|
|
111
|
+
const stats = await fs.lstat(path5);
|
|
112
|
+
if (stats.isSymbolicLink()) {
|
|
113
|
+
await fs.unlink(path5);
|
|
114
|
+
} else {
|
|
115
|
+
throw new Error(`\u8DEF\u5F84 ${path5} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F\u7B26\u53F7\u94FE\u63A5\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406\u3002`);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code !== "ENOENT") {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await fs.symlink(target, path5, type);
|
|
123
|
+
}
|
|
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
|
+
|
|
133
|
+
// src/commands/init.ts
|
|
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 = `---
|
|
144
|
+
name: \u793A\u4F8B\u6280\u80FD
|
|
145
|
+
description: \u8FD9\u662F\u4E00\u4E2A\u7531 Skillink \u751F\u6210\u7684\u6F14\u793A\u6280\u80FD\u3002
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
# \u4F7F\u7528\u8BF4\u660E
|
|
149
|
+
|
|
150
|
+
\u6FC0\u6D3B\u6B64\u6280\u80FD\u5373\u53EF\u4F7F\u7528\u3002
|
|
151
|
+
`;
|
|
152
|
+
var DEFAULT_TARGETS = [
|
|
153
|
+
{ name: "Cursor", value: "cursor", path: ".cursor/rules" },
|
|
154
|
+
{ name: "Windsurf", value: "windsurf", path: ".windsurf/rules" },
|
|
155
|
+
{ name: "VSCode", value: "vscode", path: ".vscode/skills" },
|
|
156
|
+
{ name: "Gemini", value: "gemini", path: ".gemini/skills" }
|
|
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
|
+
}
|
|
188
|
+
async function initCommand(cwd = process.cwd()) {
|
|
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);
|
|
200
|
+
const skillsDir = path.join(cwd, ".agents", "skills");
|
|
201
|
+
const configFile = path.join(cwd, "skillink.config.ts");
|
|
202
|
+
if (!existsSync2(skillsDir)) {
|
|
203
|
+
const create = await confirm({
|
|
204
|
+
message: text.createSkillsDir(skillsDir),
|
|
205
|
+
default: true
|
|
206
|
+
});
|
|
207
|
+
if (create) {
|
|
208
|
+
await ensureDir(skillsDir);
|
|
209
|
+
const exampleDir = path.join(skillsDir, "example-skill");
|
|
210
|
+
await ensureDir(exampleDir);
|
|
211
|
+
await fs2.writeFile(path.join(exampleDir, "SKILL.md"), templateSkill);
|
|
212
|
+
console.log(text.exampleSkillCreated);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
console.log(text.skillsDirExists);
|
|
216
|
+
}
|
|
217
|
+
const selectedTargets = await checkbox({
|
|
218
|
+
message: text.selectTargets,
|
|
219
|
+
choices: DEFAULT_TARGETS.map((t) => ({ name: t.name, value: t }))
|
|
220
|
+
});
|
|
221
|
+
if (selectedTargets.length === 0) {
|
|
222
|
+
console.log(text.noTargets);
|
|
223
|
+
}
|
|
224
|
+
if (existsSync2(configFile)) {
|
|
225
|
+
const overwrite = await confirm({
|
|
226
|
+
message: text.overwriteConfig,
|
|
227
|
+
default: false
|
|
228
|
+
});
|
|
229
|
+
if (!overwrite) {
|
|
230
|
+
console.log(text.initCancelled);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const configContent = `import { defineConfig } from '@boses/skillink';
|
|
235
|
+
|
|
236
|
+
export default defineConfig({
|
|
237
|
+
locale: '${locale}',
|
|
238
|
+
source: '.agents/skills',
|
|
239
|
+
targets: [
|
|
240
|
+
${selectedTargets.map(
|
|
241
|
+
(t) => ` {
|
|
242
|
+
name: '${t.value}',
|
|
243
|
+
path: '${t.path}',
|
|
244
|
+
enabled: true,
|
|
245
|
+
},`
|
|
246
|
+
).join("\n")}
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
`;
|
|
250
|
+
await fs2.writeFile(configFile, configContent);
|
|
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);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/commands/sync.ts
|
|
260
|
+
import chokidar from "chokidar";
|
|
261
|
+
import path3 from "path";
|
|
262
|
+
|
|
263
|
+
// src/core/linker.ts
|
|
264
|
+
import path2 from "path";
|
|
265
|
+
import fs3 from "fs/promises";
|
|
266
|
+
import { existsSync as existsSync3 } from "fs";
|
|
267
|
+
var Linker = class {
|
|
268
|
+
config;
|
|
269
|
+
root;
|
|
270
|
+
constructor(root, config) {
|
|
271
|
+
this.root = root;
|
|
272
|
+
this.config = config;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 将所有技能同步到所有目标
|
|
276
|
+
*/
|
|
277
|
+
async sync() {
|
|
278
|
+
const results = [];
|
|
279
|
+
const sourceDir = path2.resolve(
|
|
280
|
+
this.root,
|
|
281
|
+
this.config.source || ".agents/skills"
|
|
282
|
+
);
|
|
283
|
+
if (!existsSync3(sourceDir)) {
|
|
284
|
+
throw new Error(`\u672A\u627E\u5230\u6E90\u76EE\u5F55: ${sourceDir}`);
|
|
285
|
+
}
|
|
286
|
+
const skills = await this.getSkills(sourceDir);
|
|
287
|
+
const targets = this.config.targets.filter((t) => t.enabled !== false);
|
|
288
|
+
for (const target of targets) {
|
|
289
|
+
const targetDir = path2.resolve(this.root, target.path);
|
|
290
|
+
try {
|
|
291
|
+
await ensureDir(targetDir);
|
|
292
|
+
for (const skill of skills) {
|
|
293
|
+
const result = await this.syncSkill(sourceDir, targetDir, skill);
|
|
294
|
+
results.push({ ...result, target: target.name });
|
|
295
|
+
}
|
|
296
|
+
const cleanResults = await this.cleanStale(targetDir, skills);
|
|
297
|
+
results.push(
|
|
298
|
+
...cleanResults.map((r) => ({ ...r, target: target.name }))
|
|
299
|
+
);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
results.push({
|
|
302
|
+
skill: "*",
|
|
303
|
+
target: target.name,
|
|
304
|
+
status: "failed",
|
|
305
|
+
message: `\u76EE\u6807\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return results;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 同步单个技能(创建或修复链接)
|
|
313
|
+
*/
|
|
314
|
+
async syncSkill(sourceRoot, targetRoot, skillName) {
|
|
315
|
+
const sourcePath = path2.join(sourceRoot, skillName);
|
|
316
|
+
const targetPath = path2.join(targetRoot, skillName);
|
|
317
|
+
try {
|
|
318
|
+
if (existsSync3(targetPath)) {
|
|
319
|
+
if (isSymlink(targetPath)) {
|
|
320
|
+
const currentTarget = await fs3.readlink(targetPath);
|
|
321
|
+
const absCurrent = path2.resolve(
|
|
322
|
+
path2.dirname(targetPath),
|
|
323
|
+
currentTarget
|
|
324
|
+
);
|
|
325
|
+
const absSource = path2.resolve(sourcePath);
|
|
326
|
+
if (absCurrent === absSource) {
|
|
327
|
+
return {
|
|
328
|
+
skill: skillName,
|
|
329
|
+
target: "",
|
|
330
|
+
status: "skipped",
|
|
331
|
+
message: "\u5DF2\u6B63\u786E\u94FE\u63A5"
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await createSymlink(sourcePath, targetPath);
|
|
337
|
+
return { skill: skillName, target: "", status: "linked" };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
skill: skillName,
|
|
341
|
+
target: "",
|
|
342
|
+
status: "failed",
|
|
343
|
+
message: error instanceof Error ? error.message : String(error)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 清理目标目录中的失效链接
|
|
349
|
+
*/
|
|
350
|
+
async cleanStale(targetRoot, validSkills) {
|
|
351
|
+
const results = [];
|
|
352
|
+
if (!existsSync3(targetRoot)) return [];
|
|
353
|
+
const items = await fs3.readdir(targetRoot, { withFileTypes: true });
|
|
354
|
+
for (const item of items) {
|
|
355
|
+
if (item.isDirectory() || item.isSymbolicLink()) {
|
|
356
|
+
if (!validSkills.includes(item.name)) {
|
|
357
|
+
const itemPath = path2.join(targetRoot, item.name);
|
|
358
|
+
if (isSymlink(itemPath)) {
|
|
359
|
+
await fs3.unlink(itemPath);
|
|
360
|
+
results.push({
|
|
361
|
+
skill: item.name,
|
|
362
|
+
target: "",
|
|
363
|
+
status: "cleaned",
|
|
364
|
+
message: "\u5DF2\u79FB\u9664\u5931\u6548\u94FE\u63A5"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return results;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* 将特定技能同步到所有目标(用于 Watch 模式)
|
|
374
|
+
*/
|
|
375
|
+
async syncSkillToAll(skillName) {
|
|
376
|
+
const sourceDir = path2.resolve(
|
|
377
|
+
this.root,
|
|
378
|
+
this.config.source || ".agents/skills"
|
|
379
|
+
);
|
|
380
|
+
const targets = this.config.targets.filter((t) => t.enabled !== false);
|
|
381
|
+
for (const target of targets) {
|
|
382
|
+
const targetDir = path2.resolve(this.root, target.path);
|
|
383
|
+
await ensureDir(targetDir);
|
|
384
|
+
await this.syncSkill(sourceDir, targetDir, skillName);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 从所有目标中移除特定技能(用于 Watch 模式)
|
|
389
|
+
*/
|
|
390
|
+
async removeSkillFromAll(skillName) {
|
|
391
|
+
const targets = this.config.targets.filter((t) => t.enabled !== false);
|
|
392
|
+
for (const target of targets) {
|
|
393
|
+
const targetDir = path2.resolve(this.root, target.path);
|
|
394
|
+
const targetPath = path2.join(targetDir, skillName);
|
|
395
|
+
if (existsSync3(targetPath)) {
|
|
396
|
+
if (isSymlink(targetPath)) {
|
|
397
|
+
await fs3.unlink(targetPath);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 清理所有由 Skillink 创建的符号链接
|
|
404
|
+
*/
|
|
405
|
+
async cleanAll() {
|
|
406
|
+
const targets = this.config.targets;
|
|
407
|
+
const absSourceDir = path2.resolve(
|
|
408
|
+
this.root,
|
|
409
|
+
this.config.source || ".agents/skills"
|
|
410
|
+
);
|
|
411
|
+
for (const target of targets) {
|
|
412
|
+
const targetDir = path2.resolve(this.root, target.path);
|
|
413
|
+
if (!existsSync3(targetDir)) continue;
|
|
414
|
+
const items = await fs3.readdir(targetDir, { withFileTypes: true });
|
|
415
|
+
for (const item of items) {
|
|
416
|
+
if (item.isSymbolicLink()) {
|
|
417
|
+
const itemPath = path2.join(targetDir, item.name);
|
|
418
|
+
try {
|
|
419
|
+
const linkTarget = await fs3.readlink(itemPath);
|
|
420
|
+
const absLinkTarget = path2.resolve(targetDir, linkTarget);
|
|
421
|
+
const relative = path2.relative(absSourceDir, absLinkTarget);
|
|
422
|
+
if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) {
|
|
423
|
+
await fs3.unlink(itemPath);
|
|
424
|
+
console.log(`\u5DF2\u4ECE ${target.name} \u79FB\u9664 ${item.name}`);
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 获取源目录下的所有有效技能(子目录)
|
|
434
|
+
*/
|
|
435
|
+
async getSkills(sourceDir) {
|
|
436
|
+
const items = await fs3.readdir(sourceDir, { withFileTypes: true });
|
|
437
|
+
return items.filter((item) => item.isDirectory() && !item.name.startsWith(".")).map((item) => item.name);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/commands/sync.ts
|
|
442
|
+
import pc from "picocolors";
|
|
443
|
+
async function syncCommand(options) {
|
|
444
|
+
const cwd = options.cwd || process.cwd();
|
|
445
|
+
const fallbackLocale = resolveLocale();
|
|
446
|
+
const fallbackChinese = isChineseLocale(fallbackLocale);
|
|
447
|
+
const config = await loadConfig(cwd);
|
|
448
|
+
if (!config) {
|
|
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
|
+
);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
const locale = resolveLocale(config.locale);
|
|
457
|
+
const isChinese = isChineseLocale(locale);
|
|
458
|
+
const linker = new Linker(cwd, config);
|
|
459
|
+
console.log(
|
|
460
|
+
pc.cyan(isChinese ? "\u{1F504} \u6B63\u5728\u540C\u6B65\u6280\u80FD..." : "\u{1F504} Syncing skills...")
|
|
461
|
+
);
|
|
462
|
+
const results = await linker.sync();
|
|
463
|
+
let changes = 0;
|
|
464
|
+
results.forEach((r) => {
|
|
465
|
+
if (r.status === "linked" || r.status === "cleaned") {
|
|
466
|
+
console.log(
|
|
467
|
+
`${pc.green(r.status === "linked" ? "+" : "-")} ${r.skill} -> ${r.target}`
|
|
468
|
+
);
|
|
469
|
+
changes++;
|
|
470
|
+
} else if (r.status === "failed") {
|
|
471
|
+
console.error(
|
|
472
|
+
pc.red(
|
|
473
|
+
isChinese ? `\u274C ${r.skill} -> ${r.target}: ${r.message}` : `\u274C ${r.skill} -> ${r.target}: ${r.message}`
|
|
474
|
+
)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
if (changes === 0) {
|
|
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
|
+
);
|
|
484
|
+
} else {
|
|
485
|
+
console.log(
|
|
486
|
+
pc.green(
|
|
487
|
+
isChinese ? `\u2705 \u5DF2\u540C\u6B65 ${changes} \u5904\u53D8\u66F4\u3002` : `\u2705 Synced ${changes} change(s).`
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (options.watch) {
|
|
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
|
+
);
|
|
497
|
+
const sourceDir = path3.resolve(cwd, config.source || ".agents/skills");
|
|
498
|
+
const watcher = chokidar.watch(sourceDir, {
|
|
499
|
+
ignoreInitial: true,
|
|
500
|
+
depth: 1,
|
|
501
|
+
awaitWriteFinish: {
|
|
502
|
+
stabilityThreshold: 100,
|
|
503
|
+
pollInterval: 100
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
watcher.on("all", async (event, filePath) => {
|
|
507
|
+
if (path3.dirname(filePath) !== sourceDir) return;
|
|
508
|
+
const fileName = path3.basename(filePath);
|
|
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
|
+
);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/utils/logger.ts
|
|
538
|
+
import pc2 from "picocolors";
|
|
539
|
+
var logger = {
|
|
540
|
+
info: (msg) => console.log(pc2.cyan(msg)),
|
|
541
|
+
success: (msg) => console.log(pc2.green(msg)),
|
|
542
|
+
warn: (msg) => console.log(pc2.yellow(msg)),
|
|
543
|
+
error: (msg) => console.error(pc2.red(msg)),
|
|
544
|
+
gray: (msg) => console.log(pc2.gray(msg)),
|
|
545
|
+
title: (msg) => console.log(pc2.bold(pc2.magenta(msg))),
|
|
546
|
+
newline: () => console.log("")
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/commands/status.ts
|
|
550
|
+
import pc3 from "picocolors";
|
|
551
|
+
import path4 from "path";
|
|
552
|
+
import fs4 from "fs/promises";
|
|
553
|
+
import { existsSync as existsSync4 } from "fs";
|
|
554
|
+
async function statusCommand(options) {
|
|
555
|
+
const cwd = options.cwd || process.cwd();
|
|
556
|
+
const config = await loadConfig(cwd);
|
|
557
|
+
const locale = resolveLocale(config?.locale);
|
|
558
|
+
const isChinese = isChineseLocale(locale);
|
|
559
|
+
if (!config) {
|
|
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
|
+
);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
logger.title(isChinese ? "Skillink \u540C\u6B65\u72B6\u6001" : "Skillink Sync Status");
|
|
566
|
+
logger.newline();
|
|
567
|
+
const sourcePath = path4.resolve(cwd, config.source || ".agents/skills");
|
|
568
|
+
logger.info(
|
|
569
|
+
isChinese ? `\u6E90\u76EE\u5F55: ${sourcePath}` : `Source directory: ${sourcePath}`
|
|
570
|
+
);
|
|
571
|
+
if (!existsSync4(sourcePath)) {
|
|
572
|
+
logger.error(
|
|
573
|
+
isChinese ? "\u6E90\u76EE\u5F55\u4E0D\u5B58\u5728\uFF01" : "Source directory does not exist!"
|
|
574
|
+
);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const skills = await fs4.readdir(sourcePath, { withFileTypes: true });
|
|
578
|
+
const validSkills = skills.filter((s) => s.isDirectory() && !s.name.startsWith(".")).map((s) => s.name);
|
|
579
|
+
logger.gray(
|
|
580
|
+
isChinese ? `\u627E\u5230 ${validSkills.length} \u4E2A\u6280\u80FD\u3002` : `Found ${validSkills.length} skill(s).`
|
|
581
|
+
);
|
|
582
|
+
logger.newline();
|
|
583
|
+
logger.info(isChinese ? "\u76EE\u6807\u5DE5\u5177:" : "Targets:");
|
|
584
|
+
for (const target of config.targets) {
|
|
585
|
+
if (target.enabled === false) continue;
|
|
586
|
+
const targetDir = path4.resolve(cwd, target.path);
|
|
587
|
+
console.log(`${pc3.bold(target.name)} [${targetDir}]`);
|
|
588
|
+
if (!existsSync4(targetDir)) {
|
|
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
|
+
);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
let syncedCount = 0;
|
|
597
|
+
let missingCount = 0;
|
|
598
|
+
let brokenCount = 0;
|
|
599
|
+
let mismatchedCount = 0;
|
|
600
|
+
let occupiedCount = 0;
|
|
601
|
+
for (const skill of validSkills) {
|
|
602
|
+
const linkPath = path4.join(targetDir, skill);
|
|
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
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const currentTarget = await fs4.readlink(linkPath);
|
|
612
|
+
const absCurrentTarget = path4.resolve(targetDir, currentTarget);
|
|
613
|
+
if (absCurrentTarget === sourceSkillPath) {
|
|
614
|
+
syncedCount++;
|
|
615
|
+
} else {
|
|
616
|
+
mismatchedCount++;
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
brokenCount++;
|
|
620
|
+
}
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error.code === "ENOENT") {
|
|
623
|
+
missingCount++;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
missingCount++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
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!"));
|
|
661
|
+
console.log("");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/commands/clean.ts
|
|
666
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
667
|
+
async function cleanCommand(options = {}) {
|
|
668
|
+
const cwd = options.cwd || process.cwd();
|
|
669
|
+
const config = await loadConfig(cwd);
|
|
670
|
+
const locale = resolveLocale(config?.locale);
|
|
671
|
+
const isChinese = isChineseLocale(locale);
|
|
672
|
+
if (!config) {
|
|
673
|
+
logger.error(isChinese ? "\u672A\u627E\u5230\u914D\u7F6E\u3002" : "Configuration not found.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const answer = await confirm2({
|
|
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?",
|
|
678
|
+
default: false
|
|
679
|
+
});
|
|
680
|
+
if (!answer) return;
|
|
681
|
+
const linker = new Linker(cwd, config);
|
|
682
|
+
logger.info(isChinese ? "\u6B63\u5728\u6E05\u7406\u94FE\u63A5..." : "Cleaning links...");
|
|
683
|
+
await linker.cleanAll();
|
|
684
|
+
logger.success(isChinese ? "\u6E05\u7406\u5B8C\u6210\u3002" : "Clean completed.");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/commands/check.ts
|
|
688
|
+
import pc4 from "picocolors";
|
|
689
|
+
|
|
690
|
+
// src/utils/update.ts
|
|
691
|
+
import semver from "semver";
|
|
692
|
+
var pkg = require_package();
|
|
693
|
+
var currentVersion = pkg.version;
|
|
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
|
+
}
|
|
705
|
+
async function checkUpdate() {
|
|
706
|
+
const controller = new AbortController();
|
|
707
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
708
|
+
try {
|
|
709
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg.name}`, {
|
|
710
|
+
signal: controller.signal
|
|
711
|
+
});
|
|
712
|
+
clearTimeout(timeoutId);
|
|
713
|
+
if (!res.ok) {
|
|
714
|
+
throw new Error(`\u8BF7\u6C42\u5931\u8D25: ${res.status} ${res.statusText}`);
|
|
715
|
+
}
|
|
716
|
+
const data = await res.json();
|
|
717
|
+
const latestVersion = resolveLatestSemverVersion(
|
|
718
|
+
Object.keys(data.versions ?? {})
|
|
719
|
+
);
|
|
720
|
+
return {
|
|
721
|
+
latest: latestVersion,
|
|
722
|
+
current: currentVersion,
|
|
723
|
+
hasUpdate: semver.gt(latestVersion, currentVersion),
|
|
724
|
+
name: pkgName
|
|
725
|
+
};
|
|
726
|
+
} catch (error) {
|
|
727
|
+
clearTimeout(timeoutId);
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/check.ts
|
|
733
|
+
async function checkCommand() {
|
|
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
|
+
);
|
|
740
|
+
try {
|
|
741
|
+
const info = await checkUpdate();
|
|
742
|
+
if (info.hasUpdate) {
|
|
743
|
+
console.log();
|
|
744
|
+
console.log(
|
|
745
|
+
pc4.yellow(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")
|
|
746
|
+
);
|
|
747
|
+
console.log(
|
|
748
|
+
pc4.yellow(" \u2502 \u2502")
|
|
749
|
+
);
|
|
750
|
+
console.log(
|
|
751
|
+
pc4.yellow(
|
|
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`
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
console.log(
|
|
756
|
+
pc4.yellow(" \u2502 \u2502")
|
|
757
|
+
);
|
|
758
|
+
console.log(
|
|
759
|
+
pc4.yellow(
|
|
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`
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
console.log(
|
|
764
|
+
pc4.yellow(" \u2502 \u2502")
|
|
765
|
+
);
|
|
766
|
+
console.log(
|
|
767
|
+
pc4.yellow(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")
|
|
768
|
+
);
|
|
769
|
+
console.log();
|
|
770
|
+
} else {
|
|
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
|
+
);
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
logger.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)}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/cli.ts
|
|
783
|
+
var cli = cac("skillink");
|
|
784
|
+
cli.version(currentVersion);
|
|
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());
|
|
793
|
+
cli.help();
|
|
794
|
+
try {
|
|
795
|
+
cli.parse();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|