@skillshub-labs/cli 0.1.17 → 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 +28 -12
  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
@@ -0,0 +1,599 @@
1
+ import fs from 'fs-extra'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import matter from 'gray-matter'
5
+ import simpleGit from 'simple-git'
6
+ import {
7
+ addKitLoadout,
8
+ listKitLoadouts,
9
+ updateKitLoadout,
10
+ } from '../core/kit-core.mjs'
11
+
12
+ const CONFIG_PATH = path.join(os.homedir(), '.skills-hub', 'config.json')
13
+ const SKIP_SCAN_DIRS = new Set(['.git', 'node_modules'])
14
+
15
+ function normalizeRepoWebUrl(url) {
16
+ return String(url || '')
17
+ .trim()
18
+ .replace(/\/$/, '')
19
+ .replace(/\.git$/i, '')
20
+ }
21
+
22
+ function normalizeRelativePath(input) {
23
+ const normalized = String(input || '')
24
+ .replace(/\\/g, '/')
25
+ .replace(/^\/+/, '')
26
+ .replace(/\/+$/, '')
27
+
28
+ return normalized
29
+ }
30
+
31
+ function normalizeRootSubdir(input) {
32
+ const normalized = normalizeRelativePath(input)
33
+ return normalized || '/'
34
+ }
35
+
36
+ function buildImportSourceKey(repoWebUrl, rootSubdir) {
37
+ return `${normalizeRepoWebUrl(repoWebUrl).toLowerCase()}::${normalizeRelativePath(rootSubdir).toLowerCase()}`
38
+ }
39
+
40
+ function getPathBasename(inputPath) {
41
+ const normalized = normalizeRelativePath(inputPath)
42
+ if (!normalized) return ''
43
+ const segments = normalized.split('/').filter(Boolean)
44
+ return segments[segments.length - 1] || ''
45
+ }
46
+
47
+ function joinRelativePath(...parts) {
48
+ const normalized = parts
49
+ .map((part) => normalizeRelativePath(part))
50
+ .filter(Boolean)
51
+ return normalized.join('/')
52
+ }
53
+
54
+ function toOptionalText(value) {
55
+ const normalized = String(value || '').trim()
56
+ return normalized || undefined
57
+ }
58
+
59
+ async function readRuntimeConfig() {
60
+ try {
61
+ const raw = await fs.readFile(CONFIG_PATH, 'utf-8')
62
+ if (!raw.trim()) {
63
+ return { hubPath: path.join(os.homedir(), 'skills-hub') }
64
+ }
65
+
66
+ const parsed = JSON.parse(raw)
67
+ return {
68
+ hubPath: String(parsed?.hubPath || path.join(os.homedir(), 'skills-hub')),
69
+ }
70
+ } catch {
71
+ return { hubPath: path.join(os.homedir(), 'skills-hub') }
72
+ }
73
+ }
74
+
75
+ function parseLoadoutImportUrl(url) {
76
+ const originalUrl = String(url || '').trim()
77
+ if (!originalUrl) {
78
+ throw new Error('kit loadout-import requires --url')
79
+ }
80
+
81
+ if (originalUrl.includes('github.com/') && originalUrl.includes('/tree/')) {
82
+ const parsedUrl = new URL(originalUrl)
83
+ const segments = parsedUrl.pathname.split('/').filter(Boolean)
84
+ if (segments.length < 4 || segments[2] !== 'tree') {
85
+ throw new Error('Invalid GitHub tree URL.')
86
+ }
87
+
88
+ const owner = segments[0]
89
+ const repo = segments[1].replace(/\.git$/i, '')
90
+ const branch = segments[3]
91
+ const subdir = segments.slice(4).join('/')
92
+
93
+ return {
94
+ originalUrl,
95
+ repoUrl: `https://github.com/${owner}/${repo}.git`,
96
+ repoWebUrl: `https://github.com/${owner}/${repo}`,
97
+ repoName: repo,
98
+ branch: branch || undefined,
99
+ explicitSubdir: normalizeRelativePath(subdir),
100
+ isGithub: true,
101
+ }
102
+ }
103
+
104
+ if (originalUrl.includes('github.com/')) {
105
+ const parsedUrl = new URL(originalUrl)
106
+ const segments = parsedUrl.pathname.split('/').filter(Boolean)
107
+ if (segments.length < 2) {
108
+ throw new Error('Invalid GitHub repository URL.')
109
+ }
110
+
111
+ const owner = segments[0]
112
+ const repo = segments[1].replace(/\.git$/i, '')
113
+ return {
114
+ originalUrl,
115
+ repoUrl: `https://github.com/${owner}/${repo}.git`,
116
+ repoWebUrl: `https://github.com/${owner}/${repo}`,
117
+ repoName: repo,
118
+ branch: undefined,
119
+ explicitSubdir: '',
120
+ isGithub: true,
121
+ }
122
+ }
123
+
124
+ const repoWebUrl = normalizeRepoWebUrl(originalUrl)
125
+ const repoName = getPathBasename(repoWebUrl) || 'imported-skills'
126
+ return {
127
+ originalUrl,
128
+ repoUrl: originalUrl,
129
+ repoWebUrl,
130
+ repoName,
131
+ branch: undefined,
132
+ explicitSubdir: '',
133
+ isGithub: false,
134
+ }
135
+ }
136
+
137
+ async function cloneRemoteRepository(repoUrl, branch) {
138
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skills-hub-kit-loadout-'))
139
+ const git = simpleGit()
140
+
141
+ try {
142
+ const cloneArgs = ['--depth', '1']
143
+ if (branch) {
144
+ cloneArgs.push('--branch', branch)
145
+ }
146
+
147
+ await git.clone(repoUrl, tempDir, cloneArgs)
148
+ const localGit = simpleGit(tempDir)
149
+ const resolvedBranch = await localGit
150
+ .revparse(['--abbrev-ref', 'HEAD'])
151
+ .then((value) => String(value || '').trim())
152
+ .catch(() => branch || '')
153
+
154
+ return {
155
+ tempDir,
156
+ git: localGit,
157
+ resolvedBranch: resolvedBranch || branch || 'unknown',
158
+ }
159
+ } catch (error) {
160
+ await fs.remove(tempDir)
161
+ throw error
162
+ }
163
+ }
164
+
165
+ async function resolveImportRoot(tempDir, parsedSource) {
166
+ if (parsedSource.explicitSubdir) {
167
+ const explicitRoot = path.join(tempDir, parsedSource.explicitSubdir)
168
+ const explicitExists = await fs.pathExists(explicitRoot)
169
+ if (!explicitExists) {
170
+ throw new Error(`Directory '${parsedSource.explicitSubdir}' not found in remote repository.`)
171
+ }
172
+
173
+ return {
174
+ rootPath: explicitRoot,
175
+ rootSubdir: normalizeRootSubdir(parsedSource.explicitSubdir),
176
+ }
177
+ }
178
+
179
+ const skillsRoot = path.join(tempDir, 'skills')
180
+ if (await fs.pathExists(skillsRoot)) {
181
+ return {
182
+ rootPath: skillsRoot,
183
+ rootSubdir: 'skills',
184
+ }
185
+ }
186
+
187
+ return {
188
+ rootPath: tempDir,
189
+ rootSubdir: '/',
190
+ }
191
+ }
192
+
193
+ async function collectInstallableSkills(basePath, currentPath, output) {
194
+ const skillMdPath = path.join(currentPath, 'SKILL.md')
195
+ if (await fs.pathExists(skillMdPath)) {
196
+ const relativePath = normalizeRelativePath(path.relative(basePath, currentPath)) || '.'
197
+ output.push({
198
+ name: getPathBasename(currentPath),
199
+ relativePath,
200
+ fullPath: currentPath,
201
+ })
202
+ return
203
+ }
204
+
205
+ const entries = await fs.readdir(currentPath, { withFileTypes: true })
206
+ for (const entry of entries) {
207
+ if (!entry.isDirectory()) continue
208
+ if (SKIP_SCAN_DIRS.has(entry.name)) continue
209
+ await collectInstallableSkills(basePath, path.join(currentPath, entry.name), output)
210
+ }
211
+ }
212
+
213
+ function assertUniqueSkillNames(entries) {
214
+ const collisions = new Map()
215
+
216
+ for (const entry of entries) {
217
+ const key = entry.name
218
+ const list = collisions.get(key) || []
219
+ list.push(entry.relativePath)
220
+ collisions.set(key, list)
221
+ }
222
+
223
+ const conflictingEntries = []
224
+ for (const [name, paths] of collisions.entries()) {
225
+ if (paths.length < 2) continue
226
+ conflictingEntries.push(`${name}: ${paths.join(', ')}`)
227
+ }
228
+
229
+ if (conflictingEntries.length > 0) {
230
+ throw new Error(
231
+ `Duplicate skill directory names found in remote source: ${conflictingEntries.join('; ')}`
232
+ )
233
+ }
234
+ }
235
+
236
+ function selectInstallableSkills(entries, skillNames, sourceLabel) {
237
+ const normalizedNames = Array.from(
238
+ new Set(
239
+ (Array.isArray(skillNames) ? skillNames : [])
240
+ .map((value) => String(value || '').trim())
241
+ .filter(Boolean)
242
+ )
243
+ )
244
+ if (normalizedNames.length === 0) {
245
+ return entries
246
+ }
247
+
248
+ const entryByName = new Map(entries.map((entry) => [entry.name, entry]))
249
+ const missing = normalizedNames.filter((name) => !entryByName.has(name))
250
+ if (missing.length > 0) {
251
+ const available = entries.map((entry) => entry.name).sort().join(', ')
252
+ throw new Error(
253
+ `Remote source '${sourceLabel}' is missing expected skills: ${missing.join(', ')}. Available skills: ${available}`
254
+ )
255
+ }
256
+
257
+ return normalizedNames
258
+ .map((name) => entryByName.get(name))
259
+ .filter(Boolean)
260
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath))
261
+ }
262
+
263
+ function buildDefaultLoadoutName(parsedSource, rootSubdir) {
264
+ if (parsedSource.explicitSubdir) {
265
+ const baseName = getPathBasename(parsedSource.explicitSubdir)
266
+ if (baseName && baseName.toLowerCase() !== 'skills') {
267
+ return baseName
268
+ }
269
+ }
270
+
271
+ if (normalizeRelativePath(rootSubdir).toLowerCase() === 'skills') {
272
+ return parsedSource.repoName
273
+ }
274
+
275
+ return parsedSource.repoName
276
+ }
277
+
278
+ function buildGithubTreeUrl(repoWebUrl, branch, subdir) {
279
+ const normalizedRepoWebUrl = normalizeRepoWebUrl(repoWebUrl)
280
+ if (!branch || !normalizedRepoWebUrl.startsWith('http')) {
281
+ return normalizedRepoWebUrl
282
+ }
283
+
284
+ const normalizedSubdir = normalizeRelativePath(subdir)
285
+ if (!normalizedSubdir) {
286
+ return `${normalizedRepoWebUrl}/tree/${branch}`
287
+ }
288
+
289
+ return `${normalizedRepoWebUrl}/tree/${branch}/${normalizedSubdir}`
290
+ }
291
+
292
+ async function readLastUpdatedAt(git, subdir) {
293
+ const args = ['log', '-1', '--format=%cI']
294
+ const normalizedSubdir = normalizeRelativePath(subdir)
295
+ if (normalizedSubdir) {
296
+ args.push('--', normalizedSubdir)
297
+ }
298
+
299
+ try {
300
+ const raw = await git.raw(args)
301
+ return String(raw || '').trim() || new Date().toISOString()
302
+ } catch {
303
+ return new Date().toISOString()
304
+ }
305
+ }
306
+
307
+ async function readSkillLoadoutKey(skillDirPath) {
308
+ const skillMdPath = path.join(skillDirPath, 'SKILL.md')
309
+ if (!(await fs.pathExists(skillMdPath))) {
310
+ return undefined
311
+ }
312
+
313
+ try {
314
+ const raw = await fs.readFile(skillMdPath, 'utf-8')
315
+ const parsed = matter(raw)
316
+ return toOptionalText(parsed.data?.source_loadout_key)
317
+ } catch {
318
+ return undefined
319
+ }
320
+ }
321
+
322
+ async function writeSkillImportMetadata(skillDirPath, metadata) {
323
+ const skillMdPath = path.join(skillDirPath, 'SKILL.md')
324
+ if (!(await fs.pathExists(skillMdPath))) {
325
+ return
326
+ }
327
+
328
+ const raw = await fs.readFile(skillMdPath, 'utf-8')
329
+ const parsed = matter(raw)
330
+ const restFrontmatter = { ...(parsed.data || {}) }
331
+ delete restFrontmatter.source_branch
332
+
333
+ const nextFrontmatter = {
334
+ ...restFrontmatter,
335
+ source_repo: metadata.sourceRepo,
336
+ source_url: metadata.sourceUrl,
337
+ source_subdir: metadata.sourceSubdir,
338
+ source_last_updated: metadata.sourceLastUpdated,
339
+ imported_at: metadata.importedAt,
340
+ source_loadout_key: metadata.sourceLoadoutKey,
341
+ }
342
+
343
+ const nextRaw = matter.stringify(parsed.content, nextFrontmatter)
344
+ await fs.writeFile(skillMdPath, nextRaw, 'utf-8')
345
+ }
346
+
347
+ function sameImportSource(left, right) {
348
+ if (!left || !right) return false
349
+ return (
350
+ buildImportSourceKey(left.repoWebUrl, left.rootSubdir) ===
351
+ buildImportSourceKey(right.repoWebUrl, right.rootSubdir)
352
+ )
353
+ }
354
+
355
+ async function assessImportedSkillsSafety(entries) {
356
+ const flaggedExtensions = new Set([
357
+ '.sh',
358
+ '.bash',
359
+ '.zsh',
360
+ '.fish',
361
+ '.ps1',
362
+ '.bat',
363
+ '.cmd',
364
+ '.exe',
365
+ '.dll',
366
+ '.so',
367
+ '.dylib',
368
+ '.jar',
369
+ '.app',
370
+ ])
371
+ const warnings = []
372
+ const flaggedFiles = []
373
+ let scannedFiles = 0
374
+ let hasExecutableLikeFile = false
375
+ let hasLargeFile = false
376
+
377
+ for (const entry of entries) {
378
+ const stack = [entry.fullPath]
379
+ while (stack.length > 0) {
380
+ const currentPath = stack.pop()
381
+ const stats = await fs.stat(currentPath)
382
+ if (stats.isDirectory()) {
383
+ const children = await fs.readdir(currentPath)
384
+ for (const child of children) {
385
+ stack.push(path.join(currentPath, child))
386
+ }
387
+ continue
388
+ }
389
+
390
+ scannedFiles += 1
391
+ const ext = path.extname(currentPath).toLowerCase()
392
+ if (flaggedExtensions.has(ext)) {
393
+ flaggedFiles.push(currentPath)
394
+ hasExecutableLikeFile = true
395
+ }
396
+ if (stats.size > 1024 * 1024) {
397
+ flaggedFiles.push(currentPath)
398
+ hasLargeFile = true
399
+ }
400
+ }
401
+ }
402
+
403
+ if (hasExecutableLikeFile) {
404
+ warnings.push('Imported skills contain shell/binary style executable files that should be reviewed.')
405
+ }
406
+ if (hasLargeFile) {
407
+ warnings.push('Imported skills contain files larger than 1MB that should be reviewed.')
408
+ }
409
+
410
+ return {
411
+ checkedAt: Date.now(),
412
+ status: warnings.length > 0 ? 'warn' : 'pass',
413
+ scannedFiles,
414
+ warnings,
415
+ flaggedFiles: flaggedFiles.map((filePath) => String(filePath)),
416
+ }
417
+ }
418
+
419
+ async function importKitLoadoutFromRepo(input) {
420
+ const parsedSource = parseLoadoutImportUrl(input?.url)
421
+ const config = await readRuntimeConfig()
422
+ const hubPath = path.resolve(config.hubPath)
423
+ const explicitName = toOptionalText(input?.name)
424
+ const explicitDescription = input?.description === undefined ? undefined : toOptionalText(input.description)
425
+ const overwrite = input?.overwrite === true
426
+ const skillNames = Array.isArray(input?.skillNames) ? input.skillNames : []
427
+
428
+ const cloned = await cloneRemoteRepository(parsedSource.repoUrl, parsedSource.branch)
429
+
430
+ try {
431
+ const { rootPath, rootSubdir } = await resolveImportRoot(cloned.tempDir, parsedSource)
432
+ const entries = []
433
+ await collectInstallableSkills(rootPath, rootPath, entries)
434
+ entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
435
+
436
+ if (entries.length === 0) {
437
+ throw new Error('No installable skills found in remote source.')
438
+ }
439
+
440
+ assertUniqueSkillNames(entries)
441
+ const selectedEntries = selectInstallableSkills(entries, skillNames, parsedSource.repoWebUrl)
442
+
443
+ const sourceLastUpdatedAt = await readLastUpdatedAt(
444
+ cloned.git,
445
+ rootSubdir === '/' ? '' : rootSubdir
446
+ )
447
+ const importSourceKey = buildImportSourceKey(parsedSource.repoWebUrl, rootSubdir)
448
+ const existingLoadout = listKitLoadouts().find((loadout) =>
449
+ sameImportSource(loadout.importSource, {
450
+ repoWebUrl: parsedSource.repoWebUrl,
451
+ rootSubdir,
452
+ })
453
+ )
454
+
455
+ const derivedName = buildDefaultLoadoutName(parsedSource, rootSubdir)
456
+ const loadoutName = explicitName || existingLoadout?.name || derivedName
457
+
458
+ if (!existingLoadout && !explicitName) {
459
+ const conflictingName = listKitLoadouts().find(
460
+ (loadout) => !loadout.importSource && loadout.name === loadoutName
461
+ )
462
+ if (conflictingName) {
463
+ throw new Error(
464
+ `Skills package name '${loadoutName}' is already used by a local package. Use --name to choose a different package name.`
465
+ )
466
+ }
467
+ }
468
+
469
+ await fs.ensureDir(hubPath)
470
+
471
+ const conflicts = []
472
+ let overwrittenCount = 0
473
+ for (const entry of selectedEntries) {
474
+ const destinationPath = path.join(hubPath, entry.name)
475
+ if (!(await fs.pathExists(destinationPath))) {
476
+ continue
477
+ }
478
+
479
+ overwrittenCount += 1
480
+ const existingKey = await readSkillLoadoutKey(destinationPath)
481
+ if (existingKey && existingKey === importSourceKey) {
482
+ continue
483
+ }
484
+
485
+ conflicts.push(destinationPath)
486
+ }
487
+
488
+ if (conflicts.length > 0 && !overwrite) {
489
+ throw new Error(
490
+ `Hub skill destinations already exist: ${conflicts.join(', ')}. Re-run with --yes to overwrite them.`
491
+ )
492
+ }
493
+
494
+ const importedAt = new Date().toISOString()
495
+ const lastSafetyCheck = await assessImportedSkillsSafety(selectedEntries)
496
+ const importSource = {
497
+ repoWebUrl: parsedSource.repoWebUrl,
498
+ repoUrl: parsedSource.repoUrl,
499
+ originalUrl: parsedSource.originalUrl,
500
+ branch: cloned.resolvedBranch || parsedSource.branch,
501
+ rootSubdir,
502
+ importedAt,
503
+ lastSourceUpdatedAt: sourceLastUpdatedAt,
504
+ lastSafetyCheck,
505
+ }
506
+
507
+ const importedSkillPaths = []
508
+ const items = []
509
+ for (const [index, entry] of selectedEntries.entries()) {
510
+ const destinationPath = path.join(hubPath, entry.name)
511
+ const sourceSubdir = joinRelativePath(rootSubdir, entry.relativePath === '.' ? '' : entry.relativePath)
512
+ const sourceUrl = parsedSource.isGithub
513
+ ? buildGithubTreeUrl(
514
+ parsedSource.repoWebUrl,
515
+ importSource.branch,
516
+ sourceSubdir
517
+ )
518
+ : parsedSource.originalUrl
519
+ const sourceLastUpdated = await readLastUpdatedAt(
520
+ cloned.git,
521
+ sourceSubdir
522
+ )
523
+
524
+ await fs.remove(destinationPath)
525
+ await fs.copy(entry.fullPath, destinationPath, { overwrite: true })
526
+ await writeSkillImportMetadata(destinationPath, {
527
+ sourceRepo: parsedSource.repoWebUrl,
528
+ sourceUrl,
529
+ sourceSubdir: sourceSubdir || '/',
530
+ sourceLastUpdated,
531
+ importedAt,
532
+ sourceLoadoutKey: importSourceKey,
533
+ })
534
+
535
+ importedSkillPaths.push(destinationPath)
536
+ items.push({
537
+ skillPath: destinationPath,
538
+ mode: 'copy',
539
+ sortOrder: index,
540
+ })
541
+ }
542
+
543
+ let removedCount = 0
544
+ if (existingLoadout) {
545
+ const nextPaths = new Set(importedSkillPaths)
546
+ for (const item of existingLoadout.items) {
547
+ if (nextPaths.has(item.skillPath)) {
548
+ continue
549
+ }
550
+
551
+ const existingKey = await readSkillLoadoutKey(item.skillPath)
552
+ if (existingKey !== importSourceKey) {
553
+ continue
554
+ }
555
+
556
+ await fs.remove(item.skillPath)
557
+ removedCount += 1
558
+ }
559
+ }
560
+
561
+ const loadout = existingLoadout
562
+ ? updateKitLoadout({
563
+ id: existingLoadout.id,
564
+ name: explicitName || existingLoadout.name,
565
+ description:
566
+ explicitDescription === undefined
567
+ ? existingLoadout.description
568
+ : explicitDescription,
569
+ items,
570
+ importSource,
571
+ })
572
+ : addKitLoadout({
573
+ name: loadoutName,
574
+ description: explicitDescription,
575
+ items,
576
+ importSource,
577
+ })
578
+
579
+ return {
580
+ loadout,
581
+ loadoutStatus: existingLoadout ? 'updated' : 'created',
582
+ importedSkillPaths,
583
+ overwrittenCount,
584
+ removedCount,
585
+ discoveredCount: selectedEntries.length,
586
+ source: importSource,
587
+ }
588
+ } finally {
589
+ await fs.remove(cloned.tempDir)
590
+ }
591
+ }
592
+
593
+ export {
594
+ buildDefaultLoadoutName,
595
+ buildImportSourceKey,
596
+ importKitLoadoutFromRepo,
597
+ parseLoadoutImportUrl,
598
+ resolveImportRoot,
599
+ }
@@ -1,5 +1,8 @@
1
1
  import type {
2
+ ManagedKitSource,
3
+ OfficialPresetBatchInstallResult,
2
4
  KitApplyResult,
5
+ KitLoadoutImportResult,
3
6
  KitLoadoutRecord,
4
7
  KitPolicyRecord,
5
8
  KitRecord,
@@ -40,13 +43,94 @@ export function updateKitLoadout(values: {
40
43
  items?: Array<{ skillPath: string; mode?: KitSyncMode; sortOrder?: number }>
41
44
  }): KitLoadoutRecord
42
45
  export function deleteKitLoadout(id: string): boolean
46
+ export function importKitLoadoutFromRepo(values: {
47
+ url: string
48
+ name?: string
49
+ description?: string
50
+ overwrite?: boolean
51
+ skillNames?: string[]
52
+ }): Promise<KitLoadoutImportResult>
53
+
54
+ export function listOfficialPresets(): Promise<
55
+ Array<{
56
+ id: string
57
+ name: string
58
+ description?: string
59
+ policyName: string
60
+ sourceCount: number
61
+ skillCount: number
62
+ }>
63
+ >
64
+ export function searchOfficialPresets(values: {
65
+ query?: string
66
+ }): Promise<
67
+ Array<{
68
+ id: string
69
+ name: string
70
+ description?: string
71
+ policyName: string
72
+ sourceCount: number
73
+ skillCount: number
74
+ }>
75
+ >
76
+ export function getOfficialPreset(values: {
77
+ id: string
78
+ }): Promise<{
79
+ id: string
80
+ name: string
81
+ description?: string
82
+ policy: {
83
+ name: string
84
+ description?: string
85
+ template: string
86
+ }
87
+ sources: Array<{
88
+ id: string
89
+ name: string
90
+ url: string
91
+ description?: string
92
+ selectedSkillDetails?: Array<{
93
+ name: string
94
+ description?: string
95
+ }>
96
+ selectedSkills: string[]
97
+ }>
98
+ skillCount: number
99
+ }>
100
+ export function installOfficialPreset(values: {
101
+ id: string
102
+ overwrite?: boolean
103
+ }): Promise<{
104
+ preset: {
105
+ id: string
106
+ name: string
107
+ description?: string
108
+ }
109
+ policy: KitPolicyRecord
110
+ loadout: KitLoadoutRecord
111
+ kit: KitRecord
112
+ importedSources: Array<{
113
+ id: string
114
+ name: string
115
+ loadoutId: string
116
+ importedSkillCount: number
117
+ selectedSkillCount: number
118
+ }>
119
+ }>
120
+ export function installAllOfficialPresets(values?: {
121
+ overwrite?: boolean
122
+ }): Promise<OfficialPresetBatchInstallResult>
123
+ export function ensureManagedOfficialPresetsInstalled(values?: {
124
+ overwrite?: boolean
125
+ }): Promise<OfficialPresetBatchInstallResult>
43
126
 
44
127
  export function listKits(): KitRecord[]
45
128
  export function addKit(values: {
46
129
  name: string
47
130
  description?: string
48
- policyId: string
49
- loadoutId: string
131
+ policyId?: string
132
+ loadoutId?: string
133
+ managedSource?: ManagedKitSource
50
134
  }): KitRecord
51
135
  export function updateKit(values: {
52
136
  id: string
@@ -54,8 +138,10 @@ export function updateKit(values: {
54
138
  description?: string
55
139
  policyId?: string
56
140
  loadoutId?: string
141
+ managedSource?: ManagedKitSource | null
57
142
  }): KitRecord
58
143
  export function deleteKit(id: string): boolean
144
+ export function restoreManagedKitBaseline(id: string): KitRecord
59
145
 
60
146
  export function applyKit(values: {
61
147
  kitId: string
@@ -63,4 +149,6 @@ export function applyKit(values: {
63
149
  agentName: string
64
150
  mode?: KitSyncMode
65
151
  overwriteAgentsMd?: boolean
152
+ includeSkills?: string[]
153
+ excludeSkills?: string[]
66
154
  }): Promise<KitApplyResult>