ronin-skillforce 0.0.0-stage → 0.2.0-beta.0

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 (434) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +19 -0
  4. package/README.md +258 -2
  5. package/bin/cli.js +823 -0
  6. package/bin/commands/archive.js +192 -0
  7. package/bin/commands/capstone-status.js +82 -0
  8. package/bin/commands/check.js +298 -0
  9. package/bin/commands/create.js +226 -0
  10. package/bin/commands/doctor.js +392 -0
  11. package/bin/commands/import.js +557 -0
  12. package/bin/commands/next.js +125 -0
  13. package/bin/commands/review.js +207 -0
  14. package/bin/commands/status.js +114 -0
  15. package/bin/commands/studio-autostart.js +414 -0
  16. package/bin/commands/studio-state.js +159 -0
  17. package/bin/commands/studio.js +831 -0
  18. package/bin/commands/trail.js +361 -0
  19. package/bin/commands/web.js +348 -0
  20. package/bin/course-reader.js +602 -0
  21. package/bin/node-version.js +60 -0
  22. package/bin/platforms/_shared.js +337 -0
  23. package/bin/platforms/claude.js +85 -0
  24. package/bin/platforms/cursor.js +52 -0
  25. package/bin/platforms/gemini.js +64 -0
  26. package/bin/platforms/opencode.js +114 -0
  27. package/bin/registry-helpers.js +504 -0
  28. package/bin/static-server.js +153 -0
  29. package/bin/studio-store.js +94 -0
  30. package/bin/sync-shared.sh +5 -0
  31. package/package.json +92 -4
  32. package/plugin.json +139 -0
  33. package/shared/SKILL.md +598 -0
  34. package/shared/agents/coach.md +228 -0
  35. package/shared/agents/navigator.md +208 -0
  36. package/shared/agents/professor.md +354 -0
  37. package/shared/agents/researcher.md +181 -0
  38. package/shared/agents/spotter.md +128 -0
  39. package/shared/commands/professor/archive.md +96 -0
  40. package/shared/commands/professor/capstone-review.md +79 -0
  41. package/shared/commands/professor/capstone.md +34 -0
  42. package/shared/commands/professor/discuss.md +18 -0
  43. package/shared/commands/professor/done.md +72 -0
  44. package/shared/commands/professor/export.md +69 -0
  45. package/shared/commands/professor/hint.md +120 -0
  46. package/shared/commands/professor/navigator.md +93 -0
  47. package/shared/commands/professor/new-topic.md +73 -0
  48. package/shared/commands/professor/next.md +178 -0
  49. package/shared/commands/professor/note.md +63 -0
  50. package/shared/commands/professor/onboard.md +95 -0
  51. package/shared/commands/professor/progress.md +46 -0
  52. package/shared/commands/professor/publish.md +93 -0
  53. package/shared/commands/professor/quiz.md +27 -0
  54. package/shared/commands/professor/recall.md +74 -0
  55. package/shared/commands/professor/review.md +57 -0
  56. package/shared/commands/professor/schedule.md +75 -0
  57. package/shared/commands/professor/skill-export.md +86 -0
  58. package/shared/commands/professor/spotter.md +93 -0
  59. package/shared/commands/professor/stuck.md +51 -0
  60. package/shared/commands/professor/switch.md +50 -0
  61. package/shared/commands/professor/syllabus.md +9 -0
  62. package/shared/commands/professor/template-export.md +91 -0
  63. package/shared/commands/professor/template-import.md +111 -0
  64. package/shared/commands/professor/worktrees.md +54 -0
  65. package/shared/hooks/pre-compact.js +221 -0
  66. package/studio/README.md +313 -0
  67. package/studio/dist/assets/BetaReportPanel-BJ9HIAnz.js +1 -0
  68. package/studio/dist/assets/Capstone-CoUU-jP7.js +6 -0
  69. package/studio/dist/assets/CodeWorkspace-BNrL4qvR.js +2 -0
  70. package/studio/dist/assets/Course-BMGTXus_.js +1 -0
  71. package/studio/dist/assets/Editor-Cw9j3cnp.js +53 -0
  72. package/studio/dist/assets/Home-D2woITq5.js +1 -0
  73. package/studio/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  74. package/studio/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  75. package/studio/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  76. package/studio/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  77. package/studio/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  78. package/studio/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  79. package/studio/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  80. package/studio/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  81. package/studio/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  82. package/studio/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  83. package/studio/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  84. package/studio/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  85. package/studio/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  86. package/studio/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  87. package/studio/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  88. package/studio/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  89. package/studio/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  90. package/studio/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  91. package/studio/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  92. package/studio/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  93. package/studio/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  94. package/studio/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  95. package/studio/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  96. package/studio/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  97. package/studio/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  98. package/studio/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  99. package/studio/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  100. package/studio/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  101. package/studio/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  102. package/studio/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  103. package/studio/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  104. package/studio/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  105. package/studio/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  106. package/studio/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  107. package/studio/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  108. package/studio/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  109. package/studio/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  110. package/studio/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  111. package/studio/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  112. package/studio/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  113. package/studio/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  114. package/studio/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  115. package/studio/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  116. package/studio/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  117. package/studio/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  118. package/studio/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  119. package/studio/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  120. package/studio/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  121. package/studio/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  122. package/studio/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  123. package/studio/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  124. package/studio/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  125. package/studio/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  126. package/studio/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  127. package/studio/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  128. package/studio/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  129. package/studio/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  130. package/studio/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  131. package/studio/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  132. package/studio/dist/assets/Lesson-CYQhrDCZ.js +2 -0
  133. package/studio/dist/assets/NewCourse-CMpwVRQb.js +952 -0
  134. package/studio/dist/assets/Progress-2rS19Fur.js +1 -0
  135. package/studio/dist/assets/Review-SlqK-hjp.js +1 -0
  136. package/studio/dist/assets/Settings-t42u7n9H.js +1 -0
  137. package/studio/dist/assets/TutorPanel-DQDWIU7f.js +123 -0
  138. package/studio/dist/assets/abnfDiagram-WEYOIA3C-jTUmGBuB.js +1 -0
  139. package/studio/dist/assets/anthropic-iIm2l85r.js +15 -0
  140. package/studio/dist/assets/arc-Y1ARJVZ4.js +1 -0
  141. package/studio/dist/assets/architecture-WOLXFQ4H-Ch9Ld_vv.js +1 -0
  142. package/studio/dist/assets/architectureDiagram-ZHVYKT53-Dy-z6wsy.js +36 -0
  143. package/studio/dist/assets/array-BifhSqXX.js +1 -0
  144. package/studio/dist/assets/blockDiagram-RJAE7QA3-2fHVrc6N.js +140 -0
  145. package/studio/dist/assets/bridge-Cr0q4rLp.js +1 -0
  146. package/studio/dist/assets/c4Diagram-XDPPPE54-BuuKlG4V.js +38 -0
  147. package/studio/dist/assets/capstone-DhwBeNqk.js +135 -0
  148. package/studio/dist/assets/card-store-CVMBc-Pv.js +1 -0
  149. package/studio/dist/assets/channel-M_JkdHrL.js +1 -0
  150. package/studio/dist/assets/chunk-2E7WHVMD-lZdQWttk.js +88 -0
  151. package/studio/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  152. package/studio/dist/assets/chunk-4EMNKR2T-C6kbMwke.js +1 -0
  153. package/studio/dist/assets/chunk-55XCU76K-rkBzkZog.js +1 -0
  154. package/studio/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  155. package/studio/dist/assets/chunk-7S6XSTFO-Skg347QF.js +2 -0
  156. package/studio/dist/assets/chunk-AQDTSKZB-0pSDBJ23.js +72 -0
  157. package/studio/dist/assets/chunk-DSNLSHJZ-BaeQDl2i.js +129 -0
  158. package/studio/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  159. package/studio/dist/assets/chunk-HAGFOTED-BEm00juw.js +62 -0
  160. package/studio/dist/assets/chunk-HNE4Q36P-DyCR-LvS.js +1 -0
  161. package/studio/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  162. package/studio/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  163. package/studio/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  164. package/studio/dist/assets/chunk-NQOKRVIF-BNJ9uMmB.js +213 -0
  165. package/studio/dist/assets/chunk-UHW7PFBP-DqJczBLs.js +1 -0
  166. package/studio/dist/assets/chunk-VQ5W62QI-CVWYSP5Q.js +1 -0
  167. package/studio/dist/assets/chunk-WQQL4CDK-DJf0Dx05.js +10 -0
  168. package/studio/dist/assets/chunk-XXDRQBXY-CnbRkih_.js +1 -0
  169. package/studio/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  170. package/studio/dist/assets/chunk-YZO67HFG-BFUm_JHA.js +1 -0
  171. package/studio/dist/assets/chunk-ZAUX2LTX-C5ETkkSE.js +1 -0
  172. package/studio/dist/assets/chunk-ZCHMQSOR-WVX9r9PZ.js +2 -0
  173. package/studio/dist/assets/classDiagram-v2-4MYWR3UA-CI77SpuR.js +217 -0
  174. package/studio/dist/assets/cose-bilkent-JH36ORCC-DMBAZFnj.js +1 -0
  175. package/studio/dist/assets/course-export-C8jSPuda.js +1 -0
  176. package/studio/dist/assets/cynefin-EF2NZ3EQ-CLffmM25.js +1 -0
  177. package/studio/dist/assets/cynefinDiagram-WF24UDRP-BvLwocOb.js +62 -0
  178. package/studio/dist/assets/cytoscape.esm-CHBj4VkZ.js +321 -0
  179. package/studio/dist/assets/dagre-NK4GIZVX-C2nMAVdh.js +4 -0
  180. package/studio/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  181. package/studio/dist/assets/diagram-CBK3MWTC-Bid-pdax.js +24 -0
  182. package/studio/dist/assets/diagram-I5XIIV2D-DYJeAvzl.js +24 -0
  183. package/studio/dist/assets/diagram-ODIL2UCW-B06X2r_F.js +30 -0
  184. package/studio/dist/assets/diagram-PFBEJUXE-CwY--5Rz.js +3 -0
  185. package/studio/dist/assets/diagram-RJA3I4VV-4SqhH8zw.js +41 -0
  186. package/studio/dist/assets/diagram-TYUNFSNR-CrW4AIM9.js +200 -0
  187. package/studio/dist/assets/dist-CumnGVmJ.js +1 -0
  188. package/studio/dist/assets/ebnfDiagram-V4P3TTPT-xeoCmH-d.js +1 -0
  189. package/studio/dist/assets/elk-QONTBCTQ-Dadmz6iS.js +27 -0
  190. package/studio/dist/assets/erDiagram-KYQIGQAA-CSuH0bbF.js +99 -0
  191. package/studio/dist/assets/error-BTdmRKoB.js +1 -0
  192. package/studio/dist/assets/errors-BZa_CNHL.js +1 -0
  193. package/studio/dist/assets/eventmodeling-K75KTNOO-CciVfx3I.js +1 -0
  194. package/studio/dist/assets/fetch-local-DG3O3hmQ.js +4 -0
  195. package/studio/dist/assets/flowDiagram-IUGLLWBU-DgNR54qK.js +1 -0
  196. package/studio/dist/assets/ganttDiagram-R7GPNTJY-C0wtA-2y.js +292 -0
  197. package/studio/dist/assets/gitGraph-VSP46ZUC-BXoDzaA3.js +1 -0
  198. package/studio/dist/assets/gitGraphDiagram-PDYL7XHY-vZBlNRPe.js +106 -0
  199. package/studio/dist/assets/graphlib-DS17s2tU.js +1 -0
  200. package/studio/dist/assets/index-CS8XPYbB.js +313 -0
  201. package/studio/dist/assets/index-CVenTTOY.css +2 -0
  202. package/studio/dist/assets/info-OHQRW6UA-bs2Qpf7P.js +1 -0
  203. package/studio/dist/assets/infoDiagram-MVDRKAX5-rPJio6-1.js +2 -0
  204. package/studio/dist/assets/init-C-OQMol4.js +1 -0
  205. package/studio/dist/assets/ishikawaDiagram-HPEHV2GE-CXf9XFff.js +70 -0
  206. package/studio/dist/assets/journeyDiagram-BOSHW7O7-C3MSH79o.js +139 -0
  207. package/studio/dist/assets/js.worker-AD7DjksO.js +4 -0
  208. package/studio/dist/assets/kanban-definition-FNSA4HC5-Bdh7SB4n.js +89 -0
  209. package/studio/dist/assets/katex-B_JcIwqj.js +257 -0
  210. package/studio/dist/assets/katex-BgsA2lO1.css +1 -0
  211. package/studio/dist/assets/line-CHkoQXJF.js +1 -0
  212. package/studio/dist/assets/linear-C9iwaMOi.js +1 -0
  213. package/studio/dist/assets/mermaid-parser.core-Do1oxD1S.js +7 -0
  214. package/studio/dist/assets/mermaid.core-DgZUD0sU.js +44 -0
  215. package/studio/dist/assets/mindmap-definition-6OY57UG3-PUaK6oE_.js +98 -0
  216. package/studio/dist/assets/node.browser-DkpMze_r.js +1 -0
  217. package/studio/dist/assets/node.browser-HQ5SKBOf.js +1 -0
  218. package/studio/dist/assets/ollama-bJNp3jaD.js +1 -0
  219. package/studio/dist/assets/openai-compatible-0tmAyMlN.js +1 -0
  220. package/studio/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  221. package/studio/dist/assets/packet-JDAUHWVQ-vf2FtqFr.js +1 -0
  222. package/studio/dist/assets/path-fybaL0A-.js +1 -0
  223. package/studio/dist/assets/pegDiagram-ECIUJNBX-NEVrgPTY.js +1 -0
  224. package/studio/dist/assets/pie-XZMESJXO-BIWydVNZ.js +1 -0
  225. package/studio/dist/assets/pieDiagram-GNMPY74T-BI9_u6rT.js +39 -0
  226. package/studio/dist/assets/python.worker-CzTrFdd9.js +24 -0
  227. package/studio/dist/assets/quadrantDiagram-D57K4MC3-ryNpoBC5.js +7 -0
  228. package/studio/dist/assets/radar-ABXABTNO-D8TjnvBB.js +1 -0
  229. package/studio/dist/assets/railroad-I3PHUGI6-BmEDV5kf.js +1 -0
  230. package/studio/dist/assets/railroad-abnf-LNEFI6M7-Cs_sArMc.js +1 -0
  231. package/studio/dist/assets/railroad-ebnf-DWYD2UWJ-DYuCq46d.js +1 -0
  232. package/studio/dist/assets/railroad-peg-JR7BVNR7-BcXSgdMy.js +1 -0
  233. package/studio/dist/assets/railroadDiagram-Y72NRBL7-DBcc8b2c.js +1 -0
  234. package/studio/dist/assets/react-B8SKf8RM.js +1 -0
  235. package/studio/dist/assets/rehype-katex-BfUIq5As.js +259 -0
  236. package/studio/dist/assets/remark-math-XvZOjmDO.js +5 -0
  237. package/studio/dist/assets/requirementDiagram-EQYSW5RA-CVcBgM1L.js +84 -0
  238. package/studio/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  239. package/studio/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  240. package/studio/dist/assets/sankeyDiagram-SY24JHO5-DjZ81hDs.js +40 -0
  241. package/studio/dist/assets/schemas-BOc2YsOP.js +55 -0
  242. package/studio/dist/assets/sequenceDiagram-BLEHNHQU-BNwOa2S7.js +169 -0
  243. package/studio/dist/assets/settings-DhwuyNPU.js +1 -0
  244. package/studio/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  245. package/studio/dist/assets/spec-nAnsSMkw.js +1 -0
  246. package/studio/dist/assets/sql-wasm-C1D_RJuy.wasm +0 -0
  247. package/studio/dist/assets/sql.worker-D7pBHOrq.js +4 -0
  248. package/studio/dist/assets/src-BWF7jMDv.js +1 -0
  249. package/studio/dist/assets/stateDiagram-v2-A6IESKWZ-BFWa5lEx.js +291 -0
  250. package/studio/dist/assets/swimlanes-B3ENRKFQ-LfM_Jg6Z.js +1 -0
  251. package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-BDmpoiGp.js +8 -0
  252. package/studio/dist/assets/timeline-definition-JXYKGLGB-BwGzkWgZ.js +120 -0
  253. package/studio/dist/assets/treeView-D4JQ5SDB-MLk3VuRt.js +1 -0
  254. package/studio/dist/assets/treemap-SAJKECNS-D8iSjp7L.js +1 -0
  255. package/studio/dist/assets/useStream-rdsR6A7P.js +1 -0
  256. package/studio/dist/assets/usecaseDiagram-WPOLBNNX-BqxpaNlT.js +328 -0
  257. package/studio/dist/assets/vennDiagram-Q3Y3IDCW-CpeUDMhQ.js +34 -0
  258. package/studio/dist/assets/wardley-7MLQ67FV-tAMbCZAj.js +1 -0
  259. package/studio/dist/assets/wardleyDiagram-IVAIDKWA-CDJViWzt.js +78 -0
  260. package/studio/dist/assets/xychartDiagram-4URNHDU4-CwaQrd3_.js +7 -0
  261. package/studio/dist/index.html +20 -0
  262. package/studio/dist/preview.html +22 -0
  263. package/studio/migrations/0001_init.sql +188 -0
  264. package/studio/migrations/0002_external_ids.sql +20 -0
  265. package/studio/migrations/0003_course_language.sql +12 -0
  266. package/studio/migrations/0004_capstone.sql +51 -0
  267. package/studio/migrations/0005_beta_loop.sql +73 -0
  268. package/studio/migrations/0006_interactive_exercises.sql +49 -0
  269. package/studio/src/lib/activity.js +92 -0
  270. package/studio/src/lib/ai/anthropic.js +109 -0
  271. package/studio/src/lib/ai/bridge.js +126 -0
  272. package/studio/src/lib/ai/catalog.js +160 -0
  273. package/studio/src/lib/ai/config.js +70 -0
  274. package/studio/src/lib/ai/errors.js +95 -0
  275. package/studio/src/lib/ai/fetch-local.js +104 -0
  276. package/studio/src/lib/ai/index.js +194 -0
  277. package/studio/src/lib/ai/ollama.js +136 -0
  278. package/studio/src/lib/ai/openai-compatible.js +101 -0
  279. package/studio/src/lib/ai/provider-utils.js +107 -0
  280. package/studio/src/lib/ai/sse.js +99 -0
  281. package/studio/src/lib/ai/status.js +92 -0
  282. package/studio/src/lib/ai/useStream.js +109 -0
  283. package/studio/src/lib/beta/metrics.js +147 -0
  284. package/studio/src/lib/beta/report.js +134 -0
  285. package/studio/src/lib/capstone-evidence.js +758 -0
  286. package/studio/src/lib/capstone.js +663 -0
  287. package/studio/src/lib/chat-store.js +128 -0
  288. package/studio/src/lib/course-export.js +38 -0
  289. package/studio/src/lib/course.js +187 -0
  290. package/studio/src/lib/exercises/journal-entry.js +259 -0
  291. package/studio/src/lib/exercises/placement.js +31 -0
  292. package/studio/src/lib/exercises/registry.js +19 -0
  293. package/studio/src/lib/exercises/spec.js +129 -0
  294. package/studio/src/lib/i18n/detect.js +162 -0
  295. package/studio/src/lib/i18n/en/app.json +23 -0
  296. package/studio/src/lib/i18n/en/betaReport.json +55 -0
  297. package/studio/src/lib/i18n/en/capstone.json +57 -0
  298. package/studio/src/lib/i18n/en/common.json +25 -0
  299. package/studio/src/lib/i18n/en/course.json +27 -0
  300. package/studio/src/lib/i18n/en/data.json +35 -0
  301. package/studio/src/lib/i18n/en/durability.json +5 -0
  302. package/studio/src/lib/i18n/en/errors.json +20 -0
  303. package/studio/src/lib/i18n/en/exercises.json +55 -0
  304. package/studio/src/lib/i18n/en/externalEvidence.json +76 -0
  305. package/studio/src/lib/i18n/en/home.json +18 -0
  306. package/studio/src/lib/i18n/en/lesson.json +33 -0
  307. package/studio/src/lib/i18n/en/nav.json +8 -0
  308. package/studio/src/lib/i18n/en/newCourse.json +51 -0
  309. package/studio/src/lib/i18n/en/onboarding.json +35 -0
  310. package/studio/src/lib/i18n/en/playwrightTemplate.json +20 -0
  311. package/studio/src/lib/i18n/en/practice.json +79 -0
  312. package/studio/src/lib/i18n/en/provider.json +27 -0
  313. package/studio/src/lib/i18n/en/providerMeta.json +26 -0
  314. package/studio/src/lib/i18n/en/reflection.json +17 -0
  315. package/studio/src/lib/i18n/en/revision.json +52 -0
  316. package/studio/src/lib/i18n/en/runnerNotice.json +14 -0
  317. package/studio/src/lib/i18n/en/settings.json +12 -0
  318. package/studio/src/lib/i18n/en/storage.json +49 -0
  319. package/studio/src/lib/i18n/en/tutor.json +39 -0
  320. package/studio/src/lib/i18n/en/workspace.json +50 -0
  321. package/studio/src/lib/i18n/index.js +144 -0
  322. package/studio/src/lib/i18n/languages.js +93 -0
  323. package/studio/src/lib/i18n/vi/app.json +23 -0
  324. package/studio/src/lib/i18n/vi/betaReport.json +55 -0
  325. package/studio/src/lib/i18n/vi/capstone.json +57 -0
  326. package/studio/src/lib/i18n/vi/common.json +25 -0
  327. package/studio/src/lib/i18n/vi/course.json +27 -0
  328. package/studio/src/lib/i18n/vi/data.json +35 -0
  329. package/studio/src/lib/i18n/vi/durability.json +5 -0
  330. package/studio/src/lib/i18n/vi/errors.json +20 -0
  331. package/studio/src/lib/i18n/vi/exercises.json +55 -0
  332. package/studio/src/lib/i18n/vi/externalEvidence.json +76 -0
  333. package/studio/src/lib/i18n/vi/home.json +18 -0
  334. package/studio/src/lib/i18n/vi/lesson.json +33 -0
  335. package/studio/src/lib/i18n/vi/nav.json +8 -0
  336. package/studio/src/lib/i18n/vi/newCourse.json +51 -0
  337. package/studio/src/lib/i18n/vi/onboarding.json +35 -0
  338. package/studio/src/lib/i18n/vi/playwrightTemplate.json +20 -0
  339. package/studio/src/lib/i18n/vi/practice.json +79 -0
  340. package/studio/src/lib/i18n/vi/provider.json +27 -0
  341. package/studio/src/lib/i18n/vi/providerMeta.json +26 -0
  342. package/studio/src/lib/i18n/vi/reflection.json +17 -0
  343. package/studio/src/lib/i18n/vi/revision.json +52 -0
  344. package/studio/src/lib/i18n/vi/runnerNotice.json +14 -0
  345. package/studio/src/lib/i18n/vi/settings.json +12 -0
  346. package/studio/src/lib/i18n/vi/storage.json +49 -0
  347. package/studio/src/lib/i18n/vi/tutor.json +39 -0
  348. package/studio/src/lib/i18n/vi/workspace.json +50 -0
  349. package/studio/src/lib/ids.js +10 -0
  350. package/studio/src/lib/json.js +26 -0
  351. package/studio/src/lib/markdown.js +74 -0
  352. package/studio/src/lib/prompts/base.js +139 -0
  353. package/studio/src/lib/prompts/capstone.js +777 -0
  354. package/studio/src/lib/prompts/course.js +150 -0
  355. package/studio/src/lib/prompts/lesson.js +275 -0
  356. package/studio/src/lib/prompts/practice.js +511 -0
  357. package/studio/src/lib/prompts/tutor.js +509 -0
  358. package/studio/src/lib/repo/blocked-adapter.js +69 -0
  359. package/studio/src/lib/repo/events.js +239 -0
  360. package/studio/src/lib/repo/idb-adapter.js +226 -0
  361. package/studio/src/lib/repo/index.js +1315 -0
  362. package/studio/src/lib/repo/memory-adapter.js +115 -0
  363. package/studio/src/lib/repo/remote-adapter.js +158 -0
  364. package/studio/src/lib/repo/schema.js +143 -0
  365. package/studio/src/lib/repo/select-adapter.js +345 -0
  366. package/studio/src/lib/router.js +71 -0
  367. package/studio/src/lib/runner/browser.js +244 -0
  368. package/studio/src/lib/runner/checks.js +248 -0
  369. package/studio/src/lib/runner/current-code.js +98 -0
  370. package/studio/src/lib/runner/esm-cjs.js +109 -0
  371. package/studio/src/lib/runner/hash.js +34 -0
  372. package/studio/src/lib/runner/history.js +48 -0
  373. package/studio/src/lib/runner/index.js +168 -0
  374. package/studio/src/lib/runner/languages.js +258 -0
  375. package/studio/src/lib/runner/local.js +139 -0
  376. package/studio/src/lib/runner/path.js +104 -0
  377. package/studio/src/lib/runner/skeleton.js +315 -0
  378. package/studio/src/lib/runner/strip-types.js +134 -0
  379. package/studio/src/lib/settings.js +399 -0
  380. package/studio/src/lib/srs.js +39 -0
  381. package/templates/agent/settings.json +6 -0
  382. package/templates/claude/plugin.json +139 -0
  383. package/templates/claude/settings.json +15 -0
  384. package/templates/cursor/professor.mdc +4 -0
  385. package/templates/cursor/settings.json +18 -0
  386. package/templates/gemini/GEMINI.md +30 -0
  387. package/templates/gemini/settings.json +5 -0
  388. package/templates/opencode/opencode.json +3 -0
  389. package/templates/researcher/README.md +61 -0
  390. package/templates/researcher/settings.json +18 -0
  391. package/web/client/components.json +15 -0
  392. package/web/client/index.html +12 -0
  393. package/web/client/package-lock.json +8732 -0
  394. package/web/client/package.json +39 -0
  395. package/web/client/postcss.config.js +6 -0
  396. package/web/client/src/App.jsx +140 -0
  397. package/web/client/src/api/client.js +357 -0
  398. package/web/client/src/components/ChatPanel.jsx +417 -0
  399. package/web/client/src/components/CommandPills.jsx +50 -0
  400. package/web/client/src/components/DeprecationBanner.jsx +21 -0
  401. package/web/client/src/components/ExerciseEditor.jsx +164 -0
  402. package/web/client/src/components/Header.jsx +119 -0
  403. package/web/client/src/components/LecturePanel.jsx +342 -0
  404. package/web/client/src/components/MobileTabSwitcher.jsx +51 -0
  405. package/web/client/src/components/SplitPane.jsx +42 -0
  406. package/web/client/src/components/ui/alert.jsx +38 -0
  407. package/web/client/src/components/ui/badge.jsx +24 -0
  408. package/web/client/src/components/ui/button.jsx +23 -0
  409. package/web/client/src/components/ui/select.jsx +74 -0
  410. package/web/client/src/hooks/useSSE.js +152 -0
  411. package/web/client/src/hooks/useWebSocket.js +98 -0
  412. package/web/client/src/index.css +23 -0
  413. package/web/client/src/lib/utils.js +6 -0
  414. package/web/client/src/main.jsx +12 -0
  415. package/web/client/src/utils/markdown.js +50 -0
  416. package/web/client/tailwind.config.js +8 -0
  417. package/web/client/vite.config.js +21 -0
  418. package/web/dist/assets/index-C1W0eXIQ.js +294 -0
  419. package/web/dist/assets/index-C3OUx70T.css +1 -0
  420. package/web/dist/index.html +13 -0
  421. package/web/lecture-archive.js +172 -0
  422. package/web/legacy-server.js +1277 -0
  423. package/web/package-lock.json +855 -0
  424. package/web/package.json +10 -0
  425. package/web/security.js +1016 -0
  426. package/web/server.js +390 -0
  427. package/web/studio-api/agents.js +580 -0
  428. package/web/studio-api/bridge.js +199 -0
  429. package/web/studio-api/db.js +66 -0
  430. package/web/studio-api/guards.js +201 -0
  431. package/web/studio-api/health.js +538 -0
  432. package/web/studio-api.js +214 -0
  433. package/web/studio-db.js +380 -0
  434. package/web/studio-runner.js +568 -0
package/bin/cli.js ADDED
@@ -0,0 +1,823 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Registry helpers (extracted for testability)
4
+ import {
5
+ fetchRegistryText,
6
+ fetchRegistryIndex,
7
+ style,
8
+ validateItemName,
9
+ resolveScope,
10
+ confirmOverwrite,
11
+ installSkill,
12
+ installCourse,
13
+ scanSkillDirs,
14
+ } from './registry-helpers.js';
15
+
16
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, statSync, mkdtempSync } from 'fs';
17
+ import { join, dirname, basename } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import os from 'os';
20
+ import chalk from 'chalk';
21
+ import inquirer from 'inquirer';
22
+ import ora from 'ora';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const SUPPORTED_AGENTS = ['claude', 'gemini', 'opencode', 'cursor'];
26
+
27
+ // ─── Agent / setup helpers ────────────────────────────────────────────────────
28
+
29
+ async function detectAgent() {
30
+ const detected = [];
31
+ for (const name of SUPPORTED_AGENTS) {
32
+ try {
33
+ const { detect } = await import(`./platforms/${name}.js`);
34
+ if (detect()) detected.push(name);
35
+ } catch {}
36
+ }
37
+ return detected;
38
+ }
39
+
40
+ async function promptScope(platform) {
41
+ if (!process.stdin.isTTY) {
42
+ console.error(style('error', 'Non-interactive mode. Run with --local or --global.'));
43
+ process.exit(1);
44
+ }
45
+ const globalPath = {
46
+ claude: '~/.claude/plugins/professor/',
47
+ gemini: '~/.gemini/GEMINI.md',
48
+ opencode: '~/.config/opencode/',
49
+ cursor: '~/.cursor/rules/professor.mdc',
50
+ }[platform] || '~/';
51
+
52
+ const { scope } = await inquirer.prompt([{
53
+ type: 'list',
54
+ name: 'scope',
55
+ message: `Install scope for ${platform}:`,
56
+ choices: [
57
+ { name: `Global — ${globalPath} (available in all projects)`, value: 'global' },
58
+ { name: 'Local — current project only', value: 'local' },
59
+ ],
60
+ default: 'local',
61
+ }]);
62
+ return scope;
63
+ }
64
+
65
+ async function setupAgent(agent) {
66
+ const agentLower = agent.toLowerCase();
67
+ if (!SUPPORTED_AGENTS.includes(agentLower)) {
68
+ console.error(style('error', `Unsupported agent: ${agent}. Supported: ${SUPPORTED_AGENTS.join(', ')}. Run 'skillforce list' for details.`));
69
+ process.exit(1);
70
+ }
71
+ const scope = isGlobal ? 'global' : isLocal ? 'local' : await promptScope(agentLower);
72
+ const { install } = await import(`./platforms/${agentLower}.js`);
73
+ await install(scope);
74
+ }
75
+
76
+ async function promptAgentSelection(agents) {
77
+ if (!process.stdin.isTTY) {
78
+ console.error(style('error', 'Non-interactive mode. Run with: skillforce setup <agent> --local or --global'));
79
+ process.exit(1);
80
+ }
81
+ const { agent } = await inquirer.prompt([{
82
+ type: 'list',
83
+ name: 'agent',
84
+ message: 'Which agent would you like to set up?',
85
+ choices: agents.map((a) => ({ name: a, value: a })),
86
+ }]);
87
+ return agent;
88
+ }
89
+
90
+ async function init() {
91
+ const detected = await detectAgent();
92
+ if (detected.length === 0) {
93
+ console.log(style('info', 'No agent detected automatically.\n'));
94
+ const choice = await promptAgentSelection(SUPPORTED_AGENTS);
95
+ await setupAgent(choice);
96
+ return;
97
+ }
98
+ if (detected.length === 1) {
99
+ console.log(style('info', `Detected: ${detected[0]}\n`));
100
+ await setupAgent(detected[0]);
101
+ return;
102
+ }
103
+ console.log(style('info', `Multiple agents detected: ${detected.join(', ')}\n`));
104
+ const choice = await promptAgentSelection(detected);
105
+ await setupAgent(choice);
106
+ }
107
+
108
+ // ─── CLI entry point ──────────────────────────────────────────────────────────
109
+
110
+ const args = process.argv.slice(2);
111
+ const isGlobal = args.includes('--global');
112
+ const isLocal = args.includes('--local');
113
+ const isForce = args.includes('--force');
114
+ const command = args[0] || 'help';
115
+
116
+ // Registry commands are beta until they work end to end. One notice, one place: printed to stderr
117
+ // so `--json` output stays machine-clean.
118
+ const BETA_COMMANDS = new Set(['courses', 'search', 'install', 'update', 'installed', 'remove', 'publish']);
119
+ if (BETA_COMMANDS.has(command)) {
120
+ console.error(chalk.dim('[beta] Registry commands are in beta and may not work as expected.'));
121
+ }
122
+
123
+ function warnBrokenLink(name) {
124
+ console.error(chalk.dim(`skipped ${name}: broken link`));
125
+ }
126
+
127
+ switch (command) {
128
+
129
+ case 'web': {
130
+ const { run } = await import('./commands/web.js');
131
+ await run({ args: args.slice(1), cwd: process.cwd(), chalk, style });
132
+ break;
133
+ }
134
+
135
+ case 'init':
136
+ case 'setup': {
137
+ printBanner();
138
+ console.log('');
139
+ const agentArg = args.slice(1).find((a) => !a.startsWith('--'));
140
+ if (agentArg) {
141
+ await setupAgent(agentArg);
142
+ } else {
143
+ await init();
144
+ }
145
+ console.log(
146
+ chalk.dim('\n Studio: npx ronin-skillforce studio (Studio app + local bridge, Node 22.5+)')
147
+ );
148
+ break;
149
+ }
150
+
151
+ case 'courses': {
152
+ const spinner = ora('Fetching registry…').start();
153
+ try {
154
+ const index = await fetchRegistryIndex();
155
+ spinner.stop();
156
+ const courses = index.courses || [];
157
+ if (courses.length === 0) { console.log('No courses found in registry.'); break; }
158
+ const nw = Math.max(4, ...courses.map((c) => c.name.length));
159
+ const tw = Math.max(5, ...courses.map((c) => c.title.length));
160
+ const lw = Math.max(5, ...courses.map((c) => (c.level || '').length));
161
+ const header = `${'Name'.padEnd(nw)} ${'Title'.padEnd(tw)} ${'Level'.padEnd(lw)} Sections`;
162
+ console.log('\n' + chalk.bold(header));
163
+ console.log(chalk.dim('─'.repeat(header.length)));
164
+ for (const c of courses) {
165
+ console.log(`${c.name.padEnd(nw)} ${c.title.padEnd(tw)} ${(c.level || '').padEnd(lw)} ${c.sections}`);
166
+ }
167
+ console.log('');
168
+ } catch {
169
+ spinner.fail('Registry fetch failed.');
170
+ }
171
+ break;
172
+ }
173
+
174
+ case 'search': {
175
+ const keyword = args.slice(1).find((a) => !a.startsWith('--'));
176
+ if (!keyword) {
177
+ console.error(style('error', 'Usage: skillforce search <keyword>'));
178
+ process.exit(1);
179
+ }
180
+ const spinner = ora('Searching registry…').start();
181
+ try {
182
+ const index = await fetchRegistryIndex();
183
+ spinner.stop();
184
+ const q = keyword.toLowerCase();
185
+ const matches = (index.courses || []).filter(
186
+ (c) =>
187
+ c.name.toLowerCase().includes(q) ||
188
+ (c.title || '').toLowerCase().includes(q) ||
189
+ (c.description || '').toLowerCase().includes(q) ||
190
+ (c.topics || []).some((t) => t.toLowerCase().includes(q))
191
+ );
192
+ if (matches.length === 0) { console.log(`No courses found matching "${keyword}".`); break; }
193
+ for (const c of matches) {
194
+ console.log(`\n${chalk.bold(c.name)} ${chalk.dim(`(${c.level}, ${c.sections} sections)`)}`);
195
+ console.log(` ${c.title}`);
196
+ console.log(` ${chalk.dim(c.description)}`);
197
+ }
198
+ console.log('');
199
+ } catch {
200
+ spinner.fail('Registry fetch failed.');
201
+ }
202
+ break;
203
+ }
204
+
205
+ case 'install': {
206
+ const isSkill = args.includes('--skill');
207
+ const noImport = args.includes('--no-import');
208
+ const itemName = args.slice(1).find((a) => !a.startsWith('--'));
209
+ if (!itemName) {
210
+ console.error(style('error', 'Usage: skillforce install <name>'));
211
+ console.error(' skillforce install --skill <name> — install a skill instead');
212
+ console.error(' skillforce courses — list available courses');
213
+ process.exit(1);
214
+ }
215
+ validateItemName(itemName);
216
+
217
+ const index = await fetchRegistryIndex();
218
+ if (isSkill) {
219
+ const scope = await resolveScope(itemName, 'skill', { isGlobal, isLocal });
220
+ await installSkill(itemName, scope, { force: isForce, index });
221
+ } else {
222
+ await installCourse(itemName, { force: isForce, index, importToStudio: !noImport });
223
+ }
224
+ break;
225
+ }
226
+
227
+ // ── TASK 2: new commands ─────────────────────────────────────────────────
228
+
229
+ case 'update': {
230
+ const isUpdateAll = args.includes('--all');
231
+ const itemName = !isUpdateAll ? args.slice(1).find((a) => !a.startsWith('--')) : null;
232
+
233
+ if (isUpdateAll) {
234
+ // Update all installed skills
235
+ const spinner = ora('Fetching registry…').start();
236
+ let index;
237
+ try {
238
+ index = await fetchRegistryIndex();
239
+ } catch {
240
+ spinner.fail('Registry fetch failed.');
241
+ process.exit(1);
242
+ }
243
+ spinner.stop();
244
+
245
+ const localDir = join(process.cwd(), '.claude', 'skills');
246
+ const globalDir = join(os.homedir(), '.claude', 'skills');
247
+ const scopes = isGlobal ? [['global', globalDir]] :
248
+ isLocal ? [['local', localDir]] :
249
+ [['local', localDir], ['global', globalDir]];
250
+
251
+ const results = { updated: 0, skipped: 0, failed: 0 };
252
+ for (const [scope, dir] of scopes) {
253
+ for (const skillDir of scanSkillDirs(dir, { onSkip: warnBrokenLink })) {
254
+ const name = basename(skillDir);
255
+ try {
256
+ const skill = (index.skills || []).find((s) => s.name === name);
257
+ if (!skill) {
258
+ console.log(style('warning', `Skipped ${name}: not found in registry.`));
259
+ results.skipped++;
260
+ continue;
261
+ }
262
+ await installSkill(name, scope, { force: true, index });
263
+ results.updated++;
264
+ } catch (err) {
265
+ console.log(style('warning', `Failed ${name}: ${err.message}`));
266
+ results.failed++;
267
+ }
268
+ }
269
+ }
270
+ console.log(style('info', `Updated: ${results.updated} | Skipped: ${results.skipped} | Failed: ${results.failed}`));
271
+ if (results.updated === 0) {
272
+ console.error(style('error', 'No skills updated — all were manually installed or registry unavailable.'));
273
+ process.exit(1);
274
+ }
275
+ } else {
276
+ // Update a single skill
277
+ if (!itemName) {
278
+ console.error(style('error', 'Usage: skillforce update <skill-name>'));
279
+ process.exit(1);
280
+ }
281
+ validateItemName(itemName);
282
+ const scope = await resolveScope(itemName, 'skill', { isGlobal, isLocal });
283
+
284
+ const destDir = scope === 'global'
285
+ ? join(os.homedir(), '.claude', 'skills', itemName)
286
+ : join(process.cwd(), '.claude', 'skills', itemName);
287
+
288
+ if (!existsSync(join(destDir, 'SKILL.md'))) {
289
+ console.error(style('error', `Skill "${itemName}" not installed. Run: skillforce install --skill ${itemName}`));
290
+ process.exit(1);
291
+ }
292
+
293
+ const spinner = ora('Fetching registry…').start();
294
+ let index;
295
+ try {
296
+ index = await fetchRegistryIndex();
297
+ } catch {
298
+ spinner.fail('Registry fetch failed.');
299
+ process.exit(1);
300
+ }
301
+ spinner.stop();
302
+
303
+ await installSkill(itemName, scope, { force: true, index });
304
+ }
305
+ break;
306
+ }
307
+
308
+ case 'installed': {
309
+ const localDir = join(process.cwd(), '.claude', 'skills');
310
+ const globalDir = join(os.homedir(), '.claude', 'skills');
311
+
312
+ /** @type {{ name: string; title: string; version: string; scope: string; path: string }[]} */
313
+ const rows = [];
314
+
315
+ for (const [scope, dir] of [['local', localDir], ['global', globalDir]]) {
316
+ for (const skillDir of scanSkillDirs(dir, { onSkip: warnBrokenLink })) {
317
+ const name = basename(skillDir);
318
+ let title = name, version = '—';
319
+ const metaPath = join(skillDir, 'meta.json');
320
+ if (existsSync(metaPath)) {
321
+ try {
322
+ const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
323
+ title = meta.title || name;
324
+ version = meta.version || '—';
325
+ } catch {}
326
+ }
327
+ rows.push({ name, title, version, scope, path: skillDir });
328
+ }
329
+ }
330
+
331
+ if (rows.length === 0) {
332
+ console.log(style('info', 'No skills installed. Run: skillforce search'));
333
+ break;
334
+ }
335
+
336
+ const nw = Math.max(4, ...rows.map((r) => r.name.length));
337
+ const tw = Math.max(5, ...rows.map((r) => r.title.length));
338
+ const vw = Math.max(7, ...rows.map((r) => r.version.length));
339
+ const sw = Math.max(7, ...rows.map((r) => r.scope.length));
340
+
341
+ const header = `${'Name'.padEnd(nw)} ${'Title'.padEnd(tw)} ${'Ver'.padEnd(vw)} ${'Scope'.padEnd(sw)} Path`;
342
+ console.log('\n' + chalk.bold(header));
343
+ console.log(chalk.dim('─'.repeat(header.length + 30)));
344
+ for (const r of rows) {
345
+ console.log(
346
+ `${r.name.padEnd(nw)} ${r.title.padEnd(tw)} ${r.version.padEnd(vw)} ${r.scope.padEnd(sw)} ${r.path}`
347
+ );
348
+ }
349
+ console.log('');
350
+ break;
351
+ }
352
+
353
+ case 'remove': {
354
+ const itemName = args.slice(1).find((a) => !a.startsWith('--'));
355
+ if (!itemName) {
356
+ console.error(style('error', 'Usage: skillforce remove <skill-name> [--local|--global] [--force]'));
357
+ process.exit(1);
358
+ }
359
+ validateItemName(itemName);
360
+
361
+ const localDir = join(process.cwd(), '.claude', 'skills', itemName);
362
+ const globalDir = join(os.homedir(), '.claude', 'skills', itemName);
363
+ const localExists = existsSync(join(localDir, 'SKILL.md'));
364
+ const globalExists = existsSync(join(globalDir, 'SKILL.md'));
365
+
366
+ if (!localExists && !globalExists) {
367
+ console.error(style('error', `Skill "${itemName}" not found.`));
368
+ process.exit(1);
369
+ }
370
+
371
+ // Resolve scope
372
+ let removeScopes = [];
373
+ if (isGlobal) {
374
+ removeScopes = ['global'];
375
+ } else if (isLocal) {
376
+ removeScopes = ['local'];
377
+ } else if (localExists && globalExists) {
378
+ // Both — must choose
379
+ if (!process.stdin.isTTY) {
380
+ console.error(style('error', 'Skill found in both scopes. Use --local, --global, or --force --local/--global.'));
381
+ process.exit(1);
382
+ }
383
+ const { choice } = await inquirer.prompt([{
384
+ type: 'list',
385
+ name: 'choice',
386
+ message: `Skill "${itemName}" is installed in both scopes. Which to remove?`,
387
+ choices: [
388
+ { name: `Local (${localDir})`, value: 'local' },
389
+ { name: `Global (${globalDir})`, value: 'global' },
390
+ { name: 'Both', value: 'both' },
391
+ ],
392
+ }]);
393
+ removeScopes = choice === 'both' ? ['local', 'global'] : [choice];
394
+ } else {
395
+ removeScopes = [localExists ? 'local' : 'global'];
396
+ }
397
+
398
+ // --force without scope when in both = error
399
+ if (isForce && !isGlobal && !isLocal && localExists && globalExists) {
400
+ // Already handled above — but double-check
401
+ if (removeScopes.length > 1 && !removeScopes.includes('local') && !removeScopes.includes('global')) {
402
+ console.error(style('error', 'Skill found in both scopes. Use --local, --global, or --force --local/--global.'));
403
+ process.exit(1);
404
+ }
405
+ }
406
+
407
+ // Confirm (unless --force)
408
+ if (!isForce) {
409
+ const scopeLabels = removeScopes.map((s) =>
410
+ s === 'local' ? localDir : globalDir
411
+ ).join(' and ');
412
+ const { confirmed } = await inquirer.prompt([{
413
+ type: 'confirm',
414
+ name: 'confirmed',
415
+ message: `Remove skill "${itemName}" from ${scopeLabels}? (y/N)`,
416
+ default: false,
417
+ }]);
418
+ if (!confirmed) {
419
+ console.log('Aborted.');
420
+ process.exit(0);
421
+ }
422
+ }
423
+
424
+ for (const scope of removeScopes) {
425
+ const dir = scope === 'local' ? localDir : globalDir;
426
+ rmSync(dir, { recursive: true, force: true });
427
+ console.log(style('success', `Removed ${itemName} from ${dir}`));
428
+ }
429
+ break;
430
+ }
431
+
432
+ case 'publish': {
433
+ const {
434
+ validateItemName,
435
+ validateSourceCourse,
436
+ validateSourceSkill,
437
+ buildStagingDir,
438
+ buildCourseMetaJson,
439
+ buildSkillMetaJson,
440
+ generateBrowserFallbackUrl,
441
+ style,
442
+ } = await import('./registry-helpers.js');
443
+
444
+ const { execSync } = await import('child_process');
445
+
446
+ // Parse args
447
+ const isDryRun = args.includes('--dry-run');
448
+ const isForce = args.includes('--force');
449
+ const typeIdx = args.indexOf('--type');
450
+ const typeArg = typeIdx !== -1 ? args[typeIdx + 1] : null;
451
+ const pathIdx = args.indexOf('--path');
452
+ const pathArg = pathIdx !== -1 ? args[pathIdx + 1] : null;
453
+ const nameArg = args.find((a) => a.startsWith('--name='))?.split('=')[1];
454
+
455
+ // Parse metadata flags (collected interactively in professor:publish, passed through to CLI)
456
+ const authorArg = args.find((a) => a.startsWith('--author='))?.split('=')[1];
457
+ const titleArg = args.find((a) => a.startsWith('--title='))?.split('=')[1];
458
+ const descArg = args.find((a) => a.startsWith('--description='))?.split('=')[1];
459
+ const topicsArg = args.find((a) => a.startsWith('--topics='))?.split('=')[1];
460
+ const levelArg = args.find((a) => a.startsWith('--level='))?.split('=')[1];
461
+ const originCourseArg = args.find((a) => a.startsWith('--origin-course='))?.split('=')[1];
462
+
463
+ const sourceDir = pathArg || process.cwd();
464
+ const slug = nameArg || basename(sourceDir);
465
+
466
+ // Guard: validate slug before use
467
+ validateItemName(slug);
468
+ // Sanitize slug to [a-z0-9-] for safe use in shell commands and branch names
469
+ const safeSlug = slug.replace(/[^a-z0-9-]/gi, '-');
470
+
471
+ // Auto-detect type
472
+ let contentType = typeArg;
473
+ if (!contentType) {
474
+ const hasSkill = existsSync(join(sourceDir, 'SKILL.md')) && existsSync(join(sourceDir, 'COMPLETION.md'));
475
+ const hasCourse = existsSync(join(sourceDir, 'COURSE.md'));
476
+ if (hasSkill && hasCourse) {
477
+ console.error(style('error', 'Both course and skill detected. Use --type course|skill to specify.'));
478
+ process.exit(1);
479
+ }
480
+ contentType = hasSkill ? 'skill' : hasCourse ? 'course' : null;
481
+ if (!contentType) {
482
+ console.error(style('error', 'No COURSE.md or SKILL.md found. Run from a course directory.'));
483
+ process.exit(1);
484
+ }
485
+ }
486
+ if (contentType !== 'course' && contentType !== 'skill') {
487
+ console.error(style('error', '--type must be "course" or "skill".'));
488
+ process.exit(1);
489
+ }
490
+
491
+ // Validate source
492
+ const validator = contentType === 'course' ? validateSourceCourse(sourceDir) : validateSourceSkill(sourceDir);
493
+ if (!validator.valid) {
494
+ for (const err of validator.errors) console.error(style('error', err));
495
+ process.exit(1);
496
+ }
497
+
498
+ // Build metadata from flags (collected interactively by professor:publish)
499
+ const metaOverrides = contentType === 'course'
500
+ ? { name: safeSlug, title: titleArg, description: descArg, author: authorArg, level: levelArg, topics: topicsArg?.split(',') }
501
+ : { name: safeSlug, title: titleArg, description: descArg, author: authorArg, origin_course: originCourseArg, topics: topicsArg?.split(',') };
502
+ const builtMeta = contentType === 'course'
503
+ ? buildCourseMetaJson(sourceDir, metaOverrides)
504
+ : buildSkillMetaJson(sourceDir, metaOverrides);
505
+
506
+ // Build staging dir with populated metadata
507
+ const stagingDir = buildStagingDir(contentType, safeSlug, sourceDir, builtMeta);
508
+
509
+ if (isDryRun) {
510
+ console.log(style('info', `Dry run — packaged files at: ${stagingDir}`));
511
+ console.log(style('info', 'Contents:'));
512
+ for (const f of readdirSync(stagingDir)) console.log(` - ${f}`);
513
+ process.exit(0);
514
+ }
515
+
516
+ // gh availability check
517
+ const ghAvailable = (() => {
518
+ try {
519
+ execSync('gh --version', { stdio: 'ignore' });
520
+ return true;
521
+ } catch { return false; }
522
+ })();
523
+
524
+ if (!ghAvailable) {
525
+ console.log(style('warning', 'gh CLI not found — manual submission required.'));
526
+ console.log(style('info', 'Steps to submit manually:'));
527
+ console.log(` 1. Fork: https://github.com/professor-skills-hub/courses-skills-registry`);
528
+ console.log(` 2. Create branch: contribute/${contentType}/${safeSlug}`);
529
+ console.log(` 3. Upload files from: ${stagingDir}`);
530
+ console.log(` 4. Open a PR to main`);
531
+ console.log(style('info', 'Install gh: https://cli.github.com'));
532
+ process.exit(0);
533
+ }
534
+
535
+ // gh auth check
536
+ try { execSync('gh auth status', { stdio: 'ignore' }); } catch {
537
+ console.log(style('warning', 'gh not authenticated — manual submission required.'));
538
+ console.log(style('info', 'Steps to submit manually:'));
539
+ console.log(` 1. Fork: https://github.com/professor-skills-hub/courses-skills-registry`);
540
+ console.log(` 2. Create branch: contribute/${contentType}/${safeSlug}`);
541
+ console.log(` 3. Upload files from: ${stagingDir}`);
542
+ console.log(` 4. Open a PR to main`);
543
+ console.log(style('info', 'Run: gh auth login'));
544
+ process.exit(0);
545
+ }
546
+
547
+ // Setup git credentials (Eng Decision 2 — use gh auth setup-git, not token URL injection)
548
+ try {
549
+ execSync('gh auth setup-git', { stdio: 'pipe' });
550
+ } catch {
551
+ console.error(style('error', 'gh auth setup-git failed. Run gh auth login first.'));
552
+ process.exit(1);
553
+ }
554
+
555
+ // Fork (ignore non-zero — fork may already exist from a prior publish run)
556
+ const forkSpinner = ora('Forking registry…').start();
557
+ try {
558
+ execSync('gh repo fork professor-skills-hub/courses-skills-registry --clone=false', { stdio: 'pipe' });
559
+ } catch {
560
+ // Fork already exists or transient — continue; clone will fail if truly broken
561
+ }
562
+ forkSpinner.succeed('Fork ready');
563
+
564
+ // Resolve fork URL via GitHub API (not local git remote — that points to the user's own repo)
565
+ let forkUrl;
566
+ try {
567
+ const login = execSync('gh api user --jq .login', { encoding: 'utf-8', stdio: 'pipe' }).trim();
568
+ forkUrl = `https://github.com/${login}/courses-skills-registry.git`;
569
+ } catch {
570
+ console.error(style('error', 'Could not resolve GitHub username. Run: gh auth login'));
571
+ process.exit(1);
572
+ }
573
+
574
+ const tmpDir = mkdtempSync(join(os.tmpdir(), 'professor-publish-'));
575
+ const repoDir = join(tmpDir, 'repo');
576
+ const cloneSpinner = ora('Cloning fork…').start();
577
+ try {
578
+ execSync(`git clone --depth=1 "${forkUrl}" "${repoDir}"`, { stdio: 'pipe' });
579
+ cloneSpinner.succeed();
580
+ } catch (err) {
581
+ cloneSpinner.fail();
582
+ console.error(style('error', `Clone failed: ${err.message}`));
583
+ rmSync(tmpDir, { recursive: true, force: true });
584
+ process.exit(1);
585
+ }
586
+
587
+ // Copy files to destination dir
588
+ const destDir = join(repoDir, contentType === 'course' ? 'courses' : 'skills', safeSlug);
589
+ mkdirSync(destDir, { recursive: true });
590
+ for (const f of readdirSync(stagingDir)) {
591
+ const src = join(stagingDir, f);
592
+ if (statSync(src).isFile()) {
593
+ writeFileSync(join(destDir, f), readFileSync(src));
594
+ }
595
+ }
596
+
597
+ // Branch, commit, push
598
+ const branch = `contribute/${contentType}/${safeSlug}`;
599
+ const commitMsg = `Add ${contentType}: ${safeSlug}`;
600
+ try {
601
+ execSync(`git checkout -b "${branch}"`, { cwd: repoDir, stdio: 'pipe' });
602
+ execSync(`git add . && git commit -m "${commitMsg}"`, { cwd: repoDir, stdio: 'pipe' });
603
+ const pushFlags = isForce ? '-f' : '';
604
+ execSync(`git push origin "${branch}" ${pushFlags}`.trim(), { cwd: repoDir, stdio: 'pipe' });
605
+ } catch (err) {
606
+ console.error(style('error', `Git push failed: ${err.message}`));
607
+ rmSync(tmpDir, { recursive: true, force: true });
608
+ process.exit(1);
609
+ }
610
+
611
+ // Create PR against the upstream registry (not the fork's own main)
612
+ const prSpinner = ora('Creating PR…').start();
613
+ let prUrl;
614
+ try {
615
+ const prOutput = execSync(
616
+ `gh pr create --json url --jq .url --repo professor-skills-hub/courses-skills-registry --title "${commitMsg}" --body "Published via skillforce publish."`,
617
+ { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' }
618
+ );
619
+ prUrl = prOutput.trim();
620
+ if (!prUrl.startsWith('https://')) throw new Error(`Unexpected gh output: ${prUrl}`);
621
+ prSpinner.succeed();
622
+ console.log(style('success', `PR created: ${prUrl}`));
623
+ } catch (err) {
624
+ // Check if a PR already exists for this branch (gh exits non-zero with existing URL in stderr)
625
+ const stderr = err.stderr || '';
626
+ const existingMatch = stderr.match(/https:\/\/github\.com\/[^\s]+/);
627
+ if (existingMatch) {
628
+ prSpinner.succeed('PR already open');
629
+ console.log(style('success', `Existing PR: ${existingMatch[0]}`));
630
+ prUrl = existingMatch[0];
631
+ } else if (err.message?.includes('Unexpected gh output')) {
632
+ // gh returned something unexpected — PR may still have been created
633
+ prSpinner.fail();
634
+ console.log(style('warning', `gh returned unexpected output: ${err.message}`));
635
+ console.log(style('info', 'PR may have been created — check: https://github.com/professor-skills-hub/courses-skills-registry/pulls'));
636
+ rmSync(tmpDir, { recursive: true, force: true });
637
+ process.exit(0);
638
+ } else {
639
+ prSpinner.fail();
640
+ console.error(style('error', `PR creation failed: ${err.message}`));
641
+ rmSync(tmpDir, { recursive: true, force: true });
642
+ process.exit(1);
643
+ }
644
+ }
645
+
646
+ // Cleanup temp clone (staging dir kept intentionally — user can inspect submitted files)
647
+ rmSync(tmpDir, { recursive: true, force: true });
648
+ console.log(style('info', `Submitted files kept at: ${stagingDir}`));
649
+ process.exit(0);
650
+ }
651
+
652
+ // ── Dev CLI fast-path (Phase 30) ─────────────────────────────────────────
653
+ // `status`, `next`, `check` and `doctor` live in bin/commands/* and keep
654
+ // cli.js thin — each case just delegates with the parsed args.
655
+
656
+ case 'status':
657
+ case 'next':
658
+ case 'check':
659
+ case 'doctor':
660
+ case 'studio':
661
+ case 'review':
662
+ case 'import':
663
+ case 'create':
664
+ case 'save':
665
+ case 'archive':
666
+ case 'context':
667
+ case 'trail':
668
+ case 'verdict':
669
+ case 'capstone-status': {
670
+ const file = { save: 'create', context: 'trail', verdict: 'trail' }[command] || command;
671
+ const { run } = await import(`./commands/${file}.js`);
672
+ const code = await run({ args: args.slice(1), cwd: process.cwd(), chalk, style, command });
673
+ if (typeof code === 'number' && code !== 0) process.exit(code);
674
+ break;
675
+ }
676
+
677
+ case 'list':
678
+ console.log('\nSupported agents:\n');
679
+ SUPPORTED_AGENTS.forEach((agent, i) => {
680
+ console.log(` ${i + 1}. ${agent}`);
681
+ });
682
+ console.log('\nUse skillforce setup <agent> to install.\n');
683
+ break;
684
+
685
+ case 'help':
686
+ default:
687
+ printBanner();
688
+ console.log('');
689
+ printUsage();
690
+ break;
691
+ }
692
+
693
+ // ─── UI helpers (used by switch cases above) ─────────────────────────────────
694
+
695
+ function printBanner() {
696
+ const c = chalk.cyan;
697
+ const art = `
698
+ _____ _______ _____
699
+ /\\ \\ /::\\ \\ /\\ \\
700
+ /::\\ \\ /::::\\ \\ /::\\ \\
701
+ /::::\\ \\ /::::::\\ \\ /::::\\ \\
702
+ /::::::\\ \\ /::::::::\\ \\ /::::::\\ \\
703
+ /:::/\\:::\\ \\ /:::/~~\\:::\\ \\ /:::/\\:::\\ \\
704
+ /:::/__\\:::\\ \\ /:::/ \\:::\\ \\ /:::/__\\:::\\ \\
705
+ \\:::\\ \\:::\\ \\ /:::/ / \\:::\\ \\ /::::\\ \\:::\\ \\
706
+ ___\\:::\\ \\:::\\ \\ /:::/____/ \\:::\\____\\ /::::::\\ \\:::\\ \\
707
+ /\\ \\:::\\ \\:::\\ \\ |:::| | |:::| | /:::/\\:::\\ \\:::\\ ___\\
708
+ /::\\ \\:::\\ \\:::\\____\\|:::|____| |:::|____|/:::/__\\:::\\ \\:::| |
709
+ \\:::\\ \\:::\\ \\::/ / \\:::\\ _\\___/:::/ / \\:::\\ \\:::\\ /:::|____|
710
+ \\:::\\ \\:::\\ \\/____/ \\:::\\ |::| /:::/ / \\:::\\ \\:::\\/:::/ /
711
+ \\:::\\ \\:::\\ \\ \\:::\\|::|/:::/ / \\:::\\ \\::::::/ /
712
+ \\:::\\ \\:::\\____\\ \\::::::::::/ / \\:::\\ \\::::/ /
713
+ \\:::\\ /:::/ / \\::::::::/ / \\:::\\ /:::/ /
714
+ \\:::\\/:::/ / \\::::::/ / \\:::\\/:::/ /
715
+ \\::::::/ / \\::::/____/ \\::::::/ /
716
+ \\::::/ / |::| | \\::::/ /
717
+ \\::/____/ |::|____| \\::/____/
718
+ ~~ ~~ ~~`;
719
+ const line = chalk.dim('─'.repeat(52));
720
+ console.log(
721
+ '\n' + line + '\n' + c(art.trimEnd()) + '\n\n' + chalk.cyan.dim(' ◆ skill quest builder ◆') + '\n' + line + '\n'
722
+ );
723
+ }
724
+
725
+ /** One REGISTRY (BETA) row, padded to a fixed description column. */
726
+ function registryRow(command, description) {
727
+ const pad = ' '.repeat(Math.max(1, 23 - command.length));
728
+ return ` ${chalk.bold(command)}${pad}${description}`;
729
+ }
730
+
731
+ function printUsage() {
732
+ console.log(`
733
+ Usage: skillforce [command] [options]
734
+
735
+ Description:
736
+ Socratic learning assistant — teaches by asking questions. Set up for your
737
+ agent (Claude Code, Gemini, OpenCode, Cursor), then start Professor Studio
738
+ with npx ronin-skillforce studio (Node 22.5+).
739
+
740
+ COMMANDS
741
+ ${chalk.bold('status')} [--json] Show every course: progress, current section, due cards, streak
742
+ ${chalk.bold('next')} [slug] Show the active lecture summary and how to continue
743
+ ${chalk.bold('check')} [slug] Run the active exercise's self-checks
744
+ ${chalk.bold('create')} <slug> --kind K Create a course folder (programming = verified git worktree)
745
+ ${chalk.bold('save')} <slug> [-m msg] Commit a programming course's worktree
746
+ ${chalk.bold('archive')} <slug> [--json] Copy a course to .course_archive/ and keep its branch
747
+ ${chalk.bold('context')} [slug] --json Course context for Coach (path, section, exercise, trail)
748
+ ${chalk.bold('trail')} <slug> --learner .. --asked .. --concept .. Record a Coach review round
749
+ ${chalk.bold('verdict')} <slug> --stdin Record a Coach done-gate verdict (section entry + watch flag)
750
+ ${chalk.bold('capstone-status')} <slug> --set locked|active|done Update the capstone header and row
751
+ ${chalk.bold('doctor')} Check the local environment (Node, git, agents, Ollama, port, DB)
752
+ ${chalk.bold('studio')} Start local server + Studio, pair and open the browser (Node 22.5+)
753
+ ${chalk.bold('review')} [--limit N] Review due flashcards in the terminal (SM-2) (Node 22.5+)
754
+ ${chalk.bold('import')} [slug|--all] Import CLI courses into Studio's SQLite database (Node 22.5+)
755
+ ${chalk.bold('setup')} [agent] Set up Professor for a specific agent
756
+ ${chalk.bold('init')} Auto-detect and set up
757
+ ${chalk.bold('list')} List supported agents
758
+ ${chalk.bold('web')} [port] [--production] Studio bridge + built Studio app (default port 3000);
759
+ --production requires an existing Studio build
760
+ ${chalk.bold('help')} Show this help message
761
+
762
+ REGISTRY (BETA)
763
+ Community registry commands — not reliable yet, expect rough edges.
764
+ ${registryRow('courses', 'List all courses in the registry')}
765
+ ${registryRow('search <keyword>', 'Search registry courses by keyword')}
766
+ ${registryRow('install <name>', 'Install a course locally (→ learning/{name}/)')}
767
+ ${registryRow('install --skill <name>', 'Install a skill locally (→ .claude/skills/{name}/)')}
768
+ ${registryRow('update <skill-name>', 'Update an installed skill to the latest version')}
769
+ ${registryRow('update --all', 'Update all installed skills')}
770
+ ${registryRow('installed', 'List all installed skills')}
771
+ ${registryRow('remove <skill-name>', 'Remove an installed skill')}
772
+ ${registryRow('publish [options]', 'Publish a course or skill to the community registry')}
773
+
774
+ FLAGS
775
+ --global Target global scope (user-wide, e.g. ~/.claude/skills/)
776
+ --local Target local scope (current project, .claude/skills/)
777
+ --force Bypass overwrite/removal confirmation prompts
778
+ --all Apply to all items (update --all)
779
+ --no-import For \`install <course>\`, skip the automatic import into Studio
780
+ --cloud Deprecated: legacy only — no effect without web --legacy
781
+ --legacy Deprecated: mount the legacy web UI in web (or set PROFESSOR_LEGACY=1)
782
+ --rotate-token For web/studio, generate a new Studio pairing token and exit
783
+ --port <n> For \`studio\`, the Studio port (default 5180)
784
+ --api-port <n> For \`studio\`, the local server port (default 3000)
785
+ --no-open For \`studio\`, do not open the browser
786
+ --strict-port For \`studio\`, never fall back to another port when one is busy
787
+ --autostart on|off For \`studio\`, start at login with a macOS LaunchAgent (global install only)
788
+ --status For \`studio\`, show whether Studio is running, its URLs and the log path
789
+ --stop For \`studio\`, stop the running Studio
790
+ --limit <n> For \`review\`, max cards to review (default 20)
791
+ --dry-run For \`import\`, print what would be imported without writing
792
+
793
+ EXAMPLES
794
+ npx ronin-skillforce status # See every course + overall streak
795
+ npx ronin-skillforce status --json # Machine-readable status
796
+ npx ronin-skillforce next # What to work on, and how to continue
797
+ npx ronin-skillforce check nextjs # Run the active exercise's self-checks
798
+ npx ronin-skillforce archive nextjs # Archive a course, keeping every file and commit
799
+ npx ronin-skillforce doctor # Diagnose your local setup
800
+ npx ronin-skillforce studio # One command: server + Studio + pairing
801
+ npx ronin-skillforce review # Review due flashcards in the terminal
802
+ npx ronin-skillforce import --all # Import every CLI course into Studio
803
+ npx ronin-skillforce import react-hooks # Import one course
804
+ npx ronin-skillforce import --all --dry-run # Preview the import, write nothing
805
+ npx ronin-skillforce init # Auto-detect and setup
806
+ npx ronin-skillforce setup claude # Setup for Claude Code (prompts for scope)
807
+ npx ronin-skillforce setup claude --global # Install globally
808
+ npx ronin-skillforce courses # Browse community courses
809
+ npx ronin-skillforce search react # Search for React courses
810
+ npx ronin-skillforce install react-hooks # Install course → learning/react-hooks/
811
+ npx ronin-skillforce install --skill react-hooks-reviewer # Install skill (prompts scope)
812
+ npx ronin-skillforce install --skill react-hooks-reviewer --global # Install skill globally
813
+ npx ronin-skillforce update react-hooks-reviewer # Update a skill
814
+ npx ronin-skillforce update --all # Update all installed skills
815
+ npx ronin-skillforce installed # List installed skills
816
+ npx ronin-skillforce remove react-hooks-reviewer # Remove a skill
817
+ npx ronin-skillforce publish --dry-run # Preview packaged files (no GitHub)
818
+ npx ronin-skillforce publish --type skill --name foo # Publish a skill
819
+
820
+ SUPPORTED AGENTS
821
+ ${SUPPORTED_AGENTS.join(', ')}
822
+ `.trim());
823
+ }