skillrepo 4.4.0 → 4.5.1

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
@@ -179,14 +179,48 @@ One-shot fetch. Does NOT mutate your library or the server — just reads
179
179
  `GET /api/v1/skills/{owner}/{name}` and writes the skill to disk. Use
180
180
  this to preview or pin a specific skill without adding it to your library.
181
181
 
182
- ### `list` — show what's in your library
182
+ ### `list` — show what's in your library and what needs syncing
183
183
 
184
184
  ```sh
185
185
  skillrepo list [--json]
186
186
  ```
187
187
 
188
- Renders your library as a table: owner, name, version, description. Uses
189
- the same cached ETag as `update`.
188
+ Renders your library as a table with a per-row `Local` column showing
189
+ on-disk drift state for each detected vendor:
190
+
191
+ - `OK` — local copy matches the last sync.
192
+ - `STALE` — library has a newer version than what's on disk.
193
+ - `MISS` — library has the skill but no on-disk placement (or no sync
194
+ history yet — run `skillrepo update` to establish a baseline).
195
+ - `EDIT` — local files have been modified since the last sync (different
196
+ SHA than what was persisted).
197
+
198
+ When multiple vendors are detected, the column shows a worst-state-wins
199
+ rollup (`missing > edited > stale > current`). The `--json` output
200
+ includes a `placements[]` array per item with per-vendor states for
201
+ scripts that want the full breakdown.
202
+
203
+ A footer reports library-level sync state:
204
+
205
+ - `library in sync — local skills up to date` — everything is current.
206
+ - `library in sync — but N skills show local drift` — library hasn't
207
+ changed but some local placements need attention.
208
+ - `library has changed since last sync` — registry has new content; run
209
+ `skillrepo update`.
210
+ - `No sync history on this machine` — fresh install or
211
+ baseline-less state; run `skillrepo update`.
212
+
213
+ Glyphs (`✓` / `⚠`) are used in TTY contexts; ASCII fallbacks (`OK` /
214
+ `[!]` / `STALE` / `MISS` / `EDIT`) appear when stdout is not a TTY or
215
+ `NO_COLOR` is set, so piped output stays clean.
216
+
217
+ `--json` is a bare array of skill objects with the additional fields
218
+ `state` (rollup) and `placements[]`. Existing scripts that consume
219
+ the pre-#1555 `--json` shape keep working — the additions are purely
220
+ per-item, no top-level wrapper.
221
+
222
+ Uses the same cached ETag as `update` for the library-level footer
223
+ state.
190
224
 
191
225
  ### `search` — explore the registry
192
226
 
@@ -474,8 +508,28 @@ Two scenarios worth calling out:
474
508
  | `SKILLREPO_ACCESS_KEY` | Access key for any command. Takes precedence over the config file but not CLI flags. |
475
509
  | `SKILLREPO_URL` | Server URL. Same precedence as above. |
476
510
  | `SKILLREPO_TIMEOUT_MS` | Per-request fetch timeout in milliseconds (default 30000). Set to `0` to disable. |
511
+ | `SKILLREPO_NO_UPDATE_CHECK` | Set to any non-empty truthy value (`1`, `yes`, `true`) to disable the post-command npm-registry self-staleness check. The check otherwise runs at most once per 24 hours, hits `registry.npmjs.org/skillrepo/latest`, and prints a one-line upgrade hint to stderr if a newer version is available. Auto-disabled when `CI=true`. |
477
512
  | `NO_COLOR` | Set any non-empty value to disable ANSI color in CLI output. |
478
513
 
514
+ ### Update nudge
515
+
516
+ After every command that isn't `--json`, the CLI does a best-effort
517
+ check against `https://registry.npmjs.org/skillrepo/latest` and prints
518
+ a one-line hint on stderr when a newer version is available:
519
+
520
+ ```
521
+ A newer skillrepo is available: 4.3.0 → 4.5.0
522
+ Upgrade: npm install -g skillrepo@latest
523
+ ```
524
+
525
+ The check is cached at `~/.claude/skillrepo/.npm-version-check` for
526
+ 24 hours on success and 1 hour on failure, has a 2-second fetch
527
+ timeout, and is fire-and-forget — every failure mode (network error,
528
+ non-2xx, parse failure, read-only FS) is swallowed silently. Set
529
+ `SKILLREPO_NO_UPDATE_CHECK=1` to disable. `CI=true` auto-disables.
530
+ Output is suppressed entirely when `--json` is on the parent command,
531
+ so structured pipelines aren't disturbed.
532
+
479
533
  ### Skill placement
480
534
 
481
535
  Skills land at one of two project paths, depending on the agent:
package/bin/skillrepo.mjs CHANGED
@@ -35,6 +35,8 @@ import { runSearch } from "../src/commands/search.mjs";
35
35
  import { runUninstall } from "../src/commands/uninstall.mjs";
36
36
  import { runSessionSync } from "../src/commands/session-sync.mjs";
37
37
  import { CliError, EXIT_OK, EXIT_VALIDATION } from "../src/lib/errors.mjs";
38
+ import { checkForCliUpdate } from "../src/lib/npm-update-check.mjs";
39
+ import { getCliVersion } from "../src/lib/cli-version.mjs";
38
40
 
39
41
  // ── Command registry ────────────────────────────────────────────────────
40
42
 
@@ -151,6 +153,49 @@ async function main(command, fullArgv) {
151
153
  }
152
154
 
153
155
  await COMMANDS[command].run(rest);
156
+
157
+ // After the primary command completes, do a best-effort npm-registry
158
+ // staleness check (#1554). Never blocks exit beyond a single tick:
159
+ // `Promise.race` against `setTimeout(0)` means if the fetch hasn't
160
+ // completed yet, we drop it and the user gets the nudge on the next
161
+ // invocation (when the cache file is more likely to be fresh).
162
+ //
163
+ // Suppressed entirely when the parent argv carries `--json` so we
164
+ // never inject text into a structured stream — not even on stderr,
165
+ // because some pipelines tee both streams.
166
+ if (!rest.includes("--json")) {
167
+ await runUpdateCheck();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Best-effort update check. On a CACHE HIT this resolves synchronously
173
+ * (within a microtask) and we emit the nudge before the 0ms timer
174
+ * fires — that's the steady-state behavior after the first invocation.
175
+ *
176
+ * On a cache miss we race against `setTimeout(0)` so the user's primary
177
+ * command exit isn't held up by the nudge logic when they don't have
178
+ * a cached answer yet. The in-flight fetch is INTENTIONALLY left
179
+ * running after the race resolves: it has its own 2s `AbortController`
180
+ * (inside `checkForCliUpdate`) and is silent on failure, so the worst
181
+ * case is the process appearing to take slightly longer on its very
182
+ * first invocation while the cache warms. Every subsequent invocation
183
+ * benefits from the 24h positive cache and exits immediately.
184
+ *
185
+ * Errors are swallowed at the source by `checkForCliUpdate` itself; the
186
+ * outer `.catch` is belt-and-braces for the rare case where loading
187
+ * the module synchronously throws.
188
+ */
189
+ async function runUpdateCheck() {
190
+ let currentVersion;
191
+ try {
192
+ currentVersion = getCliVersion();
193
+ } catch {
194
+ return;
195
+ }
196
+ const checker = checkForCliUpdate({ currentVersion }).catch(() => {});
197
+ const yieldOnce = new Promise((resolve) => setTimeout(resolve, 0));
198
+ await Promise.race([checker, yieldOnce]);
154
199
  }
155
200
 
156
201
  // ── Help printing ───────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "4.4.0",
3
+ "version": "4.5.1",
4
4
  "description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library from any IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "license": "SEE LICENSE IN LICENSE",
27
27
  "dependencies": {
28
28
  "cli-table3": "^0.6.5",
29
- "gray-matter": "^4.0.3"
29
+ "gray-matter": "^4.0.3",
30
+ "semver": "^7.6.3"
30
31
  }
31
32
  }
@@ -46,6 +46,7 @@ import {
46
46
  effectiveVendors,
47
47
  requireVendorTargets,
48
48
  } from "../lib/cli-config.mjs";
49
+ import { upsertLastSyncEntry } from "../lib/sync.mjs";
49
50
  import { parseIdentifier, formatIdentifier } from "../lib/identifier.mjs";
50
51
  import { validationError } from "../lib/errors.mjs";
51
52
 
@@ -158,6 +159,20 @@ export async function runAdd(argv, io = {}) {
158
159
 
159
160
  writeSkillDir(skill, { vendors, global: flags.global });
160
161
 
162
+ // Update `.last-sync` so `list` immediately recognizes this skill
163
+ // as `current`. Without this the user adds a skill, then runs
164
+ // `list`, and sees their freshly-added skill marked MISS — the
165
+ // exact same class of contract gap that PR #1574 fixed for the
166
+ // multi-vendor case in `update`/`list`. See `upsertLastSyncEntry`
167
+ // docstring for the etag-preservation rationale.
168
+ try {
169
+ upsertLastSyncEntry(skill);
170
+ } catch {
171
+ // Non-fatal: skill is on disk, the state file write failure
172
+ // does not change that. Same semantics as runSync's warn-and-
173
+ // proceed for last-sync persistence failures.
174
+ }
175
+
161
176
  if (flags.json) {
162
177
  stdout.write(
163
178
  JSON.stringify(
@@ -36,6 +36,7 @@ import {
36
36
  effectiveVendors,
37
37
  requireVendorTargets,
38
38
  } from "../lib/cli-config.mjs";
39
+ import { upsertLastSyncEntry } from "../lib/sync.mjs";
39
40
  import { parseIdentifier, formatIdentifier } from "../lib/identifier.mjs";
40
41
  import { validationError } from "../lib/errors.mjs";
41
42
 
@@ -109,6 +110,21 @@ export async function runGet(argv, io = {}) {
109
110
 
110
111
  writeSkillDir(skill, { vendors, global: flags.global });
111
112
 
113
+ // Update `.last-sync` so `list` recognizes this skill as `current`
114
+ // afterward instead of reporting a stale `MISSING`. Best-effort:
115
+ // a state-file write failure is non-fatal (the files are already
116
+ // on disk), but writeLastSync throws diskError if it cannot
117
+ // persist — we catch and continue. Pre-PR #1574 `get` skipped
118
+ // this entirely, which surfaced every freshly-fetched skill as
119
+ // MISS in the very next `list` invocation.
120
+ try {
121
+ upsertLastSyncEntry(skill);
122
+ } catch {
123
+ // Same failure semantics as runSync's "couldn't persist last
124
+ // sync state" path: warn and proceed, the user's intent (files
125
+ // on disk) was honored.
126
+ }
127
+
112
128
  if (flags.json) {
113
129
  stdout.write(
114
130
  JSON.stringify({