@skillshub-labs/cli 0.1.18 → 0.1.19

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.
Files changed (28) hide show
  1. package/README.md +16 -2
  2. package/bin/skills-hub +193 -15
  3. package/data/official-presets/catalog.json +436 -0
  4. package/data/official-presets/policies/policy-azure-cloud.md +18 -0
  5. package/data/official-presets/policies/policy-cloudflare-edge.md +18 -0
  6. package/data/official-presets/policies/policy-fastapi-py.md +31 -0
  7. package/data/official-presets/policies/policy-fullstack-web.md +24 -0
  8. package/data/official-presets/policies/policy-go-service.md +31 -0
  9. package/data/official-presets/policies/policy-hf-ml.md +18 -0
  10. package/data/official-presets/policies/policy-langchain-apps.md +18 -0
  11. package/data/official-presets/policies/policy-literature-review.md +18 -0
  12. package/data/official-presets/policies/policy-monorepo-turbo.md +31 -0
  13. package/data/official-presets/policies/policy-nextjs-ts-strict.md +31 -0
  14. package/data/official-presets/policies/policy-node-api-ts.md +31 -0
  15. package/data/official-presets/policies/policy-python-api.md +18 -0
  16. package/data/official-presets/policies/policy-release-ci.md +18 -0
  17. package/data/official-presets/policies/policy-release-maintainer.md +31 -0
  18. package/data/official-presets/policies/policy-scientific-discovery.md +18 -0
  19. package/data/official-presets/policies/policy-scientific-python.md +31 -0
  20. package/data/official-presets/policies/policy-security-audit.md +18 -0
  21. package/data/official-presets/policies/policy-web-frontend.md +18 -0
  22. package/lib/core/kit-core.d.ts +14 -3
  23. package/lib/core/kit-core.mjs +327 -20
  24. package/lib/core/kit-types.ts +128 -3
  25. package/lib/services/kit-loadout-import.mjs +599 -0
  26. package/lib/services/kit-service.d.ts +90 -2
  27. package/lib/services/kit-service.mjs +665 -38
  28. package/package.json +9 -1
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
+ import { fileURLToPath } from 'url'
4
5
  import {
5
6
  addKit as addKitCore,
6
7
  addKitLoadout as addKitLoadoutCore,
@@ -15,12 +16,34 @@ import {
15
16
  listKitPolicies as listKitPoliciesCore,
16
17
  listKits as listKitsCore,
17
18
  markKitApplied,
19
+ restoreManagedKitBaseline as restoreManagedKitBaselineCore,
18
20
  updateKit as updateKitCore,
19
21
  updateKitLoadout as updateKitLoadoutCore,
20
22
  updateKitPolicy as updateKitPolicyCore,
21
23
  } from '../core/kit-core.mjs'
24
+ import {
25
+ buildImportSourceKey,
26
+ importKitLoadoutFromRepo as importKitLoadoutFromRepoService,
27
+ parseLoadoutImportUrl,
28
+ } from './kit-loadout-import.mjs'
22
29
 
23
30
  const CONFIG_PATH = path.join(os.homedir(), '.skills-hub', 'config.json')
31
+ const DEFAULT_POLICY_FILE_NAME = 'AGENTS.md'
32
+ const DEFAULT_OFFICIAL_PRESETS_DIR = path.resolve(
33
+ path.dirname(fileURLToPath(import.meta.url)),
34
+ '../../data/official-presets'
35
+ )
36
+
37
+ function resolveInstructionFileName(agent) {
38
+ const explicit = String(agent?.instructionFileName || '').trim()
39
+ if (explicit) {
40
+ return explicit
41
+ }
42
+
43
+ return String(agent?.name || '').trim().toLowerCase() === 'claude code'
44
+ ? 'CLAUDE.md'
45
+ : DEFAULT_POLICY_FILE_NAME
46
+ }
24
47
 
25
48
  function normalizeKitMode(value) {
26
49
  return value === 'link' ? 'link' : 'copy'
@@ -51,14 +74,275 @@ function normalizeLoadoutItems(items) {
51
74
  return normalized
52
75
  }
53
76
 
77
+ function toOptionalText(value) {
78
+ const normalized = String(value || '').trim()
79
+ return normalized || undefined
80
+ }
81
+
82
+ function normalizeOfficialSource(source, index) {
83
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
84
+ throw new Error(`Invalid official preset source at index ${index}.`)
85
+ }
86
+
87
+ const id = String(source.id || '').trim()
88
+ const name = String(source.name || '').trim()
89
+ const url = String(source.url || '').trim()
90
+ const description = toOptionalText(source.description)
91
+ const selectedSkillDetails = Array.isArray(source.selectedSkillDetails)
92
+ ? source.selectedSkillDetails
93
+ .map((entry) => {
94
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null
95
+ const name = String(entry.name || '').trim()
96
+ const detailDescription = toOptionalText(entry.description)
97
+ if (!name) return null
98
+ return {
99
+ name,
100
+ description: detailDescription,
101
+ }
102
+ })
103
+ .filter(Boolean)
104
+ : []
105
+ const selectedSkills = Array.isArray(source.selectedSkills)
106
+ ? source.selectedSkills.map((value) => String(value || '').trim()).filter(Boolean)
107
+ : []
108
+
109
+ if (!id || !name || !url || selectedSkills.length === 0) {
110
+ throw new Error(`Official preset source is missing required fields: ${id || `index-${index}`}`)
111
+ }
112
+
113
+ return {
114
+ id,
115
+ name,
116
+ url,
117
+ description,
118
+ selectedSkillDetails,
119
+ selectedSkills,
120
+ }
121
+ }
122
+
123
+ function normalizeOfficialPreset(preset, index) {
124
+ if (!preset || typeof preset !== 'object' || Array.isArray(preset)) {
125
+ throw new Error(`Invalid official preset at index ${index}.`)
126
+ }
127
+
128
+ const id = String(preset.id || '').trim()
129
+ const name = String(preset.name || '').trim()
130
+ const description = toOptionalText(preset.description)
131
+ const policy = preset.policy
132
+
133
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
134
+ throw new Error(`Official preset is missing policy metadata: ${id || `index-${index}`}`)
135
+ }
136
+
137
+ const policyName = String(policy.name || '').trim()
138
+ const policyTemplate = String(policy.template || '').trim()
139
+ const policyDescription = toOptionalText(policy.description)
140
+ const sources = Array.isArray(preset.sources)
141
+ ? preset.sources.map((entry, sourceIndex) => normalizeOfficialSource(entry, sourceIndex))
142
+ : []
143
+
144
+ if (!id || !name || !policyName || !policyTemplate || sources.length === 0) {
145
+ throw new Error(`Official preset is missing required fields: ${id || `index-${index}`}`)
146
+ }
147
+
148
+ return {
149
+ id,
150
+ name,
151
+ description,
152
+ policy: {
153
+ name: policyName,
154
+ description: policyDescription,
155
+ template: policyTemplate,
156
+ },
157
+ sources,
158
+ }
159
+ }
160
+
161
+ async function loadOfficialPresetCatalog() {
162
+ const catalogDir = path.resolve(process.env.SKILLS_HUB_OFFICIAL_PRESETS_DIR || DEFAULT_OFFICIAL_PRESETS_DIR)
163
+ const catalogPath = path.join(catalogDir, 'catalog.json')
164
+ const raw = await fs.readJson(catalogPath)
165
+ const presets = Array.isArray(raw?.presets)
166
+ ? raw.presets.map((entry, index) => normalizeOfficialPreset(entry, index))
167
+ : []
168
+
169
+ return {
170
+ dirPath: catalogDir,
171
+ version: Number(raw?.version) || 1,
172
+ presets,
173
+ }
174
+ }
175
+
176
+ function getOfficialPolicyTemplatePath(catalogDir, preset) {
177
+ return path.resolve(catalogDir, preset.policy.template)
178
+ }
179
+
180
+ function buildOfficialSourceLoadoutName(preset, source) {
181
+ return `Official Source: ${preset.name} / ${source.name}`
182
+ }
183
+
184
+ function buildOfficialCuratedLoadoutName(preset) {
185
+ return `Official: ${preset.name}`
186
+ }
187
+
188
+ function buildOfficialKitName(preset) {
189
+ return `Official: ${preset.name}`
190
+ }
191
+
192
+ function isOfficialSourceLoadoutName(name) {
193
+ return String(name || '').trim().startsWith('Official Source: ')
194
+ }
195
+
196
+ function pruneUnusedOfficialSourceLoadouts() {
197
+ const referencedLoadoutIds = new Set(
198
+ listKitsCore()
199
+ .map((kit) => String(kit?.loadoutId || '').trim())
200
+ .filter(Boolean)
201
+ )
202
+
203
+ const staleLoadouts = listKitLoadoutsCore().filter(
204
+ (loadout) =>
205
+ isOfficialSourceLoadoutName(loadout?.name) && !referencedLoadoutIds.has(loadout.id)
206
+ )
207
+
208
+ for (const loadout of staleLoadouts) {
209
+ deleteKitLoadoutCore(loadout.id)
210
+ }
211
+
212
+ return staleLoadouts.length
213
+ }
214
+
215
+ function buildOfficialSourceImportKey(source) {
216
+ const parsedSource = parseLoadoutImportUrl(source.url)
217
+ return buildImportSourceKey(parsedSource.repoWebUrl, parsedSource.explicitSubdir || '/')
218
+ }
219
+
220
+ function appendOfficialSourceSelections(selectionMap, preset) {
221
+ for (const source of preset.sources) {
222
+ const sourceKey = buildOfficialSourceImportKey(source)
223
+ const selected = selectionMap.get(sourceKey) || new Set()
224
+ for (const skillName of source.selectedSkills) {
225
+ selected.add(skillName)
226
+ }
227
+ selectionMap.set(sourceKey, selected)
228
+ }
229
+ }
230
+
231
+ function buildOfficialSourceSelectionPlan(catalog, currentPreset) {
232
+ const selectionMap = new Map()
233
+ const presetById = new Map(catalog.presets.map((preset) => [preset.id, preset]))
234
+
235
+ for (const kit of listKits()) {
236
+ const presetId = String(kit?.managedSource?.presetId || '').trim()
237
+ if (kit?.managedSource?.kind !== 'official_preset' || !presetId || presetId === currentPreset.id) {
238
+ continue
239
+ }
240
+
241
+ const installedPreset = presetById.get(presetId)
242
+ if (!installedPreset) {
243
+ continue
244
+ }
245
+
246
+ appendOfficialSourceSelections(selectionMap, installedPreset)
247
+ }
248
+
249
+ appendOfficialSourceSelections(selectionMap, currentPreset)
250
+ return selectionMap
251
+ }
252
+
253
+ function buildOfficialManagedSource(preset, catalogVersion, policy, loadout, importedSources) {
254
+ return {
255
+ kind: 'official_preset',
256
+ presetId: preset.id,
257
+ presetName: preset.name,
258
+ catalogVersion,
259
+ installedAt: Date.now(),
260
+ restoreCount: 0,
261
+ baseline: {
262
+ name: buildOfficialKitName(preset),
263
+ description: preset.description,
264
+ policy: {
265
+ id: policy.id,
266
+ name: policy.name,
267
+ description: policy.description,
268
+ content: policy.content,
269
+ },
270
+ loadout: {
271
+ id: loadout.id,
272
+ name: loadout.name,
273
+ description: loadout.description,
274
+ items: loadout.items.map((item) => ({ ...item })),
275
+ },
276
+ },
277
+ securityChecks: importedSources
278
+ .map((source) => {
279
+ const check = source.loadout.importSource?.lastSafetyCheck
280
+ if (!check) {
281
+ return null
282
+ }
283
+ return {
284
+ sourceId: source.id,
285
+ sourceName: source.name,
286
+ check,
287
+ }
288
+ })
289
+ .filter(Boolean),
290
+ }
291
+ }
292
+
293
+ function findByExactName(records, name) {
294
+ return records.find((record) => String(record?.name || '').trim() === name) || null
295
+ }
296
+
297
+ function buildCuratedLoadoutItems(importedSources) {
298
+ const items = []
299
+ const seen = new Set()
300
+
301
+ for (const source of importedSources) {
302
+ const itemByName = new Map(
303
+ source.loadout.items.map((item) => [path.basename(item.skillPath), item])
304
+ )
305
+ const missing = []
306
+
307
+ for (const skillName of source.selectedSkills) {
308
+ const item = itemByName.get(skillName)
309
+ if (!item) {
310
+ missing.push(skillName)
311
+ continue
312
+ }
313
+
314
+ if (seen.has(item.skillPath)) {
315
+ continue
316
+ }
317
+
318
+ items.push({
319
+ skillPath: item.skillPath,
320
+ mode: 'copy',
321
+ sortOrder: items.length,
322
+ })
323
+ seen.add(item.skillPath)
324
+ }
325
+
326
+ if (missing.length > 0) {
327
+ const available = [...itemByName.keys()].sort().join(', ')
328
+ throw new Error(
329
+ `Official preset source '${source.name}' is missing expected skills: ${missing.join(', ')}. Available skills: ${available}`
330
+ )
331
+ }
332
+ }
333
+
334
+ return items
335
+ }
336
+
54
337
  async function readRuntimeConfig() {
55
338
  try {
56
339
  const raw = await fs.readFile(CONFIG_PATH, 'utf-8')
57
340
  if (!raw.trim()) {
58
- return { projects: [], agents: [] }
341
+ return { hubPath: path.join(os.homedir(), 'skills-hub'), projects: [], agents: [] }
59
342
  }
60
343
 
61
344
  const parsed = JSON.parse(raw)
345
+ const hubPath = String(parsed?.hubPath || path.join(os.homedir(), 'skills-hub'))
62
346
  const projects = Array.isArray(parsed.projects)
63
347
  ? parsed.projects.map((entry) => String(entry || '').trim()).filter(Boolean)
64
348
  : []
@@ -66,12 +350,82 @@ async function readRuntimeConfig() {
66
350
  ? parsed.agents.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))
67
351
  : []
68
352
 
69
- return { projects, agents }
353
+ return { hubPath, projects, agents }
70
354
  } catch {
71
- return { projects: [], agents: [] }
355
+ return { hubPath: path.join(os.homedir(), 'skills-hub'), projects: [], agents: [] }
72
356
  }
73
357
  }
74
358
 
359
+ function normalizeSkillSelector(value) {
360
+ return String(value || '').trim()
361
+ }
362
+
363
+ function skillSelectorCandidates(skillPath) {
364
+ const normalizedPath = path.resolve(skillPath)
365
+ const base = path.basename(normalizedPath)
366
+ return new Set([normalizedPath, skillPath, base])
367
+ }
368
+
369
+ async function resolveHubSkillPath(hubPath, selector) {
370
+ const normalized = normalizeSkillSelector(selector)
371
+ if (!normalized) {
372
+ throw new Error('Skill selector cannot be empty')
373
+ }
374
+
375
+ const resolvedPath = path.resolve(normalized)
376
+ if (await fs.pathExists(resolvedPath)) {
377
+ return resolvedPath
378
+ }
379
+
380
+ const candidateInHub = path.join(hubPath, normalized)
381
+ if (await fs.pathExists(candidateInHub)) {
382
+ return candidateInHub
383
+ }
384
+
385
+ throw new Error(`Skill not found in hub: ${selector}`)
386
+ }
387
+
388
+ function buildEffectiveLoadoutItems(loadoutItems, includeSkillPaths, excludeSelectors) {
389
+ const excluded = new Set(excludeSelectors.map((entry) => normalizeSkillSelector(entry)).filter(Boolean))
390
+ const effectiveItems = []
391
+ const seenPaths = new Set()
392
+
393
+ for (const item of loadoutItems) {
394
+ const candidates = skillSelectorCandidates(item.skillPath)
395
+ if ([...excluded].some((selector) => candidates.has(selector))) {
396
+ continue
397
+ }
398
+
399
+ const resolvedPath = path.resolve(item.skillPath)
400
+ if (seenPaths.has(resolvedPath)) {
401
+ continue
402
+ }
403
+
404
+ effectiveItems.push({
405
+ skillPath: item.skillPath,
406
+ mode: item.mode,
407
+ sortOrder: effectiveItems.length,
408
+ })
409
+ seenPaths.add(resolvedPath)
410
+ }
411
+
412
+ for (const skillPath of includeSkillPaths) {
413
+ const resolvedPath = path.resolve(skillPath)
414
+ if (seenPaths.has(resolvedPath)) {
415
+ continue
416
+ }
417
+
418
+ effectiveItems.push({
419
+ skillPath,
420
+ mode: 'copy',
421
+ sortOrder: effectiveItems.length,
422
+ })
423
+ seenPaths.add(resolvedPath)
424
+ }
425
+
426
+ return effectiveItems
427
+ }
428
+
75
429
  async function atomicWriteText(filePath, content) {
76
430
  const dirPath = path.dirname(filePath)
77
431
  await fs.ensureDir(dirPath)
@@ -129,6 +483,7 @@ function deleteKitPolicy(id) {
129
483
  }
130
484
 
131
485
  function listKitLoadouts() {
486
+ pruneUnusedOfficialSourceLoadouts()
132
487
  return listKitLoadoutsCore()
133
488
  }
134
489
 
@@ -150,6 +505,10 @@ function deleteKitLoadout(id) {
150
505
  return deleteKitLoadoutCore(id)
151
506
  }
152
507
 
508
+ async function importKitLoadoutFromRepo(values) {
509
+ return importKitLoadoutFromRepoService(values)
510
+ }
511
+
153
512
  function listKits() {
154
513
  return listKitsCore()
155
514
  }
@@ -159,11 +518,253 @@ function addKit(values) {
159
518
  }
160
519
 
161
520
  function updateKit(values) {
162
- return updateKitCore(values)
521
+ const updated = updateKitCore(values)
522
+ pruneUnusedOfficialSourceLoadouts()
523
+ return updated
163
524
  }
164
525
 
165
526
  function deleteKit(id) {
166
- return deleteKitCore(id)
527
+ const deleted = deleteKitCore(id)
528
+ pruneUnusedOfficialSourceLoadouts()
529
+ return deleted
530
+ }
531
+
532
+ async function listOfficialPresets() {
533
+ const catalog = await loadOfficialPresetCatalog()
534
+ return catalog.presets.map((preset) => ({
535
+ id: preset.id,
536
+ name: preset.name,
537
+ description: preset.description,
538
+ policyName: preset.policy.name,
539
+ sourceCount: preset.sources.length,
540
+ skillCount: preset.sources.reduce((sum, source) => sum + source.selectedSkills.length, 0),
541
+ }))
542
+ }
543
+
544
+ async function searchOfficialPresets(values) {
545
+ const query = String(values?.query || '').trim().toLowerCase()
546
+ const presets = await listOfficialPresets()
547
+ if (!query) {
548
+ return presets
549
+ }
550
+
551
+ const catalog = await loadOfficialPresetCatalog()
552
+ const byId = new Map(catalog.presets.map((preset) => [preset.id, preset]))
553
+
554
+ return presets.filter((preset) => {
555
+ const full = byId.get(preset.id)
556
+ if (!full) return false
557
+
558
+ const haystacks = [
559
+ preset.id,
560
+ preset.name,
561
+ preset.description || '',
562
+ preset.policyName,
563
+ ...full.sources.flatMap((source) => [
564
+ source.id,
565
+ source.name,
566
+ source.description || '',
567
+ source.url,
568
+ ...source.selectedSkills,
569
+ ]),
570
+ ]
571
+
572
+ return haystacks.some((entry) => String(entry || '').toLowerCase().includes(query))
573
+ })
574
+ }
575
+
576
+ async function getOfficialPreset(values) {
577
+ const presetId = String(values?.id || '').trim()
578
+ if (!presetId) {
579
+ throw new Error('Official preset id is required')
580
+ }
581
+
582
+ const catalog = await loadOfficialPresetCatalog()
583
+ const preset = catalog.presets.find((entry) => entry.id === presetId)
584
+ if (!preset) {
585
+ throw new Error(`Official preset not found: ${presetId}`)
586
+ }
587
+
588
+ return {
589
+ id: preset.id,
590
+ name: preset.name,
591
+ description: preset.description,
592
+ policy: {
593
+ name: preset.policy.name,
594
+ description: preset.policy.description,
595
+ template: preset.policy.template,
596
+ },
597
+ sources: preset.sources.map((source) => ({
598
+ id: source.id,
599
+ name: source.name,
600
+ url: source.url,
601
+ description: source.description,
602
+ selectedSkillDetails: source.selectedSkillDetails,
603
+ selectedSkills: [...source.selectedSkills],
604
+ })),
605
+ skillCount: preset.sources.reduce((sum, source) => sum + source.selectedSkills.length, 0),
606
+ }
607
+ }
608
+
609
+ async function installOfficialPreset(values) {
610
+ const presetId = String(values?.id || '').trim()
611
+ if (!presetId) {
612
+ throw new Error('Official preset id is required')
613
+ }
614
+
615
+ const catalog = await loadOfficialPresetCatalog()
616
+ const preset = catalog.presets.find((entry) => entry.id === presetId)
617
+ if (!preset) {
618
+ throw new Error(`Official preset not found: ${presetId}`)
619
+ }
620
+
621
+ const policyTemplatePath = getOfficialPolicyTemplatePath(catalog.dirPath, preset)
622
+ const policyContent = String(await fs.readFile(policyTemplatePath, 'utf-8') || '').trim()
623
+ if (!policyContent) {
624
+ throw new Error(`Official policy template is empty: ${policyTemplatePath}`)
625
+ }
626
+
627
+ const sourceSelectionPlan = buildOfficialSourceSelectionPlan(catalog, preset)
628
+ const importedSources = []
629
+ for (const source of preset.sources) {
630
+ const sourceKey = buildOfficialSourceImportKey(source)
631
+ const requiredSkillNames = [...(sourceSelectionPlan.get(sourceKey) || new Set(source.selectedSkills))]
632
+ const sourceLoadout = await importKitLoadoutFromRepo({
633
+ url: source.url,
634
+ name: buildOfficialSourceLoadoutName(preset, source),
635
+ description: source.description,
636
+ overwrite: values?.overwrite === true,
637
+ skillNames: requiredSkillNames,
638
+ })
639
+
640
+ importedSources.push({
641
+ id: source.id,
642
+ name: source.name,
643
+ selectedSkills: source.selectedSkills,
644
+ loadout: sourceLoadout.loadout,
645
+ })
646
+ }
647
+
648
+ const curatedItems = buildCuratedLoadoutItems(importedSources)
649
+ const curatedLoadoutName = buildOfficialCuratedLoadoutName(preset)
650
+ const existingPolicy = findByExactName(listKitPolicies(), preset.policy.name)
651
+ const existingCuratedLoadout = findByExactName(listKitLoadouts(), curatedLoadoutName)
652
+ const existingKit = findByExactName(listKits(), buildOfficialKitName(preset))
653
+
654
+ const policy = existingPolicy
655
+ ? updateKitPolicy({
656
+ id: existingPolicy.id,
657
+ description: preset.policy.description,
658
+ content: policyContent,
659
+ })
660
+ : addKitPolicy({
661
+ name: preset.policy.name,
662
+ description: preset.policy.description,
663
+ content: policyContent,
664
+ })
665
+
666
+ const curatedLoadout = existingCuratedLoadout
667
+ ? updateKitLoadout({
668
+ id: existingCuratedLoadout.id,
669
+ description: preset.description,
670
+ items: curatedItems,
671
+ })
672
+ : addKitLoadout({
673
+ name: curatedLoadoutName,
674
+ description: preset.description,
675
+ items: curatedItems,
676
+ })
677
+
678
+ const managedSource = buildOfficialManagedSource(
679
+ preset,
680
+ catalog.version,
681
+ policy,
682
+ curatedLoadout,
683
+ importedSources
684
+ )
685
+
686
+ const kit = existingKit
687
+ ? updateKit({
688
+ id: existingKit.id,
689
+ description: preset.description,
690
+ policyId: policy.id,
691
+ loadoutId: curatedLoadout.id,
692
+ managedSource,
693
+ })
694
+ : addKit({
695
+ name: buildOfficialKitName(preset),
696
+ description: preset.description,
697
+ policyId: policy.id,
698
+ loadoutId: curatedLoadout.id,
699
+ managedSource,
700
+ })
701
+
702
+ pruneUnusedOfficialSourceLoadouts()
703
+
704
+ return {
705
+ preset: {
706
+ id: preset.id,
707
+ name: preset.name,
708
+ description: preset.description,
709
+ },
710
+ policy,
711
+ loadout: curatedLoadout,
712
+ kit,
713
+ importedSources: importedSources.map((source) => ({
714
+ id: source.id,
715
+ name: source.name,
716
+ loadoutId: source.loadout.id,
717
+ importedSkillCount: source.loadout.items.length,
718
+ selectedSkillCount: source.selectedSkills.length,
719
+ })),
720
+ }
721
+ }
722
+
723
+ async function installAllOfficialPresets(values) {
724
+ const presets = await listOfficialPresets()
725
+ const installed = []
726
+ for (const preset of presets) {
727
+ installed.push(
728
+ await installOfficialPreset({
729
+ id: preset.id,
730
+ overwrite: values?.overwrite === true,
731
+ })
732
+ )
733
+ }
734
+
735
+ return { installed }
736
+ }
737
+
738
+ async function ensureManagedOfficialPresetsInstalled(values) {
739
+ const catalog = await loadOfficialPresetCatalog()
740
+ const installed = []
741
+ const kits = listKits()
742
+
743
+ for (const preset of catalog.presets) {
744
+ const existingKit = kits.find(
745
+ (kit) =>
746
+ kit.managedSource?.kind === 'official_preset' &&
747
+ kit.managedSource?.presetId === preset.id &&
748
+ Number(kit.managedSource?.catalogVersion || 0) >= Number(catalog.version || 1)
749
+ )
750
+
751
+ if (existingKit) {
752
+ continue
753
+ }
754
+
755
+ installed.push(
756
+ await installOfficialPreset({
757
+ id: preset.id,
758
+ overwrite: values?.overwrite !== false,
759
+ })
760
+ )
761
+ }
762
+
763
+ return { installed }
764
+ }
765
+
766
+ function restoreManagedKitBaseline(id) {
767
+ return restoreManagedKitBaselineCore(id)
167
768
  }
168
769
 
169
770
  async function applyKit(values) {
@@ -172,6 +773,8 @@ async function applyKit(values) {
172
773
  const agentName = String(values?.agentName || '').trim()
173
774
  const applyMode = normalizeKitMode(values?.mode)
174
775
  const overwriteAgentsMd = values?.overwriteAgentsMd === true
776
+ const includeSkills = Array.isArray(values?.includeSkills) ? values.includeSkills : []
777
+ const excludeSkills = Array.isArray(values?.excludeSkills) ? values.excludeSkills : []
175
778
 
176
779
  if (!kitId) throw new Error('kitId is required')
177
780
  if (!projectPath) throw new Error('projectPath is required')
@@ -179,12 +782,13 @@ async function applyKit(values) {
179
782
 
180
783
  const kit = getKitById(kitId)
181
784
  if (!kit) throw new Error(`Kit not found: ${kitId}`)
182
-
183
- const policy = getKitPolicyById(kit.policyId)
184
- if (!policy) throw new Error(`AGENTS.md not found: ${kit.policyId}`)
185
-
186
- const loadout = getKitLoadoutById(kit.loadoutId)
187
- if (!loadout) throw new Error(`Skills package not found: ${kit.loadoutId}`)
785
+ const policy = kit.policyId ? getKitPolicyById(kit.policyId) : null
786
+ if (kit.policyId && !policy) throw new Error(`AGENTS.md not found: ${kit.policyId}`)
787
+ const loadout = kit.loadoutId ? getKitLoadoutById(kit.loadoutId) : null
788
+ if (kit.loadoutId && !loadout) throw new Error(`Skills package not found: ${kit.loadoutId}`)
789
+ if (!policy && !loadout) {
790
+ throw new Error('Kit must include at least AGENTS.md or Skills package')
791
+ }
188
792
 
189
793
  const config = await readRuntimeConfig()
190
794
  const projectExists = config.projects.includes(projectPath)
@@ -197,38 +801,50 @@ async function applyKit(values) {
197
801
  throw new Error('Target agent is not enabled or not found.')
198
802
  }
199
803
 
200
- const targetSkillParent = path.join(projectPath, targetAgent.projectPath)
201
804
  const loadoutResults = []
202
-
203
- for (const item of loadout.items) {
204
- try {
205
- const destination = await syncSkill(item.skillPath, targetSkillParent, applyMode)
206
- loadoutResults.push({
207
- skillPath: item.skillPath,
208
- mode: applyMode,
209
- destination,
210
- status: 'success',
211
- })
212
- } catch (error) {
213
- loadoutResults.push({
214
- skillPath: item.skillPath,
215
- mode: applyMode,
216
- destination: path.join(targetSkillParent, path.basename(item.skillPath)),
217
- status: 'failed',
218
- error: error instanceof Error ? error.message : String(error),
219
- })
220
- throw new Error(`Failed to sync skill: ${item.skillPath}`)
805
+ if (loadout) {
806
+ const includeSkillPaths = []
807
+ for (const selector of includeSkills) {
808
+ includeSkillPaths.push(await resolveHubSkillPath(config.hubPath, selector))
809
+ }
810
+ const effectiveItems = buildEffectiveLoadoutItems(loadout.items, includeSkillPaths, excludeSkills)
811
+ const targetSkillParent = path.join(projectPath, targetAgent.projectPath)
812
+ for (const item of effectiveItems) {
813
+ try {
814
+ const destination = await syncSkill(item.skillPath, targetSkillParent, applyMode)
815
+ loadoutResults.push({
816
+ skillPath: item.skillPath,
817
+ mode: applyMode,
818
+ destination,
819
+ status: 'success',
820
+ })
821
+ } catch (error) {
822
+ loadoutResults.push({
823
+ skillPath: item.skillPath,
824
+ mode: applyMode,
825
+ destination: path.join(targetSkillParent, path.basename(item.skillPath)),
826
+ status: 'failed',
827
+ error: error instanceof Error ? error.message : String(error),
828
+ })
829
+ throw new Error(`Failed to sync skill: ${item.skillPath}`)
830
+ }
221
831
  }
222
832
  }
223
833
 
224
- const policyPath = path.join(projectPath, 'AGENTS.md')
225
- const policyExists = await fs.pathExists(policyPath)
226
- if (policyExists && !overwriteAgentsMd) {
227
- throw new Error(`AGENTS_MD_EXISTS::${policyPath}`)
228
- }
834
+ let policyPath
835
+ let policyFileName
836
+ let policyExists = false
837
+ if (policy) {
838
+ policyFileName = resolveInstructionFileName(targetAgent)
839
+ policyPath = path.join(projectPath, policyFileName)
840
+ policyExists = await fs.pathExists(policyPath)
841
+ if (policyExists && !overwriteAgentsMd) {
842
+ throw new Error(`POLICY_FILE_EXISTS::${policyPath}`)
843
+ }
229
844
 
230
- const policyContent = policy.content.endsWith('\n') ? policy.content : `${policy.content}\n`
231
- await atomicWriteText(policyPath, policyContent)
845
+ const policyContent = policy.content.endsWith('\n') ? policy.content : `${policy.content}\n`
846
+ await atomicWriteText(policyPath, policyContent)
847
+ }
232
848
 
233
849
  const applied = markKitApplied({ id: kitId, projectPath, agentName })
234
850
  if (!applied) {
@@ -239,6 +855,7 @@ async function applyKit(values) {
239
855
  kitId,
240
856
  kitName: applied.name,
241
857
  policyPath,
858
+ policyFileName,
242
859
  projectPath,
243
860
  agentName,
244
861
  appliedAt: applied.lastAppliedAt || Date.now(),
@@ -258,9 +875,19 @@ export {
258
875
  addKitLoadout,
259
876
  updateKitLoadout,
260
877
  deleteKitLoadout,
878
+ importKitLoadoutFromRepo,
879
+ listOfficialPresets,
880
+ searchOfficialPresets,
881
+ getOfficialPreset,
882
+ installOfficialPreset,
883
+ installAllOfficialPresets,
884
+ ensureManagedOfficialPresetsInstalled,
261
885
  listKits,
262
886
  addKit,
263
887
  updateKit,
264
888
  deleteKit,
889
+ restoreManagedKitBaseline,
265
890
  applyKit,
891
+ buildEffectiveLoadoutItems,
892
+ resolveHubSkillPath,
266
893
  }