devskill 2.0.2 → 2.0.3

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 CHANGED
@@ -48,10 +48,17 @@ npx skills add vuluu2k/skills --skill='pinia-options'
48
48
  ```
49
49
 
50
50
  ### Options 3: Interactive CLI
51
- We've also built an elegant, interactive CLI for advanced management (requires cloning this repo):
51
+ We've also built an elegant, interactive CLI for advanced management:
52
52
 
53
53
  ```bash
54
+ # Open interactive menu
55
+ npx devskill
56
+
57
+ # Option 1: Install entire collections of skills
54
58
  npx devskill install
59
+
60
+ # Option 2: Add specific individual skills (use Space to select)
61
+ npx devskill add
55
62
  ```
56
63
 
57
64
  ---
package/README.vn.md CHANGED
@@ -27,9 +27,17 @@ Bằng cách cung cấp cho AI các tài liệu `SKILL.md` chuyên dụng, bạn
27
27
 
28
28
  ## 📦 Cài đặt & Bắt đầu nhanh
29
29
 
30
- Quên việc copy thủ công đi. Chúng tôi đã xây dựng một CLI tương tác thanh lịch để đưa kiến thức trực tiếpChạy CLI trong dự án của bạn:
30
+ Quên việc copy thủ công đi. Chúng tôi đã xây dựng một CLI tương tác thanh lịch để đưa kiến thức trực tiếp vào repo của bạn.
31
+
31
32
  ```bash
33
+ # Mở menu tương tác chính
34
+ npx devskill
35
+
36
+ # Tuỳ chọn 1: Cài đặt toàn bộ một nhóm (collection) skills
32
37
  npx devskill install
38
+
39
+ # Tuỳ chọn 2: Thêm các skills đơn lẻ (chọn bằng phím Space)
40
+ npx devskill add
33
41
  ```
34
42
 
35
43
  > **Mẹo:** Bạn cũng có thể cài đặt mọi skill trên toàn hệ thống bằng công cụ chính thức:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devskill",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "devskill": "bin/devskill.js"
package/scripts/cli.ts CHANGED
@@ -97,9 +97,9 @@ async function initSubmodules(skipPrompt = false) {
97
97
  const shouldRemove = skipPrompt
98
98
  ? true
99
99
  : await p.confirm({
100
- message: 'Remove these extra submodules?',
101
- initialValue: true,
102
- })
100
+ message: 'Remove these extra submodules?',
101
+ initialValue: true,
102
+ })
103
103
 
104
104
  if (p.isCancel(shouldRemove)) {
105
105
  p.cancel('Cancelled')
@@ -130,14 +130,14 @@ async function initSubmodules(skipPrompt = false) {
130
130
  const selected = skipPrompt
131
131
  ? newProjects
132
132
  : await p.multiselect({
133
- message: 'Select projects to initialize',
134
- options: newProjects.map(project => ({
135
- value: project,
136
- label: `${project.name} (${project.type})`,
137
- hint: project.url,
138
- })),
139
- initialValues: newProjects,
140
- })
133
+ message: 'Select projects to initialize',
134
+ options: newProjects.map(project => ({
135
+ value: project,
136
+ label: `${project.name} (${project.type})`,
137
+ hint: project.url,
138
+ })),
139
+ initialValues: newProjects,
140
+ })
141
141
 
142
142
  if (p.isCancel(selected)) {
143
143
  p.cancel('Cancelled')
@@ -332,9 +332,9 @@ async function cleanup(skipPrompt = false) {
332
332
  const shouldRemove = skipPrompt
333
333
  ? true
334
334
  : await p.confirm({
335
- message: 'Remove these extra skills?',
336
- initialValue: false,
337
- })
335
+ message: 'Remove these extra skills?',
336
+ initialValue: false,
337
+ })
338
338
 
339
339
  if (p.isCancel(shouldRemove)) {
340
340
  p.cancel('Cancelled')
@@ -366,7 +366,7 @@ async function cleanup(skipPrompt = false) {
366
366
 
367
367
  async function installSkills() {
368
368
  const spinner = p.spinner()
369
-
369
+
370
370
  const allCollections: Record<string, string[]> = { ...collections }
371
371
  for (const [vendorName, config] of Object.entries(vendors)) {
372
372
  const vendorConfig = config as VendorConfig
@@ -379,7 +379,7 @@ async function installSkills() {
379
379
  }
380
380
 
381
381
  const selectedCollectionNames = await p.multiselect({
382
- message: 'Select collections to install',
382
+ message: 'Select collections to install (Space to select, Enter to confirm)',
383
383
  options: Object.keys(allCollections).map(name => ({
384
384
  value: name,
385
385
  label: name,
@@ -412,21 +412,45 @@ async function installSkills() {
412
412
  return
413
413
  }
414
414
 
415
- const skillsDirName = await p.text({
416
- message: 'Name of the skills directory inside target project?',
417
- initialValue: 'skills',
418
- placeholder: 'skills'
415
+ const toolChoice = await p.select({
416
+ message: 'Which AI tool are you installing these skills for?',
417
+ options: [
418
+ { value: 'skills', label: 'Generic (skills/)', hint: 'Default skills directory' },
419
+ { value: '.cursor/skills', label: 'Cursor (.cursor/skills)' },
420
+ { value: '.windsurf/skills', label: 'Windsurf (.windsurf/skills)' },
421
+ { value: '.claude/skills', label: 'Claude Desktop (.claude/skills)' },
422
+ { value: '.claudecode/skills', label: 'Claude Code (.claudecode/skills)' },
423
+ { value: '.agents/skills', label: 'Antigravity (.agents/skills)' },
424
+ { value: '.vscode/skills', label: 'VSCode / Copilot (.vscode/skills)' },
425
+ { value: 'custom', label: 'Custom path...' }
426
+ ]
419
427
  })
420
428
 
421
- if (p.isCancel(skillsDirName)) {
429
+ if (p.isCancel(toolChoice)) {
422
430
  p.cancel('Cancelled')
423
431
  return
424
432
  }
425
433
 
434
+ let skillsDirName = toolChoice as string
435
+
436
+ if (toolChoice === 'custom') {
437
+ const customDir = await p.text({
438
+ message: 'Enter custom skills directory path:',
439
+ initialValue: 'skills',
440
+ placeholder: 'skills'
441
+ })
442
+
443
+ if (p.isCancel(customDir)) {
444
+ p.cancel('Cancelled')
445
+ return
446
+ }
447
+ skillsDirName = customDir as string
448
+ }
449
+
426
450
  const selectedSkills = Array.from(new Set(
427
451
  (selectedCollectionNames as string[]).flatMap(name => allCollections[name])
428
452
  ))
429
- const targetDir = join(targetProject as string, skillsDirName as string)
453
+ const targetDir = join(targetProject as string, skillsDirName)
430
454
 
431
455
  if (!existsSync(targetDir)) {
432
456
  mkdirSync(targetDir, { recursive: true })
@@ -446,9 +470,129 @@ async function installSkills() {
446
470
  if (existsSync(destPath)) {
447
471
  rmSync(destPath, { recursive: true })
448
472
  }
449
-
473
+
450
474
  mkdirSync(destPath, { recursive: true })
451
-
475
+
476
+ const files = readdirSync(sourcePath, { recursive: true, withFileTypes: true })
477
+ for (const file of files) {
478
+ if (file.isFile()) {
479
+ const fullPath = join(file.parentPath, file.name)
480
+ const relativePath = fullPath.replace(sourcePath, '')
481
+ const dp = join(destPath, relativePath)
482
+ const dd = dirname(dp)
483
+ if (!existsSync(dd)) {
484
+ mkdirSync(dd, { recursive: true })
485
+ }
486
+ cpSync(fullPath, dp)
487
+ }
488
+ }
489
+ successCount++;
490
+ }
491
+
492
+ spinner.stop(`Installed ${successCount}/${selectedSkills.length} skills to target project`)
493
+ }
494
+
495
+ async function installSpecificSkills() {
496
+ const spinner = p.spinner()
497
+
498
+ const allSkills = getExistingSkillNames()
499
+
500
+ if (allSkills.length === 0) {
501
+ p.log.warn('No skills found in skills directory. Try running init or sync first.')
502
+ return
503
+ }
504
+
505
+ const selectedSkills = await p.multiselect({
506
+ message: 'Select specific skills to install (Space to select, Enter to confirm)',
507
+ options: allSkills.map(name => ({
508
+ value: name,
509
+ label: name
510
+ }))
511
+ })
512
+
513
+ if (p.isCancel(selectedSkills)) {
514
+ p.cancel('Cancelled')
515
+ return
516
+ }
517
+
518
+ if (selectedSkills.length === 0) {
519
+ p.log.warn('No skills selected')
520
+ return
521
+ }
522
+
523
+ const targetProject = await p.text({
524
+ message: 'Enter target project directory path (relative or absolute)',
525
+ initialValue: process.cwd(),
526
+ placeholder: process.cwd(),
527
+ validate: (value) => {
528
+ if (!value) return 'Path is required'
529
+ if (!existsSync(value)) return 'Directory does not exist'
530
+ }
531
+ })
532
+
533
+ if (p.isCancel(targetProject)) {
534
+ p.cancel('Cancelled')
535
+ return
536
+ }
537
+
538
+ const toolChoice = await p.select({
539
+ message: 'Which AI tool are you installing these skills for?',
540
+ options: [
541
+ { value: 'skills', label: 'Generic (skills/)', hint: 'Default skills directory' },
542
+ { value: '.cursor/skills', label: 'Cursor (.cursor/skills)' },
543
+ { value: '.windsurf/rules', label: 'Windsurf (.windsurf/rules)' },
544
+ { value: '.claude/skills', label: 'Claude Desktop (.claude/skills)' },
545
+ { value: '.claudecode/skills', label: 'Claude Code (.claudecode/skills)' },
546
+ { value: '.agents/skills', label: 'Antigravity (.agents/skills)' },
547
+ { value: '.vscode/skills', label: 'VSCode / Copilot (.vscode/skills)' },
548
+ { value: 'custom', label: 'Custom path...' }
549
+ ]
550
+ })
551
+
552
+ if (p.isCancel(toolChoice)) {
553
+ p.cancel('Cancelled')
554
+ return
555
+ }
556
+
557
+ let skillsDirName = toolChoice as string
558
+
559
+ if (toolChoice === 'custom') {
560
+ const customDir = await p.text({
561
+ message: 'Enter custom skills directory path:',
562
+ initialValue: 'skills',
563
+ placeholder: 'skills'
564
+ })
565
+
566
+ if (p.isCancel(customDir)) {
567
+ p.cancel('Cancelled')
568
+ return
569
+ }
570
+ skillsDirName = customDir as string
571
+ }
572
+
573
+ const targetDir = join(targetProject as string, skillsDirName)
574
+
575
+ if (!existsSync(targetDir)) {
576
+ mkdirSync(targetDir, { recursive: true })
577
+ }
578
+
579
+ spinner.start(`Installing ${selectedSkills.length} skills to ${targetDir}...`)
580
+
581
+ let successCount = 0;
582
+ for (const skill of selectedSkills as string[]) {
583
+ const sourcePath = join(root, 'skills', skill)
584
+ if (!existsSync(sourcePath)) {
585
+ p.log.warn(`Skill not found: ${skill}`)
586
+ continue
587
+ }
588
+
589
+ const destPath = join(targetDir, skill)
590
+ if (existsSync(destPath)) {
591
+ rmSync(destPath, { recursive: true })
592
+ }
593
+
594
+ mkdirSync(destPath, { recursive: true })
595
+
452
596
  const files = readdirSync(sourcePath, { recursive: true, withFileTypes: true })
453
597
  for (const file of files) {
454
598
  if (file.isFile()) {
@@ -502,15 +646,22 @@ async function main() {
502
646
  }
503
647
 
504
648
  if (command === 'install') {
505
- p.intro('Skills Manager - Install')
649
+ p.intro('Skills Manager - Install Groups')
506
650
  await installSkills()
507
651
  p.outro('Done')
508
652
  return
509
653
  }
510
654
 
655
+ if (command === 'add') {
656
+ p.intro('Skills Manager - Add Specific Skills')
657
+ await installSpecificSkills()
658
+ p.outro('Done')
659
+ return
660
+ }
661
+
511
662
  if (skipPrompt) {
512
663
  p.log.error('Command required when using -y flag')
513
- p.log.info('Available commands: install, init, sync, check, cleanup')
664
+ p.log.info('Available commands: install, add, init, sync, check, cleanup')
514
665
  process.exit(1)
515
666
  }
516
667
 
@@ -519,7 +670,8 @@ async function main() {
519
670
  const action = await p.select({
520
671
  message: 'What would you like to do?',
521
672
  options: [
522
- { value: 'install', label: 'Install collections', hint: 'Copy skill collections to a local project' },
673
+ { value: 'install', label: 'Install collections', hint: 'Copy entire skill collections to a local project' },
674
+ { value: 'add', label: 'Add specific skills', hint: 'Choose individual skills to add to your project' },
523
675
  { value: 'sync', label: 'Sync submodules', hint: 'Pull latest and sync Type 2 skills' },
524
676
  { value: 'init', label: 'Init submodules', hint: 'Add new submodules from meta.ts' },
525
677
  { value: 'check', label: 'Check updates', hint: 'See available updates' },
@@ -536,6 +688,9 @@ async function main() {
536
688
  case 'install':
537
689
  await installSkills()
538
690
  break
691
+ case 'add':
692
+ await installSpecificSkills()
693
+ break
539
694
  case 'init':
540
695
  await initSubmodules()
541
696
  break