@wangkh/skill-sync-cli 0.2.1 → 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/
@@ -262,6 +265,11 @@ working include `https://gh-proxy.com/`, `https://ghproxy.net/`, and
262
265
  `https://ghfast.top/`. To speed up the `skills` CLI download itself on a fresh
263
266
  machine, configure an npm registry mirror such as `https://registry.npmmirror.com`.
264
267
 
268
+ Child `skills` commands run with `DO_NOT_TRACK=1`, so telemetry stays off. A
269
+ known Windows exit crash in the `skills` CLI after a completed install is
270
+ tolerated: when the files are in place, `sync` treats the group as installed
271
+ instead of failing.
272
+
265
273
  For project-scoped skills, set:
266
274
 
267
275
  ```json
@@ -279,6 +287,21 @@ location. From the target application project, run:
279
287
  npx @wangkh/skill-sync-cli sync --config /path/to/my-skills/skills.json
280
288
  ```
281
289
 
290
+ ## Agent Skill
291
+
292
+ The repository ships an agent skill (`skills/skill-sync/SKILL.md`) that teaches
293
+ an agent how to use this CLI. Install it from the repository:
294
+
295
+ ```bash
296
+ npx skills add yudaiyan/skill-sync-cli
297
+ ```
298
+
299
+ Or declare it in the manifest and let `sync` install it:
300
+
301
+ ```json
302
+ { "name": "skill-sync", "source": "yudaiyan/skill-sync-cli" }
303
+ ```
304
+
282
305
  ## Run Before the First Release
283
306
 
284
307
  From a source checkout, the following commands work without installing project
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/
@@ -132,6 +132,8 @@ npx @wangkh/skill-sync-cli sync --github-mirror https://gh-proxy.com/
132
132
 
133
133
  镜像通过 git 的 `insteadOf` 重写注入,只对本次命令生效,不会修改全局 git 配置。实测可用的免费公益镜像包括 `https://gh-proxy.com/`、`https://ghproxy.net/`、`https://ghfast.top/`。新机器上如果 `skills` CLI 下载慢,可以配置 npm 镜像源 `https://registry.npmmirror.com`。
134
134
 
135
+ wrapper 会以 `DO_NOT_TRACK=1` 运行 `skills` CLI(关闭遥测)。它在 Windows 上安装完成后偶发退出崩溃(0xC0000409);只要文件已就位,`sync` 会把该组视为安装成功,不再误判失败。
136
+
135
137
  只预览,或者在单个 Skill 失败后继续处理其他条目:
136
138
 
137
139
  ```bash
@@ -186,6 +188,20 @@ Git 负责在机器之间传递清单,`sync` 根据本地清单安装 Skills
186
188
 
187
189
  清单是这个脚本定义的格式;脚本读取后逐项调用官方 `skills` CLI。上游的 `.skill-lock.json` 继续由官方 CLI 维护。
188
190
 
191
+ ## 给 agent 用的 skill
192
+
193
+ 仓库自带一个 agent skill(`skills/skill-sync/SKILL.md`),用来告诉 agent 如何使用本 CLI。可以直接安装:
194
+
195
+ ```bash
196
+ npx skills add yudaiyan/skill-sync-cli
197
+ ```
198
+
199
+ 也可以加进清单,交给 `sync` 安装:
200
+
201
+ ```json
202
+ { "name": "skill-sync", "source": "yudaiyan/skill-sync-cli" }
203
+ ```
204
+
189
205
  ## 发布前使用
190
206
 
191
207
  获取项目源码后,可以在源码目录直接运行,无需安装项目依赖:
@@ -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.1",
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": {
@@ -27,6 +27,7 @@
27
27
  "bin",
28
28
  "src",
29
29
  "schemas",
30
+ "skills",
30
31
  "README.md",
31
32
  "README.zh-CN.md",
32
33
  "RELEASING.md",
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: skill-sync
3
+ description: >-
4
+ Manage the user's portable agent-skill manifest with the skill-sync CLI
5
+ (@wangkh/skill-sync-cli). Use when the user wants to sync, back up, or
6
+ restore agent skills across machines ("同步我的 skills", "恢复 skill 环境",
7
+ "新电脑安装 skills"), add or remove a skill in their manifest
8
+ ("把 skill 加到清单"), preview or validate the manifest, or install skills
9
+ when GitHub is unreachable. Also use when a skill installation fails and the
10
+ user asks why or how to retry.
11
+ ---
12
+
13
+ # skill-sync
14
+
15
+ `@wangkh/skill-sync-cli` keeps a portable JSON manifest of third-party agent
16
+ skills and installs them through the official `skills` CLI. The manifest is the
17
+ single source of truth; the tool only writes it during `init`.
18
+
19
+ ## When to use
20
+
21
+ - Sync or restore skills on a machine: `sync`
22
+ - Add a skill to the manifest after finding it: edit the manifest, then `sync`
23
+ - Preview or validate before installing: `plan`, `validate`
24
+ - GitHub is blocked or slow: run `sync --github-mirror https://gh-proxy.com/`
25
+ - An install failed: check retries, the mirror, or `--continue-on-error`
26
+
27
+ ## Commands
28
+
29
+ Run through npx so no global install is needed:
30
+
31
+ ```bash
32
+ npx @wangkh/skill-sync-cli init # create the manifest (keeps an existing one)
33
+ npx @wangkh/skill-sync-cli path # print the manifest path in use
34
+ npx @wangkh/skill-sync-cli validate # parse and validate without network
35
+ npx @wangkh/skill-sync-cli plan # print the exact skills commands
36
+ npx @wangkh/skill-sync-cli sync # install or update declared skills
37
+ ```
38
+
39
+ Useful options:
40
+
41
+ | Option | Meaning |
42
+ | --- | --- |
43
+ | `-c, --config <file>` | Use another manifest file (all commands) |
44
+ | `--from <url>` | `init` from a published manifest URL |
45
+ | `--dry-run` | Preview without installing |
46
+ | `--continue-on-error` | Keep going after a failure; exit code stays non-zero |
47
+ | `--retries <n>` | Retry failed installs (default 2) |
48
+ | `--github-mirror <url>` | Route GitHub clones and update checks through a mirror |
49
+ | `--force` | `init`: replace the file; `sync`: reinstall even if unchanged |
50
+
51
+ ## Manifest
52
+
53
+ Default location:
54
+
55
+ - Windows: `%USERPROFILE%\.config\skill-sync\skills.json`
56
+ - macOS/Linux: `~/.config/skill-sync/skills.json`
57
+
58
+ Skill entries require `name` (the upstream skill name) and `source`
59
+ (`owner/repo`, Git URL, skill URL, or archive). Optional fields are
60
+ `description`, `ref`, `enabled`, `agents`, `scope`, and `copy`; shared
61
+ defaults live under `defaults`, and the `skills` CLI version is pinned under
62
+ `cli`.
63
+
64
+ To add a skill, append an entry and run `sync`. Confirm the upstream name
65
+ first:
66
+
67
+ ```bash
68
+ npx --yes skills@1.5.22 add <owner>/<repo> --list
69
+ ```
70
+
71
+ ## Behavior worth knowing
72
+
73
+ - Entries sharing `source`, `ref`, `agents`, `scope`, and `copy` install with
74
+ one `skills add` call; a failed batch falls back to installing each skill.
75
+ - `sync` records revisions in `~/.config/skill-sync/state.json` and skips
76
+ groups whose revision and installed files are unchanged. `--force`
77
+ overrides the skip.
78
+ - When the update check cannot reach the remote but the installed copy still
79
+ exists, `sync` keeps it instead of failing.
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`.
85
+
86
+ ## Security
87
+
88
+ A skill is agent instructions and may run scripts; review third-party skills
89
+ before adding them. Prefer trusted repositories and immutable commit refs.
90
+
91
+ Full documentation lives in the project README and
92
+ `schemas/skills-sync.schema.json`.
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,
@@ -30,6 +36,9 @@ import { downloadManifest } from "./remote.mjs"
30
36
 
31
37
  export const DEFAULT_RETRIES = 2
32
38
  const DEFAULT_RETRY_DELAY_MS = 2000
39
+ // The skills CLI occasionally aborts on exit after a completed install on
40
+ // Windows (libuv teardown assertion). These are the observed exit codes.
41
+ const POST_INSTALL_CRASH_CODES = new Set([3221226505, -1073740791])
33
42
 
34
43
  const SAMPLE_MANIFEST = {
35
44
  version: 1,
@@ -96,17 +105,26 @@ function resolveGroupRevision(skills, probe, probeOptions) {
96
105
  return probe(url, ref, probeOptions)
97
106
  }
98
107
 
99
- 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
100
110
  for (let attempt = 0; ; attempt += 1) {
101
- const result = execute(command, { cwd, env })
111
+ result = execute(command, { cwd, env })
102
112
  const code = statusCode(result)
103
- if (code === 0 || result.signal || attempt >= retries) return result
113
+ if (code === 0 || result.signal || attempt >= retries) break
104
114
 
105
115
  const delayMs = retryDelayMs * (attempt + 1)
106
116
  const delayText = delayMs > 0 ? ` in ${Math.round(delayMs / 1000)}s` : ""
107
117
  console.error(`[${label}] attempt ${attempt + 1}/${retries + 1} failed with exit code ${code}; retrying${delayText}`)
108
118
  if (delayMs > 0) await sleep(delayMs)
109
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
110
128
  }
111
129
 
112
130
  export async function planCommand(manifestFilename) {
@@ -137,7 +155,6 @@ export async function syncCommand(manifestFilename, options = {}) {
137
155
  const home = options.home ?? os.homedir()
138
156
  const cwd = options.cwd ?? process.cwd()
139
157
  const probe = options.probe ?? probeRevision
140
- const gitEnv = mirrorGitEnv(options.githubMirror)
141
158
 
142
159
  if (skills.length === 0) {
143
160
  console.log("No enabled skills.")
@@ -151,6 +168,30 @@ export async function syncCommand(manifestFilename, options = {}) {
151
168
  command: commandForGroup(group, manifest.cli),
152
169
  }))
153
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
+
154
195
  const stateFilename = options.stateFile ?? statePath(home)
155
196
  const state = await readState(stateFilename)
156
197
 
@@ -171,18 +212,28 @@ export async function syncCommand(manifestFilename, options = {}) {
171
212
 
172
213
  const key = stateKey(group.skills)
173
214
  const entry = stateEntry(state, filename, key)
174
- const installedHere = allInstalled(group.skills, {
215
+ const installScope = {
175
216
  global: group.skills[0].scope === "global",
176
217
  cwd,
177
218
  home,
178
- })
219
+ }
220
+ const installedHere = allInstalled(group.skills, installScope)
221
+ // A completed install followed by the known exit crash counts as success.
222
+ const executeTolerant = (command, runOptions) => {
223
+ const result = execute(command, runOptions)
224
+ if (POST_INSTALL_CRASH_CODES.has(statusCode(result)) && allInstalled(group.skills, installScope)) {
225
+ console.error(`[${sourceLabel(group.skills)}] the skills CLI crashed after installing (known Windows bug); treating the install as complete`)
226
+ return { status: 0 }
227
+ }
228
+ return result
229
+ }
179
230
  // Only a previous install can be skipped, so check the remote before
180
231
  // installing only when there is a record and the files are still present.
181
232
  // A first install records the revision afterwards.
182
233
  let revision = null
183
234
 
184
235
  if (!options.force && entry && installedHere) {
185
- revision = resolveGroupRevision(group.skills, probe, { env: gitEnv })
236
+ revision = resolveRevision(group)
186
237
  if (revision.status === "known" && entry.revision === revision.revision) {
187
238
  console.log(`# unchanged (${revision.revision.slice(0, 7)}); skipping, use --force to reinstall`)
188
239
  skipped += 1
@@ -202,9 +253,10 @@ export async function syncCommand(manifestFilename, options = {}) {
202
253
  if (group.skills.length === 1) {
203
254
  const skill = group.skills[0]
204
255
  printDescriptions(group.skills)
205
- const result = await runWithRetries(execute, group.command, {
256
+ const result = await runWithRetries(executeTolerant, group.command, {
206
257
  cwd: options.cwd,
207
258
  env: gitEnv,
259
+ fallbackEnv,
208
260
  retries,
209
261
  retryDelayMs,
210
262
  label: skill.name,
@@ -219,7 +271,11 @@ export async function syncCommand(manifestFilename, options = {}) {
219
271
  // A batched command shares one fetch. Since a failure could come from a
220
272
  // single bad name or a transient error, retry each entry individually
221
273
  // when the batch does not succeed.
222
- const batch = execute(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
+ }
223
279
  if (statusCode(batch) !== 0) {
224
280
  if (batch.signal) {
225
281
  failures += group.skills.length
@@ -229,9 +285,10 @@ export async function syncCommand(manifestFilename, options = {}) {
229
285
  } else {
230
286
  console.error(`[${sourceLabel(group.skills)}] batch install failed; retrying each skill individually`)
231
287
  for (const skill of group.skills) {
232
- const result = await runWithRetries(execute, commandFor(skill, manifest.cli), {
288
+ const result = await runWithRetries(executeTolerant, commandFor(skill, manifest.cli), {
233
289
  cwd: options.cwd,
234
290
  env: gitEnv,
291
+ fallbackEnv,
235
292
  retries,
236
293
  retryDelayMs,
237
294
  label: skill.name,
@@ -251,7 +308,7 @@ export async function syncCommand(manifestFilename, options = {}) {
251
308
  }
252
309
 
253
310
  if (!groupFailed) {
254
- if (!revision) revision = resolveGroupRevision(group.skills, probe, { env: gitEnv })
311
+ if (!revision) revision = resolveRevision(group)
255
312
  if (revision.status === "known") {
256
313
  setStateEntry(state, filename, key, {
257
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
+ }
package/src/runner.mjs CHANGED
@@ -89,6 +89,13 @@ function windowsNpxCli(env) {
89
89
  return filename
90
90
  }
91
91
 
92
+ // The skills CLI can crash on exit on some Windows setups (a libuv teardown
93
+ // assertion) while its telemetry flush is pending. DO_NOT_TRACK disables that
94
+ // flush so the process exits cleanly; it is also a privacy default.
95
+ export function childEnv(env = process.env) {
96
+ return { ...env, DO_NOT_TRACK: "1" }
97
+ }
98
+
92
99
  export function runCommand(command, { cwd, env = process.env, timeout } = {}) {
93
100
  try {
94
101
  // Windows cannot spawn .cmd files directly. Run npx's JS entry point so
@@ -97,7 +104,7 @@ export function runCommand(command, { cwd, env = process.env, timeout } = {}) {
97
104
  return spawnSync(useNode ? process.execPath : command.executable,
98
105
  useNode ? [windowsNpxCli(env), ...command.args] : command.args, {
99
106
  cwd,
100
- env,
107
+ env: childEnv(env),
101
108
  timeout,
102
109
  stdio: "inherit",
103
110
  windowsHide: true,