skillrepo 4.15.0 → 4.17.0
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 +71 -38
- package/bin/skillrepo.mjs +1 -1
- package/package.json +2 -2
- package/src/commands/list-providers.mjs +285 -0
- package/src/commands/list-render.mjs +600 -0
- package/src/commands/list.mjs +161 -851
- package/src/lib/drift.mjs +140 -9
- package/src/lib/global-boundary.mjs +8 -44
- package/src/lib/http.mjs +5 -0
- package/src/lib/list-engine.mjs +517 -0
- package/src/lib/sync.mjs +47 -21
- package/src/test/commands/list.test.mjs +476 -1498
- package/src/test/commands/update.test.mjs +53 -13
- package/src/test/e2e/cli-commands.test.mjs +16 -1
- package/src/test/lib/list-engine.test.mjs +328 -0
- package/src/test/lib/sync.test.mjs +15 -7
package/README.md
CHANGED
|
@@ -167,9 +167,10 @@ skillrepo update [--global] [--agent <list>] [--json] [--silent]
|
|
|
167
167
|
|
|
168
168
|
Pulls the latest state of your library from the server using a delta
|
|
169
169
|
sync. Writes new and updated skills, removes skills that were removed
|
|
170
|
-
from the library, and skips skills that are unchanged.
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
from the library, and skips skills that are unchanged. In a repo that
|
|
171
|
+
declares a skillset, `update` instead reconciles that declared set (a
|
|
172
|
+
full-set resolve, not a delta). Uses ETag caching so repeat runs return
|
|
173
|
+
`304 Not Modified` when nothing has changed.
|
|
173
174
|
|
|
174
175
|
`--silent` suppresses normal output and writes a single `{}` line to
|
|
175
176
|
stdout on success. Designed for SessionStart hooks that pipe stdout
|
|
@@ -246,48 +247,80 @@ One-shot fetch. Does NOT mutate your library or the server — just reads
|
|
|
246
247
|
`GET /api/v1/skills/{owner}/{name}` and writes the skill to disk. Use
|
|
247
248
|
this to preview or pin a specific skill without adding it to your library.
|
|
248
249
|
|
|
249
|
-
### `list` — show what's
|
|
250
|
+
### `list` — show what's on disk and how it compares to the server
|
|
250
251
|
|
|
251
252
|
```sh
|
|
252
253
|
skillrepo list [--json]
|
|
253
254
|
```
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
`list` is disk-first: it walks the skills actually on disk, then
|
|
257
|
+
annotates each against your last sync (local integrity) and the server
|
|
258
|
+
(governance + freshness). This means an on-disk skill is never hidden —
|
|
259
|
+
a hand-authored directory, a skill the server has withdrawn, or one you
|
|
260
|
+
deleted all show up, alongside the ones that are healthy.
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
- `MISS` — library has the skill but no on-disk placement (or no sync
|
|
261
|
-
history yet — run `skillrepo update` to establish a baseline).
|
|
262
|
-
- `EDIT` — local files have been modified since the last sync (different
|
|
263
|
-
SHA than what was persisted).
|
|
262
|
+
The `State` column shows one worst-wins signal per row, staying quiet
|
|
263
|
+
when a row is healthy:
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
A footer reports library-level sync state:
|
|
271
|
-
|
|
272
|
-
- `library in sync — local skills up to date` — everything is current.
|
|
273
|
-
- `library in sync — but N skills show local drift` — library hasn't
|
|
274
|
-
changed but some local placements need attention.
|
|
275
|
-
- `library has changed since last sync` — registry has new content; run
|
|
265
|
+
- `OK` — on disk, matches your last sync, and the server hasn't moved on.
|
|
266
|
+
- `EDITED` — local files were modified since the last sync; `update`
|
|
267
|
+
would overwrite them.
|
|
268
|
+
- `BEHIND` — the server has a newer version than what you synced; run
|
|
276
269
|
`skillrepo update`.
|
|
277
|
-
- `
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
270
|
+
- `SERVING` — the server rolled a recalled version back to an earlier
|
|
271
|
+
one; you're being served the fallback.
|
|
272
|
+
- `WITHDRAWN` — on disk and once served to you, but the server no longer
|
|
273
|
+
serves it. The CLI can't tell a recall from an unpublish or a skillset
|
|
274
|
+
re-scope, so it doesn't guess — it flags that it's no longer served
|
|
275
|
+
(remove it, or investigate).
|
|
276
|
+
- `MISSING` — expected (in your library, or your repo's skillset) but not
|
|
277
|
+
on disk.
|
|
278
|
+
- `FOREIGN` — on disk in a managed directory, but not from your library.
|
|
279
|
+
- `NOT DELIVERED` — a skillset member the CLI declined to write (edited
|
|
280
|
+
locally, replaced, incomplete, or invalid), with the reason.
|
|
281
|
+
- `NOT CHECKED` — the server couldn't be reached, so freshness is
|
|
282
|
+
unknown. `list` never reports a stale copy as current.
|
|
283
|
+
|
|
284
|
+
`list` speaks the dashboard's `LATEST` / `SERVING` vocabulary (green is
|
|
285
|
+
used only for a served head version), so healthy and recall-fallback
|
|
286
|
+
states read identically on the CLI and the web. Where the CLI can't verify
|
|
287
|
+
a governance cause — a skill that's simply no longer served — it says
|
|
288
|
+
`WITHDRAWN` rather than asserting `RECALLED`; `recalled` appears only in a
|
|
289
|
+
`SERVING` row's reason, where the server names the recalled version. Each attention row also carries a
|
|
290
|
+
plain-English reason — what changed, why, and what to do — listed in an
|
|
291
|
+
attention block beneath the table. Both `list` views render the same way
|
|
292
|
+
(see "Same view everywhere" below).
|
|
293
|
+
|
|
294
|
+
When multiple vendors are detected, the local integrity axis is a
|
|
295
|
+
worst-wins rollup (`missing > edited > present`).
|
|
296
|
+
|
|
297
|
+
**Same view everywhere.** A repo that declares a skillset shows its
|
|
298
|
+
delivered set in the identical table — same columns, same colours, same
|
|
299
|
+
attention block as the whole-library view — plus a compliance line for
|
|
300
|
+
the declared set. `list` issues a non-recording freshness probe for the
|
|
301
|
+
declared set (a full read that resolves and records nothing) so a
|
|
302
|
+
recalled-then-restored skill reads `BEHIND` even inside the sync throttle
|
|
303
|
+
window, and a recall fallback reads `SERVING` rather than a false green.
|
|
304
|
+
|
|
305
|
+
**Offline-capable.** `list` works with no network: the local axes are
|
|
306
|
+
computed from disk, and the server axis degrades to `NOT CHECKED` rather
|
|
307
|
+
than failing.
|
|
308
|
+
|
|
309
|
+
**Read-only.** `list` never records a delivery, writes sync state, or
|
|
310
|
+
consumes a warning `update` owes you.
|
|
311
|
+
|
|
312
|
+
The `State` chip is coloured to match the dashboard — green for the one
|
|
313
|
+
healthy state (`OK` / `LATEST`), amber for `SERVING`, red for `WITHDRAWN`
|
|
314
|
+
and `MISSING`. Glyphs and colour are used in TTY contexts; plain ASCII
|
|
315
|
+
tokens (`OK` / `EDITED` / `BEHIND` / …) with no colour appear when stdout
|
|
316
|
+
is not a TTY or `NO_COLOR` is set, so piped output stays clean.
|
|
317
|
+
|
|
318
|
+
`--json` is a bare array of skill objects (a top-level object with a
|
|
319
|
+
`skillset` block in a declared repo). Each row carries the full model —
|
|
320
|
+
`classification`, `local`, `governance`, `freshness`, the collapsed
|
|
321
|
+
`signal`, and a per-vendor `placements[]` array. The pre-#2806 `state`
|
|
322
|
+
(worst-wins drift rollup) field is kept for backward compatibility, so
|
|
323
|
+
existing scripts keep working.
|
|
291
324
|
|
|
292
325
|
### `search` — explore the registry
|
|
293
326
|
|
|
@@ -460,7 +493,7 @@ Auto-refresh hooks for Windsurf and Cline are not yet supported — those agents
|
|
|
460
493
|
|
|
461
494
|
**On 304 (nothing changed) the hook is silent.** You only see output when your library actually syncs or a failure happens. No "Syncing…" noise on every session.
|
|
462
495
|
|
|
463
|
-
**Throttled to skip redundant syncs (v4.9.0+).** Because the hook fires on *every* editor session, the hook-triggered `update` (`--session-hook` / `--silent`) syncs at most once per ~15 minutes: within that window it exits immediately with **no network call**. This bounds redundant per-session load. The tradeoff is that a brand-new session started shortly after a recent sync may be up to ~15 minutes behind a just-published change — and the same bound applies to local repair: if you delete or corrupt a synced skill's folder on disk, the hook restores it on its next non-throttled run, up to ~15 minutes later. A bare `skillrepo update` **you** run yourself is never throttled — it always syncs immediately (repairing any local damage) and resets the window. `skillrepo list` is likewise never throttled
|
|
496
|
+
**Throttled to skip redundant syncs (v4.9.0+).** Because the hook fires on *every* editor session, the hook-triggered `update` (`--session-hook` / `--silent`) syncs at most once per ~15 minutes: within that window it exits immediately with **no network call**. This bounds redundant per-session load. The tradeoff is that a brand-new session started shortly after a recent sync may be up to ~15 minutes behind a just-published change — and the same bound applies to local repair: if you delete or corrupt a synced skill's folder on disk, the hook restores it on its next non-throttled run, up to ~15 minutes later. A bare `skillrepo update` **you** run yourself is never throttled — it always syncs immediately (repairing any local damage) and resets the window. `skillrepo list` is likewise never throttled: it reads the server live (a non-recording request) when reachable and degrades to local-only when it isn't, and it never resets the window. A repo that declares a skillset throttles on its **own** per-repo clock, so an unrelated repo's bulk sync elsewhere on the machine can no longer starve its refresh (v4.16.0+).
|
|
464
497
|
|
|
465
498
|
Flags:
|
|
466
499
|
|
package/bin/skillrepo.mjs
CHANGED
|
@@ -54,7 +54,7 @@ const COMMANDS = {
|
|
|
54
54
|
run: async (argv) => runInit(argv),
|
|
55
55
|
},
|
|
56
56
|
update: {
|
|
57
|
-
description: "Sync your library
|
|
57
|
+
description: "Sync your library, or this repo's declared skillset, from the registry",
|
|
58
58
|
usage: "skillrepo update [--global] [--agent <list>] [--json]",
|
|
59
59
|
run: async (argv) => runUpdate(argv),
|
|
60
60
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillrepo",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library
|
|
3
|
+
"version": "4.17.0",
|
|
4
|
+
"description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library, synced across your dev environment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skillrepo": "./bin/skillrepo.mjs"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillrepo list` — data-gathering providers (#2806).
|
|
3
|
+
*
|
|
4
|
+
* Everything that turns the environment (detected vendors, the server,
|
|
5
|
+
* the last-sync baseline, the per-repo state) into the normalized inputs
|
|
6
|
+
* the disk-first engine (`lib/list-engine.mjs`) classifies. The command
|
|
7
|
+
* orchestrators in `list.mjs` compose these with the walk + the engine +
|
|
8
|
+
* the renderers (`list-render.mjs`).
|
|
9
|
+
*
|
|
10
|
+
* READ-ONLY to the letter: the library manifest peek (#1832) and the
|
|
11
|
+
* skillset freshness probe (`manifest=1&skillset=…`) both record NO
|
|
12
|
+
* delivery, and neither writes local state. The one server-touching
|
|
13
|
+
* failure mode that MUST NOT crash a status command — an unreachable /
|
|
14
|
+
* transient server — degrades the server axis to NOT CHECKED here, never
|
|
15
|
+
* a false "up to date".
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getLibrary } from "../lib/http.mjs";
|
|
19
|
+
import { readLastSync } from "../lib/sync.mjs";
|
|
20
|
+
import { detectAgents } from "../lib/detect-agents.mjs";
|
|
21
|
+
import { getAgentByKey } from "../lib/agent-registry.mjs";
|
|
22
|
+
import { EXIT_NETWORK } from "../lib/errors.mjs";
|
|
23
|
+
import {
|
|
24
|
+
scanGlobalBoundary,
|
|
25
|
+
managedGlobalNamesFrom,
|
|
26
|
+
GLOBAL_SHADOWED_CATEGORY,
|
|
27
|
+
GLOBAL_LIBRARY_CATEGORY,
|
|
28
|
+
GLOBAL_FOREIGN_CATEGORY,
|
|
29
|
+
} from "../lib/global-boundary.mjs";
|
|
30
|
+
import {
|
|
31
|
+
scanForeignContent,
|
|
32
|
+
buildRepoCompliance,
|
|
33
|
+
} from "../lib/foreign-content.mjs";
|
|
34
|
+
|
|
35
|
+
// ── detection + baseline shaping ────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect the vendors with footprint on this machine/project and
|
|
39
|
+
* pre-resolve the ones that contribute a project-scope placement. Shared
|
|
40
|
+
* by both views. Returns the detected entries (for the "Detected:" line)
|
|
41
|
+
* and the flat key list (for the walk + engine).
|
|
42
|
+
*
|
|
43
|
+
* @returns {{ detected: object[], detectedKeys: string[] }}
|
|
44
|
+
*/
|
|
45
|
+
export function resolveDetected() {
|
|
46
|
+
const detected = detectAgents().filter((d) => d.detected);
|
|
47
|
+
const detectedVendorEntries = detected
|
|
48
|
+
.map((d) => getAgentByKey(d.key))
|
|
49
|
+
.filter((entry) => entry !== null && entry.projectTarget !== null);
|
|
50
|
+
return { detected, detectedKeys: detectedVendorEntries.map((e) => e.key) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the baseline map keyed `"owner/name"` from a `.last-sync`-style
|
|
55
|
+
* skills map, dropping entries that fail the required-string shape
|
|
56
|
+
* check (a tampered or half-written entry). The whole-library baseline.
|
|
57
|
+
*
|
|
58
|
+
* @param {Record<string, unknown>} skillsMap
|
|
59
|
+
* @returns {Map<string, {version: string, skillMdSha256: string, filesSha256: string}>}
|
|
60
|
+
*/
|
|
61
|
+
export function buildBaselineByKey(skillsMap) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
if (!skillsMap || typeof skillsMap !== "object") return out;
|
|
64
|
+
for (const [key, raw] of Object.entries(skillsMap)) {
|
|
65
|
+
const entry = validateBaselineShape(raw);
|
|
66
|
+
if (entry) out.set(key, entry);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shape a skillset repo's `resolved` manifest into the engine's expected
|
|
73
|
+
* set + baseline. Every resolved member is expected (delivered or not);
|
|
74
|
+
* only `written: true` members with a valid SHA pair contribute a
|
|
75
|
+
* baseline (a served-but-unwritten member has no on-disk content of ours
|
|
76
|
+
* to diff against).
|
|
77
|
+
*
|
|
78
|
+
* @param {import("../lib/repo-sync-state.mjs").RepoResolvedEntry[]} resolved
|
|
79
|
+
* @returns {{ expected: object[], baselineByKey: Map<string, object> }}
|
|
80
|
+
*/
|
|
81
|
+
export function buildSkillsetInputs(resolved) {
|
|
82
|
+
const expected = resolved.map((e) => ({
|
|
83
|
+
owner: e.owner,
|
|
84
|
+
name: e.name,
|
|
85
|
+
version: e.version ?? null,
|
|
86
|
+
written: e.written === true,
|
|
87
|
+
unwrittenReason: typeof e.unwrittenReason === "string" ? e.unwrittenReason : null,
|
|
88
|
+
}));
|
|
89
|
+
const baselineByKey = new Map();
|
|
90
|
+
for (const e of resolved) {
|
|
91
|
+
if (e && e.written === true) {
|
|
92
|
+
const b = validateBaselineShape({
|
|
93
|
+
version: e.version,
|
|
94
|
+
skillMdSha256: e.skillMdSha256,
|
|
95
|
+
filesSha256: e.filesSha256,
|
|
96
|
+
});
|
|
97
|
+
if (b) baselineByKey.set(`${e.owner}/${e.name}`, b);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { expected, baselineByKey };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reject a baseline entry missing required string fields — a tampered
|
|
105
|
+
* or partially-written entry returns null, treated as "no baseline".
|
|
106
|
+
*/
|
|
107
|
+
function validateBaselineShape(entry) {
|
|
108
|
+
if (!entry || typeof entry !== "object") return null;
|
|
109
|
+
if (
|
|
110
|
+
typeof entry.version !== "string" ||
|
|
111
|
+
typeof entry.skillMdSha256 !== "string" ||
|
|
112
|
+
typeof entry.filesSha256 !== "string"
|
|
113
|
+
) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── whole-library server truth ──────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fetch the non-recording whole-library manifest (#1832) and shape it
|
|
123
|
+
* into the engine's expected set + server truth + display metadata.
|
|
124
|
+
* Governance rides on every skill since #2793. No `If-None-Match` is
|
|
125
|
+
* sent — the undeclared request stays byte-identical to the pre-#2672
|
|
126
|
+
* CLI (D2 wire identity).
|
|
127
|
+
*
|
|
128
|
+
* Offline-capable: a network/unreachable/transient failure (EXIT_NETWORK
|
|
129
|
+
* covers a fetch throw, a timeout, a 5xx, and an exhausted 429) degrades
|
|
130
|
+
* to a disk-first offline view — server axis NOT CHECKED, expected set
|
|
131
|
+
* falling back to what we last synced. A bad key / bad URL (auth /
|
|
132
|
+
* validation / scope) is a real problem the user must fix, so those
|
|
133
|
+
* still throw.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} args
|
|
136
|
+
* @param {ReturnType<typeof import("../lib/cli-config.mjs").resolveFlags>} args.flags
|
|
137
|
+
* @param {Map<string, object>} args.baselineByKey - The offline fallback expected set.
|
|
138
|
+
* @returns {Promise<{ expected: object[], serverTruth: object, metaByKey: Map<string, object>, offline: boolean }>}
|
|
139
|
+
*/
|
|
140
|
+
export async function fetchLibraryData({ flags, baselineByKey }) {
|
|
141
|
+
const metaByKey = new Map();
|
|
142
|
+
try {
|
|
143
|
+
const libraryResponse = await getLibrary(flags.serverUrl, flags.apiKey, {
|
|
144
|
+
manifest: true,
|
|
145
|
+
});
|
|
146
|
+
const skills = Array.isArray(libraryResponse.skills) ? libraryResponse.skills : [];
|
|
147
|
+
const expected = skills.map((s) => ({ owner: s.owner, name: s.name, version: s.version ?? null }));
|
|
148
|
+
const byKey = new Map();
|
|
149
|
+
for (const s of skills) {
|
|
150
|
+
byKey.set(`${s.owner}/${s.name}`, {
|
|
151
|
+
servedVersion: s.version ?? null,
|
|
152
|
+
governance: s.governance ?? null,
|
|
153
|
+
});
|
|
154
|
+
metaByKey.set(`${s.owner}/${s.name}`, {
|
|
155
|
+
description: s.description ?? "",
|
|
156
|
+
updatedAt: s.updatedAt ?? null,
|
|
157
|
+
filesIncomplete: s.filesIncomplete ?? false,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return { expected, serverTruth: { checked: true, byKey }, metaByKey, offline: false };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (!(err && err.exitCode === EXIT_NETWORK)) throw err;
|
|
163
|
+
const expected = [...baselineByKey.entries()].map(([key, b]) => {
|
|
164
|
+
const slash = key.indexOf("/");
|
|
165
|
+
return { owner: key.slice(0, slash), name: key.slice(slash + 1), version: b.version ?? null };
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
expected,
|
|
169
|
+
serverTruth: { checked: false, byKey: null },
|
|
170
|
+
metaByKey,
|
|
171
|
+
offline: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── skillset server truth + governance ──────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fire the non-recording skillset freshness probe (an UNCONDITIONAL 200
|
|
180
|
+
* manifest read) and shape the result into the engine's `ServerTruth` plus
|
|
181
|
+
* the display metadata for the Updated / Description columns. NEVER throws
|
|
182
|
+
* — the probe is a best-effort annotation, so every failure mode collapses
|
|
183
|
+
* to `checked: false` (NOT CHECKED).
|
|
184
|
+
*
|
|
185
|
+
* Unconditional by design (#2807, resolving the Phase-1 boundary): the
|
|
186
|
+
* whole-library variant already issues a full 200 every time
|
|
187
|
+
* (`fetchLibraryData` sends no `If-None-Match`), and the two `list` views
|
|
188
|
+
* must read identically. A conditional `If-None-Match` → 304 would return
|
|
189
|
+
* no body, so a steady-state recall fallback would render a quiet green OK
|
|
190
|
+
* instead of `serving` (green for a non-latest served version — the exact
|
|
191
|
+
* invariant this epic exists to hold). #1847's 304-path DB-load concern is
|
|
192
|
+
* about the SessionStart sync THROTTLE (a hot, per-session path), not this
|
|
193
|
+
* cold, explicit `list` probe, so the conditional saved nothing that
|
|
194
|
+
* mattered and cost the governance signal. The read remains non-recording
|
|
195
|
+
* (records no delivery / heartbeat / sync-state) and `list` never persists
|
|
196
|
+
* its etag. See `docs/SKILL-STATE-MODEL.md` §7.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} args
|
|
199
|
+
* @param {ReturnType<typeof import("../lib/cli-config.mjs").resolveFlags>} args.flags
|
|
200
|
+
* @param {string} args.skillsetRef
|
|
201
|
+
* @param {string} args.repoName
|
|
202
|
+
* @param {string[]} args.extra
|
|
203
|
+
* @returns {Promise<{ serverTruth: import("../lib/list-engine.mjs").ServerTruth, metaByKey: Map<string, object> }>}
|
|
204
|
+
*/
|
|
205
|
+
export async function probeSkillsetFreshness({ flags, skillsetRef, repoName, extra }) {
|
|
206
|
+
const metaByKey = new Map();
|
|
207
|
+
try {
|
|
208
|
+
const resp = await getLibrary(flags.serverUrl, flags.apiKey, {
|
|
209
|
+
manifest: true,
|
|
210
|
+
skillset: skillsetRef,
|
|
211
|
+
repo: repoName,
|
|
212
|
+
extra,
|
|
213
|
+
});
|
|
214
|
+
const skills = Array.isArray(resp.skills) ? resp.skills : [];
|
|
215
|
+
const byKey = new Map();
|
|
216
|
+
for (const s of skills) {
|
|
217
|
+
byKey.set(`${s.owner}/${s.name}`, {
|
|
218
|
+
servedVersion: s.version ?? null,
|
|
219
|
+
governance: s.governance ?? null,
|
|
220
|
+
});
|
|
221
|
+
metaByKey.set(`${s.owner}/${s.name}`, {
|
|
222
|
+
description: s.description ?? "",
|
|
223
|
+
updatedAt: s.updatedAt ?? null,
|
|
224
|
+
filesIncomplete: s.filesIncomplete ?? false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return { serverTruth: { checked: true, byKey }, metaByKey };
|
|
228
|
+
} catch {
|
|
229
|
+
// Best-effort to the letter: an offline machine, a bad key, an
|
|
230
|
+
// unresolvable skillset, or a server hiccup all leave the freshness
|
|
231
|
+
// axis NOT CHECKED (and Updated / Description blank) rather than
|
|
232
|
+
// failing a read-only status command.
|
|
233
|
+
return { serverTruth: { checked: false, byKey: null }, metaByKey };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run the read-only governance scans for the skillset view ONCE and
|
|
239
|
+
* return both the #2665 compliance object and the raw global-boundary
|
|
240
|
+
* scan. Unchanged from pre-#2806: compliance replays the delivery-reason
|
|
241
|
+
* counts from `resolved` and re-scans foreign + global content fresh
|
|
242
|
+
* from disk (read-only, never committing seen-state).
|
|
243
|
+
*
|
|
244
|
+
* @param {object} args
|
|
245
|
+
* @param {string} args.skillsetRef
|
|
246
|
+
* @param {import("../lib/repo-sync-state.mjs").RepoResolvedEntry[]} args.resolved
|
|
247
|
+
* @param {string[]} args.detectedKeys
|
|
248
|
+
* @param {string} args.rootDir
|
|
249
|
+
*/
|
|
250
|
+
export function evaluateSkillsetGovernance({ skillsetRef, resolved, detectedKeys, rootDir }) {
|
|
251
|
+
const managedSkills = {};
|
|
252
|
+
for (const e of resolved) {
|
|
253
|
+
if (e && e.owner && e.name) managedSkills[`${e.owner}/${e.name}`] = { version: e.version };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let foreignCount = 0;
|
|
257
|
+
let gbScan = null;
|
|
258
|
+
try {
|
|
259
|
+
foreignCount = scanForeignContent({
|
|
260
|
+
vendors: detectedKeys,
|
|
261
|
+
global: false,
|
|
262
|
+
managedSkills,
|
|
263
|
+
baseDir: rootDir,
|
|
264
|
+
}).foreignCount;
|
|
265
|
+
gbScan = scanGlobalBoundary({
|
|
266
|
+
vendors: detectedKeys,
|
|
267
|
+
memberNames: new Set(resolved.map((e) => e.name)),
|
|
268
|
+
managedGlobalNames: managedGlobalNamesFrom(readLastSync()?.skills),
|
|
269
|
+
baseDir: rootDir,
|
|
270
|
+
});
|
|
271
|
+
} catch {
|
|
272
|
+
// Best-effort: a scan failure must never crash the delivered-set view.
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const compliance = buildRepoCompliance({
|
|
276
|
+
skillsetRef,
|
|
277
|
+
foreignCount,
|
|
278
|
+
shadowedCount: gbScan?.counts?.[GLOBAL_SHADOWED_CATEGORY] ?? 0,
|
|
279
|
+
globalBeyondCount:
|
|
280
|
+
(gbScan?.counts?.[GLOBAL_LIBRARY_CATEGORY] ?? 0) +
|
|
281
|
+
(gbScan?.counts?.[GLOBAL_FOREIGN_CATEGORY] ?? 0),
|
|
282
|
+
resolved,
|
|
283
|
+
});
|
|
284
|
+
return { compliance, gbScan };
|
|
285
|
+
}
|