@skillshub-labs/cli 0.1.17

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.
@@ -0,0 +1,56 @@
1
+ import type { KitLoadoutRecord, KitPolicyRecord, KitRecord } from './kit-types'
2
+
3
+ export function ensureDb(): unknown
4
+ export function getDbPath(): string
5
+
6
+ export function listKitPolicies(): KitPolicyRecord[]
7
+ export function getKitPolicyById(id: string): KitPolicyRecord | null
8
+ export function addKitPolicy(input: {
9
+ name: string
10
+ description?: string
11
+ content: string
12
+ }): KitPolicyRecord
13
+ export function updateKitPolicy(input: {
14
+ id: string
15
+ name?: string
16
+ description?: string
17
+ content?: string
18
+ }): KitPolicyRecord
19
+ export function deleteKitPolicy(id: string): boolean
20
+
21
+ export function listKitLoadouts(): KitLoadoutRecord[]
22
+ export function getKitLoadoutById(id: string): KitLoadoutRecord | null
23
+ export function addKitLoadout(input: {
24
+ name: string
25
+ description?: string
26
+ items: Array<{ skillPath: string; mode?: 'copy' | 'link'; sortOrder?: number }>
27
+ }): KitLoadoutRecord
28
+ export function updateKitLoadout(input: {
29
+ id: string
30
+ name?: string
31
+ description?: string
32
+ items?: Array<{ skillPath: string; mode?: 'copy' | 'link'; sortOrder?: number }>
33
+ }): KitLoadoutRecord
34
+ export function deleteKitLoadout(id: string): boolean
35
+
36
+ export function listKits(): KitRecord[]
37
+ export function getKitById(id: string): KitRecord | null
38
+ export function addKit(input: {
39
+ name: string
40
+ description?: string
41
+ policyId: string
42
+ loadoutId: string
43
+ }): KitRecord
44
+ export function updateKit(input: {
45
+ id: string
46
+ name?: string
47
+ description?: string
48
+ policyId?: string
49
+ loadoutId?: string
50
+ }): KitRecord
51
+ export function deleteKit(id: string): boolean
52
+ export function markKitApplied(input: {
53
+ id: string
54
+ projectPath: string
55
+ agentName: string
56
+ }): KitRecord
@@ -0,0 +1,626 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import crypto from 'crypto'
5
+ import Database from 'better-sqlite3'
6
+
7
+ const DB_DIR = path.join(os.homedir(), '.skills-hub')
8
+ const DB_PATH = path.join(DB_DIR, 'skills-hub.db')
9
+
10
+ let dbInstance = null
11
+
12
+ function ensureDb() {
13
+ if (dbInstance) return dbInstance
14
+
15
+ fs.mkdirSync(DB_DIR, { recursive: true })
16
+ dbInstance = new Database(DB_PATH)
17
+ dbInstance.pragma('journal_mode = WAL')
18
+ dbInstance.exec(`
19
+ CREATE TABLE IF NOT EXISTS kit_policies (
20
+ id TEXT PRIMARY KEY,
21
+ name TEXT NOT NULL,
22
+ description TEXT,
23
+ content TEXT NOT NULL,
24
+ created_at INTEGER NOT NULL,
25
+ updated_at INTEGER NOT NULL
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS kit_loadouts (
29
+ id TEXT PRIMARY KEY,
30
+ name TEXT NOT NULL,
31
+ description TEXT,
32
+ created_at INTEGER NOT NULL,
33
+ updated_at INTEGER NOT NULL
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS kit_loadout_items (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ loadout_id TEXT NOT NULL,
39
+ skill_path TEXT NOT NULL,
40
+ mode TEXT NOT NULL,
41
+ sort_order INTEGER NOT NULL DEFAULT 0
42
+ );
43
+
44
+ CREATE INDEX IF NOT EXISTS idx_kit_loadout_items_loadout_id
45
+ ON kit_loadout_items(loadout_id);
46
+
47
+ CREATE TABLE IF NOT EXISTS kit_presets (
48
+ id TEXT PRIMARY KEY,
49
+ name TEXT NOT NULL,
50
+ description TEXT,
51
+ policy_id TEXT NOT NULL,
52
+ loadout_id TEXT NOT NULL,
53
+ last_applied_at INTEGER,
54
+ last_applied_target_json TEXT,
55
+ created_at INTEGER NOT NULL,
56
+ updated_at INTEGER NOT NULL
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_kit_presets_policy_id
60
+ ON kit_presets(policy_id);
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_kit_presets_loadout_id
63
+ ON kit_presets(loadout_id);
64
+ `)
65
+
66
+ return dbInstance
67
+ }
68
+
69
+ function getDbPath() {
70
+ return DB_PATH
71
+ }
72
+
73
+ function nowTs() {
74
+ return Date.now()
75
+ }
76
+
77
+ function createId() {
78
+ if (typeof crypto.randomUUID === 'function') {
79
+ return crypto.randomUUID()
80
+ }
81
+ return `kit-${crypto.randomBytes(16).toString('hex')}`
82
+ }
83
+
84
+ function parseJsonSafe(raw, fallback) {
85
+ try {
86
+ return JSON.parse(raw)
87
+ } catch {
88
+ return fallback
89
+ }
90
+ }
91
+
92
+ function requireName(value, fieldName = 'name') {
93
+ const normalized = String(value || '').trim()
94
+ if (!normalized) {
95
+ throw new Error(`Kit ${fieldName} is required`)
96
+ }
97
+ return normalized
98
+ }
99
+
100
+ function toOptionalText(value) {
101
+ const normalized = String(value || '').trim()
102
+ return normalized || undefined
103
+ }
104
+
105
+ function normalizeLoadoutItems(items) {
106
+ if (!Array.isArray(items)) {
107
+ throw new Error('Skills package items must be an array')
108
+ }
109
+
110
+ const seen = new Set()
111
+ const normalized = []
112
+
113
+ for (const [index, item] of items.entries()) {
114
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
115
+ continue
116
+ }
117
+
118
+ const skillPath = String(item.skillPath || '').trim()
119
+ if (!skillPath || seen.has(skillPath)) {
120
+ continue
121
+ }
122
+
123
+ const mode = item.mode === 'link' ? 'link' : 'copy'
124
+ const sortOrder = Number.isInteger(item.sortOrder) ? item.sortOrder : index
125
+
126
+ normalized.push({ skillPath, mode, sortOrder })
127
+ seen.add(skillPath)
128
+ }
129
+
130
+ return normalized
131
+ }
132
+
133
+ function parsePolicyRow(row) {
134
+ if (!row) return null
135
+ return {
136
+ id: row.id,
137
+ name: row.name,
138
+ description: row.description || undefined,
139
+ content: row.content,
140
+ createdAt: row.created_at,
141
+ updatedAt: row.updated_at,
142
+ }
143
+ }
144
+
145
+ function parseLoadoutItems(rows) {
146
+ return rows.map((row) => ({
147
+ skillPath: row.skill_path,
148
+ mode: row.mode === 'link' ? 'link' : 'copy',
149
+ sortOrder: row.sort_order,
150
+ }))
151
+ }
152
+
153
+ function parseLoadoutRow(row, items) {
154
+ if (!row) return null
155
+ return {
156
+ id: row.id,
157
+ name: row.name,
158
+ description: row.description || undefined,
159
+ items,
160
+ createdAt: row.created_at,
161
+ updatedAt: row.updated_at,
162
+ }
163
+ }
164
+
165
+ function parseKitRow(row) {
166
+ if (!row) return null
167
+ return {
168
+ id: row.id,
169
+ name: row.name,
170
+ description: row.description || undefined,
171
+ policyId: row.policy_id,
172
+ loadoutId: row.loadout_id,
173
+ lastAppliedAt: row.last_applied_at || undefined,
174
+ lastAppliedTarget: row.last_applied_target_json
175
+ ? parseJsonSafe(row.last_applied_target_json, undefined)
176
+ : undefined,
177
+ createdAt: row.created_at,
178
+ updatedAt: row.updated_at,
179
+ }
180
+ }
181
+
182
+ function listKitPolicies() {
183
+ const db = ensureDb()
184
+ const rows = db
185
+ .prepare(
186
+ `SELECT id, name, description, content, created_at, updated_at
187
+ FROM kit_policies
188
+ ORDER BY updated_at DESC, name ASC`
189
+ )
190
+ .all()
191
+ return rows.map(parsePolicyRow)
192
+ }
193
+
194
+ function getKitPolicyById(id) {
195
+ if (!id) return null
196
+ const db = ensureDb()
197
+ const row = db
198
+ .prepare(
199
+ `SELECT id, name, description, content, created_at, updated_at
200
+ FROM kit_policies
201
+ WHERE id = ?`
202
+ )
203
+ .get(id)
204
+ return parsePolicyRow(row)
205
+ }
206
+
207
+ function addKitPolicy(input) {
208
+ const db = ensureDb()
209
+ const id = createId()
210
+ const ts = nowTs()
211
+
212
+ const name = requireName(input?.name, 'AGENTS.md name')
213
+ const content = String(input?.content || '').trim()
214
+ if (!content) {
215
+ throw new Error('AGENTS.md content is required')
216
+ }
217
+
218
+ db.prepare(
219
+ `INSERT INTO kit_policies (id, name, description, content, created_at, updated_at)
220
+ VALUES (@id, @name, @description, @content, @createdAt, @updatedAt)`
221
+ ).run({
222
+ id,
223
+ name,
224
+ description: toOptionalText(input?.description) || null,
225
+ content,
226
+ createdAt: ts,
227
+ updatedAt: ts,
228
+ })
229
+
230
+ return getKitPolicyById(id)
231
+ }
232
+
233
+ function updateKitPolicy(input) {
234
+ const id = String(input?.id || '').trim()
235
+ if (!id) {
236
+ throw new Error('AGENTS.md id is required')
237
+ }
238
+
239
+ const existing = getKitPolicyById(id)
240
+ if (!existing) {
241
+ throw new Error(`AGENTS.md not found: ${id}`)
242
+ }
243
+
244
+ const nextName =
245
+ input?.name === undefined ? existing.name : requireName(input.name, 'AGENTS.md name')
246
+ const nextDescription =
247
+ input?.description === undefined ? existing.description || null : toOptionalText(input.description) || null
248
+ const nextContent = input?.content === undefined ? existing.content : String(input.content || '').trim()
249
+
250
+ if (!nextContent) {
251
+ throw new Error('AGENTS.md content is required')
252
+ }
253
+
254
+ const db = ensureDb()
255
+ db.prepare(
256
+ `UPDATE kit_policies
257
+ SET name = @name,
258
+ description = @description,
259
+ content = @content,
260
+ updated_at = @updatedAt
261
+ WHERE id = @id`
262
+ ).run({
263
+ id,
264
+ name: nextName,
265
+ description: nextDescription,
266
+ content: nextContent,
267
+ updatedAt: nowTs(),
268
+ })
269
+
270
+ return getKitPolicyById(id)
271
+ }
272
+
273
+ function deleteKitPolicy(id) {
274
+ if (!id) return false
275
+ const db = ensureDb()
276
+ const usedByKit = db
277
+ .prepare(`SELECT COUNT(1) AS count FROM kit_presets WHERE policy_id = ?`)
278
+ .get(id)
279
+
280
+ if (usedByKit?.count > 0) {
281
+ throw new Error('AGENTS.md is referenced by existing kit presets; remove kits first')
282
+ }
283
+
284
+ const result = db.prepare(`DELETE FROM kit_policies WHERE id = ?`).run(id)
285
+ return result.changes > 0
286
+ }
287
+
288
+ function listLoadoutItems(loadoutId) {
289
+ const db = ensureDb()
290
+ const rows = db
291
+ .prepare(
292
+ `SELECT skill_path, mode, sort_order
293
+ FROM kit_loadout_items
294
+ WHERE loadout_id = ?
295
+ ORDER BY sort_order ASC, id ASC`
296
+ )
297
+ .all(loadoutId)
298
+ return parseLoadoutItems(rows)
299
+ }
300
+
301
+ function listKitLoadouts() {
302
+ const db = ensureDb()
303
+ const rows = db
304
+ .prepare(
305
+ `SELECT id, name, description, created_at, updated_at
306
+ FROM kit_loadouts
307
+ ORDER BY updated_at DESC, name ASC`
308
+ )
309
+ .all()
310
+
311
+ return rows.map((row) => parseLoadoutRow(row, listLoadoutItems(row.id)))
312
+ }
313
+
314
+ function getKitLoadoutById(id) {
315
+ if (!id) return null
316
+ const db = ensureDb()
317
+ const row = db
318
+ .prepare(
319
+ `SELECT id, name, description, created_at, updated_at
320
+ FROM kit_loadouts
321
+ WHERE id = ?`
322
+ )
323
+ .get(id)
324
+
325
+ if (!row) return null
326
+ return parseLoadoutRow(row, listLoadoutItems(id))
327
+ }
328
+
329
+ function addKitLoadout(input) {
330
+ const db = ensureDb()
331
+ const id = createId()
332
+ const ts = nowTs()
333
+
334
+ const name = requireName(input?.name, 'skills package name')
335
+ const items = normalizeLoadoutItems(input?.items || [])
336
+
337
+ db.prepare(
338
+ `INSERT INTO kit_loadouts (id, name, description, created_at, updated_at)
339
+ VALUES (@id, @name, @description, @createdAt, @updatedAt)`
340
+ ).run({
341
+ id,
342
+ name,
343
+ description: toOptionalText(input?.description) || null,
344
+ createdAt: ts,
345
+ updatedAt: ts,
346
+ })
347
+
348
+ if (items.length > 0) {
349
+ const insertItem = db.prepare(
350
+ `INSERT INTO kit_loadout_items (loadout_id, skill_path, mode, sort_order)
351
+ VALUES (@loadoutId, @skillPath, @mode, @sortOrder)`
352
+ )
353
+ const tx = db.transaction((inputItems) => {
354
+ for (const item of inputItems) {
355
+ insertItem.run({
356
+ loadoutId: id,
357
+ skillPath: item.skillPath,
358
+ mode: item.mode,
359
+ sortOrder: item.sortOrder,
360
+ })
361
+ }
362
+ })
363
+ tx(items)
364
+ }
365
+
366
+ return getKitLoadoutById(id)
367
+ }
368
+
369
+ function updateKitLoadout(input) {
370
+ const id = String(input?.id || '').trim()
371
+ if (!id) {
372
+ throw new Error('Skills package id is required')
373
+ }
374
+
375
+ const existing = getKitLoadoutById(id)
376
+ if (!existing) {
377
+ throw new Error(`Skills package not found: ${id}`)
378
+ }
379
+
380
+ const nextName =
381
+ input?.name === undefined ? existing.name : requireName(input.name, 'skills package name')
382
+ const nextDescription =
383
+ input?.description === undefined
384
+ ? existing.description || null
385
+ : toOptionalText(input.description) || null
386
+ const nextItems = input?.items === undefined ? existing.items : normalizeLoadoutItems(input.items)
387
+
388
+ const db = ensureDb()
389
+ const updateLoadout = db.prepare(
390
+ `UPDATE kit_loadouts
391
+ SET name = @name,
392
+ description = @description,
393
+ updated_at = @updatedAt
394
+ WHERE id = @id`
395
+ )
396
+ const deleteItems = db.prepare(`DELETE FROM kit_loadout_items WHERE loadout_id = ?`)
397
+ const insertItem = db.prepare(
398
+ `INSERT INTO kit_loadout_items (loadout_id, skill_path, mode, sort_order)
399
+ VALUES (@loadoutId, @skillPath, @mode, @sortOrder)`
400
+ )
401
+
402
+ const tx = db.transaction(() => {
403
+ updateLoadout.run({
404
+ id,
405
+ name: nextName,
406
+ description: nextDescription,
407
+ updatedAt: nowTs(),
408
+ })
409
+
410
+ deleteItems.run(id)
411
+
412
+ for (const item of nextItems) {
413
+ insertItem.run({
414
+ loadoutId: id,
415
+ skillPath: item.skillPath,
416
+ mode: item.mode,
417
+ sortOrder: item.sortOrder,
418
+ })
419
+ }
420
+ })
421
+
422
+ tx()
423
+
424
+ return getKitLoadoutById(id)
425
+ }
426
+
427
+ function deleteKitLoadout(id) {
428
+ if (!id) return false
429
+ const db = ensureDb()
430
+ const usedByKit = db
431
+ .prepare(`SELECT COUNT(1) AS count FROM kit_presets WHERE loadout_id = ?`)
432
+ .get(id)
433
+
434
+ if (usedByKit?.count > 0) {
435
+ throw new Error('Skills package is referenced by existing kit presets; remove kits first')
436
+ }
437
+
438
+ const tx = db.transaction(() => {
439
+ db.prepare(`DELETE FROM kit_loadout_items WHERE loadout_id = ?`).run(id)
440
+ return db.prepare(`DELETE FROM kit_loadouts WHERE id = ?`).run(id)
441
+ })
442
+
443
+ const result = tx()
444
+ return result.changes > 0
445
+ }
446
+
447
+ function listKits() {
448
+ const db = ensureDb()
449
+ const rows = db
450
+ .prepare(
451
+ `SELECT id, name, description, policy_id, loadout_id, last_applied_at, last_applied_target_json,
452
+ created_at, updated_at
453
+ FROM kit_presets
454
+ ORDER BY updated_at DESC, name ASC`
455
+ )
456
+ .all()
457
+ return rows.map(parseKitRow)
458
+ }
459
+
460
+ function getKitById(id) {
461
+ if (!id) return null
462
+ const db = ensureDb()
463
+ const row = db
464
+ .prepare(
465
+ `SELECT id, name, description, policy_id, loadout_id, last_applied_at, last_applied_target_json,
466
+ created_at, updated_at
467
+ FROM kit_presets
468
+ WHERE id = ?`
469
+ )
470
+ .get(id)
471
+ return parseKitRow(row)
472
+ }
473
+
474
+ function ensureKitRefsExist(policyId, loadoutId) {
475
+ if (!getKitPolicyById(policyId)) {
476
+ throw new Error(`AGENTS.md not found: ${policyId}`)
477
+ }
478
+
479
+ if (!getKitLoadoutById(loadoutId)) {
480
+ throw new Error(`Skills package not found: ${loadoutId}`)
481
+ }
482
+ }
483
+
484
+ function addKit(input) {
485
+ const db = ensureDb()
486
+ const id = createId()
487
+ const ts = nowTs()
488
+
489
+ const name = requireName(input?.name, 'kit name')
490
+ const policyId = String(input?.policyId || '').trim()
491
+ const loadoutId = String(input?.loadoutId || '').trim()
492
+ if (!policyId || !loadoutId) {
493
+ throw new Error('Kit must include both AGENTS.md and Skills package')
494
+ }
495
+
496
+ ensureKitRefsExist(policyId, loadoutId)
497
+
498
+ db.prepare(
499
+ `INSERT INTO kit_presets (
500
+ id, name, description, policy_id, loadout_id, created_at, updated_at
501
+ ) VALUES (
502
+ @id, @name, @description, @policyId, @loadoutId, @createdAt, @updatedAt
503
+ )`
504
+ ).run({
505
+ id,
506
+ name,
507
+ description: toOptionalText(input?.description) || null,
508
+ policyId,
509
+ loadoutId,
510
+ createdAt: ts,
511
+ updatedAt: ts,
512
+ })
513
+
514
+ return getKitById(id)
515
+ }
516
+
517
+ function updateKit(input) {
518
+ const id = String(input?.id || '').trim()
519
+ if (!id) {
520
+ throw new Error('Kit id is required')
521
+ }
522
+
523
+ const existing = getKitById(id)
524
+ if (!existing) {
525
+ throw new Error(`Kit not found: ${id}`)
526
+ }
527
+
528
+ const nextName = input?.name === undefined ? existing.name : requireName(input.name, 'kit name')
529
+ const nextDescription =
530
+ input?.description === undefined
531
+ ? existing.description || null
532
+ : toOptionalText(input.description) || null
533
+ const nextPolicyId =
534
+ input?.policyId === undefined ? existing.policyId : String(input.policyId || '').trim()
535
+ const nextLoadoutId =
536
+ input?.loadoutId === undefined ? existing.loadoutId : String(input.loadoutId || '').trim()
537
+
538
+ if (!nextPolicyId || !nextLoadoutId) {
539
+ throw new Error('Kit must include both AGENTS.md and Skills package')
540
+ }
541
+
542
+ ensureKitRefsExist(nextPolicyId, nextLoadoutId)
543
+
544
+ const db = ensureDb()
545
+ db.prepare(
546
+ `UPDATE kit_presets
547
+ SET name = @name,
548
+ description = @description,
549
+ policy_id = @policyId,
550
+ loadout_id = @loadoutId,
551
+ updated_at = @updatedAt
552
+ WHERE id = @id`
553
+ ).run({
554
+ id,
555
+ name: nextName,
556
+ description: nextDescription,
557
+ policyId: nextPolicyId,
558
+ loadoutId: nextLoadoutId,
559
+ updatedAt: nowTs(),
560
+ })
561
+
562
+ return getKitById(id)
563
+ }
564
+
565
+ function deleteKit(id) {
566
+ if (!id) return false
567
+ const db = ensureDb()
568
+ const result = db.prepare(`DELETE FROM kit_presets WHERE id = ?`).run(id)
569
+ return result.changes > 0
570
+ }
571
+
572
+ function markKitApplied(input) {
573
+ const id = String(input?.id || '').trim()
574
+ if (!id) {
575
+ throw new Error('Kit id is required')
576
+ }
577
+
578
+ const existing = getKitById(id)
579
+ if (!existing) {
580
+ throw new Error(`Kit not found: ${id}`)
581
+ }
582
+
583
+ const projectPath = String(input?.projectPath || '').trim()
584
+ const agentName = String(input?.agentName || '').trim()
585
+ if (!projectPath || !agentName) {
586
+ throw new Error('projectPath and agentName are required to mark kit application')
587
+ }
588
+
589
+ const appliedAt = nowTs()
590
+ const db = ensureDb()
591
+ db.prepare(
592
+ `UPDATE kit_presets
593
+ SET last_applied_at = @lastAppliedAt,
594
+ last_applied_target_json = @lastAppliedTargetJson,
595
+ updated_at = @updatedAt
596
+ WHERE id = @id`
597
+ ).run({
598
+ id,
599
+ lastAppliedAt: appliedAt,
600
+ lastAppliedTargetJson: JSON.stringify({ projectPath, agentName }),
601
+ updatedAt: appliedAt,
602
+ })
603
+
604
+ return getKitById(id)
605
+ }
606
+
607
+ export {
608
+ ensureDb,
609
+ getDbPath,
610
+ listKitPolicies,
611
+ getKitPolicyById,
612
+ addKitPolicy,
613
+ updateKitPolicy,
614
+ deleteKitPolicy,
615
+ listKitLoadouts,
616
+ getKitLoadoutById,
617
+ addKitLoadout,
618
+ updateKitLoadout,
619
+ deleteKitLoadout,
620
+ listKits,
621
+ getKitById,
622
+ addKit,
623
+ updateKit,
624
+ deleteKit,
625
+ markKitApplied,
626
+ }