@wang121ye/skillmanager 0.0.3 → 0.0.5
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 +41 -0
- package/package.json +1 -1
- package/src/cli.js +3 -0
- package/src/commands/bootstrap.js +3 -1
- package/src/commands/update.js +3 -1
- package/src/commands/webui.js +3 -1
- package/src/lib/git.js +69 -5
- package/src/lib/prereqs.js +102 -0
package/README.md
CHANGED
|
@@ -4,6 +4,47 @@
|
|
|
4
4
|
|
|
5
5
|
本项目 **基于 `openskills`** 实现安装与 `AGENTS.md` 同步。
|
|
6
6
|
|
|
7
|
+
## 环境要求与兼容性(重要)
|
|
8
|
+
|
|
9
|
+
`skillmanager` 会调用系统里的 `git` 拉取/更新 skills 来源仓库,并通过 `openskills` 执行安装与 `AGENTS.md` 同步。因此你的环境版本过低时,可能出现“看起来配置没问题但某些机器上失败”的情况。
|
|
10
|
+
|
|
11
|
+
- **Node.js(用于运行 openskills)**:建议 **>= 20.6.0**
|
|
12
|
+
- 低于该版本可能出现语法错误(例如依赖使用了 RegExp `/v` flag)。
|
|
13
|
+
- **openskills**:建议 **>= 1.5.0**(本项目依赖与运行时行为以该版本为基准)
|
|
14
|
+
- **git**:建议 **>= 2.34.0**
|
|
15
|
+
- 低版本在 GitHub HTTPS + partial clone(如 `--filter=blob:none`)场景下,可能更容易遇到 TLS/gnutls 相关中断(如 `gnutls_handshake()`)。
|
|
16
|
+
|
|
17
|
+
常见规避方案:
|
|
18
|
+
|
|
19
|
+
- **升级 git / Node / openskills**(推荐根治)
|
|
20
|
+
- **改用 SSH 拉取 GitHub 仓库**(绕开 HTTPS/TLS):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export SKILLMANAGER_GIT_PROTOCOL=ssh
|
|
24
|
+
skillmanager webui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **降低并发**(网络/中间设备对并发连接敏感时):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
skillmanager webui --concurrency 1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 缓存自动刷新(避免“只看到旧的 skills”)
|
|
34
|
+
|
|
35
|
+
为避免第三方用户忘记更新缓存,`skillmanager` 在扫描来源仓库时会检查本地缓存是否落后于远端:
|
|
36
|
+
|
|
37
|
+
- 如果检测到缓存落后,默认会**自动重新拉取**最新仓库。
|
|
38
|
+
- 如需手动控制,可使用 `--force-refresh` 强制刷新,或设置 `SKILLMANAGER_AUTO_REFRESH=0` 关闭自动刷新。
|
|
39
|
+
|
|
40
|
+
示例:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
skillmanager webui --force-refresh
|
|
44
|
+
skillmanager install --force-refresh
|
|
45
|
+
skillmanager update --force-refresh
|
|
46
|
+
```
|
|
47
|
+
|
|
7
48
|
## 安装与使用
|
|
8
49
|
|
|
9
50
|
### 全局安装(推荐)
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ async function main() {
|
|
|
30
30
|
.option('--dry-run', '仅打印将执行的内容,不实际安装', false)
|
|
31
31
|
.option('--concurrency <n>', '选择模式下的并发扫描数(默认:3)', '3')
|
|
32
32
|
.option('--profile <name>', '选择配置名(默认:来自 config 或 SKILLMANAGER_PROFILE 环境变量)')
|
|
33
|
+
.option('--force-refresh', '强制刷新来源仓库缓存(重新拉取)', false)
|
|
33
34
|
.action(async (opts) => {
|
|
34
35
|
await bootstrap(opts);
|
|
35
36
|
});
|
|
@@ -44,6 +45,7 @@ async function main() {
|
|
|
44
45
|
.option('--output <path>', '作用:sync 输出文件路径(默认:AGENTS.md)')
|
|
45
46
|
.option('--no-sync', '作用:不执行 openskills sync(仅安装/卸载,不更新 AGENTS.md)', false)
|
|
46
47
|
.option('--concurrency <n>', '作用:install 模式下并发拉取/扫描来源仓库,提高速度(默认:3)', '3')
|
|
48
|
+
.option('--force-refresh', '强制刷新来源仓库缓存(重新拉取)', false)
|
|
47
49
|
.action(async (opts) => {
|
|
48
50
|
await webui(opts);
|
|
49
51
|
});
|
|
@@ -64,6 +66,7 @@ async function main() {
|
|
|
64
66
|
.option('--no-sync', '跳过 openskills sync', false)
|
|
65
67
|
.option('--profile <name>', '按 profile 选择集更新(会重新安装选中的 skills)')
|
|
66
68
|
.option('--concurrency <n>', '选择模式下的并发扫描数(默认:3)', '3')
|
|
69
|
+
.option('--force-refresh', '强制刷新来源仓库缓存(重新拉取)', false)
|
|
67
70
|
.action(async (opts) => {
|
|
68
71
|
await update(opts);
|
|
69
72
|
});
|
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { installSourceRef, syncAgents } = require('../lib/openskills');
|
|
12
12
|
const { installFromLocalSkillDir } = require('../lib/local-install');
|
|
13
|
+
const { warnPrereqs } = require('../lib/prereqs');
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
function uniq(arr) {
|
|
@@ -17,6 +18,7 @@ function uniq(arr) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async function bootstrap(opts) {
|
|
21
|
+
await warnPrereqs({ needGit: true, needOpenSkills: true });
|
|
20
22
|
const paths = getAppPaths();
|
|
21
23
|
await ensureDir(paths.reposDir);
|
|
22
24
|
await ensureDir(paths.profilesDir);
|
|
@@ -75,7 +77,7 @@ async function bootstrap(opts) {
|
|
|
75
77
|
const skillsById = new Map();
|
|
76
78
|
const perSource = await mapWithConcurrency(enabledSources, concurrency, async (s) => {
|
|
77
79
|
try {
|
|
78
|
-
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s });
|
|
80
|
+
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s, forceRefresh: !!opts?.forceRefresh });
|
|
79
81
|
const skills = await scanSkillsInRepo({
|
|
80
82
|
sourceId: s.id,
|
|
81
83
|
sourceName: s.name || s.id,
|
package/src/commands/update.js
CHANGED
|
@@ -10,6 +10,7 @@ const { syncAgents, runOpenSkills } = require('../lib/openskills');
|
|
|
10
10
|
const { installFromLocalSkillDir } = require('../lib/local-install');
|
|
11
11
|
const { mapWithConcurrency } = require('../lib/concurrency');
|
|
12
12
|
const { getEffectiveDefaultProfile } = require('../lib/config');
|
|
13
|
+
const { warnPrereqs } = require('../lib/prereqs');
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
function uniq(arr) {
|
|
@@ -17,6 +18,7 @@ function uniq(arr) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async function update(opts) {
|
|
21
|
+
await warnPrereqs({ needGit: true, needOpenSkills: true });
|
|
20
22
|
const globalInstall = !!opts?.global;
|
|
21
23
|
const universal = !!opts?.universal;
|
|
22
24
|
|
|
@@ -51,7 +53,7 @@ async function update(opts) {
|
|
|
51
53
|
const skillsById = new Map();
|
|
52
54
|
const perSource = await mapWithConcurrency(enabledSources, concurrency, async (s) => {
|
|
53
55
|
try {
|
|
54
|
-
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s });
|
|
56
|
+
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s, forceRefresh: !!opts?.forceRefresh });
|
|
55
57
|
const skills = await scanSkillsInRepo({
|
|
56
58
|
sourceId: s.id,
|
|
57
59
|
sourceName: s.name || s.id,
|
package/src/commands/webui.js
CHANGED
|
@@ -14,6 +14,7 @@ const { installFromLocalSkillDir } = require('../lib/local-install');
|
|
|
14
14
|
const { listInstalledSkills } = require('../lib/installed');
|
|
15
15
|
const { syncAgents } = require('../lib/openskills');
|
|
16
16
|
const { launchSelectionUi } = require('../ui/server');
|
|
17
|
+
const { warnPrereqs } = require('../lib/prereqs');
|
|
17
18
|
|
|
18
19
|
function resolveTargetDir({ globalInstall, universal }) {
|
|
19
20
|
const folder = universal ? '.agent/skills' : '.claude/skills';
|
|
@@ -21,6 +22,7 @@ function resolveTargetDir({ globalInstall, universal }) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async function webui(opts) {
|
|
25
|
+
await warnPrereqs({ needGit: true, needOpenSkills: true });
|
|
24
26
|
const modeRaw = String(opts?.mode || 'install').toLowerCase();
|
|
25
27
|
const mode = modeRaw === 'uninstall' ? 'uninstall' : 'install';
|
|
26
28
|
|
|
@@ -89,7 +91,7 @@ async function webui(opts) {
|
|
|
89
91
|
const skillsById = new Map();
|
|
90
92
|
const perSource = await mapWithConcurrency(enabledSources, concurrency, async (s) => {
|
|
91
93
|
try {
|
|
92
|
-
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s });
|
|
94
|
+
const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s, forceRefresh: !!opts?.forceRefresh });
|
|
93
95
|
const skills = await scanSkillsInRepo({
|
|
94
96
|
sourceId: s.id,
|
|
95
97
|
sourceName: s.name || s.id,
|
package/src/lib/git.js
CHANGED
|
@@ -10,7 +10,53 @@ function sleep(ms) {
|
|
|
10
10
|
return new Promise((r) => setTimeout(r, ms));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function isTruthy(value) {
|
|
14
|
+
const v = String(value || '').trim().toLowerCase();
|
|
15
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldAutoRefreshCache() {
|
|
19
|
+
// 默认开启自动刷新(发现远端更新时自动重新拉取)
|
|
20
|
+
// 如需关闭:SKILLMANAGER_AUTO_REFRESH=0
|
|
21
|
+
if (process.env.SKILLMANAGER_AUTO_REFRESH != null) {
|
|
22
|
+
return isTruthy(process.env.SKILLMANAGER_AUTO_REFRESH);
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getLocalHead(repoGit) {
|
|
28
|
+
try {
|
|
29
|
+
const out = await repoGit.revparse(['HEAD']);
|
|
30
|
+
return String(out || '').trim() || null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getRemoteHead(repoGit) {
|
|
37
|
+
try {
|
|
38
|
+
const out = await repoGit.raw(['ls-remote', 'origin', 'HEAD']);
|
|
39
|
+
const first = String(out || '').trim().split(/\s+/)[0];
|
|
40
|
+
return first || null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function isRepoStale(repoGit) {
|
|
47
|
+
const [localHead, remoteHead] = await Promise.all([getLocalHead(repoGit), getRemoteHead(repoGit)]);
|
|
48
|
+
if (!localHead || !remoteHead) return false;
|
|
49
|
+
return localHead !== remoteHead;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function cloneRepo({ git, repoUrl, repoDir, cloneArgs }) {
|
|
53
|
+
await rmDir(repoDir);
|
|
54
|
+
await ensureDir(repoDir);
|
|
55
|
+
await git.clone(repoUrl, repoDir, cloneArgs);
|
|
56
|
+
return repoDir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureRepo({ reposDir, source, forceRefresh = false }) {
|
|
14
60
|
if (!source?.repo) {
|
|
15
61
|
throw new Error(`Invalid source repo for ${source?.id || '<unknown>'}`);
|
|
16
62
|
}
|
|
@@ -20,14 +66,12 @@ async function ensureRepo({ reposDir, source }) {
|
|
|
20
66
|
|
|
21
67
|
const gitDir = path.join(repoDir, '.git');
|
|
22
68
|
const git = simpleGit();
|
|
69
|
+
const cloneArgs = ['--depth', '1', '--single-branch', '--filter=blob:none'];
|
|
23
70
|
|
|
24
71
|
if (!(await fileExists(gitDir))) {
|
|
25
|
-
const cloneArgs = ['--depth', '1', '--single-branch', '--filter=blob:none'];
|
|
26
72
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
27
73
|
try {
|
|
28
|
-
await
|
|
29
|
-
await ensureDir(repoDir);
|
|
30
|
-
await git.clone(source.repo, repoDir, cloneArgs);
|
|
74
|
+
await cloneRepo({ git, repoUrl: source.repo, repoDir, cloneArgs });
|
|
31
75
|
return repoDir;
|
|
32
76
|
} catch (err) {
|
|
33
77
|
await rmDir(repoDir);
|
|
@@ -50,6 +94,26 @@ async function ensureRepo({ reposDir, source }) {
|
|
|
50
94
|
} catch {
|
|
51
95
|
// ignore ff-only failures (e.g. detached HEAD) — user can clean manually
|
|
52
96
|
}
|
|
97
|
+
|
|
98
|
+
// 如果缓存仓库落后于远端,自动重新拉取(可用 env 关闭)
|
|
99
|
+
try {
|
|
100
|
+
const stale = await isRepoStale(repoGit);
|
|
101
|
+
if (stale) {
|
|
102
|
+
const shouldRefresh = forceRefresh || shouldAutoRefreshCache();
|
|
103
|
+
if (shouldRefresh) {
|
|
104
|
+
// eslint-disable-next-line no-console
|
|
105
|
+
console.warn(`检测到缓存仓库落后于远端:${source.id},正在重新拉取最新版本…`);
|
|
106
|
+
return await cloneRepo({ git, repoUrl: source.repo, repoDir, cloneArgs });
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.warn(`检测到缓存仓库落后于远端:${source.id}。可使用 --force-refresh 或设置 SKILLMANAGER_AUTO_REFRESH=1。`);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// eslint-disable-next-line no-console
|
|
113
|
+
console.warn(
|
|
114
|
+
`提示:无法检查远端版本(可能网络/权限问题),缓存可能过期。可使用 --force-refresh 强制刷新,或删除 ${repoDir} 后重试。`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
53
117
|
return repoDir;
|
|
54
118
|
}
|
|
55
119
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const fsp = require('fs/promises');
|
|
3
|
+
|
|
4
|
+
const MIN_GIT = { major: 2, minor: 34, patch: 0 };
|
|
5
|
+
const MIN_NODE_FOR_OPENSKILLS = { major: 20, minor: 6, patch: 0 };
|
|
6
|
+
const MIN_OPENSKILLS = { major: 1, minor: 5, patch: 0 };
|
|
7
|
+
|
|
8
|
+
let warnedOnce = false;
|
|
9
|
+
|
|
10
|
+
function parseSemverLike(input) {
|
|
11
|
+
const s = String(input || '');
|
|
12
|
+
const m = s.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
13
|
+
if (!m) return null;
|
|
14
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cmpVersion(a, b) {
|
|
18
|
+
if (!a || !b) return 0;
|
|
19
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
20
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
21
|
+
return a.patch - b.patch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fmt(v) {
|
|
25
|
+
if (!v) return '(unknown)';
|
|
26
|
+
return `${v.major}.${v.minor}.${v.patch}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function execFileText(cmd, args) {
|
|
30
|
+
return await new Promise((resolve, reject) => {
|
|
31
|
+
execFile(cmd, args, { windowsHide: true }, (err, stdout, stderr) => {
|
|
32
|
+
if (err) return reject(err);
|
|
33
|
+
resolve(String(stdout || stderr || '').trim());
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getGitVersion() {
|
|
39
|
+
const out = await execFileText('git', ['--version']).catch(() => null);
|
|
40
|
+
if (!out) return null;
|
|
41
|
+
// e.g. "git version 2.25.1" / "git version 2.44.0.windows.1"
|
|
42
|
+
return parseSemverLike(out);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getOpenSkillsVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const pkgPath = require.resolve('openskills/package.json');
|
|
48
|
+
const txt = await fsp.readFile(pkgPath, 'utf8');
|
|
49
|
+
const json = JSON.parse(txt);
|
|
50
|
+
return parseSemverLike(json?.version);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getNodeVersion() {
|
|
57
|
+
return parseSemverLike(process.versions.node);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function warnPrereqs({ needGit = false, needOpenSkills = false } = {}) {
|
|
61
|
+
// 只在实际运行(非 help)且需要相关能力时提示一次,避免刷屏
|
|
62
|
+
if (warnedOnce) return;
|
|
63
|
+
warnedOnce = true;
|
|
64
|
+
|
|
65
|
+
const lines = [];
|
|
66
|
+
|
|
67
|
+
if (needGit) {
|
|
68
|
+
const gitV = await getGitVersion();
|
|
69
|
+
if (!gitV) {
|
|
70
|
+
lines.push('- git:未检测到 `git`(需要安装 git 才能拉取来源仓库)。');
|
|
71
|
+
} else if (cmpVersion(gitV, MIN_GIT) < 0) {
|
|
72
|
+
lines.push(
|
|
73
|
+
`- git:检测到 ${fmt(gitV)},建议至少 ${fmt(MIN_GIT)}。低版本在 GitHub HTTPS/partial clone 场景下可能出现 TLS/gnutls 握手中断(例如 gnutls_handshake)。`
|
|
74
|
+
);
|
|
75
|
+
lines.push(' - 建议:升级 git(或设置 `SKILLMANAGER_GIT_PROTOCOL=ssh` 使用 SSH 拉取;并避免开启 partial clone filter)。');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (needOpenSkills) {
|
|
80
|
+
const nodeV = getNodeVersion();
|
|
81
|
+
if (nodeV && cmpVersion(nodeV, MIN_NODE_FOR_OPENSKILLS) < 0) {
|
|
82
|
+
lines.push(
|
|
83
|
+
`- Node.js:检测到 ${fmt(nodeV)},openskills 需要至少 ${fmt(MIN_NODE_FOR_OPENSKILLS)}(否则可能出现语法错误,例如 RegExp /v flag)。`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const osV = await getOpenSkillsVersion();
|
|
88
|
+
if (!osV) {
|
|
89
|
+
lines.push('- openskills:未检测到依赖(或无法读取版本)。如需 sync/安装来源,请确保已安装 openskills。');
|
|
90
|
+
} else if (cmpVersion(osV, MIN_OPENSKILLS) < 0) {
|
|
91
|
+
lines.push(`- openskills:检测到 ${fmt(osV)},建议至少 ${fmt(MIN_OPENSKILLS)}。`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (lines.length) {
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.warn(['\n⚠️ 环境兼容性提示(skillmanager)', ...lines, ''].join('\n'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { warnPrereqs };
|
|
102
|
+
|