@lgtm-hq/ai-skills 0.7.0 → 0.8.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/data/vendors.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "id": "mattpocock",
5
5
  "repo": "mattpocock/skills",
6
6
  "sha": "391a2701dd948f94f56a39f7533f8eea9a859c87",
7
+ "displayRef": "latest",
7
8
  "skillRoots": [
8
9
  "skills"
9
10
  ],
@@ -14,6 +15,7 @@
14
15
  "id": "anthropics",
15
16
  "repo": "anthropics/skills",
16
17
  "sha": "9d2f1ae187231d8199c64b5b762e1bdf2244733d",
18
+ "displayRef": "latest",
17
19
  "skillRoots": [
18
20
  "skills"
19
21
  ],
@@ -24,6 +26,7 @@
24
26
  "id": "anthropics-claude-code",
25
27
  "repo": "anthropics/claude-code",
26
28
  "sha": "15a21e1b4e240e2da6a4953d5f148a806c9c9bb2",
29
+ "displayRef": "latest",
27
30
  "skillRoots": [
28
31
  "plugins/*/skills"
29
32
  ],
package/data/vendors.yaml CHANGED
@@ -3,6 +3,7 @@ vendors:
3
3
  - id: mattpocock
4
4
  repo: mattpocock/skills
5
5
  sha: "391a2701dd948f94f56a39f7533f8eea9a859c87"
6
+ displayRef: latest
6
7
  skillRoots:
7
8
  - skills
8
9
  license: MIT
@@ -10,6 +11,7 @@ vendors:
10
11
  - id: anthropics
11
12
  repo: anthropics/skills
12
13
  sha: "9d2f1ae187231d8199c64b5b762e1bdf2244733d"
14
+ displayRef: latest
13
15
  skillRoots:
14
16
  - skills
15
17
  license: Apache-2.0
@@ -17,6 +19,7 @@ vendors:
17
19
  - id: anthropics-claude-code
18
20
  repo: anthropics/claude-code
19
21
  sha: "15a21e1b4e240e2da6a4953d5f148a806c9c9bb2"
22
+ displayRef: latest
20
23
  skillRoots:
21
24
  - plugins/*/skills
22
25
  license: Commercial
package/lib/catalog.js CHANGED
@@ -27,7 +27,7 @@ export function loadBundles() {
27
27
  /**
28
28
  * Load all vendor records baked into the package.
29
29
  *
30
- * @returns {Promise<{vendors: Array<{id: string, repo: string, sha: string, skillRoots: string[], license: string, homepage: string}>}>} Vendor registry.
30
+ * @returns {Promise<{vendors: Array<{id: string, repo: string, sha: string, displayRef?: string, skillRoots: string[], license: string, homepage: string}>}>} Vendor registry.
31
31
  */
32
32
  export function loadVendors() {
33
33
  return readCatalog("vendors.json");
package/lib/install.js CHANGED
@@ -6,69 +6,111 @@ import { buildSkillsArguments, runSkills } from "./skills-runner.js";
6
6
  import { createClackUi, KNOWN_AGENTS } from "./ui.js";
7
7
 
8
8
  /**
9
- * Fill unset install selections through the terminal picker.
9
+ * @typedef {{firstParty: string[], vendors: Record<string, string[]>}} InstallCart
10
+ * Interactive skill cart keyed by catalog source.
11
+ */
12
+
13
+ /**
14
+ * @typedef {{vendor: string | null, skills: string[]}} InstallBatch
15
+ * One upstream `skills add` invocation (single source).
16
+ */
17
+
18
+ /**
19
+ * Fill unset install selections through the home/cart wizard.
10
20
  *
11
- * Happy path asks for catalog source, skills (grouped multi-select for first-party),
12
- * and agents. Scope defaults to global, installs symlink (not copy), and uses
13
- * overwrite for the gateway fail-closed `--on-conflict` API without prompting —
14
- * upstream `skills` does not implement conflict policies yet.
21
+ * Home lists catalogs to browse; selections accumulate in a cart. Proceed asks
22
+ * for agents/scope once, then returns install batches. Scope defaults to global,
23
+ * installs symlink (not copy), and uses overwrite for the gateway fail-closed
24
+ * `--on-conflict` API without prompting.
15
25
  *
16
26
  * @param {{agents: string[], bundle: string | null, copy: boolean, global: boolean, onConflict: string | null, project: boolean, skills: string[], vendor: string | null, yes: boolean}} options - Initial install options.
17
27
  * @param {ReturnType<typeof createClackUi>} [ui] - Injectable interactive UI.
18
- * @returns {Promise<typeof options>} Fully selected options.
28
+ * @returns {Promise<typeof options & {installBatches: InstallBatch[]}>} Shared options plus per-source install batches.
19
29
  */
20
30
  export async function completeInteractively(options, ui = createClackUi()) {
21
31
  ui.intro("ai-skills gateway");
22
32
 
33
+ // CLI already named a source/skills: honor it instead of discarding into an empty cart.
34
+ if (options.vendor || options.bundle || options.skills.length > 0) {
35
+ const installBatches = await batchesFromCliOptions(options);
36
+ return finishInteractiveInstall(options, ui, installBatches);
37
+ }
38
+
23
39
  const bundles = await loadBundles();
24
40
  const vendors = await loadVendors();
25
- const packageVersion = getPackageVersion();
26
- const sourceChoices = [
27
- {
28
- value: "first-party",
29
- label: `lgtm-hq/ai-skills @ v${packageVersion}`,
30
- },
31
- ...vendors.vendors.map((vendor) => ({
32
- value: `vendor:${vendor.id}`,
33
- label: vendor.repo,
34
- })),
35
- ];
36
-
37
- const sourceValue = await cancelable(
38
- ui,
39
- ui.select({
40
- message: "Install from which catalog?",
41
- options: sourceChoices,
42
- }),
43
- );
41
+ /** @type {InstallCart} */
42
+ const cart = { firstParty: [], vendors: {} };
44
43
 
45
- if (sourceValue === "first-party") {
46
- const skillGroups = buildFirstPartySkillGroups(bundles);
47
- options.skills = await cancelable(
48
- ui,
49
- ui.groupMultiselect({
50
- message: "Select skills to install",
51
- options: skillGroups,
52
- required: true,
53
- }),
54
- );
55
- } else {
56
- const vendorId = parseVendorSourceValue(sourceValue);
57
- const index = await loadVendorIndex(vendorId);
58
- options.vendor = vendorId;
59
- options.skills = await cancelable(
44
+ while (true) {
45
+ const action = await cancelable(
60
46
  ui,
61
- ui.multiselect({
62
- message: `Select skills from ${index.vendor.repo}`,
63
- options: index.skills.map((skill) => ({
64
- value: skill.name,
65
- label: skill.name,
66
- })),
67
- required: true,
47
+ ui.select({
48
+ message: "Install skills",
49
+ options: buildHomeOptions(cart, vendors),
68
50
  }),
69
51
  );
52
+
53
+ if (action === "cancel") {
54
+ throw new Error("Install cancelled");
55
+ }
56
+ if (action === "proceed") {
57
+ break;
58
+ }
59
+ if (action === "browse:first-party") {
60
+ cart.firstParty = await cancelable(
61
+ ui,
62
+ ui.groupMultiselect({
63
+ message: `Skills from lgtm-hq/ai-skills @ v${getPackageVersion()}`,
64
+ options: buildFirstPartySkillGroups(bundles),
65
+ initialValues: cart.firstParty,
66
+ required: false,
67
+ }),
68
+ );
69
+ continue;
70
+ }
71
+ if (action.startsWith("browse:vendor:")) {
72
+ const vendorId = action.slice("browse:vendor:".length);
73
+ const index = await loadVendorIndex(vendorId);
74
+ // Prefer registry record so displayRef survives (baked indexes omit it).
75
+ const vendor = vendors.vendors.find((entry) => entry.id === vendorId) ?? index.vendor;
76
+ cart.vendors[vendorId] = await cancelable(
77
+ ui,
78
+ ui.multiselect({
79
+ message: `Skills from ${vendorDisplayLabel(vendor)}`,
80
+ options: index.skills.map((skill) => ({
81
+ value: skill.name,
82
+ label: skill.name,
83
+ })),
84
+ initialValues: cart.vendors[vendorId] ?? [],
85
+ required: false,
86
+ }),
87
+ );
88
+ continue;
89
+ }
90
+ throw new Error(`Unknown home action: ${action}`);
91
+ }
92
+
93
+ const installBatches = batchesFromCart(cart);
94
+ if (installBatches.length === 0) {
95
+ throw new Error("Install cancelled");
70
96
  }
71
97
 
98
+ return finishInteractiveInstall(options, ui, installBatches);
99
+ }
100
+
101
+ /**
102
+ * Ask agents/scope/advanced once, then return shared options plus install batches.
103
+ *
104
+ * @param {{agents: string[], bundle: string | null, copy: boolean, global: boolean, onConflict: string | null, project: boolean, skills: string[], vendor: string | null, yes: boolean}} options - Initial install options.
105
+ * @param {ReturnType<typeof createClackUi>} ui - Interactive UI.
106
+ * @param {InstallBatch[]} installBatches - Per-catalog install batches.
107
+ * @returns {Promise<typeof options & {installBatches: InstallBatch[]}>} Shared options plus batches.
108
+ */
109
+ async function finishInteractiveInstall(options, ui, installBatches) {
110
+ options.bundle = null;
111
+ options.vendor = null;
112
+ options.skills = [];
113
+
72
114
  if (options.agents.length === 0) {
73
115
  const selected = await cancelable(
74
116
  ui,
@@ -139,9 +181,110 @@ export async function completeInteractively(options, ui = createClackUi()) {
139
181
  }
140
182
 
141
183
  ui.outro("Starting install…");
184
+ return { ...options, installBatches };
185
+ }
186
+
187
+ /**
188
+ * Build home-screen options for the install wizard.
189
+ *
190
+ * @param {InstallCart} cart - Current skill cart.
191
+ * @param {{vendors: Array<{id: string, repo: string, displayRef?: string}>}} vendors - Vendor registry.
192
+ * @returns {{value: string, label: string}[]} Clack select options.
193
+ */
194
+ export function buildHomeOptions(cart, vendors) {
195
+ const options = [
196
+ {
197
+ value: "browse:first-party",
198
+ label: `Browse lgtm-hq/ai-skills @ v${getPackageVersion()}`,
199
+ },
200
+ ...vendors.vendors.map((vendor) => ({
201
+ value: `browse:vendor:${vendor.id}`,
202
+ label: `Browse ${vendorDisplayLabel(vendor)}`,
203
+ })),
204
+ ];
205
+ const total = cartSkillCount(cart);
206
+ if (total > 0) {
207
+ options.push({
208
+ value: "proceed",
209
+ label: `Proceed with install (${total} skill${total === 1 ? "" : "s"})`,
210
+ });
211
+ }
212
+ options.push({ value: "cancel", label: "Cancel" });
142
213
  return options;
143
214
  }
144
215
 
216
+ /**
217
+ * Consumer-facing catalog pin for a vendor (never a SHA).
218
+ *
219
+ * @param {{repo: string, displayRef?: string}} vendor - Vendor record.
220
+ * @returns {string} `owner/repo @ pin` label.
221
+ */
222
+ export function vendorDisplayLabel(vendor) {
223
+ const displayRef = typeof vendor.displayRef === "string" ? vendor.displayRef : undefined;
224
+ const pin = displayRef?.trim() || "latest";
225
+ return `${vendor.repo} @ ${pin}`;
226
+ }
227
+
228
+ /**
229
+ * Count skills currently in the cart.
230
+ *
231
+ * @param {InstallCart} cart - Skill cart.
232
+ * @returns {number} Total selected skills.
233
+ */
234
+ export function cartSkillCount(cart) {
235
+ return (
236
+ cart.firstParty.length +
237
+ Object.values(cart.vendors).reduce((sum, skills) => sum + skills.length, 0)
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Convert a cart into install batches (first-party first).
243
+ *
244
+ * @param {InstallCart} cart - Skill cart.
245
+ * @returns {InstallBatch[]} Non-empty batches only.
246
+ */
247
+ export function batchesFromCart(cart) {
248
+ /** @type {InstallBatch[]} */
249
+ const batches = [];
250
+ if (cart.firstParty.length > 0) {
251
+ batches.push({ vendor: null, skills: [...cart.firstParty] });
252
+ }
253
+ for (const [vendorId, skills] of Object.entries(cart.vendors)) {
254
+ if (skills.length > 0) {
255
+ batches.push({ vendor: vendorId, skills: [...skills] });
256
+ }
257
+ }
258
+ return batches;
259
+ }
260
+
261
+ /**
262
+ * Build install batches from CLI source flags (interactive, without -y).
263
+ *
264
+ * @param {{bundle: string | null, skills: string[], vendor: string | null}} options - Parsed install options.
265
+ * @returns {Promise<InstallBatch[]>} One batch for the CLI-selected source.
266
+ */
267
+ export async function batchesFromCliOptions(options) {
268
+ if (options.vendor) {
269
+ if (options.skills.length === 0) {
270
+ throw new Error(
271
+ "Interactive install with --vendor requires --skill, or omit both to browse catalogs",
272
+ );
273
+ }
274
+ return [{ vendor: options.vendor, skills: [...options.skills] }];
275
+ }
276
+ if (options.bundle) {
277
+ const bundles = await loadBundles();
278
+ const bundle = bundles.groups[options.bundle];
279
+ if (!bundle) {
280
+ throw new Error(`Unknown first-party bundle: ${options.bundle}`);
281
+ }
282
+ const skills = options.skills.length > 0 ? options.skills : bundle.skills;
283
+ return [{ vendor: null, skills: [...skills] }];
284
+ }
285
+ return [{ vendor: null, skills: [...options.skills] }];
286
+ }
287
+
145
288
  /**
146
289
  * Build grouped skill options for first-party interactive install.
147
290
  *
@@ -161,20 +304,6 @@ function buildFirstPartySkillGroups(bundles) {
161
304
  return groups;
162
305
  }
163
306
 
164
- /**
165
- * Parse a vendor catalog selection value.
166
- *
167
- * @param {string} value - Combined select value (`vendor:<id>`).
168
- * @returns {string} Vendor id.
169
- */
170
- function parseVendorSourceValue(value) {
171
- const prefix = "vendor:";
172
- if (!value.startsWith(prefix)) {
173
- throw new Error(`Unknown catalog selection: ${value}`);
174
- }
175
- return value.slice(prefix.length);
176
- }
177
-
178
307
  /**
179
308
  * Abort when Clack reports cancel (Ctrl+C / escape).
180
309
  *
@@ -311,9 +440,30 @@ async function createLockEntries(options, vendor, now) {
311
440
  /**
312
441
  * Create an interactive installer session.
313
442
  *
443
+ * Runs one upstream install per selected catalog so mixed first-party + vendor
444
+ * picks complete in a single interactive session.
445
+ *
314
446
  * @param {{agents: string[], bundle: string | null, copy: boolean, global: boolean, onConflict: string | null, project: boolean, skills: string[], vendor: string | null, yes: boolean}} options - Install options.
315
447
  * @returns {Promise<void>} Resolves when installation completes.
316
448
  */
317
449
  export async function installInteractively(options) {
318
- await install(await completeInteractively(options));
450
+ const completed = await completeInteractively(options);
451
+ const finished = [];
452
+ for (const batch of completed.installBatches) {
453
+ try {
454
+ await install({
455
+ ...completed,
456
+ bundle: null,
457
+ vendor: batch.vendor,
458
+ skills: batch.skills,
459
+ });
460
+ finished.push(batch.vendor ?? "first-party");
461
+ } catch (error) {
462
+ const detail = error instanceof Error ? error.message : String(error);
463
+ throw new Error(
464
+ `Install failed for "${batch.vendor ?? "first-party"}" after completing: ` +
465
+ `${finished.length > 0 ? finished.join(", ") : "none"}. ${detail}`,
466
+ );
467
+ }
468
+ }
319
469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lgtm-hq/ai-skills",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pinned installer gateway for first-party and vendor AI skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/lgtm-hq/ai-skills#readme",