@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 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
- On networks that cannot reach GitHub directly, pass `--github-mirror <url>`
252
- (or set `SKILL_SYNC_GITHUB_MIRROR`) to route GitHub clones and update checks
253
- through a mirror:
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
- 如果所在网络无法直接访问 GitHub,可以用 `--github-mirror <url>`(或环境变量 `SKILL_SYNC_GITHUB_MIRROR`)让 clone 和版本检查走镜像:
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/
@@ -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> Clone GitHub sources through a mirror (sync only)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wangkh/skill-sync-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Sync declared agent skills across machines through the skills CLI",
5
5
  "author": "wkh",
6
6
  "repository": {
@@ -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 unreachable: `--github-mirror https://gh-proxy.com/` (alternatives:
81
- `https://ghproxy.net/`, `https://ghfast.top/`). For the `skills` CLI
82
- download itself, configure an npm registry mirror such as
83
- `https://registry.npmmirror.com`.
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 { gitUrlForSource, isCommitSha, probeRevision } from "./revision.mjs"
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
- const result = execute(command, { cwd, env })
111
+ result = execute(command, { cwd, env })
105
112
  const code = statusCode(result)
106
- if (code === 0 || result.signal || attempt >= retries) return result
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 = resolveGroupRevision(group.skills, probe, { env: gitEnv })
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
- const batch = executeTolerant(group.command, { cwd: options.cwd, env: gitEnv })
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 = resolveGroupRevision(group.skills, probe, { env: gitEnv })
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
@@ -1,4 +1,5 @@
1
1
  export const GITHUB_BASE = "https://github.com/"
2
+ export const DEFAULT_FALLBACK_MIRROR = "https://gh-proxy.com/"
2
3
 
3
4
  export function normalizeMirror(mirror) {
4
5
  if (mirror === undefined || mirror === null || mirror === "") return undefined
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
+ }