@claude-collective/cli 0.8.0 → 0.13.1

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 (221) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +26 -9
  3. package/config/skills-matrix.yaml +858 -0
  4. package/config/stacks.yaml +254 -0
  5. package/dist/{chunk-TOPAIL5W.js → chunk-3U3R4NCG.js} +2 -2
  6. package/dist/chunk-3U3R4NCG.js.map +1 -0
  7. package/dist/{chunk-YKXBGCFD.js → chunk-57Y5RALO.js} +10 -10
  8. package/dist/chunk-57Y5RALO.js.map +1 -0
  9. package/dist/{chunk-ED73HCW2.js → chunk-6DCSSORF.js} +37 -88
  10. package/dist/chunk-6DCSSORF.js.map +1 -0
  11. package/dist/{chunk-6LS7XO3H.js → chunk-6Q3Y7KVB.js} +2 -2
  12. package/dist/chunk-6Q3Y7KVB.js.map +1 -0
  13. package/dist/{chunk-A3J6IAXK.js → chunk-76DWXGQE.js} +4 -2
  14. package/dist/chunk-76DWXGQE.js.map +1 -0
  15. package/dist/{chunk-Q6DR5QUH.js → chunk-7Q44DMSP.js} +62 -27
  16. package/dist/chunk-7Q44DMSP.js.map +1 -0
  17. package/dist/chunk-ACNBKXXJ.js +321 -0
  18. package/dist/chunk-ACNBKXXJ.js.map +1 -0
  19. package/dist/{chunk-QGGSLMO3.js → chunk-B7CCVP6Q.js} +41 -9
  20. package/dist/chunk-B7CCVP6Q.js.map +1 -0
  21. package/dist/{chunk-LVKRVFYR.js → chunk-BDLUZVKU.js} +2 -2
  22. package/dist/chunk-BDLUZVKU.js.map +1 -0
  23. package/dist/{chunk-HNDT5QRB.js → chunk-CDX4W4DM.js} +3 -3
  24. package/dist/chunk-CDX4W4DM.js.map +1 -0
  25. package/dist/{chunk-MYAVQ23U.js → chunk-CJEHB4TB.js} +23 -9
  26. package/dist/chunk-CJEHB4TB.js.map +1 -0
  27. package/dist/{chunk-DKGL77IY.js → chunk-CPZOTVCI.js} +15 -14
  28. package/dist/chunk-CPZOTVCI.js.map +1 -0
  29. package/dist/{chunk-K7PTOVX4.js → chunk-D237EVNB.js} +32 -3
  30. package/dist/chunk-D237EVNB.js.map +1 -0
  31. package/dist/{chunk-Q2LH2DAB.js → chunk-DRXPNNPB.js} +19 -18
  32. package/dist/chunk-DRXPNNPB.js.map +1 -0
  33. package/dist/{chunk-Y3V43XCU.js → chunk-E3FJH4TF.js} +12 -8
  34. package/dist/chunk-E3FJH4TF.js.map +1 -0
  35. package/dist/{chunk-3HBTELJN.js → chunk-ED4E6Q2T.js} +10 -10
  36. package/dist/chunk-ED4E6Q2T.js.map +1 -0
  37. package/dist/{chunk-SYQ7R2JO.js → chunk-EHS3TWWP.js} +3 -3
  38. package/dist/chunk-EHS3TWWP.js.map +1 -0
  39. package/dist/{chunk-LQTST4WY.js → chunk-GDH553MV.js} +6 -6
  40. package/dist/chunk-GDH553MV.js.map +1 -0
  41. package/dist/{chunk-A65SBAAJ.js → chunk-HLJX2FTL.js} +31 -5
  42. package/dist/chunk-HLJX2FTL.js.map +1 -0
  43. package/dist/chunk-I2DSLOXZ.js +75 -0
  44. package/dist/chunk-I2DSLOXZ.js.map +1 -0
  45. package/dist/{chunk-SEBPPFUW.js → chunk-I4TPKIYX.js} +33 -18
  46. package/dist/chunk-I4TPKIYX.js.map +1 -0
  47. package/dist/{chunk-NGBFJJ7Q.js → chunk-IMDW5ZUP.js} +19 -11
  48. package/dist/chunk-IMDW5ZUP.js.map +1 -0
  49. package/dist/{chunk-U4VYHKPM.js → chunk-JIPWV2FX.js} +6 -6
  50. package/dist/chunk-JIPWV2FX.js.map +1 -0
  51. package/dist/{chunk-G2FBJOZG.js → chunk-K7EVM5LY.js} +4 -4
  52. package/dist/chunk-K7EVM5LY.js.map +1 -0
  53. package/dist/{chunk-MJSFR562.js → chunk-KAAEN2PO.js} +3 -3
  54. package/dist/chunk-KAAEN2PO.js.map +1 -0
  55. package/dist/{chunk-FNOYEXUE.js → chunk-LE6IY6IT.js} +22 -17
  56. package/dist/chunk-LE6IY6IT.js.map +1 -0
  57. package/dist/{chunk-CIY5UBRB.js → chunk-NDY25DTL.js} +6 -6
  58. package/dist/chunk-NDY25DTL.js.map +1 -0
  59. package/dist/{chunk-OLBOTK3O.js → chunk-P26A2K5N.js} +7 -7
  60. package/dist/chunk-P26A2K5N.js.map +1 -0
  61. package/dist/{chunk-DHFFRMF6.js → chunk-RTE64SJA.js} +2 -2
  62. package/dist/chunk-RTE64SJA.js.map +1 -0
  63. package/dist/{chunk-3ZCB5K33.js → chunk-SGJ23HIP.js} +14 -11
  64. package/dist/chunk-SGJ23HIP.js.map +1 -0
  65. package/dist/{chunk-C4ZTIYFR.js → chunk-SVYPSDWY.js} +10 -10
  66. package/dist/chunk-SVYPSDWY.js.map +1 -0
  67. package/dist/{chunk-MMDXNZPF.js → chunk-TKFPKEV3.js} +2 -2
  68. package/dist/chunk-TKFPKEV3.js.map +1 -0
  69. package/dist/{chunk-M7YCPFIX.js → chunk-UQTEPWU7.js} +2 -2
  70. package/dist/chunk-UQTEPWU7.js.map +1 -0
  71. package/dist/{chunk-QESUUPOE.js → chunk-V46GGCCI.js} +80 -27
  72. package/dist/chunk-V46GGCCI.js.map +1 -0
  73. package/dist/{chunk-UOWHJ6BE.js → chunk-X6QONICW.js} +6 -3
  74. package/dist/chunk-X6QONICW.js.map +1 -0
  75. package/dist/chunk-Y2LW7R3Y.js +23 -0
  76. package/dist/chunk-Y2LW7R3Y.js.map +1 -0
  77. package/dist/chunk-Z2CWURZ6.js +78 -0
  78. package/dist/chunk-Z2CWURZ6.js.map +1 -0
  79. package/dist/chunk-Z7G4B5HJ.js +377 -0
  80. package/dist/chunk-Z7G4B5HJ.js.map +1 -0
  81. package/dist/{chunk-XKEG3SCV.js → chunk-ZENYS6KW.js} +13 -9
  82. package/dist/chunk-ZENYS6KW.js.map +1 -0
  83. package/dist/{cli-v2 → cli}/defaults/agent-mappings.yaml +5 -5
  84. package/dist/commands/build/marketplace.js +9 -9
  85. package/dist/commands/build/marketplace.js.map +1 -1
  86. package/dist/commands/build/plugins.js +12 -12
  87. package/dist/commands/build/plugins.js.map +1 -1
  88. package/dist/commands/build/stack.js +15 -15
  89. package/dist/commands/build/stack.js.map +1 -1
  90. package/dist/commands/compile.js +21 -21
  91. package/dist/commands/compile.js.map +1 -1
  92. package/dist/commands/config/get.js +9 -8
  93. package/dist/commands/config/get.js.map +1 -1
  94. package/dist/commands/config/index.js +7 -6
  95. package/dist/commands/config/index.js.map +1 -1
  96. package/dist/commands/config/path.js +8 -7
  97. package/dist/commands/config/path.js.map +1 -1
  98. package/dist/commands/config/set-project.js +9 -8
  99. package/dist/commands/config/set-project.js.map +1 -1
  100. package/dist/commands/config/set.js +9 -8
  101. package/dist/commands/config/set.js.map +1 -1
  102. package/dist/commands/config/show.js +6 -5
  103. package/dist/commands/config/unset-project.js +9 -8
  104. package/dist/commands/config/unset-project.js.map +1 -1
  105. package/dist/commands/config/unset.js +9 -8
  106. package/dist/commands/config/unset.js.map +1 -1
  107. package/dist/commands/diff.js +12 -12
  108. package/dist/commands/diff.js.map +1 -1
  109. package/dist/commands/doctor.js +10 -10
  110. package/dist/commands/doctor.js.map +1 -1
  111. package/dist/commands/edit.js +52 -49
  112. package/dist/commands/edit.js.map +1 -1
  113. package/dist/commands/eject.js +180 -97
  114. package/dist/commands/eject.js.map +1 -1
  115. package/dist/commands/import/skill.js +339 -0
  116. package/dist/commands/import/skill.js.map +1 -0
  117. package/dist/commands/info.js +9 -9
  118. package/dist/commands/info.js.map +1 -1
  119. package/dist/commands/init.js +205 -78
  120. package/dist/commands/init.js.map +1 -1
  121. package/dist/commands/list.js +9 -9
  122. package/dist/commands/list.js.map +1 -1
  123. package/dist/commands/new/agent.js +19 -21
  124. package/dist/commands/new/agent.js.map +1 -1
  125. package/dist/commands/new/skill.js +11 -12
  126. package/dist/commands/new/skill.js.map +1 -1
  127. package/dist/commands/outdated.js +12 -12
  128. package/dist/commands/outdated.js.map +1 -1
  129. package/dist/commands/search.js +205 -17
  130. package/dist/commands/search.js.map +1 -1
  131. package/dist/commands/test-imports.js +18 -18
  132. package/dist/commands/test-imports.js.map +1 -1
  133. package/dist/commands/uninstall.js +58 -78
  134. package/dist/commands/uninstall.js.map +1 -1
  135. package/dist/commands/update.js +21 -21
  136. package/dist/commands/update.js.map +1 -1
  137. package/dist/commands/validate.js +9 -9
  138. package/dist/commands/validate.js.map +1 -1
  139. package/dist/commands/version/bump.js +8 -8
  140. package/dist/commands/version/bump.js.map +1 -1
  141. package/dist/commands/version/index.js +8 -8
  142. package/dist/commands/version/index.js.map +1 -1
  143. package/dist/commands/version/set.js +7 -7
  144. package/dist/commands/version/set.js.map +1 -1
  145. package/dist/commands/version/show.js +8 -8
  146. package/dist/commands/version/show.js.map +1 -1
  147. package/dist/components/common/confirm.js +1 -1
  148. package/dist/components/common/message.js +1 -1
  149. package/dist/components/common/message.js.map +1 -1
  150. package/dist/components/common/spinner.js +1 -1
  151. package/dist/components/common/spinner.js.map +1 -1
  152. package/dist/components/skill-search/skill-search.js +10 -0
  153. package/dist/components/skill-search/skill-search.js.map +1 -0
  154. package/dist/components/wizard/category-grid.js +1 -1
  155. package/dist/components/wizard/category-grid.test.js +213 -80
  156. package/dist/components/wizard/category-grid.test.js.map +1 -1
  157. package/dist/components/wizard/section-progress.js +1 -1
  158. package/dist/components/wizard/section-progress.test.js +2 -2
  159. package/dist/components/wizard/section-progress.test.js.map +1 -1
  160. package/dist/components/wizard/step-approach.js +4 -3
  161. package/dist/components/wizard/step-build.js +4 -3
  162. package/dist/components/wizard/step-build.test.js +29 -17
  163. package/dist/components/wizard/step-build.test.js.map +1 -1
  164. package/dist/components/wizard/step-confirm.js +2 -1
  165. package/dist/components/wizard/step-refine.js +2 -1
  166. package/dist/components/wizard/step-refine.test.js +4 -3
  167. package/dist/components/wizard/step-refine.test.js.map +1 -1
  168. package/dist/components/wizard/step-stack-options.js +3 -3
  169. package/dist/components/wizard/step-stack.js +3 -3
  170. package/dist/components/wizard/wizard-footer.js +9 -0
  171. package/dist/components/wizard/wizard-footer.js.map +1 -0
  172. package/dist/components/wizard/wizard-tabs.js +1 -1
  173. package/dist/components/wizard/wizard.js +14 -12
  174. package/dist/config/skills-matrix.yaml +858 -0
  175. package/dist/config/stacks.yaml +254 -0
  176. package/dist/hooks/init.js +5 -4
  177. package/dist/hooks/init.js.map +1 -1
  178. package/dist/index.js +1 -1
  179. package/dist/index.js.map +1 -1
  180. package/dist/stores/wizard-store.js +2 -2
  181. package/dist/stores/wizard-store.test.js +3 -3
  182. package/dist/stores/wizard-store.test.js.map +1 -1
  183. package/package.json +4 -2
  184. package/dist/chunk-3HBTELJN.js.map +0 -1
  185. package/dist/chunk-3ZCB5K33.js.map +0 -1
  186. package/dist/chunk-6LS7XO3H.js.map +0 -1
  187. package/dist/chunk-A3J6IAXK.js.map +0 -1
  188. package/dist/chunk-A65SBAAJ.js.map +0 -1
  189. package/dist/chunk-ALEPJ6YN.js +0 -80
  190. package/dist/chunk-ALEPJ6YN.js.map +0 -1
  191. package/dist/chunk-C4ZTIYFR.js.map +0 -1
  192. package/dist/chunk-CIY5UBRB.js.map +0 -1
  193. package/dist/chunk-DHFFRMF6.js.map +0 -1
  194. package/dist/chunk-DKGL77IY.js.map +0 -1
  195. package/dist/chunk-ED73HCW2.js.map +0 -1
  196. package/dist/chunk-FNOYEXUE.js.map +0 -1
  197. package/dist/chunk-G2FBJOZG.js.map +0 -1
  198. package/dist/chunk-HNDT5QRB.js.map +0 -1
  199. package/dist/chunk-K7PTOVX4.js.map +0 -1
  200. package/dist/chunk-LQTST4WY.js.map +0 -1
  201. package/dist/chunk-LVKRVFYR.js.map +0 -1
  202. package/dist/chunk-M7YCPFIX.js.map +0 -1
  203. package/dist/chunk-MJSFR562.js.map +0 -1
  204. package/dist/chunk-MMDXNZPF.js.map +0 -1
  205. package/dist/chunk-MYAVQ23U.js.map +0 -1
  206. package/dist/chunk-NGBFJJ7Q.js.map +0 -1
  207. package/dist/chunk-OLBOTK3O.js.map +0 -1
  208. package/dist/chunk-PPNTD5LO.js +0 -330
  209. package/dist/chunk-PPNTD5LO.js.map +0 -1
  210. package/dist/chunk-Q2LH2DAB.js.map +0 -1
  211. package/dist/chunk-Q6DR5QUH.js.map +0 -1
  212. package/dist/chunk-QESUUPOE.js.map +0 -1
  213. package/dist/chunk-QGGSLMO3.js.map +0 -1
  214. package/dist/chunk-SEBPPFUW.js.map +0 -1
  215. package/dist/chunk-SYQ7R2JO.js.map +0 -1
  216. package/dist/chunk-TOPAIL5W.js.map +0 -1
  217. package/dist/chunk-U4VYHKPM.js.map +0 -1
  218. package/dist/chunk-UOWHJ6BE.js.map +0 -1
  219. package/dist/chunk-XKEG3SCV.js.map +0 -1
  220. package/dist/chunk-Y3V43XCU.js.map +0 -1
  221. package/dist/chunk-YKXBGCFD.js.map +0 -1
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CategoryGrid
4
+ } from "./chunk-Z7G4B5HJ.js";
2
5
  import {
3
6
  SectionProgress
4
- } from "./chunk-LVKRVFYR.js";
7
+ } from "./chunk-BDLUZVKU.js";
5
8
  import {
6
- CategoryGrid
7
- } from "./chunk-PPNTD5LO.js";
9
+ WizardFooter
10
+ } from "./chunk-Y2LW7R3Y.js";
8
11
  import {
9
12
  init_esm_shims
10
13
  } from "./chunk-DHET7RCE.js";
11
14
 
12
- // src/cli-v2/components/wizard/step-build.tsx
15
+ // src/cli/components/wizard/step-build.tsx
13
16
  init_esm_shims();
14
17
  import { useState } from "react";
15
18
  import { Box, Text, useInput } from "ink";
16
19
 
17
- // src/cli-v2/lib/matrix-resolver.ts
20
+ // src/cli/lib/matrix-resolver.ts
18
21
  init_esm_shims();
19
22
  function resolveAlias(aliasOrId, matrix) {
20
23
  return matrix.aliases[aliasOrId] || aliasOrId;
@@ -334,9 +337,11 @@ function getAvailableSkills(categoryId, currentSelections, matrix, options) {
334
337
  return skillOptions;
335
338
  }
336
339
 
337
- // src/cli-v2/components/wizard/step-build.tsx
340
+ // src/cli/components/wizard/step-build.tsx
338
341
  import { jsx, jsxs } from "react/jsx-runtime";
339
342
  var MIN_DOMAINS_FOR_PROGRESS = 2;
343
+ var FRAMEWORK_SUBCATEGORY_ID = "framework";
344
+ var WEB_DOMAIN_ID = "web";
340
345
  function validateBuildStep(categories, selections) {
341
346
  for (const category of categories) {
342
347
  if (category.required) {
@@ -379,13 +384,48 @@ function getStateReason(skill) {
379
384
  }
380
385
  return void 0;
381
386
  }
382
- function buildCategoriesForDomain(domain, allSelections, matrix, expertMode) {
387
+ function isFrameworkSelected(selections) {
388
+ const frameworkSelections = selections[FRAMEWORK_SUBCATEGORY_ID] ?? [];
389
+ return frameworkSelections.length > 0;
390
+ }
391
+ function getSelectedFrameworks(selections, matrix) {
392
+ const frameworkSelections = selections[FRAMEWORK_SUBCATEGORY_ID] ?? [];
393
+ return frameworkSelections.map((alias) => {
394
+ return matrix.aliases[alias] ?? alias;
395
+ });
396
+ }
397
+ function isCompatibleWithSelectedFrameworks(skillId, selectedFrameworkIds, matrix) {
398
+ const skill = matrix.skills[skillId];
399
+ if (!skill) return false;
400
+ if (skill.compatibleWith.length === 0) {
401
+ return true;
402
+ }
403
+ return selectedFrameworkIds.some(
404
+ (frameworkId) => skill.compatibleWith.includes(frameworkId)
405
+ );
406
+ }
407
+ function shouldShowSubcategory(_subcategoryId, _domain, _frameworkSelected) {
408
+ return true;
409
+ }
410
+ function buildCategoriesForDomain(domain, allSelections, matrix, expertMode, selections) {
411
+ const frameworkSelected = isFrameworkSelected(selections);
412
+ const selectedFrameworkIds = frameworkSelected ? getSelectedFrameworks(selections, matrix) : [];
383
413
  const subcategories = Object.values(matrix.categories).filter((cat) => cat.domain === domain && cat.parent).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
384
- const categoryRows = subcategories.map((cat) => {
414
+ const visibleSubcategories = subcategories.filter(
415
+ (cat) => shouldShowSubcategory(cat.id, domain, frameworkSelected)
416
+ );
417
+ const categoryRows = visibleSubcategories.map((cat) => {
385
418
  const skillOptions = getAvailableSkills(cat.id, allSelections, matrix, {
386
419
  expertMode
387
420
  });
388
- const options = skillOptions.map((skill) => ({
421
+ const filteredSkillOptions = domain === WEB_DOMAIN_ID && cat.id !== FRAMEWORK_SUBCATEGORY_ID && frameworkSelected ? skillOptions.filter(
422
+ (skill) => isCompatibleWithSelectedFrameworks(
423
+ skill.id,
424
+ selectedFrameworkIds,
425
+ matrix
426
+ )
427
+ ) : skillOptions;
428
+ const options = filteredSkillOptions.map((skill) => ({
389
429
  id: skill.alias || skill.id,
390
430
  // Use alias for selection tracking
391
431
  label: getDisplayLabel(skill),
@@ -402,7 +442,7 @@ function buildCategoriesForDomain(domain, allSelections, matrix, expertMode) {
402
442
  options
403
443
  };
404
444
  });
405
- return categoryRows;
445
+ return categoryRows.filter((row) => row.options.length > 0);
406
446
  }
407
447
  function getDomainDisplayName(domain) {
408
448
  const displayNames = {
@@ -445,22 +485,16 @@ var Header = ({ domain, selectionCount }) => {
445
485
  ] })
446
486
  ] });
447
487
  };
448
- var Footer = ({
449
- showContinueHint,
450
- validationError
451
- }) => {
488
+ var Footer = ({ validationError }) => {
452
489
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
453
490
  validationError && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: validationError }) }),
454
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
455
- "\u2190",
456
- "/",
457
- "\u2192",
458
- " options ",
459
- "\u2191",
460
- "/",
461
- "\u2193",
462
- " categories SPACE select TAB descriptions E expert ENTER continue ESC back"
463
- ] })
491
+ /* @__PURE__ */ jsx(
492
+ WizardFooter,
493
+ {
494
+ navigation: "\u2190/\u2192 options \u2191/\u2193 categories SPACE select TAB desc E expert",
495
+ action: "ENTER continue ESC back"
496
+ }
497
+ )
464
498
  ] });
465
499
  };
466
500
  var StepBuild = ({
@@ -488,7 +522,8 @@ var StepBuild = ({
488
522
  domain,
489
523
  allSelections,
490
524
  matrix,
491
- expertMode
525
+ expertMode,
526
+ selections
492
527
  );
493
528
  const selectionCount = countSelections(categories);
494
529
  const showProgress = selectedDomains.length >= MIN_DOMAINS_FOR_PROGRESS;
@@ -534,7 +569,7 @@ var StepBuild = ({
534
569
  onToggleExpertMode
535
570
  }
536
571
  ),
537
- /* @__PURE__ */ jsx(Footer, { showContinueHint: true, validationError })
572
+ /* @__PURE__ */ jsx(Footer, { validationError })
538
573
  ] });
539
574
  };
540
575
 
@@ -544,4 +579,4 @@ export {
544
579
  getDisplayLabel,
545
580
  StepBuild
546
581
  };
547
- //# sourceMappingURL=chunk-Q6DR5QUH.js.map
582
+ //# sourceMappingURL=chunk-7Q44DMSP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/components/wizard/step-build.tsx","../src/cli/lib/matrix-resolver.ts"],"sourcesContent":["/**\n * StepBuild component - Main Build step for domain-based technology selection.\n *\n * Uses CategoryGrid for 2D grid selection and SectionProgress for multi-domain\n * progress indication. Replaces the old linear category->subcategory flow.\n *\n * This component is stateless - all state is managed via props from the parent\n * wizard component, following React's controlled component pattern.\n */\nimport React, { useState } from \"react\";\nimport { Box, Text, useInput } from \"ink\";\nimport type { MergedSkillsMatrix } from \"../../types-matrix.js\";\nimport { getAvailableSkills } from \"../../lib/matrix-resolver.js\";\nimport {\n CategoryGrid,\n type CategoryRow,\n type CategoryOption,\n type OptionState,\n} from \"./category-grid.js\";\nimport { SectionProgress } from \"./section-progress.js\";\nimport { WizardFooter } from \"./wizard-footer.js\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface StepBuildProps {\n /** Skills matrix for category/skill lookup */\n matrix: MergedSkillsMatrix;\n /** Current domain being configured (e.g., 'web', 'api') */\n domain: string;\n /** All selected domains (for progress indicator) */\n selectedDomains: string[];\n /** Current domain index (0-based) */\n currentDomainIndex: number;\n /** Current selections by subcategory */\n selections: Record<string, string[]>;\n /** All current selections (for state calculation across domains) */\n allSelections: string[];\n /** Grid focus state */\n focusedRow: number;\n focusedCol: number;\n /** UI toggles */\n showDescriptions: boolean;\n expertMode: boolean;\n /** Callbacks */\n onToggle: (subcategoryId: string, technologyId: string) => void;\n onFocusChange: (row: number, col: number) => void;\n onToggleDescriptions: () => void;\n onToggleExpertMode: () => void;\n onContinue: () => void;\n onBack: () => void;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Minimum number of domains to show progress indicator */\nconst MIN_DOMAINS_FOR_PROGRESS = 2;\n\n/** Framework subcategory ID for web domain (framework-first filtering) */\nconst FRAMEWORK_SUBCATEGORY_ID = \"framework\";\n\n/** Web domain ID where framework-first flow applies */\nconst WEB_DOMAIN_ID = \"web\";\n\n// =============================================================================\n// Validation\n// =============================================================================\n\nexport interface ValidationResult {\n valid: boolean;\n message?: string;\n}\n\n/**\n * Validate that required categories have at least one selection.\n */\nexport function validateBuildStep(\n categories: CategoryRow[],\n selections: Record<string, string[]>,\n): ValidationResult {\n for (const category of categories) {\n if (category.required) {\n const categorySelections = selections[category.id] || [];\n if (categorySelections.length === 0) {\n return {\n valid: false,\n message: `Please select a ${category.name}`,\n };\n }\n }\n }\n return { valid: true };\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Compute option state from skill flags.\n */\nfunction computeOptionState(skill: {\n disabled: boolean;\n discouraged: boolean;\n recommended: boolean;\n}): OptionState {\n if (skill.disabled) {\n return \"disabled\";\n }\n if (skill.discouraged) {\n return \"discouraged\";\n }\n if (skill.recommended) {\n return \"recommended\";\n }\n return \"normal\";\n}\n\n/**\n * Get clean display label for a skill option.\n * Uses name with author suffix stripped for accurate display.\n * e.g., \"React (@vince)\" -> \"React\", \"SCSS Modules (@vince)\" -> \"SCSS Modules\"\n */\nexport function getDisplayLabel(skill: {\n alias?: string;\n name: string;\n}): string {\n // Strip author suffix like \" (@vince)\" from name\n // This preserves the original capitalization (e.g., \"SCSS Modules\" stays as-is)\n const authorPattern = /\\s*\\(@[^)]+\\)\\s*$/;\n return skill.name.replace(authorPattern, \"\");\n}\n\n/**\n * Get state reason from skill flags.\n */\nfunction getStateReason(skill: {\n disabled: boolean;\n disabledReason?: string;\n discouraged: boolean;\n discouragedReason?: string;\n recommended: boolean;\n recommendedReason?: string;\n}): string | undefined {\n if (skill.disabled && skill.disabledReason) {\n return skill.disabledReason;\n }\n if (skill.discouraged && skill.discouragedReason) {\n return skill.discouragedReason;\n }\n if (skill.recommended && skill.recommendedReason) {\n return skill.recommendedReason;\n }\n return undefined;\n}\n\n// =============================================================================\n// Framework-First Flow (Web Domain)\n// =============================================================================\n\n/**\n * Check if a framework is selected in the current selections.\n * Framework skills are in the \"framework\" subcategory.\n */\nfunction isFrameworkSelected(selections: Record<string, string[]>): boolean {\n const frameworkSelections = selections[FRAMEWORK_SUBCATEGORY_ID] ?? [];\n return frameworkSelections.length > 0;\n}\n\n/**\n * Get the selected framework skill ID(s) from selections.\n * Returns the full skill IDs (e.g., \"web-framework-react\").\n */\nfunction getSelectedFrameworks(\n selections: Record<string, string[]>,\n matrix: MergedSkillsMatrix,\n): string[] {\n const frameworkSelections = selections[FRAMEWORK_SUBCATEGORY_ID] ?? [];\n // Resolve aliases to full skill IDs\n return frameworkSelections.map((alias) => {\n return matrix.aliases[alias] ?? alias;\n });\n}\n\n/**\n * Check if a skill is compatible with any of the selected frameworks.\n * Uses the skill's compatibleWith field from metadata.\n */\nfunction isCompatibleWithSelectedFrameworks(\n skillId: string,\n selectedFrameworkIds: string[],\n matrix: MergedSkillsMatrix,\n): boolean {\n const skill = matrix.skills[skillId];\n if (!skill) return false;\n\n // If skill has no compatibleWith defined, assume it's compatible with all\n // (this allows legacy skills without metadata to still appear)\n if (skill.compatibleWith.length === 0) {\n return true;\n }\n\n // Check if any selected framework is in the skill's compatibleWith list\n return selectedFrameworkIds.some((frameworkId) =>\n skill.compatibleWith.includes(frameworkId),\n );\n}\n\n/**\n * Determine if a subcategory should be shown based on framework-first flow.\n *\n * IMPORTANT: All sections are ALWAYS visible. The \"locked\" state in CategoryGrid\n * handles dimming and preventing navigation until a framework is selected.\n * We do NOT hide sections.\n */\nfunction shouldShowSubcategory(\n _subcategoryId: string,\n _domain: string,\n _frameworkSelected: boolean,\n): boolean {\n // All sections are always visible\n // Locking (dimming + preventing navigation) is handled by CategoryGrid\n return true;\n}\n\n/**\n * Build CategoryRow[] from matrix for a specific domain.\n *\n * Filters subcategories by domain and builds options using getAvailableSkills.\n * For web domain, implements framework-first flow:\n * - Initially shows only \"framework\" subcategory\n * - After framework selection, shows skills compatible with selected framework\n */\nfunction buildCategoriesForDomain(\n domain: string,\n allSelections: string[],\n matrix: MergedSkillsMatrix,\n expertMode: boolean,\n selections: Record<string, string[]>,\n): CategoryRow[] {\n // Check framework selection for framework-first flow\n const frameworkSelected = isFrameworkSelected(selections);\n const selectedFrameworkIds = frameworkSelected\n ? getSelectedFrameworks(selections, matrix)\n : [];\n\n // Get subcategories for the current domain (categories with parent and matching domain)\n const subcategories = Object.values(matrix.categories)\n .filter((cat) => cat.domain === domain && cat.parent)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n\n // Filter subcategories based on framework-first flow\n const visibleSubcategories = subcategories.filter((cat) =>\n shouldShowSubcategory(cat.id, domain, frameworkSelected),\n );\n\n // Build CategoryRow for each visible subcategory\n const categoryRows: CategoryRow[] = visibleSubcategories.map((cat) => {\n // Get available skills with computed states\n const skillOptions = getAvailableSkills(cat.id, allSelections, matrix, {\n expertMode,\n });\n\n // For web domain (non-framework categories), filter by compatibility\n // Framework category itself doesn't need filtering\n const filteredSkillOptions =\n domain === WEB_DOMAIN_ID &&\n cat.id !== FRAMEWORK_SUBCATEGORY_ID &&\n frameworkSelected\n ? skillOptions.filter((skill) =>\n isCompatibleWithSelectedFrameworks(\n skill.id,\n selectedFrameworkIds,\n matrix,\n ),\n )\n : skillOptions;\n\n // Map skills to CategoryOption[]\n const options: CategoryOption[] = filteredSkillOptions.map((skill) => ({\n id: skill.alias || skill.id, // Use alias for selection tracking\n label: getDisplayLabel(skill), // Clean display name without author\n state: computeOptionState(skill),\n stateReason: getStateReason(skill),\n selected: skill.selected,\n }));\n\n return {\n id: cat.id,\n name: cat.name,\n required: cat.required ?? false,\n exclusive: cat.exclusive ?? true,\n options,\n };\n });\n\n // Filter out categories with no options (after compatibility filtering)\n return categoryRows.filter((row) => row.options.length > 0);\n}\n\n/**\n * Get display name for a domain.\n */\nfunction getDomainDisplayName(domain: string): string {\n const displayNames: Record<string, string> = {\n web: \"Web\",\n api: \"API\",\n cli: \"CLI\",\n mobile: \"Mobile\",\n shared: \"Shared\",\n };\n return (\n displayNames[domain] || domain.charAt(0).toUpperCase() + domain.slice(1)\n );\n}\n\n/**\n * Count selected options across categories.\n */\nfunction countSelections(categories: CategoryRow[]): {\n selected: number;\n total: number;\n} {\n let selected = 0;\n let total = 0;\n for (const category of categories) {\n for (const option of category.options) {\n if (option.state !== \"disabled\") {\n total++;\n if (option.selected) {\n selected++;\n }\n }\n }\n }\n return { selected, total };\n}\n\n// =============================================================================\n// Header Component (Domain info with selection count)\n// =============================================================================\n\ninterface HeaderProps {\n domain: string;\n selectionCount: { selected: number; total: number };\n}\n\nconst Header: React.FC<HeaderProps> = ({ domain, selectionCount }) => {\n return (\n <Box justifyContent=\"space-between\" marginBottom={1}>\n <Text bold>\n Configure your <Text color=\"cyan\">{getDomainDisplayName(domain)}</Text>{\" \"}\n stack:\n </Text>\n <Text dimColor>\n {selectionCount.selected}/{selectionCount.total} selected\n </Text>\n </Box>\n );\n};\n\n// =============================================================================\n// Footer Component (Keyboard Help)\n// =============================================================================\n\ninterface FooterProps {\n validationError?: string;\n}\n\nconst Footer: React.FC<FooterProps> = ({ validationError }) => {\n return (\n <Box flexDirection=\"column\" marginTop={1}>\n {/* Validation error message */}\n {validationError && (\n <Box marginBottom={1}>\n <Text color=\"yellow\">{validationError}</Text>\n </Box>\n )}\n\n {/* Keyboard shortcuts help - split layout */}\n <WizardFooter\n navigation={\n \"\\u2190/\\u2192 options \\u2191/\\u2193 categories SPACE select TAB desc E expert\"\n }\n action=\"ENTER continue ESC back\"\n />\n </Box>\n );\n};\n\n// =============================================================================\n// Main Component\n// =============================================================================\n\nexport const StepBuild: React.FC<StepBuildProps> = ({\n matrix,\n domain,\n selectedDomains,\n currentDomainIndex,\n selections,\n allSelections,\n focusedRow,\n focusedCol,\n showDescriptions,\n expertMode,\n onToggle,\n onFocusChange,\n onToggleDescriptions,\n onToggleExpertMode,\n onContinue,\n onBack,\n}) => {\n // Validation state for showing error messages\n const [validationError, setValidationError] = useState<string | undefined>(\n undefined,\n );\n\n // Build categories for the current domain (with framework-first filtering)\n const categories = buildCategoriesForDomain(\n domain,\n allSelections,\n matrix,\n expertMode,\n selections,\n );\n\n // Selection count for header\n const selectionCount = countSelections(categories);\n\n // Multi-domain progress\n const showProgress = selectedDomains.length >= MIN_DOMAINS_FOR_PROGRESS;\n const isLastDomain = currentDomainIndex === selectedDomains.length - 1;\n const nextDomain = isLastDomain\n ? undefined\n : selectedDomains[currentDomainIndex + 1];\n\n // Handle keyboard input for Enter and Escape\n useInput((input, key) => {\n if (key.return) {\n // Validate before continuing\n const validation = validateBuildStep(categories, selections);\n if (validation.valid) {\n setValidationError(undefined);\n onContinue();\n } else {\n setValidationError(validation.message);\n }\n } else if (key.escape) {\n setValidationError(undefined);\n onBack();\n }\n });\n\n return (\n <Box flexDirection=\"column\">\n {/* Header with domain and selection count */}\n <Header domain={domain} selectionCount={selectionCount} />\n\n {/* Progress indicator for multi-domain */}\n {showProgress && (\n <SectionProgress\n label=\"Domain\"\n current={getDomainDisplayName(domain)}\n index={currentDomainIndex + 1}\n total={selectedDomains.length}\n next={nextDomain ? getDomainDisplayName(nextDomain) : undefined}\n />\n )}\n\n {/* Category grid */}\n <CategoryGrid\n categories={categories}\n focusedRow={focusedRow}\n focusedCol={focusedCol}\n showDescriptions={showDescriptions}\n expertMode={expertMode}\n onToggle={onToggle}\n onFocusChange={onFocusChange}\n onToggleDescriptions={onToggleDescriptions}\n onToggleExpertMode={onToggleExpertMode}\n />\n\n {/* Footer with keyboard hints */}\n <Footer validationError={validationError} />\n </Box>\n );\n};\n","import type {\n MergedSkillsMatrix,\n ResolvedSkill,\n SkillOption,\n SelectionValidation,\n ValidationError,\n ValidationWarning,\n} from \"../types-matrix\";\n\nexport function resolveAlias(\n aliasOrId: string,\n matrix: MergedSkillsMatrix,\n): string {\n return matrix.aliases[aliasOrId] || aliasOrId;\n}\n\nexport function getDependentSkills(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): string[] {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) return [];\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n const dependents: string[] = [];\n\n for (const selectedId of resolvedSelections) {\n if (selectedId === fullId) continue;\n\n const selectedSkill = matrix.skills[selectedId];\n if (!selectedSkill) continue;\n\n for (const requirement of selectedSkill.requires) {\n if (requirement.needsAny) {\n const satisfiedReqs = requirement.skillIds.filter((reqId) =>\n resolvedSelections.includes(reqId),\n );\n if (satisfiedReqs.length === 1 && satisfiedReqs[0] === fullId) {\n dependents.push(selectedId);\n }\n } else {\n if (requirement.skillIds.includes(fullId)) {\n dependents.push(selectedId);\n }\n }\n }\n }\n\n return dependents;\n}\n\nexport interface SkillCheckOptions {\n expertMode?: boolean;\n}\n\nexport function isDisabled(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n options?: SkillCheckOptions,\n): boolean {\n if (options?.expertMode) {\n return false;\n }\n\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return false;\n }\n\n for (const selectedId of currentSelections) {\n const selectedFullId = resolveAlias(selectedId, matrix);\n\n if (skill.conflictsWith.some((c) => c.skillId === selectedFullId)) {\n return true;\n }\n\n const selectedSkill = matrix.skills[selectedFullId];\n if (\n selectedSkill &&\n selectedSkill.conflictsWith.some((c) => c.skillId === fullId)\n ) {\n return true;\n }\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const requirement of skill.requires) {\n if (requirement.needsAny) {\n const hasAny = requirement.skillIds.some((reqId) =>\n resolvedSelections.includes(reqId),\n );\n if (!hasAny) {\n return true;\n }\n } else {\n const hasAll = requirement.skillIds.every((reqId) =>\n resolvedSelections.includes(reqId),\n );\n if (!hasAll) {\n return true;\n }\n }\n }\n\n return false;\n}\n\nexport function getDisableReason(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): string | undefined {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return undefined;\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const selectedId of resolvedSelections) {\n const conflict = skill.conflictsWith.find((c) => c.skillId === selectedId);\n if (conflict) {\n const selectedSkill = matrix.skills[selectedId];\n const selectedName = selectedSkill?.name || selectedId;\n return `${conflict.reason} (conflicts with ${selectedName})`;\n }\n\n const selectedSkill = matrix.skills[selectedId];\n if (selectedSkill) {\n const reverseConflict = selectedSkill.conflictsWith.find(\n (c) => c.skillId === fullId,\n );\n if (reverseConflict) {\n const selectedName = selectedSkill.name;\n return `${reverseConflict.reason} (conflicts with ${selectedName})`;\n }\n }\n }\n\n for (const requirement of skill.requires) {\n if (requirement.needsAny) {\n const hasAny = requirement.skillIds.some((reqId) =>\n resolvedSelections.includes(reqId),\n );\n if (!hasAny) {\n const requiredNames = requirement.skillIds\n .map((id) => matrix.skills[id]?.name || id)\n .join(\" or \");\n return `${requirement.reason} (requires ${requiredNames})`;\n }\n } else {\n const missingIds = requirement.skillIds.filter(\n (reqId) => !resolvedSelections.includes(reqId),\n );\n if (missingIds.length > 0) {\n const missingNames = missingIds\n .map((id) => matrix.skills[id]?.name || id)\n .join(\", \");\n return `${requirement.reason} (requires ${missingNames})`;\n }\n }\n }\n\n return undefined;\n}\n\nexport function isDiscouraged(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): boolean {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return false;\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const selectedId of resolvedSelections) {\n const selectedSkill = matrix.skills[selectedId];\n if (\n selectedSkill &&\n selectedSkill.discourages.some((d) => d.skillId === fullId)\n ) {\n return true;\n }\n\n if (skill.discourages.some((d) => d.skillId === selectedId)) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function getDiscourageReason(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): string | undefined {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return undefined;\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const selectedId of resolvedSelections) {\n const selectedSkill = matrix.skills[selectedId];\n if (selectedSkill) {\n const discourage = selectedSkill.discourages.find(\n (d) => d.skillId === fullId,\n );\n if (discourage) {\n return discourage.reason;\n }\n }\n\n const reverseDiscourage = skill.discourages.find(\n (d) => d.skillId === selectedId,\n );\n if (reverseDiscourage) {\n return reverseDiscourage.reason;\n }\n }\n\n return undefined;\n}\n\nexport function isRecommended(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): boolean {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return false;\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const selectedId of resolvedSelections) {\n const selectedSkill = matrix.skills[selectedId];\n if (\n selectedSkill &&\n selectedSkill.recommends.some((r) => r.skillId === fullId)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function getRecommendReason(\n skillId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n): string | undefined {\n const fullId = resolveAlias(skillId, matrix);\n const skill = matrix.skills[fullId];\n\n if (!skill) {\n return undefined;\n }\n\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const selectedId of resolvedSelections) {\n const selectedSkill = matrix.skills[selectedId];\n if (selectedSkill) {\n const recommendation = selectedSkill.recommends.find(\n (r) => r.skillId === fullId,\n );\n if (recommendation) {\n return `${recommendation.reason} (recommended by ${selectedSkill.name})`;\n }\n }\n }\n\n return undefined;\n}\n\nexport function validateSelection(\n selections: string[],\n matrix: MergedSkillsMatrix,\n): SelectionValidation {\n const errors: ValidationError[] = [];\n const warnings: ValidationWarning[] = [];\n const resolvedSelections = selections.map((s) => resolveAlias(s, matrix));\n\n for (let i = 0; i < resolvedSelections.length; i++) {\n const skillA = matrix.skills[resolvedSelections[i]];\n if (!skillA) continue;\n\n for (let j = i + 1; j < resolvedSelections.length; j++) {\n const skillBId = resolvedSelections[j];\n const conflict = skillA.conflictsWith.find((c) => c.skillId === skillBId);\n if (conflict) {\n errors.push({\n type: \"conflict\",\n message: `${skillA.name} conflicts with ${matrix.skills[skillBId]?.name || skillBId}: ${conflict.reason}`,\n skills: [skillA.id, skillBId],\n });\n }\n }\n }\n\n for (const skillId of resolvedSelections) {\n const skill = matrix.skills[skillId];\n if (!skill) continue;\n\n for (const requirement of skill.requires) {\n if (requirement.needsAny) {\n const hasAny = requirement.skillIds.some((reqId) =>\n resolvedSelections.includes(reqId),\n );\n if (!hasAny) {\n errors.push({\n type: \"missing_requirement\",\n message: `${skill.name} requires one of: ${requirement.skillIds.map((id) => matrix.skills[id]?.name || id).join(\", \")}`,\n skills: [skillId, ...requirement.skillIds],\n });\n }\n } else {\n const missingIds = requirement.skillIds.filter(\n (reqId) => !resolvedSelections.includes(reqId),\n );\n if (missingIds.length > 0) {\n errors.push({\n type: \"missing_requirement\",\n message: `${skill.name} requires: ${missingIds.map((id) => matrix.skills[id]?.name || id).join(\", \")}`,\n skills: [skillId, ...missingIds],\n });\n }\n }\n }\n }\n\n const categorySelections = new Map<string, string[]>();\n for (const skillId of resolvedSelections) {\n const skill = matrix.skills[skillId];\n if (!skill) continue;\n\n const existing = categorySelections.get(skill.category) || [];\n existing.push(skillId);\n categorySelections.set(skill.category, existing);\n }\n\n for (const [categoryId, skillIds] of categorySelections.entries()) {\n if (skillIds.length > 1) {\n const category = matrix.categories[categoryId];\n if (category?.exclusive) {\n errors.push({\n type: \"category_exclusive\",\n message: `Category \"${category.name}\" only allows one selection, but multiple selected: ${skillIds.map((id) => matrix.skills[id]?.name || id).join(\", \")}`,\n skills: skillIds,\n });\n }\n }\n }\n\n for (const skillId of resolvedSelections) {\n const skill = matrix.skills[skillId];\n if (!skill) continue;\n\n for (const recommendation of skill.recommends) {\n if (!resolvedSelections.includes(recommendation.skillId)) {\n const recommendedSkill = matrix.skills[recommendation.skillId];\n if (recommendedSkill) {\n const hasConflict = recommendedSkill.conflictsWith.some((c) =>\n resolvedSelections.includes(c.skillId),\n );\n if (!hasConflict) {\n warnings.push({\n type: \"missing_recommendation\",\n message: `${skill.name} recommends ${recommendedSkill.name}: ${recommendation.reason}`,\n skills: [skillId, recommendation.skillId],\n });\n }\n }\n }\n }\n }\n\n for (const skillId of resolvedSelections) {\n const skill = matrix.skills[skillId];\n if (!skill || skill.providesSetupFor.length === 0) continue;\n\n const hasUsageSkill = skill.providesSetupFor.some((usageId) =>\n resolvedSelections.includes(usageId),\n );\n if (!hasUsageSkill) {\n warnings.push({\n type: \"unused_setup\",\n message: `Setup skill \"${skill.name}\" selected but no corresponding usage skills: ${skill.providesSetupFor.map((id) => matrix.skills[id]?.name || id).join(\", \")}`,\n skills: [skillId, ...skill.providesSetupFor],\n });\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n };\n}\n\nexport function getAvailableSkills(\n categoryId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n options?: SkillCheckOptions,\n): SkillOption[] {\n const skillOptions: SkillOption[] = [];\n const resolvedSelections = currentSelections.map((s) =>\n resolveAlias(s, matrix),\n );\n\n for (const skill of Object.values(matrix.skills)) {\n if (skill.category !== categoryId) {\n continue;\n }\n\n const disabled = isDisabled(skill.id, currentSelections, matrix, options);\n const discouraged =\n !disabled && isDiscouraged(skill.id, currentSelections, matrix);\n const recommended =\n !disabled &&\n !discouraged &&\n isRecommended(skill.id, currentSelections, matrix);\n\n skillOptions.push({\n id: skill.id,\n alias: skill.alias,\n name: skill.name,\n description: skill.description,\n disabled,\n disabledReason: disabled\n ? getDisableReason(skill.id, currentSelections, matrix)\n : undefined,\n discouraged,\n discouragedReason: discouraged\n ? getDiscourageReason(skill.id, currentSelections, matrix)\n : undefined,\n recommended,\n recommendedReason: recommended\n ? getRecommendReason(skill.id, currentSelections, matrix)\n : undefined,\n selected: resolvedSelections.includes(skill.id),\n alternatives: skill.alternatives.map((a) => a.skillId),\n });\n }\n\n return skillOptions;\n}\n\nexport function getSkillsByCategory(\n categoryId: string,\n matrix: MergedSkillsMatrix,\n): ResolvedSkill[] {\n const skills: ResolvedSkill[] = [];\n\n for (const skill of Object.values(matrix.skills)) {\n if (skill.category === categoryId) {\n skills.push(skill);\n }\n }\n\n return skills;\n}\n\nexport function isCategoryAllDisabled(\n categoryId: string,\n currentSelections: string[],\n matrix: MergedSkillsMatrix,\n options?: SkillCheckOptions,\n): { disabled: boolean; reason?: string } {\n if (options?.expertMode) {\n return { disabled: false };\n }\n\n const skills = getSkillsByCategory(categoryId, matrix);\n\n if (skills.length === 0) {\n return { disabled: false };\n }\n\n const disabledSkills: Array<{ skillId: string; reason: string | undefined }> =\n [];\n\n for (const skill of skills) {\n if (isDisabled(skill.id, currentSelections, matrix, options)) {\n disabledSkills.push({\n skillId: skill.id,\n reason: getDisableReason(skill.id, currentSelections, matrix),\n });\n }\n }\n\n if (disabledSkills.length === skills.length) {\n const firstReason = disabledSkills[0]?.reason;\n const shortReason = firstReason?.split(\" (\")[0] || \"requirements not met\";\n return { disabled: true, reason: shortReason };\n }\n\n return { disabled: false };\n}\n\nexport function getSubcategories(\n parentCategoryId: string,\n matrix: MergedSkillsMatrix,\n): string[] {\n const subcategories: string[] = [];\n\n for (const category of Object.values(matrix.categories)) {\n if (category.parent === parentCategoryId) {\n subcategories.push(category.id);\n }\n }\n\n subcategories.sort((a, b) => {\n const catA = matrix.categories[a];\n const catB = matrix.categories[b];\n return (catA?.order ?? 0) - (catB?.order ?? 0);\n });\n\n return subcategories;\n}\n\nexport function getTopLevelCategories(matrix: MergedSkillsMatrix): string[] {\n const topLevel: string[] = [];\n\n for (const category of Object.values(matrix.categories)) {\n if (!category.parent) {\n topLevel.push(category.id);\n }\n }\n\n topLevel.sort((a, b) => {\n const catA = matrix.categories[a];\n const catB = matrix.categories[b];\n return (catA?.order ?? 0) - (catB?.order ?? 0);\n });\n\n return topLevel;\n}\n"],"mappings":"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","names":["selectedSkill"]}
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ cliTheme
4
+ } from "./chunk-Z2CWURZ6.js";
5
+ import {
6
+ init_esm_shims
7
+ } from "./chunk-DHET7RCE.js";
8
+
9
+ // src/cli/components/skill-search/skill-search.tsx
10
+ init_esm_shims();
11
+ import { useState, useCallback, useMemo } from "react";
12
+ import { Box, Text, useApp, useInput } from "ink";
13
+ import { ThemeProvider } from "@inkjs/ui";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var MAX_VISIBLE_RESULTS = 10;
16
+ var CHECKBOX_CHECKED = "[x]";
17
+ var CHECKBOX_UNCHECKED = "[ ]";
18
+ function matchesQuery(skill, query) {
19
+ if (!query.trim()) return true;
20
+ const lowerQuery = query.toLowerCase();
21
+ if (skill.name.toLowerCase().includes(lowerQuery)) return true;
22
+ if (skill.id.toLowerCase().includes(lowerQuery)) return true;
23
+ if (skill.alias?.toLowerCase().includes(lowerQuery)) return true;
24
+ if (skill.description.toLowerCase().includes(lowerQuery)) return true;
25
+ if (skill.category.toLowerCase().includes(lowerQuery)) return true;
26
+ if (skill.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))) {
27
+ return true;
28
+ }
29
+ if (skill.sourceName.toLowerCase().includes(lowerQuery)) return true;
30
+ return false;
31
+ }
32
+ function truncate(text, maxLength) {
33
+ if (text.length <= maxLength) return text;
34
+ return text.slice(0, maxLength - 3) + "...";
35
+ }
36
+ var Header = ({ sourceCount }) => {
37
+ return /* @__PURE__ */ jsxs(
38
+ Box,
39
+ {
40
+ flexDirection: "row",
41
+ justifyContent: "space-between",
42
+ borderStyle: "single",
43
+ borderBottom: false,
44
+ borderLeft: false,
45
+ borderRight: false,
46
+ paddingX: 1,
47
+ children: [
48
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Search Skills" }),
49
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
50
+ sourceCount,
51
+ " source",
52
+ sourceCount !== 1 ? "s" : ""
53
+ ] })
54
+ ]
55
+ }
56
+ );
57
+ };
58
+ var SearchInput = ({ value, onChange }) => {
59
+ useInput((input, key) => {
60
+ if (key.backspace || key.delete) {
61
+ onChange(value.slice(0, -1));
62
+ } else if (!key.ctrl && !key.meta && input && input.length === 1) {
63
+ const charCode = input.charCodeAt(0);
64
+ if (charCode >= 32 && charCode <= 126) {
65
+ onChange(value + input);
66
+ }
67
+ }
68
+ });
69
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, paddingY: 1, children: [
70
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
71
+ ">",
72
+ " "
73
+ ] }),
74
+ /* @__PURE__ */ jsx(Text, { children: value }),
75
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" })
76
+ ] });
77
+ };
78
+ var ResultItem = ({
79
+ skill,
80
+ isSelected,
81
+ isFocused
82
+ }) => {
83
+ const checkbox = isSelected ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED;
84
+ const displayName = skill.alias || skill.id;
85
+ const descriptionWidth = 30;
86
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
87
+ /* @__PURE__ */ jsxs(
88
+ Text,
89
+ {
90
+ color: isFocused ? "cyan" : isSelected ? "green" : void 0,
91
+ bold: isFocused,
92
+ backgroundColor: isFocused ? "#333333" : void 0,
93
+ children: [
94
+ " ",
95
+ checkbox,
96
+ " "
97
+ ]
98
+ }
99
+ ),
100
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncate(skill.sourceName, 12) }) }),
101
+ /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(
102
+ Text,
103
+ {
104
+ color: isFocused ? "cyan" : void 0,
105
+ bold: isFocused || isSelected,
106
+ children: truncate(displayName, 22)
107
+ }
108
+ ) }),
109
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncate(skill.description, descriptionWidth) })
110
+ ] });
111
+ };
112
+ var ResultsList = ({
113
+ results,
114
+ selectedIds,
115
+ focusedIndex,
116
+ scrollOffset
117
+ }) => {
118
+ const visibleResults = results.slice(
119
+ scrollOffset,
120
+ scrollOffset + MAX_VISIBLE_RESULTS
121
+ );
122
+ if (results.length === 0) {
123
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No skills found matching your search." }) });
124
+ }
125
+ return /* @__PURE__ */ jsx(
126
+ Box,
127
+ {
128
+ flexDirection: "column",
129
+ borderStyle: "single",
130
+ borderTop: false,
131
+ borderBottom: false,
132
+ paddingX: 1,
133
+ children: visibleResults.map((skill, index) => {
134
+ const actualIndex = scrollOffset + index;
135
+ return /* @__PURE__ */ jsx(
136
+ ResultItem,
137
+ {
138
+ skill,
139
+ isSelected: selectedIds.has(skill.id),
140
+ isFocused: actualIndex === focusedIndex
141
+ },
142
+ skill.id
143
+ );
144
+ })
145
+ }
146
+ );
147
+ };
148
+ var StatusBar = ({
149
+ resultCount,
150
+ selectedCount
151
+ }) => {
152
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
153
+ resultCount,
154
+ " result",
155
+ resultCount !== 1 ? "s" : "",
156
+ selectedCount > 0 && /* @__PURE__ */ jsxs(Text, { color: "green", children: [
157
+ " | ",
158
+ selectedCount,
159
+ " selected"
160
+ ] })
161
+ ] }) });
162
+ };
163
+ var Footer = ({ hasSelection }) => {
164
+ return /* @__PURE__ */ jsxs(
165
+ Box,
166
+ {
167
+ flexDirection: "row",
168
+ justifyContent: "center",
169
+ gap: 2,
170
+ borderStyle: "single",
171
+ borderTop: false,
172
+ borderLeft: false,
173
+ borderRight: false,
174
+ paddingX: 1,
175
+ marginTop: 1,
176
+ children: [
177
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
178
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "j/k" }),
179
+ " navigate"
180
+ ] }),
181
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
182
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "SPACE" }),
183
+ " select"
184
+ ] }),
185
+ hasSelection && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
186
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "ENTER" }),
187
+ " import"
188
+ ] }),
189
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
190
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "c" }),
191
+ " copy link"
192
+ ] }),
193
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
194
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "ESC" }),
195
+ " cancel"
196
+ ] })
197
+ ]
198
+ }
199
+ );
200
+ };
201
+ var SkillSearch = ({
202
+ skills,
203
+ sourceCount,
204
+ initialQuery = "",
205
+ onComplete,
206
+ onCancel
207
+ }) => {
208
+ const { exit } = useApp();
209
+ const [query, setQuery] = useState(initialQuery);
210
+ const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
211
+ const [focusedIndex, setFocusedIndex] = useState(0);
212
+ const [scrollOffset, setScrollOffset] = useState(0);
213
+ const [copiedMessage, setCopiedMessage] = useState(null);
214
+ const filteredResults = useMemo(() => {
215
+ return skills.filter((skill) => matchesQuery(skill, query));
216
+ }, [skills, query]);
217
+ const safeFocusedIndex = Math.min(
218
+ focusedIndex,
219
+ Math.max(0, filteredResults.length - 1)
220
+ );
221
+ const focusedSkill = filteredResults[safeFocusedIndex];
222
+ const handleQueryChange = useCallback((newQuery) => {
223
+ setQuery(newQuery);
224
+ setFocusedIndex(0);
225
+ setScrollOffset(0);
226
+ }, []);
227
+ const toggleSelection = useCallback((skillId) => {
228
+ setSelectedIds((prev) => {
229
+ const next = new Set(prev);
230
+ if (next.has(skillId)) {
231
+ next.delete(skillId);
232
+ } else {
233
+ next.add(skillId);
234
+ }
235
+ return next;
236
+ });
237
+ }, []);
238
+ const copySkillLink = useCallback(async (skill) => {
239
+ const link = skill.sourceUrl ? `${skill.sourceUrl}/${skill.id}` : skill.id;
240
+ try {
241
+ const encoded = Buffer.from(link).toString("base64");
242
+ process.stdout.write(`\x1B]52;c;${encoded}\x07`);
243
+ setCopiedMessage(`Copied: ${link}`);
244
+ setTimeout(() => setCopiedMessage(null), 2e3);
245
+ } catch {
246
+ setCopiedMessage(`Link: ${link}`);
247
+ setTimeout(() => setCopiedMessage(null), 3e3);
248
+ }
249
+ }, []);
250
+ useInput((input, key) => {
251
+ if (key.escape) {
252
+ onCancel();
253
+ exit();
254
+ return;
255
+ }
256
+ if (key.return) {
257
+ if (selectedIds.size > 0) {
258
+ const selectedSkills = filteredResults.filter(
259
+ (s) => selectedIds.has(s.id)
260
+ );
261
+ onComplete({
262
+ selectedSkills,
263
+ cancelled: false
264
+ });
265
+ exit();
266
+ }
267
+ return;
268
+ }
269
+ if (input === " " && focusedSkill) {
270
+ toggleSelection(focusedSkill.id);
271
+ return;
272
+ }
273
+ if ((input === "c" || input === "C") && focusedSkill) {
274
+ void copySkillLink(focusedSkill);
275
+ return;
276
+ }
277
+ const isUp = key.upArrow || input === "k";
278
+ const isDown = key.downArrow || input === "j";
279
+ if (isUp && safeFocusedIndex > 0) {
280
+ const newIndex = safeFocusedIndex - 1;
281
+ setFocusedIndex(newIndex);
282
+ if (newIndex < scrollOffset) {
283
+ setScrollOffset(newIndex);
284
+ }
285
+ }
286
+ if (isDown && safeFocusedIndex < filteredResults.length - 1) {
287
+ const newIndex = safeFocusedIndex + 1;
288
+ setFocusedIndex(newIndex);
289
+ if (newIndex >= scrollOffset + MAX_VISIBLE_RESULTS) {
290
+ setScrollOffset(newIndex - MAX_VISIBLE_RESULTS + 1);
291
+ }
292
+ }
293
+ });
294
+ return /* @__PURE__ */ jsx(ThemeProvider, { theme: cliTheme, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
295
+ /* @__PURE__ */ jsx(Header, { sourceCount }),
296
+ /* @__PURE__ */ jsx(SearchInput, { value: query, onChange: handleQueryChange }),
297
+ /* @__PURE__ */ jsx(
298
+ ResultsList,
299
+ {
300
+ results: filteredResults,
301
+ selectedIds,
302
+ focusedIndex: safeFocusedIndex,
303
+ scrollOffset
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx(
307
+ StatusBar,
308
+ {
309
+ resultCount: filteredResults.length,
310
+ selectedCount: selectedIds.size
311
+ }
312
+ ),
313
+ copiedMessage && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "green", children: copiedMessage }) }),
314
+ /* @__PURE__ */ jsx(Footer, { hasSelection: selectedIds.size > 0 })
315
+ ] }) });
316
+ };
317
+
318
+ export {
319
+ SkillSearch
320
+ };
321
+ //# sourceMappingURL=chunk-ACNBKXXJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/components/skill-search/skill-search.tsx"],"sourcesContent":["/**\n * Interactive skill search component.\n *\n * Features:\n * - Live text filtering as you type\n * - Multi-select checkboxes\n * - Keyboard navigation (arrows, vim keys, space, enter, c, esc)\n * - Source attribution for each skill\n *\n * Keyboard controls:\n * - Up/Down/k/j: Navigate results\n * - Space: Toggle selection\n * - Enter: Import selected skills\n * - c: Copy link of focused skill\n * - Esc: Cancel/exit\n */\nimport React, { useState, useCallback, useMemo } from \"react\";\nimport { Box, Text, useApp, useInput } from \"ink\";\nimport { ThemeProvider } from \"@inkjs/ui\";\nimport { cliTheme } from \"../themes/default.js\";\nimport type { ResolvedSkill } from \"../../types-matrix.js\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SourcedSkill extends ResolvedSkill {\n /** Source name where this skill came from */\n sourceName: string;\n /** Source URL for reference */\n sourceUrl?: string;\n}\n\nexport interface SkillSearchResult {\n /** Skills selected for import */\n selectedSkills: SourcedSkill[];\n /** Whether the user cancelled */\n cancelled: boolean;\n}\n\nexport interface SkillSearchProps {\n /** All available skills from all sources */\n skills: SourcedSkill[];\n /** Total number of sources */\n sourceCount: number;\n /** Initial search query (from command args) */\n initialQuery?: string;\n /** Called when user completes selection (Enter) */\n onComplete: (result: SkillSearchResult) => void;\n /** Called when user cancels (Esc) */\n onCancel: () => void;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Maximum results to show at once */\nconst MAX_VISIBLE_RESULTS = 10;\n\n/** Checkbox display */\nconst CHECKBOX_CHECKED = \"[x]\";\nconst CHECKBOX_UNCHECKED = \"[ ]\";\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Match skill against search query (case-insensitive substring)\n */\nfunction matchesQuery(skill: SourcedSkill, query: string): boolean {\n if (!query.trim()) return true;\n\n const lowerQuery = query.toLowerCase();\n\n // Match against name\n if (skill.name.toLowerCase().includes(lowerQuery)) return true;\n\n // Match against ID\n if (skill.id.toLowerCase().includes(lowerQuery)) return true;\n\n // Match against alias\n if (skill.alias?.toLowerCase().includes(lowerQuery)) return true;\n\n // Match against description\n if (skill.description.toLowerCase().includes(lowerQuery)) return true;\n\n // Match against category\n if (skill.category.toLowerCase().includes(lowerQuery)) return true;\n\n // Match against tags\n if (skill.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))) {\n return true;\n }\n\n // Match against source name\n if (skill.sourceName.toLowerCase().includes(lowerQuery)) return true;\n\n return false;\n}\n\n/**\n * Truncate text with ellipsis\n */\nfunction truncate(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength - 3) + \"...\";\n}\n\n// =============================================================================\n// Sub-Components\n// =============================================================================\n\ninterface HeaderProps {\n sourceCount: number;\n}\n\nconst Header: React.FC<HeaderProps> = ({ sourceCount }) => {\n return (\n <Box\n flexDirection=\"row\"\n justifyContent=\"space-between\"\n borderStyle=\"single\"\n borderBottom={false}\n borderLeft={false}\n borderRight={false}\n paddingX={1}\n >\n <Text bold color=\"cyan\">\n Search Skills\n </Text>\n <Text dimColor>\n {sourceCount} source{sourceCount !== 1 ? \"s\" : \"\"}\n </Text>\n </Box>\n );\n};\n\ninterface SearchInputProps {\n value: string;\n onChange: (value: string) => void;\n}\n\nconst SearchInput: React.FC<SearchInputProps> = ({ value, onChange }) => {\n // Handle text input\n useInput((input, key) => {\n if (key.backspace || key.delete) {\n onChange(value.slice(0, -1));\n } else if (!key.ctrl && !key.meta && input && input.length === 1) {\n // Only handle printable characters\n const charCode = input.charCodeAt(0);\n if (charCode >= 32 && charCode <= 126) {\n onChange(value + input);\n }\n }\n });\n\n return (\n <Box paddingX={1} paddingY={1}>\n <Text color=\"cyan\">{\">\"} </Text>\n <Text>{value}</Text>\n <Text color=\"cyan\">_</Text>\n </Box>\n );\n};\n\ninterface ResultItemProps {\n skill: SourcedSkill;\n isSelected: boolean;\n isFocused: boolean;\n}\n\nconst ResultItem: React.FC<ResultItemProps> = ({\n skill,\n isSelected,\n isFocused,\n}) => {\n const checkbox = isSelected ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED;\n const displayName = skill.alias || skill.id;\n const descriptionWidth = 30;\n\n return (\n <Box flexDirection=\"row\" gap={1}>\n <Text\n color={isFocused ? \"cyan\" : isSelected ? \"green\" : undefined}\n bold={isFocused}\n backgroundColor={isFocused ? \"#333333\" : undefined}\n >\n {\" \"}\n {checkbox}{\" \"}\n </Text>\n <Box width={14}>\n <Text dimColor>{truncate(skill.sourceName, 12)}</Text>\n </Box>\n <Box width={24}>\n <Text\n color={isFocused ? \"cyan\" : undefined}\n bold={isFocused || isSelected}\n >\n {truncate(displayName, 22)}\n </Text>\n </Box>\n <Text dimColor>{truncate(skill.description, descriptionWidth)}</Text>\n </Box>\n );\n};\n\ninterface ResultsListProps {\n results: SourcedSkill[];\n selectedIds: Set<string>;\n focusedIndex: number;\n scrollOffset: number;\n}\n\nconst ResultsList: React.FC<ResultsListProps> = ({\n results,\n selectedIds,\n focusedIndex,\n scrollOffset,\n}) => {\n const visibleResults = results.slice(\n scrollOffset,\n scrollOffset + MAX_VISIBLE_RESULTS,\n );\n\n if (results.length === 0) {\n return (\n <Box paddingX={1} paddingY={1}>\n <Text dimColor>No skills found matching your search.</Text>\n </Box>\n );\n }\n\n return (\n <Box\n flexDirection=\"column\"\n borderStyle=\"single\"\n borderTop={false}\n borderBottom={false}\n paddingX={1}\n >\n {visibleResults.map((skill, index) => {\n const actualIndex = scrollOffset + index;\n return (\n <ResultItem\n key={skill.id}\n skill={skill}\n isSelected={selectedIds.has(skill.id)}\n isFocused={actualIndex === focusedIndex}\n />\n );\n })}\n </Box>\n );\n};\n\ninterface StatusBarProps {\n resultCount: number;\n selectedCount: number;\n}\n\nconst StatusBar: React.FC<StatusBarProps> = ({\n resultCount,\n selectedCount,\n}) => {\n return (\n <Box paddingX={1}>\n <Text dimColor>\n {resultCount} result{resultCount !== 1 ? \"s\" : \"\"}\n {selectedCount > 0 && (\n <Text color=\"green\"> | {selectedCount} selected</Text>\n )}\n </Text>\n </Box>\n );\n};\n\ninterface FooterProps {\n hasSelection: boolean;\n}\n\nconst Footer: React.FC<FooterProps> = ({ hasSelection }) => {\n return (\n <Box\n flexDirection=\"row\"\n justifyContent=\"center\"\n gap={2}\n borderStyle=\"single\"\n borderTop={false}\n borderLeft={false}\n borderRight={false}\n paddingX={1}\n marginTop={1}\n >\n <Text dimColor>\n <Text color=\"white\">j/k</Text> navigate\n </Text>\n <Text dimColor>\n <Text color=\"white\">SPACE</Text> select\n </Text>\n {hasSelection && (\n <Text dimColor>\n <Text color=\"green\">ENTER</Text> import\n </Text>\n )}\n <Text dimColor>\n <Text color=\"white\">c</Text> copy link\n </Text>\n <Text dimColor>\n <Text color=\"yellow\">ESC</Text> cancel\n </Text>\n </Box>\n );\n};\n\n// =============================================================================\n// Main Component\n// =============================================================================\n\nexport const SkillSearch: React.FC<SkillSearchProps> = ({\n skills,\n sourceCount,\n initialQuery = \"\",\n onComplete,\n onCancel,\n}) => {\n const { exit } = useApp();\n\n // State\n const [query, setQuery] = useState(initialQuery);\n const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());\n const [focusedIndex, setFocusedIndex] = useState(0);\n const [scrollOffset, setScrollOffset] = useState(0);\n const [copiedMessage, setCopiedMessage] = useState<string | null>(null);\n\n // Filter results based on query\n const filteredResults = useMemo(() => {\n return skills.filter((skill) => matchesQuery(skill, query));\n }, [skills, query]);\n\n // Ensure focus stays in bounds when results change\n const safeFocusedIndex = Math.min(\n focusedIndex,\n Math.max(0, filteredResults.length - 1),\n );\n\n // Get the currently focused skill\n const focusedSkill = filteredResults[safeFocusedIndex];\n\n // Handle query change\n const handleQueryChange = useCallback((newQuery: string) => {\n setQuery(newQuery);\n setFocusedIndex(0);\n setScrollOffset(0);\n }, []);\n\n // Toggle selection\n const toggleSelection = useCallback((skillId: string) => {\n setSelectedIds((prev) => {\n const next = new Set(prev);\n if (next.has(skillId)) {\n next.delete(skillId);\n } else {\n next.add(skillId);\n }\n return next;\n });\n }, []);\n\n // Copy skill link to clipboard\n const copySkillLink = useCallback(async (skill: SourcedSkill) => {\n const link = skill.sourceUrl ? `${skill.sourceUrl}/${skill.id}` : skill.id;\n\n try {\n // Use native clipboard API via process.stdout.write with OSC 52\n // This is a cross-platform terminal clipboard method\n const encoded = Buffer.from(link).toString(\"base64\");\n process.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n setCopiedMessage(`Copied: ${link}`);\n setTimeout(() => setCopiedMessage(null), 2000);\n } catch {\n setCopiedMessage(`Link: ${link}`);\n setTimeout(() => setCopiedMessage(null), 3000);\n }\n }, []);\n\n // Handle keyboard navigation\n useInput((input, key) => {\n // Escape to cancel\n if (key.escape) {\n onCancel();\n exit();\n return;\n }\n\n // Enter to import selected\n if (key.return) {\n if (selectedIds.size > 0) {\n const selectedSkills = filteredResults.filter((s) =>\n selectedIds.has(s.id),\n );\n onComplete({\n selectedSkills,\n cancelled: false,\n });\n exit();\n }\n return;\n }\n\n // Space to toggle selection\n if (input === \" \" && focusedSkill) {\n toggleSelection(focusedSkill.id);\n return;\n }\n\n // c to copy link\n if ((input === \"c\" || input === \"C\") && focusedSkill) {\n void copySkillLink(focusedSkill);\n return;\n }\n\n // Navigation\n const isUp = key.upArrow || input === \"k\";\n const isDown = key.downArrow || input === \"j\";\n\n if (isUp && safeFocusedIndex > 0) {\n const newIndex = safeFocusedIndex - 1;\n setFocusedIndex(newIndex);\n // Scroll up if needed\n if (newIndex < scrollOffset) {\n setScrollOffset(newIndex);\n }\n }\n\n if (isDown && safeFocusedIndex < filteredResults.length - 1) {\n const newIndex = safeFocusedIndex + 1;\n setFocusedIndex(newIndex);\n // Scroll down if needed\n if (newIndex >= scrollOffset + MAX_VISIBLE_RESULTS) {\n setScrollOffset(newIndex - MAX_VISIBLE_RESULTS + 1);\n }\n }\n });\n\n return (\n <ThemeProvider theme={cliTheme}>\n <Box flexDirection=\"column\">\n <Header sourceCount={sourceCount} />\n <SearchInput value={query} onChange={handleQueryChange} />\n <ResultsList\n results={filteredResults}\n selectedIds={selectedIds}\n focusedIndex={safeFocusedIndex}\n scrollOffset={scrollOffset}\n />\n <StatusBar\n resultCount={filteredResults.length}\n selectedCount={selectedIds.size}\n />\n {copiedMessage && (\n <Box paddingX={1}>\n <Text color=\"green\">{copiedMessage}</Text>\n </Box>\n )}\n <Footer hasSelection={selectedIds.size > 0} />\n </Box>\n </ThemeProvider>\n );\n};\n"],"mappings":";;;;;;;;;AAAA;AAgBA,SAAgB,UAAU,aAAa,eAAe;AACtD,SAAS,KAAK,MAAM,QAAQ,gBAAgB;AAC5C,SAAS,qBAAqB;AA+GxB,cAGA,YAHA;AAvEN,IAAM,sBAAsB;AAG5B,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAS3B,SAAS,aAAa,OAAqB,OAAwB;AACjE,MAAI,CAAC,MAAM,KAAK,EAAG,QAAO;AAE1B,QAAM,aAAa,MAAM,YAAY;AAGrC,MAAI,MAAM,KAAK,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAG1D,MAAI,MAAM,GAAG,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAGxD,MAAI,MAAM,OAAO,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAG5D,MAAI,MAAM,YAAY,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAGjE,MAAI,MAAM,SAAS,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAG9D,MAAI,MAAM,KAAK,KAAK,CAAC,QAAQ,IAAI,YAAY,EAAE,SAAS,UAAU,CAAC,GAAG;AACpE,WAAO;AAAA,EACT;AAGA,MAAI,MAAM,WAAW,YAAY,EAAE,SAAS,UAAU,EAAG,QAAO;AAEhE,SAAO;AACT;AAKA,SAAS,SAAS,MAAc,WAA2B;AACzD,MAAI,KAAK,UAAU,UAAW,QAAO;AACrC,SAAO,KAAK,MAAM,GAAG,YAAY,CAAC,IAAI;AACxC;AAUA,IAAM,SAAgC,CAAC,EAAE,YAAY,MAAM;AACzD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,aAAY;AAAA,MACZ,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,MAEV;AAAA,4BAAC,QAAK,MAAI,MAAC,OAAM,QAAO,2BAExB;AAAA,QACA,qBAAC,QAAK,UAAQ,MACX;AAAA;AAAA,UAAY;AAAA,UAAQ,gBAAgB,IAAI,MAAM;AAAA,WACjD;AAAA;AAAA;AAAA,EACF;AAEJ;AAOA,IAAM,cAA0C,CAAC,EAAE,OAAO,SAAS,MAAM;AAEvE,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,aAAa,IAAI,QAAQ;AAC/B,eAAS,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,IAC7B,WAAW,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ,SAAS,MAAM,WAAW,GAAG;AAEhE,YAAM,WAAW,MAAM,WAAW,CAAC;AACnC,UAAI,YAAY,MAAM,YAAY,KAAK;AACrC,iBAAS,QAAQ,KAAK;AAAA,MACxB;AAAA,IACF;AAAA,EACF,CAAC;AAED,SACE,qBAAC,OAAI,UAAU,GAAG,UAAU,GAC1B;AAAA,yBAAC,QAAK,OAAM,QAAQ;AAAA;AAAA,MAAI;AAAA,OAAC;AAAA,IACzB,oBAAC,QAAM,iBAAM;AAAA,IACb,oBAAC,QAAK,OAAM,QAAO,eAAC;AAAA,KACtB;AAEJ;AAQA,IAAM,aAAwC,CAAC;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,WAAW,aAAa,mBAAmB;AACjD,QAAM,cAAc,MAAM,SAAS,MAAM;AACzC,QAAM,mBAAmB;AAEzB,SACE,qBAAC,OAAI,eAAc,OAAM,KAAK,GAC5B;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,YAAY,SAAS,aAAa,UAAU;AAAA,QACnD,MAAM;AAAA,QACN,iBAAiB,YAAY,YAAY;AAAA,QAExC;AAAA;AAAA,UACA;AAAA,UAAU;AAAA;AAAA;AAAA,IACb;AAAA,IACA,oBAAC,OAAI,OAAO,IACV,8BAAC,QAAK,UAAQ,MAAE,mBAAS,MAAM,YAAY,EAAE,GAAE,GACjD;AAAA,IACA,oBAAC,OAAI,OAAO,IACV;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,YAAY,SAAS;AAAA,QAC5B,MAAM,aAAa;AAAA,QAElB,mBAAS,aAAa,EAAE;AAAA;AAAA,IAC3B,GACF;AAAA,IACA,oBAAC,QAAK,UAAQ,MAAE,mBAAS,MAAM,aAAa,gBAAgB,GAAE;AAAA,KAChE;AAEJ;AASA,IAAM,cAA0C,CAAC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,QAAQ;AAAA,IAC7B;AAAA,IACA,eAAe;AAAA,EACjB;AAEA,MAAI,QAAQ,WAAW,GAAG;AACxB,WACE,oBAAC,OAAI,UAAU,GAAG,UAAU,GAC1B,8BAAC,QAAK,UAAQ,MAAC,mDAAqC,GACtD;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,WAAW;AAAA,MACX,cAAc;AAAA,MACd,UAAU;AAAA,MAET,yBAAe,IAAI,CAAC,OAAO,UAAU;AACpC,cAAM,cAAc,eAAe;AACnC,eACE;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,YAAY,YAAY,IAAI,MAAM,EAAE;AAAA,YACpC,WAAW,gBAAgB;AAAA;AAAA,UAHtB,MAAM;AAAA,QAIb;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;AAOA,IAAM,YAAsC,CAAC;AAAA,EAC3C;AAAA,EACA;AACF,MAAM;AACJ,SACE,oBAAC,OAAI,UAAU,GACb,+BAAC,QAAK,UAAQ,MACX;AAAA;AAAA,IAAY;AAAA,IAAQ,gBAAgB,IAAI,MAAM;AAAA,IAC9C,gBAAgB,KACf,qBAAC,QAAK,OAAM,SAAQ;AAAA;AAAA,MAAI;AAAA,MAAc;AAAA,OAAS;AAAA,KAEnD,GACF;AAEJ;AAMA,IAAM,SAAgC,CAAC,EAAE,aAAa,MAAM;AAC1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,KAAK;AAAA,MACL,aAAY;AAAA,MACZ,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,MACV,WAAW;AAAA,MAEX;AAAA,6BAAC,QAAK,UAAQ,MACZ;AAAA,8BAAC,QAAK,OAAM,SAAQ,iBAAG;AAAA,UAAO;AAAA,WAChC;AAAA,QACA,qBAAC,QAAK,UAAQ,MACZ;AAAA,8BAAC,QAAK,OAAM,SAAQ,mBAAK;AAAA,UAAO;AAAA,WAClC;AAAA,QACC,gBACC,qBAAC,QAAK,UAAQ,MACZ;AAAA,8BAAC,QAAK,OAAM,SAAQ,mBAAK;AAAA,UAAO;AAAA,WAClC;AAAA,QAEF,qBAAC,QAAK,UAAQ,MACZ;AAAA,8BAAC,QAAK,OAAM,SAAQ,eAAC;AAAA,UAAO;AAAA,WAC9B;AAAA,QACA,qBAAC,QAAK,UAAQ,MACZ;AAAA,8BAAC,QAAK,OAAM,UAAS,iBAAG;AAAA,UAAO;AAAA,WACjC;AAAA;AAAA;AAAA,EACF;AAEJ;AAMO,IAAM,cAA0C,CAAC;AAAA,EACtD;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,KAAK,IAAI,OAAO;AAGxB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAsB,oBAAI,IAAI,CAAC;AACrE,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,IAAI;AAGtE,QAAM,kBAAkB,QAAQ,MAAM;AACpC,WAAO,OAAO,OAAO,CAAC,UAAU,aAAa,OAAO,KAAK,CAAC;AAAA,EAC5D,GAAG,CAAC,QAAQ,KAAK,CAAC;AAGlB,QAAM,mBAAmB,KAAK;AAAA,IAC5B;AAAA,IACA,KAAK,IAAI,GAAG,gBAAgB,SAAS,CAAC;AAAA,EACxC;AAGA,QAAM,eAAe,gBAAgB,gBAAgB;AAGrD,QAAM,oBAAoB,YAAY,CAAC,aAAqB;AAC1D,aAAS,QAAQ;AACjB,oBAAgB,CAAC;AACjB,oBAAgB,CAAC;AAAA,EACnB,GAAG,CAAC,CAAC;AAGL,QAAM,kBAAkB,YAAY,CAAC,YAAoB;AACvD,mBAAe,CAAC,SAAS;AACvB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,OAAO,GAAG;AACrB,aAAK,OAAO,OAAO;AAAA,MACrB,OAAO;AACL,aAAK,IAAI,OAAO;AAAA,MAClB;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAGL,QAAM,gBAAgB,YAAY,OAAO,UAAwB;AAC/D,UAAM,OAAO,MAAM,YAAY,GAAG,MAAM,SAAS,IAAI,MAAM,EAAE,KAAK,MAAM;AAExE,QAAI;AAGF,YAAM,UAAU,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AACnD,cAAQ,OAAO,MAAM,aAAa,OAAO,MAAM;AAC/C,uBAAiB,WAAW,IAAI,EAAE;AAClC,iBAAW,MAAM,iBAAiB,IAAI,GAAG,GAAI;AAAA,IAC/C,QAAQ;AACN,uBAAiB,SAAS,IAAI,EAAE;AAChC,iBAAW,MAAM,iBAAiB,IAAI,GAAG,GAAI;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,WAAS,CAAC,OAAO,QAAQ;AAEvB,QAAI,IAAI,QAAQ;AACd,eAAS;AACT,WAAK;AACL;AAAA,IACF;AAGA,QAAI,IAAI,QAAQ;AACd,UAAI,YAAY,OAAO,GAAG;AACxB,cAAM,iBAAiB,gBAAgB;AAAA,UAAO,CAAC,MAC7C,YAAY,IAAI,EAAE,EAAE;AAAA,QACtB;AACA,mBAAW;AAAA,UACT;AAAA,UACA,WAAW;AAAA,QACb,CAAC;AACD,aAAK;AAAA,MACP;AACA;AAAA,IACF;AAGA,QAAI,UAAU,OAAO,cAAc;AACjC,sBAAgB,aAAa,EAAE;AAC/B;AAAA,IACF;AAGA,SAAK,UAAU,OAAO,UAAU,QAAQ,cAAc;AACpD,WAAK,cAAc,YAAY;AAC/B;AAAA,IACF;AAGA,UAAM,OAAO,IAAI,WAAW,UAAU;AACtC,UAAM,SAAS,IAAI,aAAa,UAAU;AAE1C,QAAI,QAAQ,mBAAmB,GAAG;AAChC,YAAM,WAAW,mBAAmB;AACpC,sBAAgB,QAAQ;AAExB,UAAI,WAAW,cAAc;AAC3B,wBAAgB,QAAQ;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,UAAU,mBAAmB,gBAAgB,SAAS,GAAG;AAC3D,YAAM,WAAW,mBAAmB;AACpC,sBAAgB,QAAQ;AAExB,UAAI,YAAY,eAAe,qBAAqB;AAClD,wBAAgB,WAAW,sBAAsB,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,CAAC;AAED,SACE,oBAAC,iBAAc,OAAO,UACpB,+BAAC,OAAI,eAAc,UACjB;AAAA,wBAAC,UAAO,aAA0B;AAAA,IAClC,oBAAC,eAAY,OAAO,OAAO,UAAU,mBAAmB;AAAA,IACxD;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT;AAAA,QACA,cAAc;AAAA,QACd;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,aAAa,gBAAgB;AAAA,QAC7B,eAAe,YAAY;AAAA;AAAA,IAC7B;AAAA,IACC,iBACC,oBAAC,OAAI,UAAU,GACb,8BAAC,QAAK,OAAM,SAAS,yBAAc,GACrC;AAAA,IAEF,oBAAC,UAAO,cAAc,YAAY,OAAO,GAAG;AAAA,KAC9C,GACF;AAEJ;","names":[]}