ronin-skillforce 0.0.0-stage → 0.1.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 (429) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +225 -2
  4. package/bin/cli.js +811 -0
  5. package/bin/commands/check.js +298 -0
  6. package/bin/commands/create.js +169 -0
  7. package/bin/commands/doctor.js +392 -0
  8. package/bin/commands/import.js +557 -0
  9. package/bin/commands/next.js +125 -0
  10. package/bin/commands/review.js +207 -0
  11. package/bin/commands/status.js +114 -0
  12. package/bin/commands/studio.js +497 -0
  13. package/bin/commands/trail.js +211 -0
  14. package/bin/commands/web.js +350 -0
  15. package/bin/course-reader.js +611 -0
  16. package/bin/node-version.js +60 -0
  17. package/bin/platforms/_shared.js +211 -0
  18. package/bin/platforms/claude.js +104 -0
  19. package/bin/platforms/cursor.js +49 -0
  20. package/bin/platforms/gemini.js +65 -0
  21. package/bin/platforms/opencode.js +95 -0
  22. package/bin/registry-helpers.js +504 -0
  23. package/bin/static-server.js +153 -0
  24. package/bin/studio-store.js +94 -0
  25. package/bin/sync-shared.sh +5 -0
  26. package/package.json +91 -4
  27. package/plugin.json +139 -0
  28. package/shared/SKILL.md +568 -0
  29. package/shared/agents/coach.md +225 -0
  30. package/shared/agents/navigator.md +215 -0
  31. package/shared/agents/professor.md +372 -0
  32. package/shared/agents/researcher.md +181 -0
  33. package/shared/agents/spotter.md +129 -0
  34. package/shared/commands/professor/archive.md +103 -0
  35. package/shared/commands/professor/capstone-review.md +79 -0
  36. package/shared/commands/professor/capstone.md +34 -0
  37. package/shared/commands/professor/discuss.md +18 -0
  38. package/shared/commands/professor/done.md +44 -0
  39. package/shared/commands/professor/export.md +69 -0
  40. package/shared/commands/professor/hint.md +108 -0
  41. package/shared/commands/professor/navigator.md +67 -0
  42. package/shared/commands/professor/new-topic.md +73 -0
  43. package/shared/commands/professor/next.md +174 -0
  44. package/shared/commands/professor/note.md +63 -0
  45. package/shared/commands/professor/onboard.md +95 -0
  46. package/shared/commands/professor/progress.md +46 -0
  47. package/shared/commands/professor/publish.md +93 -0
  48. package/shared/commands/professor/quiz.md +27 -0
  49. package/shared/commands/professor/recall.md +74 -0
  50. package/shared/commands/professor/review.md +43 -0
  51. package/shared/commands/professor/schedule.md +75 -0
  52. package/shared/commands/professor/skill-export.md +87 -0
  53. package/shared/commands/professor/spotter.md +76 -0
  54. package/shared/commands/professor/stuck.md +27 -0
  55. package/shared/commands/professor/switch.md +50 -0
  56. package/shared/commands/professor/syllabus.md +9 -0
  57. package/shared/commands/professor/template-export.md +91 -0
  58. package/shared/commands/professor/template-import.md +111 -0
  59. package/shared/commands/professor/worktrees.md +58 -0
  60. package/shared/hooks/pre-compact.js +232 -0
  61. package/studio/README.md +313 -0
  62. package/studio/dist/assets/BetaReportPanel-8NrDLiPm.js +1 -0
  63. package/studio/dist/assets/Capstone-Cv9kH6LY.js +6 -0
  64. package/studio/dist/assets/CodeWorkspace-BwCEINWo.js +2 -0
  65. package/studio/dist/assets/Course-BJ_hafUP.js +1 -0
  66. package/studio/dist/assets/Editor-CV0mNhlz.js +53 -0
  67. package/studio/dist/assets/Home-C1ry89ak.js +1 -0
  68. package/studio/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  69. package/studio/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  70. package/studio/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  71. package/studio/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  72. package/studio/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  73. package/studio/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  74. package/studio/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  75. package/studio/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  76. package/studio/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  77. package/studio/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  78. package/studio/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  79. package/studio/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  80. package/studio/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  81. package/studio/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  82. package/studio/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  83. package/studio/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  84. package/studio/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  85. package/studio/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  86. package/studio/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  87. package/studio/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  88. package/studio/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  89. package/studio/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  90. package/studio/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  91. package/studio/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  92. package/studio/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  93. package/studio/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  94. package/studio/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  95. package/studio/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  96. package/studio/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  97. package/studio/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  98. package/studio/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  99. package/studio/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  100. package/studio/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  101. package/studio/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  102. package/studio/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  103. package/studio/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  104. package/studio/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  105. package/studio/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  106. package/studio/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  107. package/studio/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  108. package/studio/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  109. package/studio/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  110. package/studio/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  111. package/studio/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  112. package/studio/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  113. package/studio/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  114. package/studio/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  115. package/studio/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  116. package/studio/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  117. package/studio/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  118. package/studio/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  119. package/studio/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  120. package/studio/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  121. package/studio/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  122. package/studio/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  123. package/studio/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  124. package/studio/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  125. package/studio/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  126. package/studio/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  127. package/studio/dist/assets/Lesson-2e9TD9yL.js +2 -0
  128. package/studio/dist/assets/NewCourse-BZeOg2xn.js +952 -0
  129. package/studio/dist/assets/Progress-CRc_xEWC.js +1 -0
  130. package/studio/dist/assets/Review-6QEbHEfk.js +1 -0
  131. package/studio/dist/assets/Settings-NFnqVQui.js +1 -0
  132. package/studio/dist/assets/TutorPanel-DXQ6Z3BY.js +123 -0
  133. package/studio/dist/assets/abnfDiagram-WEYOIA3C-qeSjKlGM.js +1 -0
  134. package/studio/dist/assets/anthropic-D7NiQCUH.js +15 -0
  135. package/studio/dist/assets/arc-Y1ARJVZ4.js +1 -0
  136. package/studio/dist/assets/architecture-WOLXFQ4H-COG-m35l.js +1 -0
  137. package/studio/dist/assets/architectureDiagram-ZHVYKT53-VO7uKoeD.js +36 -0
  138. package/studio/dist/assets/array-BifhSqXX.js +1 -0
  139. package/studio/dist/assets/blockDiagram-RJAE7QA3-BGJF_qiL.js +140 -0
  140. package/studio/dist/assets/bridge-BTh0AeNX.js +1 -0
  141. package/studio/dist/assets/c4Diagram-XDPPPE54-C-S10RgU.js +38 -0
  142. package/studio/dist/assets/capstone-Amn6Hztq.js +135 -0
  143. package/studio/dist/assets/card-store-BrbrtX9w.js +1 -0
  144. package/studio/dist/assets/channel-SJiUQu_N.js +1 -0
  145. package/studio/dist/assets/chunk-2E7WHVMD-fv-iqTyC.js +88 -0
  146. package/studio/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  147. package/studio/dist/assets/chunk-4EMNKR2T-CY9IZrgv.js +1 -0
  148. package/studio/dist/assets/chunk-55XCU76K-DyTZz7v_.js +1 -0
  149. package/studio/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  150. package/studio/dist/assets/chunk-7S6XSTFO-BNMzTLRW.js +2 -0
  151. package/studio/dist/assets/chunk-AQDTSKZB-CEYANohH.js +72 -0
  152. package/studio/dist/assets/chunk-DSNLSHJZ-CDk5EXjH.js +129 -0
  153. package/studio/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  154. package/studio/dist/assets/chunk-HAGFOTED-hNAi1zh_.js +62 -0
  155. package/studio/dist/assets/chunk-HNE4Q36P-COJnSYoZ.js +1 -0
  156. package/studio/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  157. package/studio/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  158. package/studio/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  159. package/studio/dist/assets/chunk-NQOKRVIF-BU6KVxRr.js +213 -0
  160. package/studio/dist/assets/chunk-UHW7PFBP-DYvxL9JG.js +1 -0
  161. package/studio/dist/assets/chunk-VQ5W62QI-CuRfk65j.js +1 -0
  162. package/studio/dist/assets/chunk-WQQL4CDK-KaG7x-Mb.js +10 -0
  163. package/studio/dist/assets/chunk-XXDRQBXY-CnbRkih_.js +1 -0
  164. package/studio/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  165. package/studio/dist/assets/chunk-YZO67HFG-DbuqIZmk.js +1 -0
  166. package/studio/dist/assets/chunk-ZAUX2LTX-bk4ivUmc.js +1 -0
  167. package/studio/dist/assets/chunk-ZCHMQSOR-B_LElfwh.js +2 -0
  168. package/studio/dist/assets/classDiagram-v2-4MYWR3UA-DtCJ6H_X.js +217 -0
  169. package/studio/dist/assets/cose-bilkent-JH36ORCC-DMBAZFnj.js +1 -0
  170. package/studio/dist/assets/course-export-DpaOTT3a.js +1 -0
  171. package/studio/dist/assets/cynefin-EF2NZ3EQ-D9btHJvU.js +1 -0
  172. package/studio/dist/assets/cynefinDiagram-WF24UDRP-CsFJuv27.js +62 -0
  173. package/studio/dist/assets/cytoscape.esm-CHBj4VkZ.js +321 -0
  174. package/studio/dist/assets/dagre-NK4GIZVX-Cg1Duec7.js +4 -0
  175. package/studio/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  176. package/studio/dist/assets/diagram-CBK3MWTC-_KU2m5gd.js +24 -0
  177. package/studio/dist/assets/diagram-I5XIIV2D-5RaBrwUA.js +24 -0
  178. package/studio/dist/assets/diagram-ODIL2UCW-C0VKxoD3.js +30 -0
  179. package/studio/dist/assets/diagram-PFBEJUXE-BaHcZEP2.js +3 -0
  180. package/studio/dist/assets/diagram-RJA3I4VV-cwSAZ90p.js +41 -0
  181. package/studio/dist/assets/diagram-TYUNFSNR-BJImxCEN.js +200 -0
  182. package/studio/dist/assets/dist-CumnGVmJ.js +1 -0
  183. package/studio/dist/assets/ebnfDiagram-V4P3TTPT-OQQLRXcm.js +1 -0
  184. package/studio/dist/assets/elk-QONTBCTQ-BJGhQHxQ.js +27 -0
  185. package/studio/dist/assets/erDiagram-KYQIGQAA-D91EKSep.js +99 -0
  186. package/studio/dist/assets/error-BTdmRKoB.js +1 -0
  187. package/studio/dist/assets/errors-BZa_CNHL.js +1 -0
  188. package/studio/dist/assets/eventmodeling-K75KTNOO-DgdTRgnw.js +1 -0
  189. package/studio/dist/assets/fetch-local-DG3O3hmQ.js +4 -0
  190. package/studio/dist/assets/flowDiagram-IUGLLWBU-CVnJYMum.js +1 -0
  191. package/studio/dist/assets/ganttDiagram-R7GPNTJY-CSYLPWEI.js +292 -0
  192. package/studio/dist/assets/gitGraph-VSP46ZUC-D6GWaV9y.js +1 -0
  193. package/studio/dist/assets/gitGraphDiagram-PDYL7XHY-CaV31bvT.js +106 -0
  194. package/studio/dist/assets/graphlib-DS17s2tU.js +1 -0
  195. package/studio/dist/assets/index-CVenTTOY.css +2 -0
  196. package/studio/dist/assets/index-DioXTZVc.js +313 -0
  197. package/studio/dist/assets/info-OHQRW6UA-BN-x8GoN.js +1 -0
  198. package/studio/dist/assets/infoDiagram-MVDRKAX5-CIose_cy.js +2 -0
  199. package/studio/dist/assets/init-C-OQMol4.js +1 -0
  200. package/studio/dist/assets/ishikawaDiagram-HPEHV2GE-DCihSNBw.js +70 -0
  201. package/studio/dist/assets/journeyDiagram-BOSHW7O7-Bz3o3RIX.js +139 -0
  202. package/studio/dist/assets/js.worker-AD7DjksO.js +4 -0
  203. package/studio/dist/assets/kanban-definition-FNSA4HC5-BUyqvlFE.js +89 -0
  204. package/studio/dist/assets/katex-B_JcIwqj.js +257 -0
  205. package/studio/dist/assets/katex-BgsA2lO1.css +1 -0
  206. package/studio/dist/assets/line-B08T8KlK.js +1 -0
  207. package/studio/dist/assets/linear-C9iwaMOi.js +1 -0
  208. package/studio/dist/assets/mermaid-parser.core-BYwreSUy.js +7 -0
  209. package/studio/dist/assets/mermaid.core-DTO1oDmq.js +44 -0
  210. package/studio/dist/assets/mindmap-definition-6OY57UG3-BGLwKheY.js +98 -0
  211. package/studio/dist/assets/node.browser-BISniYn8.js +1 -0
  212. package/studio/dist/assets/node.browser-HQ5SKBOf.js +1 -0
  213. package/studio/dist/assets/ollama-De5w84tG.js +1 -0
  214. package/studio/dist/assets/openai-compatible-Joy1oK3L.js +1 -0
  215. package/studio/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  216. package/studio/dist/assets/packet-JDAUHWVQ-CDIMjHS_.js +1 -0
  217. package/studio/dist/assets/path-fybaL0A-.js +1 -0
  218. package/studio/dist/assets/pegDiagram-ECIUJNBX-DWXm4mwS.js +1 -0
  219. package/studio/dist/assets/pie-XZMESJXO-CY3QQiKZ.js +1 -0
  220. package/studio/dist/assets/pieDiagram-GNMPY74T-M1aCnI7W.js +39 -0
  221. package/studio/dist/assets/python.worker-CzTrFdd9.js +24 -0
  222. package/studio/dist/assets/quadrantDiagram-D57K4MC3-BFFtO7U5.js +7 -0
  223. package/studio/dist/assets/radar-ABXABTNO-DT0W4_Xn.js +1 -0
  224. package/studio/dist/assets/railroad-I3PHUGI6-_VCpgZR5.js +1 -0
  225. package/studio/dist/assets/railroad-abnf-LNEFI6M7-DkSpQ5YY.js +1 -0
  226. package/studio/dist/assets/railroad-ebnf-DWYD2UWJ-BqWILdGb.js +1 -0
  227. package/studio/dist/assets/railroad-peg-JR7BVNR7-BB0bQL-Z.js +1 -0
  228. package/studio/dist/assets/railroadDiagram-Y72NRBL7-CjLKjIdJ.js +1 -0
  229. package/studio/dist/assets/react-B8SKf8RM.js +1 -0
  230. package/studio/dist/assets/rehype-katex-C-sKddsH.js +259 -0
  231. package/studio/dist/assets/remark-math-C-ox67hE.js +5 -0
  232. package/studio/dist/assets/requirementDiagram-EQYSW5RA-CnwzZqR8.js +84 -0
  233. package/studio/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  234. package/studio/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  235. package/studio/dist/assets/sankeyDiagram-SY24JHO5-CPDugTUl.js +40 -0
  236. package/studio/dist/assets/schemas-BOc2YsOP.js +55 -0
  237. package/studio/dist/assets/sequenceDiagram-BLEHNHQU-1dLfeU9Y.js +169 -0
  238. package/studio/dist/assets/settings-DhwuyNPU.js +1 -0
  239. package/studio/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  240. package/studio/dist/assets/spec-nAnsSMkw.js +1 -0
  241. package/studio/dist/assets/sql-wasm-C1D_RJuy.wasm +0 -0
  242. package/studio/dist/assets/sql.worker-D7pBHOrq.js +4 -0
  243. package/studio/dist/assets/src-BWF7jMDv.js +1 -0
  244. package/studio/dist/assets/stateDiagram-v2-A6IESKWZ-DR7CmEjj.js +291 -0
  245. package/studio/dist/assets/swimlanes-B3ENRKFQ-BAWB9omQ.js +1 -0
  246. package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-qeuHru16.js +8 -0
  247. package/studio/dist/assets/timeline-definition-JXYKGLGB-DNkj8z77.js +120 -0
  248. package/studio/dist/assets/treeView-D4JQ5SDB-BA-ouhMb.js +1 -0
  249. package/studio/dist/assets/treemap-SAJKECNS-BrrGxsAb.js +1 -0
  250. package/studio/dist/assets/useStream-rdsR6A7P.js +1 -0
  251. package/studio/dist/assets/usecaseDiagram-WPOLBNNX-BB3D0Dt_.js +328 -0
  252. package/studio/dist/assets/vennDiagram-Q3Y3IDCW-BrtqtKTE.js +34 -0
  253. package/studio/dist/assets/wardley-7MLQ67FV-BKkemDql.js +1 -0
  254. package/studio/dist/assets/wardleyDiagram-IVAIDKWA-DFnI3P5z.js +78 -0
  255. package/studio/dist/assets/xychartDiagram-4URNHDU4-Da0DZG_q.js +7 -0
  256. package/studio/dist/index.html +20 -0
  257. package/studio/dist/preview.html +22 -0
  258. package/studio/migrations/0001_init.sql +188 -0
  259. package/studio/migrations/0002_external_ids.sql +20 -0
  260. package/studio/migrations/0003_course_language.sql +12 -0
  261. package/studio/migrations/0004_capstone.sql +51 -0
  262. package/studio/migrations/0005_beta_loop.sql +73 -0
  263. package/studio/migrations/0006_interactive_exercises.sql +49 -0
  264. package/studio/src/lib/activity.js +92 -0
  265. package/studio/src/lib/ai/anthropic.js +109 -0
  266. package/studio/src/lib/ai/bridge.js +126 -0
  267. package/studio/src/lib/ai/catalog.js +160 -0
  268. package/studio/src/lib/ai/config.js +70 -0
  269. package/studio/src/lib/ai/errors.js +95 -0
  270. package/studio/src/lib/ai/fetch-local.js +104 -0
  271. package/studio/src/lib/ai/index.js +194 -0
  272. package/studio/src/lib/ai/ollama.js +136 -0
  273. package/studio/src/lib/ai/openai-compatible.js +101 -0
  274. package/studio/src/lib/ai/provider-utils.js +107 -0
  275. package/studio/src/lib/ai/sse.js +99 -0
  276. package/studio/src/lib/ai/status.js +92 -0
  277. package/studio/src/lib/ai/useStream.js +109 -0
  278. package/studio/src/lib/beta/metrics.js +147 -0
  279. package/studio/src/lib/beta/report.js +134 -0
  280. package/studio/src/lib/capstone-evidence.js +758 -0
  281. package/studio/src/lib/capstone.js +663 -0
  282. package/studio/src/lib/chat-store.js +128 -0
  283. package/studio/src/lib/course-export.js +38 -0
  284. package/studio/src/lib/course.js +187 -0
  285. package/studio/src/lib/exercises/journal-entry.js +259 -0
  286. package/studio/src/lib/exercises/placement.js +31 -0
  287. package/studio/src/lib/exercises/registry.js +19 -0
  288. package/studio/src/lib/exercises/spec.js +129 -0
  289. package/studio/src/lib/i18n/detect.js +162 -0
  290. package/studio/src/lib/i18n/en/app.json +23 -0
  291. package/studio/src/lib/i18n/en/betaReport.json +55 -0
  292. package/studio/src/lib/i18n/en/capstone.json +57 -0
  293. package/studio/src/lib/i18n/en/common.json +25 -0
  294. package/studio/src/lib/i18n/en/course.json +27 -0
  295. package/studio/src/lib/i18n/en/data.json +35 -0
  296. package/studio/src/lib/i18n/en/durability.json +5 -0
  297. package/studio/src/lib/i18n/en/errors.json +20 -0
  298. package/studio/src/lib/i18n/en/exercises.json +55 -0
  299. package/studio/src/lib/i18n/en/externalEvidence.json +76 -0
  300. package/studio/src/lib/i18n/en/home.json +18 -0
  301. package/studio/src/lib/i18n/en/lesson.json +33 -0
  302. package/studio/src/lib/i18n/en/nav.json +8 -0
  303. package/studio/src/lib/i18n/en/newCourse.json +51 -0
  304. package/studio/src/lib/i18n/en/onboarding.json +35 -0
  305. package/studio/src/lib/i18n/en/playwrightTemplate.json +20 -0
  306. package/studio/src/lib/i18n/en/practice.json +79 -0
  307. package/studio/src/lib/i18n/en/provider.json +27 -0
  308. package/studio/src/lib/i18n/en/providerMeta.json +26 -0
  309. package/studio/src/lib/i18n/en/reflection.json +17 -0
  310. package/studio/src/lib/i18n/en/revision.json +52 -0
  311. package/studio/src/lib/i18n/en/runnerNotice.json +14 -0
  312. package/studio/src/lib/i18n/en/settings.json +12 -0
  313. package/studio/src/lib/i18n/en/storage.json +49 -0
  314. package/studio/src/lib/i18n/en/tutor.json +39 -0
  315. package/studio/src/lib/i18n/en/workspace.json +50 -0
  316. package/studio/src/lib/i18n/index.js +144 -0
  317. package/studio/src/lib/i18n/languages.js +93 -0
  318. package/studio/src/lib/i18n/vi/app.json +23 -0
  319. package/studio/src/lib/i18n/vi/betaReport.json +55 -0
  320. package/studio/src/lib/i18n/vi/capstone.json +57 -0
  321. package/studio/src/lib/i18n/vi/common.json +25 -0
  322. package/studio/src/lib/i18n/vi/course.json +27 -0
  323. package/studio/src/lib/i18n/vi/data.json +35 -0
  324. package/studio/src/lib/i18n/vi/durability.json +5 -0
  325. package/studio/src/lib/i18n/vi/errors.json +20 -0
  326. package/studio/src/lib/i18n/vi/exercises.json +55 -0
  327. package/studio/src/lib/i18n/vi/externalEvidence.json +76 -0
  328. package/studio/src/lib/i18n/vi/home.json +18 -0
  329. package/studio/src/lib/i18n/vi/lesson.json +33 -0
  330. package/studio/src/lib/i18n/vi/nav.json +8 -0
  331. package/studio/src/lib/i18n/vi/newCourse.json +51 -0
  332. package/studio/src/lib/i18n/vi/onboarding.json +35 -0
  333. package/studio/src/lib/i18n/vi/playwrightTemplate.json +20 -0
  334. package/studio/src/lib/i18n/vi/practice.json +79 -0
  335. package/studio/src/lib/i18n/vi/provider.json +27 -0
  336. package/studio/src/lib/i18n/vi/providerMeta.json +26 -0
  337. package/studio/src/lib/i18n/vi/reflection.json +17 -0
  338. package/studio/src/lib/i18n/vi/revision.json +52 -0
  339. package/studio/src/lib/i18n/vi/runnerNotice.json +14 -0
  340. package/studio/src/lib/i18n/vi/settings.json +12 -0
  341. package/studio/src/lib/i18n/vi/storage.json +49 -0
  342. package/studio/src/lib/i18n/vi/tutor.json +39 -0
  343. package/studio/src/lib/i18n/vi/workspace.json +50 -0
  344. package/studio/src/lib/ids.js +10 -0
  345. package/studio/src/lib/json.js +26 -0
  346. package/studio/src/lib/markdown.js +74 -0
  347. package/studio/src/lib/prompts/base.js +139 -0
  348. package/studio/src/lib/prompts/capstone.js +777 -0
  349. package/studio/src/lib/prompts/course.js +150 -0
  350. package/studio/src/lib/prompts/lesson.js +275 -0
  351. package/studio/src/lib/prompts/practice.js +511 -0
  352. package/studio/src/lib/prompts/tutor.js +509 -0
  353. package/studio/src/lib/repo/blocked-adapter.js +69 -0
  354. package/studio/src/lib/repo/events.js +239 -0
  355. package/studio/src/lib/repo/idb-adapter.js +226 -0
  356. package/studio/src/lib/repo/index.js +1315 -0
  357. package/studio/src/lib/repo/memory-adapter.js +115 -0
  358. package/studio/src/lib/repo/remote-adapter.js +158 -0
  359. package/studio/src/lib/repo/schema.js +143 -0
  360. package/studio/src/lib/repo/select-adapter.js +345 -0
  361. package/studio/src/lib/router.js +71 -0
  362. package/studio/src/lib/runner/browser.js +244 -0
  363. package/studio/src/lib/runner/checks.js +248 -0
  364. package/studio/src/lib/runner/current-code.js +98 -0
  365. package/studio/src/lib/runner/esm-cjs.js +109 -0
  366. package/studio/src/lib/runner/hash.js +34 -0
  367. package/studio/src/lib/runner/history.js +48 -0
  368. package/studio/src/lib/runner/index.js +168 -0
  369. package/studio/src/lib/runner/languages.js +258 -0
  370. package/studio/src/lib/runner/local.js +139 -0
  371. package/studio/src/lib/runner/path.js +104 -0
  372. package/studio/src/lib/runner/skeleton.js +315 -0
  373. package/studio/src/lib/runner/strip-types.js +134 -0
  374. package/studio/src/lib/settings.js +399 -0
  375. package/studio/src/lib/srs.js +39 -0
  376. package/templates/agent/settings.json +6 -0
  377. package/templates/claude/plugin.json +139 -0
  378. package/templates/claude/settings.json +15 -0
  379. package/templates/cursor/professor.mdc +4 -0
  380. package/templates/cursor/settings.json +18 -0
  381. package/templates/gemini/GEMINI.md +28 -0
  382. package/templates/gemini/settings.json +5 -0
  383. package/templates/opencode/opencode.json +3 -0
  384. package/templates/researcher/README.md +61 -0
  385. package/templates/researcher/settings.json +18 -0
  386. package/web/client/components.json +15 -0
  387. package/web/client/index.html +12 -0
  388. package/web/client/package-lock.json +8732 -0
  389. package/web/client/package.json +39 -0
  390. package/web/client/postcss.config.js +6 -0
  391. package/web/client/src/App.jsx +140 -0
  392. package/web/client/src/api/client.js +357 -0
  393. package/web/client/src/components/ChatPanel.jsx +417 -0
  394. package/web/client/src/components/CommandPills.jsx +50 -0
  395. package/web/client/src/components/DeprecationBanner.jsx +21 -0
  396. package/web/client/src/components/ExerciseEditor.jsx +164 -0
  397. package/web/client/src/components/Header.jsx +119 -0
  398. package/web/client/src/components/LecturePanel.jsx +342 -0
  399. package/web/client/src/components/MobileTabSwitcher.jsx +51 -0
  400. package/web/client/src/components/SplitPane.jsx +42 -0
  401. package/web/client/src/components/ui/alert.jsx +38 -0
  402. package/web/client/src/components/ui/badge.jsx +24 -0
  403. package/web/client/src/components/ui/button.jsx +23 -0
  404. package/web/client/src/components/ui/select.jsx +74 -0
  405. package/web/client/src/hooks/useSSE.js +152 -0
  406. package/web/client/src/hooks/useWebSocket.js +98 -0
  407. package/web/client/src/index.css +23 -0
  408. package/web/client/src/lib/utils.js +6 -0
  409. package/web/client/src/main.jsx +12 -0
  410. package/web/client/src/utils/markdown.js +50 -0
  411. package/web/client/tailwind.config.js +8 -0
  412. package/web/client/vite.config.js +21 -0
  413. package/web/dist/assets/index-C1W0eXIQ.js +294 -0
  414. package/web/dist/assets/index-C3OUx70T.css +1 -0
  415. package/web/dist/index.html +13 -0
  416. package/web/lecture-archive.js +172 -0
  417. package/web/legacy-server.js +1277 -0
  418. package/web/package-lock.json +855 -0
  419. package/web/package.json +10 -0
  420. package/web/security.js +1016 -0
  421. package/web/server.js +390 -0
  422. package/web/studio-api/agents.js +580 -0
  423. package/web/studio-api/bridge.js +199 -0
  424. package/web/studio-api/db.js +66 -0
  425. package/web/studio-api/guards.js +201 -0
  426. package/web/studio-api/health.js +538 -0
  427. package/web/studio-api.js +214 -0
  428. package/web/studio-db.js +380 -0
  429. package/web/studio-runner.js +568 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * `skillforce doctor` — environment checklist with fix hints.
3
+ */
4
+ import { existsSync, statSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { spawnSync } from 'child_process';
7
+ import http from 'http';
8
+ import net from 'net';
9
+ import os from 'os';
10
+ import chalk from 'chalk';
11
+ import { nodeSupportsSqlite } from '../node-version.js';
12
+ import * as security from '../../web/security.js';
13
+ import { AGENT_BINARIES } from '../../web/studio-api/health.js';
14
+
15
+ const AGENT_CLIS = ['claude', 'opencode', 'cursor-agent', 'gemini'];
16
+ const PLATFORMS = ['claude', 'gemini', 'opencode', 'cursor'];
17
+
18
+ /** Look for an executable on PATH without spawning a shell. */
19
+ export function whichPath(cmd, pathValue = process.env.PATH || '') {
20
+ const sep = process.platform === 'win32' ? ';' : ':';
21
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
22
+ for (const dir of pathValue.split(sep)) {
23
+ for (const ext of exts) {
24
+ const full = join(dir, cmd + ext);
25
+ try { if (statSync(full).isFile()) return full; } catch {}
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Evaluate the Node.js version string against the >= 18 requirement.
33
+ * @param {string} version e.g. "v20.11.0"
34
+ * @returns {{ status: 'ok'|'fail', detail: string, hint?: string }}
35
+ */
36
+ export function evaluateNodeVersion(version) {
37
+ const major = Number(String(version).replace(/^v/, '').split('.')[0]);
38
+ if (Number.isFinite(major) && major >= 18) {
39
+ return { status: 'ok', detail: String(version) };
40
+ }
41
+ return {
42
+ status: 'fail',
43
+ detail: `${version} (need >= 18)`,
44
+ hint: 'Install Node.js 18+ from https://nodejs.org',
45
+ };
46
+ }
47
+
48
+ /** Reuse per-platform detection from `bin/platforms/*`. */
49
+ export async function detectAgents() {
50
+ const found = [];
51
+ for (const name of PLATFORMS) {
52
+ try {
53
+ const mod = await import(`../platforms/${name}.js`);
54
+ if (mod.detect()) found.push(name);
55
+ } catch {}
56
+ }
57
+ return found;
58
+ }
59
+
60
+ function professorInstalledFor(cwd) {
61
+ const home = os.homedir();
62
+ const markers = {
63
+ claude: [
64
+ join(home, '.claude', 'commands', 'professor'),
65
+ join(cwd, '.claude', 'commands', 'professor'),
66
+ ],
67
+ gemini: [join(home, '.gemini')],
68
+ opencode: [join(home, '.config', 'opencode', 'command'), join(cwd, '.opencode')],
69
+ cursor: [join(cwd, '.cursor', 'rules', 'professor.mdc')],
70
+ };
71
+ return Object.entries(markers)
72
+ .filter(([, paths]) => paths.some((p) => existsSync(p)))
73
+ .map(([name]) => name);
74
+ }
75
+
76
+ function httpGet(url, timeoutMs = 2000, headers = {}) {
77
+ return new Promise((resolve) => {
78
+ let settled = false;
79
+ const done = (value) => { if (!settled) { settled = true; resolve(value); } };
80
+ let req;
81
+ try {
82
+ req = http.get(url, { headers }, (res) => {
83
+ let body = '';
84
+ res.on('data', (chunk) => { body += chunk; });
85
+ res.on('end', () => {
86
+ const ok = res.statusCode >= 200 && res.statusCode < 300;
87
+ done({ ok, status: res.statusCode, body });
88
+ });
89
+ });
90
+ } catch { done({ ok: false }); return; }
91
+ req.on('error', () => done({ ok: false }));
92
+ req.setTimeout(timeoutMs, () => {
93
+ try { req.destroy(); } catch {}
94
+ done({ ok: false, timeout: true });
95
+ });
96
+ });
97
+ }
98
+
99
+ function portOpen(port, timeoutMs = 800) {
100
+ return new Promise((resolve) => {
101
+ const socket = net.createConnection({ port, host: '127.0.0.1' });
102
+ let settled = false;
103
+ const done = (open) => {
104
+ if (settled) return;
105
+ settled = true;
106
+ try { socket.destroy(); } catch {}
107
+ resolve(open);
108
+ };
109
+ socket.setTimeout(timeoutMs, () => done(false));
110
+ socket.on('connect', () => done(true));
111
+ socket.on('error', () => done(false));
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Pure security-posture checks (D-16, D-26). Exported so tests can assert the whole posture without
117
+ * a live server or the real home directory.
118
+ *
119
+ * @param {object} [opts]
120
+ * @param {Record<string,string|undefined>} [opts.env]
121
+ * @param {string} [opts.platform]
122
+ * @param {typeof statSync} [opts.statImpl]
123
+ * @param {(cmd: string, opts?: object) => string|null} [opts.resolveExecutableImpl]
124
+ * @returns {object[]}
125
+ */
126
+ export function buildPostureChecks({
127
+ env = process.env,
128
+ platform = process.platform,
129
+ statImpl = statSync,
130
+ resolveExecutableImpl = security.resolveExecutable,
131
+ } = {}) {
132
+ const checks = [];
133
+ const add = (name, status, detail, hint) => checks.push({ name, status, detail, hint });
134
+
135
+ const legacy = env.PROFESSOR_LEGACY === '1';
136
+ add(
137
+ 'Security · legacy web',
138
+ legacy ? 'fail' : 'ok',
139
+ legacy ? 'legacy routes/UI are mounted' : 'Studio-only (legacy disabled)',
140
+ legacy
141
+ ? 'Stop using --legacy/PROFESSOR_LEGACY and use Professor Studio at http://localhost:5180.'
142
+ : undefined
143
+ );
144
+
145
+ const host = env.HOST;
146
+ const loopbackHosts = new Set(['', '127.0.0.1', 'localhost', '::1', '[::1]']);
147
+ if (host && !loopbackHosts.has(host)) {
148
+ add(
149
+ 'Security · bind address',
150
+ 'warn',
151
+ `${host} (not loopback)`,
152
+ 'Bind 127.0.0.1 (default) unless you explicitly need LAN access.'
153
+ );
154
+ } else {
155
+ add('Security · bind address', 'ok', host || '127.0.0.1 (default loopback)');
156
+ }
157
+
158
+ const tokenFile = security.tokenFilePath(env);
159
+ let mode = null;
160
+ try {
161
+ mode = statImpl(tokenFile).mode & 0o777;
162
+ } catch {
163
+ mode = null;
164
+ }
165
+ if (mode === null) {
166
+ add('Security · token file', 'warn', 'not created yet', 'Created by `skillforce studio`.');
167
+ } else if (mode === 0o600) {
168
+ add('Security · token file', 'ok', `${tokenFile} (0600)`);
169
+ } else {
170
+ add(
171
+ 'Security · token file',
172
+ 'fail',
173
+ `${tokenFile} (0${mode.toString(8)})`,
174
+ `Run: chmod 600 ${tokenFile}`
175
+ );
176
+ }
177
+
178
+ add(
179
+ 'Security · bash runner',
180
+ env.PROFESSOR_ALLOW_BASH === '1' ? 'warn' : 'ok',
181
+ env.PROFESSOR_ALLOW_BASH === '1' ? 'Bash runs are allowed' : 'Bash disabled (default)',
182
+ env.PROFESSOR_ALLOW_BASH === '1'
183
+ ? 'Unset PROFESSOR_ALLOW_BASH unless a course explicitly needs shell exercises.'
184
+ : undefined
185
+ );
186
+
187
+ const wildcardOrigins = security.isWildcardOrigins(env.STUDIO_ORIGINS || '');
188
+ const relaxedHosts = security.isNonLoopbackOptIn(env);
189
+ if (wildcardOrigins && relaxedHosts) {
190
+ add(
191
+ 'Security · relaxed env',
192
+ 'fail',
193
+ 'STUDIO_ORIGINS=* with STUDIO_ALLOW_NON_LOOPBACK',
194
+ 'The server refuses this combination. Unset one of them.'
195
+ );
196
+ } else if (wildcardOrigins) {
197
+ add(
198
+ 'Security · relaxed env',
199
+ 'warn',
200
+ 'STUDIO_ORIGINS=*',
201
+ 'Any non-null Origin is echoed. Use only while paired on a trusted machine.'
202
+ );
203
+ } else if (relaxedHosts) {
204
+ add(
205
+ 'Security · relaxed env',
206
+ 'warn',
207
+ 'STUDIO_ALLOW_NON_LOOPBACK is set',
208
+ 'Non-loopback clients and hosts are allowed. Use only on a trusted network.'
209
+ );
210
+ } else {
211
+ add('Security · relaxed env', 'ok', 'default exact-origin allowlist');
212
+ }
213
+
214
+ if (platform !== 'win32') {
215
+ add('Windows agent shims', 'ok', 'not applicable');
216
+ } else {
217
+ const unsupported = [];
218
+ for (const [agent, binaries] of Object.entries(AGENT_BINARIES)) {
219
+ for (const binary of binaries) {
220
+ const resolved = resolveExecutableImpl(binary, { platform });
221
+ if (resolved && security.isWindowsCommandShim(resolved, platform)) {
222
+ unsupported.push({ agent, resolved });
223
+ }
224
+ }
225
+ }
226
+ if (unsupported.length) {
227
+ add(
228
+ 'Windows agent shims',
229
+ 'fail',
230
+ unsupported.map((entry) => `${entry.agent} → ${entry.resolved}`).join(', '),
231
+ security.unsupportedWindowsShimMessage('Agent CLI', unsupported[0].resolved)
232
+ );
233
+ } else {
234
+ add('Windows agent shims', 'ok', 'native binaries only');
235
+ }
236
+ }
237
+
238
+ return checks;
239
+ }
240
+
241
+ /**
242
+ * Token-aware health probe (D-26). Never creates a token: an absent file means the request is sent
243
+ * without `X-Studio-Token`, and a 401 is reported as "reachable but unpaired" rather than "down".
244
+ *
245
+ * @param {{ env?: Record<string,string|undefined>, port?: number, timeoutMs?: number,
246
+ * httpGetImpl?: typeof httpGet, portOpenImpl?: typeof portOpen }} [opts]
247
+ * @returns {Promise<object>}
248
+ */
249
+ export async function buildStudioHealthCheck({
250
+ env = process.env,
251
+ port = 3000,
252
+ timeoutMs = 1000,
253
+ httpGetImpl = httpGet,
254
+ portOpenImpl = portOpen,
255
+ } = {}) {
256
+ const name = `Studio server (:${port})`;
257
+ const token = security.readTokenFile(security.tokenFilePath(env));
258
+ const headers = token ? { 'X-Studio-Token': token } : {};
259
+ const health = await httpGetImpl(`http://127.0.0.1:${port}/api/studio/health`, timeoutMs, headers);
260
+
261
+ if (health.ok) {
262
+ return { name, status: 'ok', detail: 'health endpoint responding' };
263
+ }
264
+ if (health.status === 401) {
265
+ return {
266
+ name,
267
+ status: 'warn',
268
+ detail: 'server reachable but unpaired',
269
+ hint: 'Open the pairing URL printed by `skillforce studio`.',
270
+ };
271
+ }
272
+ const open = await portOpenImpl(port);
273
+ if (!open) return { name, status: 'ok', detail: `port ${port} free` };
274
+ return {
275
+ name,
276
+ status: 'warn',
277
+ detail: 'port in use but no health endpoint',
278
+ hint: `Something else is on ${port}. Stop it or start the server with \`npx ronin-skillforce web\`.`,
279
+ };
280
+ }
281
+
282
+ /** Count checks by status (for a footer line). */
283
+ export function summarizeChecks(checks) {
284
+ return checks.reduce((acc, c) => {
285
+ acc[c.status] = (acc[c.status] || 0) + 1;
286
+ return acc;
287
+ }, { ok: 0, warn: 0, fail: 0 });
288
+ }
289
+
290
+ /**
291
+ * Run every environment check.
292
+ * @param {{ cwd?: string, nodeVersion?: string, home?: string }} [deps]
293
+ * @returns {Promise<{ checks: object[] }>}
294
+ */
295
+ export async function runChecks({
296
+ cwd = process.cwd(),
297
+ nodeVersion = process.version,
298
+ env = process.env,
299
+ platform = process.platform,
300
+ } = {}) {
301
+ const checks = [];
302
+ const add = (name, status, detail, hint) => checks.push({ name, status, detail, hint });
303
+
304
+ const node = evaluateNodeVersion(nodeVersion);
305
+ add('Node.js >= 18', node.status, node.detail, node.hint);
306
+
307
+ const studioReady = nodeSupportsSqlite(nodeVersion);
308
+ add(
309
+ 'Node.js 22.5+ (Studio)',
310
+ studioReady ? 'ok' : 'warn',
311
+ studioReady ? nodeVersion : `${nodeVersion} (Studio needs >= 22.5)`,
312
+ studioReady
313
+ ? undefined
314
+ : 'Studio, review and import use node:sqlite. Update Node from https://nodejs.org'
315
+ );
316
+
317
+ if (!whichPath('git')) {
318
+ add('git', 'fail', 'not found on PATH', 'Install git from https://git-scm.com');
319
+ } else {
320
+ const args = ['rev-parse', '--is-inside-work-tree'];
321
+ const res = spawnSync('git', args, { cwd, encoding: 'utf-8' });
322
+ const inRepo = res.status === 0 && res.stdout.trim() === 'true';
323
+ if (inRepo) add('git repository', 'ok', 'inside a git work tree');
324
+ else add('git repository', 'warn', 'not inside a git repository',
325
+ 'Only programming courses need git. Run `git init` in your project root.');
326
+ }
327
+
328
+ const foundClis = AGENT_CLIS.filter((cmd) => whichPath(cmd));
329
+ if (foundClis.length) add('Agent CLIs', 'ok', foundClis.join(', '));
330
+ else add('Agent CLIs', 'warn', 'none found on PATH',
331
+ 'Install one: claude, opencode, cursor-agent or gemini');
332
+
333
+ const platforms = await detectAgents();
334
+ const installed = professorInstalledFor(cwd);
335
+ if (platforms.length) add('Agent detected', 'ok', platforms.join(', '));
336
+ else add('Agent detected', 'warn', 'no agent home directory found',
337
+ 'Run: npx ronin-skillforce init');
338
+ if (installed.length) add('Professor installed', 'ok', installed.join(', '));
339
+ else add('Professor installed', 'warn', 'no installation marker found',
340
+ 'Run: npx ronin-skillforce setup <agent>');
341
+
342
+ const ollama = await httpGet('http://127.0.0.1:11434/api/tags');
343
+ if (ollama.ok) add('Ollama (:11434)', 'ok', 'reachable');
344
+ else add('Ollama (:11434)', 'warn', 'not reachable',
345
+ 'Optional. Start Ollama, or set OLLAMA_ORIGINS for browser use.');
346
+
347
+ const studioCheck = await buildStudioHealthCheck({ env });
348
+ add(studioCheck.name, studioCheck.status, studioCheck.detail, studioCheck.hint);
349
+
350
+ const professorHome = env.PROFESSOR_HOME || join(os.homedir(), '.professor');
351
+ const dbPath = join(professorHome, 'professor.db');
352
+ if (existsSync(dbPath)) add('Professor DB', 'ok', dbPath);
353
+ else add('Professor DB', 'warn', 'not created yet', 'Created when the local server runs.');
354
+
355
+ const toolchains = ['python3', 'go'].filter((cmd) => whichPath(cmd));
356
+ const missing = ['python3', 'go'].filter((cmd) => !whichPath(cmd));
357
+ if (missing.length === 0) add('Check toolchains', 'ok', toolchains.join(', '));
358
+ else add('Check toolchains', 'warn', `missing: ${missing.join(', ')}`,
359
+ 'Optional — needed only to run those exercise file types with `skillforce check`.');
360
+
361
+ checks.push(...buildPostureChecks({ env, platform }));
362
+
363
+ return { checks };
364
+ }
365
+
366
+ /**
367
+ * @param {{ cwd?: string, nodeVersion?: string }} ctx
368
+ */
369
+ export async function run({ cwd = process.cwd(), env = process.env } = {}) {
370
+ const { checks } = await runChecks({ cwd, env });
371
+
372
+ const mark = { ok: '✓', warn: '⚠', fail: '✗' };
373
+
374
+ console.log('');
375
+ console.log('Professor doctor');
376
+ console.log('');
377
+ for (const check of checks) {
378
+ const badge = `${mark[check.status]} ${check.name}`;
379
+ const line = `${badge.padEnd(26)} ${check.detail}`;
380
+ if (check.status === 'ok') console.log(` ${line}`);
381
+ else if (check.status === 'warn') console.log(` ${chalk.yellow(line)}`);
382
+ else console.log(` ${chalk.red(line)}`);
383
+ if (check.hint && check.status !== 'ok') {
384
+ console.log(chalk.dim(` ${' '.repeat(26)} → ${check.hint}`));
385
+ }
386
+ }
387
+
388
+ const counts = summarizeChecks(checks);
389
+ console.log('');
390
+ console.log(` ${counts.ok} ok · ${counts.warn} warnings · ${counts.fail} failures`);
391
+ console.log('');
392
+ }