@wangkh/skill-sync-cli 0.2.2 → 0.2.3
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 +6 -3
- package/README.zh-CN.md +1 -1
- package/bin/skill-sync.mjs +1 -1
- package/package.json +1 -1
- package/skills/skill-sync/SKILL.md +5 -4
- package/src/commands.mjs +53 -9
- package/src/mirror.mjs +1 -0
- package/src/revision.mjs +18 -0
package/README.md
CHANGED
|
@@ -248,9 +248,12 @@ downloaded again. If the update check cannot reach the remote but the previous
|
|
|
248
248
|
install is still present, the current copy is kept. Use `--force` to reinstall
|
|
249
249
|
regardless.
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
through
|
|
251
|
+
`sync` prefers direct connections and falls back automatically: it probes
|
|
252
|
+
github.com with a 5-second timeout first, and when the probe fails, clones and
|
|
253
|
+
update checks run through `https://gh-proxy.com/` for that run. A direct
|
|
254
|
+
install that fails anyway gets one retry through the fallback mirror. Pass
|
|
255
|
+
`--github-mirror <url>` (or set `SKILL_SYNC_GITHUB_MIRROR`) to force a specific
|
|
256
|
+
mirror instead of the automatic fallback:
|
|
254
257
|
|
|
255
258
|
```bash
|
|
256
259
|
npx @wangkh/skill-sync-cli sync --github-mirror https://gh-proxy.com/
|
package/README.zh-CN.md
CHANGED
|
@@ -124,7 +124,7 @@ npx --yes skills@1.5.22 add vercel-labs/skills --agent opencode --skill find-ski
|
|
|
124
124
|
|
|
125
125
|
`sync` 会把每个分组的已安装版本记录在 `~/.config/skill-sync/state.json`。之后的运行会用 `git ls-remote` 检查远端版本(固定为 commit SHA 的 `ref` 无需检查),版本和已安装文件都没有变化时跳过该组,不再重复下载。检查更新失败但本地文件仍然存在时,会保留现有安装。需要强制重装时使用 `--force`。
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
`sync` 默认优先直连 GitHub、失败自动回退:先用 5 秒超时探测 github.com,探测失败时本次运行的 clone 和版本检查自动走 `https://gh-proxy.com/`;直连安装失败也会再用镜像重试一次。需要强制指定镜像时用 `--github-mirror <url>`(或环境变量 `SKILL_SYNC_GITHUB_MIRROR`):
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
130
|
npx @wangkh/skill-sync-cli sync --github-mirror https://gh-proxy.com/
|
package/bin/skill-sync.mjs
CHANGED
|
@@ -29,7 +29,7 @@ Options:
|
|
|
29
29
|
--dry-run Print commands without downloading anything
|
|
30
30
|
--continue-on-error Continue after a skill installation fails
|
|
31
31
|
--retries <n> Retry each failed install up to n times (default 2)
|
|
32
|
-
--github-mirror <url>
|
|
32
|
+
--github-mirror <url> Force a GitHub mirror instead of the automatic fallback
|
|
33
33
|
--force Replace the selected manifest with init, or reinstall with sync
|
|
34
34
|
-h, --help Show this help
|
|
35
35
|
-v, --version Show the version
|
package/package.json
CHANGED
|
@@ -77,10 +77,11 @@ npx --yes skills@1.5.22 add <owner>/<repo> --list
|
|
|
77
77
|
overrides the skip.
|
|
78
78
|
- When the update check cannot reach the remote but the installed copy still
|
|
79
79
|
exists, `sync` keeps it instead of failing.
|
|
80
|
-
- GitHub
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`https://
|
|
80
|
+
- GitHub access: `sync` probes github.com with a 5-second timeout and falls
|
|
81
|
+
back to `https://gh-proxy.com/` automatically; `--github-mirror <url>` forces
|
|
82
|
+
a specific mirror (alternatives: `https://ghproxy.net/`,
|
|
83
|
+
`https://ghfast.top/`). For the `skills` CLI download itself, configure an
|
|
84
|
+
npm registry mirror such as `https://registry.npmmirror.com`.
|
|
84
85
|
|
|
85
86
|
## Security
|
|
86
87
|
|
package/src/commands.mjs
CHANGED
|
@@ -15,8 +15,14 @@ import {
|
|
|
15
15
|
runCommand,
|
|
16
16
|
statusCode,
|
|
17
17
|
} from "./runner.mjs"
|
|
18
|
-
import { mirrorGitEnv } from "./mirror.mjs"
|
|
19
|
-
import {
|
|
18
|
+
import { DEFAULT_FALLBACK_MIRROR, mirrorGitEnv } from "./mirror.mjs"
|
|
19
|
+
import {
|
|
20
|
+
checkGithubReachable,
|
|
21
|
+
gitUrlForSource,
|
|
22
|
+
isCommitSha,
|
|
23
|
+
isGithubSource,
|
|
24
|
+
probeRevision,
|
|
25
|
+
} from "./revision.mjs"
|
|
20
26
|
import {
|
|
21
27
|
allInstalled,
|
|
22
28
|
readState,
|
|
@@ -99,17 +105,26 @@ function resolveGroupRevision(skills, probe, probeOptions) {
|
|
|
99
105
|
return probe(url, ref, probeOptions)
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
async function runWithRetries(execute, command, { cwd, env, retries, retryDelayMs, label }) {
|
|
108
|
+
async function runWithRetries(execute, command, { cwd, env, fallbackEnv, retries, retryDelayMs, label }) {
|
|
109
|
+
let result
|
|
103
110
|
for (let attempt = 0; ; attempt += 1) {
|
|
104
|
-
|
|
111
|
+
result = execute(command, { cwd, env })
|
|
105
112
|
const code = statusCode(result)
|
|
106
|
-
if (code === 0 || result.signal || attempt >= retries)
|
|
113
|
+
if (code === 0 || result.signal || attempt >= retries) break
|
|
107
114
|
|
|
108
115
|
const delayMs = retryDelayMs * (attempt + 1)
|
|
109
116
|
const delayText = delayMs > 0 ? ` in ${Math.round(delayMs / 1000)}s` : ""
|
|
110
117
|
console.error(`[${label}] attempt ${attempt + 1}/${retries + 1} failed with exit code ${code}; retrying${delayText}`)
|
|
111
118
|
if (delayMs > 0) await sleep(delayMs)
|
|
112
119
|
}
|
|
120
|
+
|
|
121
|
+
// Direct connections can work for the reachability probe and still time out
|
|
122
|
+
// later; one mirror attempt covers that case.
|
|
123
|
+
if (statusCode(result) !== 0 && !result.signal && fallbackEnv) {
|
|
124
|
+
console.error(`[${label}] retrying via fallback mirror`)
|
|
125
|
+
result = execute(command, { cwd, env: fallbackEnv })
|
|
126
|
+
}
|
|
127
|
+
return result
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
export async function planCommand(manifestFilename) {
|
|
@@ -140,7 +155,6 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
140
155
|
const home = options.home ?? os.homedir()
|
|
141
156
|
const cwd = options.cwd ?? process.cwd()
|
|
142
157
|
const probe = options.probe ?? probeRevision
|
|
143
|
-
const gitEnv = mirrorGitEnv(options.githubMirror)
|
|
144
158
|
|
|
145
159
|
if (skills.length === 0) {
|
|
146
160
|
console.log("No enabled skills.")
|
|
@@ -154,6 +168,30 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
154
168
|
command: commandForGroup(group, manifest.cli),
|
|
155
169
|
}))
|
|
156
170
|
|
|
171
|
+
// Route GitHub traffic for this run: an explicit mirror always wins;
|
|
172
|
+
// otherwise probe github.com briefly and fall back to the default mirror
|
|
173
|
+
// when it does not answer.
|
|
174
|
+
let gitEnv = mirrorGitEnv(options.githubMirror)
|
|
175
|
+
let fallbackEnv = null
|
|
176
|
+
if (!options.githubMirror && !options.dryRun && groups.some((group) => isGithubSource(group.skills[0].source))) {
|
|
177
|
+
const check = options.checkGithub ?? checkGithubReachable
|
|
178
|
+
if (check()) {
|
|
179
|
+
console.log("# GitHub reachable; using direct connections")
|
|
180
|
+
fallbackEnv = mirrorGitEnv(DEFAULT_FALLBACK_MIRROR)
|
|
181
|
+
} else {
|
|
182
|
+
gitEnv = mirrorGitEnv(DEFAULT_FALLBACK_MIRROR)
|
|
183
|
+
console.log(`# GitHub unreachable; using fallback mirror ${DEFAULT_FALLBACK_MIRROR}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const resolveRevision = (group) => {
|
|
188
|
+
let revision = resolveGroupRevision(group.skills, probe, { env: gitEnv })
|
|
189
|
+
if (revision.status === "error" && fallbackEnv) {
|
|
190
|
+
revision = resolveGroupRevision(group.skills, probe, { env: fallbackEnv })
|
|
191
|
+
}
|
|
192
|
+
return revision
|
|
193
|
+
}
|
|
194
|
+
|
|
157
195
|
const stateFilename = options.stateFile ?? statePath(home)
|
|
158
196
|
const state = await readState(stateFilename)
|
|
159
197
|
|
|
@@ -195,7 +233,7 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
195
233
|
let revision = null
|
|
196
234
|
|
|
197
235
|
if (!options.force && entry && installedHere) {
|
|
198
|
-
revision =
|
|
236
|
+
revision = resolveRevision(group)
|
|
199
237
|
if (revision.status === "known" && entry.revision === revision.revision) {
|
|
200
238
|
console.log(`# unchanged (${revision.revision.slice(0, 7)}); skipping, use --force to reinstall`)
|
|
201
239
|
skipped += 1
|
|
@@ -218,6 +256,7 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
218
256
|
const result = await runWithRetries(executeTolerant, group.command, {
|
|
219
257
|
cwd: options.cwd,
|
|
220
258
|
env: gitEnv,
|
|
259
|
+
fallbackEnv,
|
|
221
260
|
retries,
|
|
222
261
|
retryDelayMs,
|
|
223
262
|
label: skill.name,
|
|
@@ -232,7 +271,11 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
232
271
|
// A batched command shares one fetch. Since a failure could come from a
|
|
233
272
|
// single bad name or a transient error, retry each entry individually
|
|
234
273
|
// when the batch does not succeed.
|
|
235
|
-
|
|
274
|
+
let batch = executeTolerant(group.command, { cwd: options.cwd, env: gitEnv })
|
|
275
|
+
if (statusCode(batch) !== 0 && !batch.signal && fallbackEnv) {
|
|
276
|
+
console.error(`[${sourceLabel(group.skills)}] batch install failed; retrying via fallback mirror`)
|
|
277
|
+
batch = executeTolerant(group.command, { cwd: options.cwd, env: fallbackEnv })
|
|
278
|
+
}
|
|
236
279
|
if (statusCode(batch) !== 0) {
|
|
237
280
|
if (batch.signal) {
|
|
238
281
|
failures += group.skills.length
|
|
@@ -245,6 +288,7 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
245
288
|
const result = await runWithRetries(executeTolerant, commandFor(skill, manifest.cli), {
|
|
246
289
|
cwd: options.cwd,
|
|
247
290
|
env: gitEnv,
|
|
291
|
+
fallbackEnv,
|
|
248
292
|
retries,
|
|
249
293
|
retryDelayMs,
|
|
250
294
|
label: skill.name,
|
|
@@ -264,7 +308,7 @@ export async function syncCommand(manifestFilename, options = {}) {
|
|
|
264
308
|
}
|
|
265
309
|
|
|
266
310
|
if (!groupFailed) {
|
|
267
|
-
if (!revision) revision =
|
|
311
|
+
if (!revision) revision = resolveRevision(group)
|
|
268
312
|
if (revision.status === "known") {
|
|
269
313
|
setStateEntry(state, filename, key, {
|
|
270
314
|
revision: revision.revision,
|
package/src/mirror.mjs
CHANGED
package/src/revision.mjs
CHANGED
|
@@ -53,3 +53,21 @@ export function probeRevision(url, ref, { timeoutMs = 10000, env = process.env }
|
|
|
53
53
|
if (!COMMIT_SHA.test(revision)) return { status: "unknown" }
|
|
54
54
|
return { status: "known", revision: revision.toLowerCase() }
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
const GITHUB_CHECK_URL = "https://github.com/octocat/Hello-World.git"
|
|
58
|
+
|
|
59
|
+
export function isGithubSource(source) {
|
|
60
|
+
if (SHORTHAND.test(source) && !source.split("/").some((part) => part.startsWith("."))) return true
|
|
61
|
+
if (/^git@github\.com:/i.test(source)) return true
|
|
62
|
+
try {
|
|
63
|
+
return /(^|\.)github\.com$/i.test(new URL(source).hostname)
|
|
64
|
+
} catch {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A short direct probe decides whether sync runs against github.com or the
|
|
70
|
+
// fallback mirror for this run.
|
|
71
|
+
export function checkGithubReachable({ timeoutMs = 5000 } = {}) {
|
|
72
|
+
return probeRevision(GITHUB_CHECK_URL, "HEAD", { timeoutMs }).status === "known"
|
|
73
|
+
}
|