@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.
- package/README.md +16 -2
- package/bin/skills-hub +193 -15
- package/data/official-presets/catalog.json +436 -0
- package/data/official-presets/policies/policy-azure-cloud.md +18 -0
- package/data/official-presets/policies/policy-cloudflare-edge.md +18 -0
- package/data/official-presets/policies/policy-fastapi-py.md +31 -0
- package/data/official-presets/policies/policy-fullstack-web.md +24 -0
- package/data/official-presets/policies/policy-go-service.md +31 -0
- package/data/official-presets/policies/policy-hf-ml.md +18 -0
- package/data/official-presets/policies/policy-langchain-apps.md +18 -0
- package/data/official-presets/policies/policy-literature-review.md +18 -0
- package/data/official-presets/policies/policy-monorepo-turbo.md +31 -0
- package/data/official-presets/policies/policy-nextjs-ts-strict.md +31 -0
- package/data/official-presets/policies/policy-node-api-ts.md +31 -0
- package/data/official-presets/policies/policy-python-api.md +18 -0
- package/data/official-presets/policies/policy-release-ci.md +18 -0
- package/data/official-presets/policies/policy-release-maintainer.md +31 -0
- package/data/official-presets/policies/policy-scientific-discovery.md +18 -0
- package/data/official-presets/policies/policy-scientific-python.md +31 -0
- package/data/official-presets/policies/policy-security-audit.md +18 -0
- package/data/official-presets/policies/policy-web-frontend.md +18 -0
- package/lib/core/kit-core.d.ts +14 -3
- package/lib/core/kit-core.mjs +327 -20
- package/lib/core/kit-types.ts +128 -3
- package/lib/services/kit-loadout-import.mjs +599 -0
- package/lib/services/kit-service.d.ts +90 -2
- package/lib/services/kit-service.mjs +665 -38
- 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
|
-
|
|
521
|
+
const updated = updateKitCore(values)
|
|
522
|
+
pruneUnusedOfficialSourceLoadouts()
|
|
523
|
+
return updated
|
|
163
524
|
}
|
|
164
525
|
|
|
165
526
|
function deleteKit(id) {
|
|
166
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
}
|