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,168 @@
1
+ /**
2
+ * Runner interface (CODE-02, CODE-04) — the single entry point the Workspace UI calls.
3
+ *
4
+ * run({ language, files, entry, tests, signal, onOutput }) -> RunResult
5
+ *
6
+ * Backend selection is deliberate and boring:
7
+ * 1. browser — when the language is supported with no install (JS/TS, Python, SQL, HTML/CSS)
8
+ * 2. local — otherwise, if the learner's local server answers `/toolchains`
9
+ * 3. error — with an actionable hint telling them how to get the toolchain
10
+ *
11
+ * `onOutput` streams `{ stream: 'stdout'|'stderr'|'status', text }` as the run progresses.
12
+ * Nothing here ever writes, reveals or generates a solution — it only executes the learner's code.
13
+ */
14
+ import { languageForPath, normalizeLanguage, isBrowserLanguage, isLocalLanguage, getLanguage } from './languages.js';
15
+ import { runBrowser, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, RunnerError } from './browser.js';
16
+ import { runLocal, checkToolchains, runnerBase } from './local.js';
17
+ import { loadSettings } from '../settings.js';
18
+
19
+ export { DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES };
20
+ export { isBrowserLanguage, isLocalLanguage };
21
+
22
+ /**
23
+ * @typedef {import('./browser.js').RunResult} RunResult
24
+ */
25
+
26
+ /** @returns {boolean} whether a Web Worker can be constructed in this environment */
27
+ export function canUseWorkers() {
28
+ return typeof Worker !== 'undefined';
29
+ }
30
+
31
+ /**
32
+ * Pure backend choice — exported so it can be unit-tested without a browser.
33
+ * @param {{ language: string, browserUsable?: boolean, localAvailable?: boolean, preferLocal?: boolean }} input
34
+ * @returns {'browser'|'local'|null}
35
+ */
36
+ export function selectRunner({ language, browserUsable = true, localAvailable = false, preferLocal = false }) {
37
+ const browser = isBrowserLanguage(language);
38
+ if (preferLocal && localAvailable) return 'local';
39
+ if (browser && browserUsable) return 'browser';
40
+ if (isLocalLanguage(language) && localAvailable) return 'local';
41
+ if (browser && !browserUsable && localAvailable) return 'local';
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * The runner the UI should advertise for a language (no network).
47
+ * @param {string|null|undefined} language
48
+ * @param {{ toolchains?: Record<string, boolean>|null }} [opts]
49
+ */
50
+ export function describeRunner(language, { toolchains = null } = {}) {
51
+ const lang = normalizeLanguage(language);
52
+ const spec = getLanguage(lang);
53
+ if (!spec) return { id: null, label: 'Unknown', kind: null, ready: false };
54
+ const browser = spec.backend === 'browser';
55
+ if (browser) return { id: lang, label: 'Browser', kind: 'browser', ready: true };
56
+ const command = spec.command;
57
+ const ready = Boolean(toolchains && command && toolchains[command]);
58
+ return { id: lang, label: 'Local server', kind: 'local', ready, command };
59
+ }
60
+
61
+ /**
62
+ * @param {object} input
63
+ * @param {string} input.language
64
+ * @param {Record<string,string>|Array<{path:string,content:string}>} input.files
65
+ * @param {string|null} [input.entry]
66
+ * @param {any} [input.tests]
67
+ * @param {AbortSignal} [input.signal]
68
+ * @param {(chunk:{stream:string,text:string})=>void} [input.onOutput]
69
+ * @param {string} [input.courseSlug]
70
+ * @param {object} [input.settings]
71
+ * @param {number} [input.timeoutMs]
72
+ * @param {number} [input.maxOutputBytes]
73
+ * @param {boolean} [input.preferLocal]
74
+ * @returns {Promise<RunResult>}
75
+ */
76
+ export async function run(input) {
77
+ const {
78
+ language,
79
+ files = {},
80
+ entry = null,
81
+ tests = null,
82
+ signal,
83
+ onOutput,
84
+ courseSlug = 'workspace',
85
+ settings = null,
86
+ timeoutMs,
87
+ maxOutputBytes,
88
+ preferLocal = false,
89
+ } = input || {};
90
+
91
+ const resolvedSettings = settings || safeSettings();
92
+ const lang = normalizeLanguage(language) || languageForPath(entry) || normalizeLanguage(Object.keys(files)[0]) || 'javascript';
93
+
94
+ const browserUsable = canUseWorkers();
95
+ const browserPath = { language: lang, files: fileMap(files), entry, tests, signal, onOutput, timeoutMs, maxOutputBytes };
96
+
97
+ if (isBrowserLanguage(lang) && browserUsable && !preferLocal) {
98
+ return runBrowser(browserPath);
99
+ }
100
+
101
+ // Local (or browser-as-fallback when a browser language but no workers, or preferLocal).
102
+ let localAvailable = false;
103
+ let localError = null;
104
+ if (!isBrowserLanguage(lang) || preferLocal) {
105
+ try {
106
+ const toolchains = await checkToolchains(resolvedSettings, { signal });
107
+ localAvailable = true;
108
+ const spec = getLanguage(lang);
109
+ if (spec?.command && toolchains && toolchains[spec.command] === false) {
110
+ throw localToolchainError(lang, spec.command);
111
+ }
112
+ } catch (err) {
113
+ localError = err;
114
+ localAvailable = false;
115
+ }
116
+ }
117
+
118
+ const choice = selectRunner({ language: lang, browserUsable, localAvailable, preferLocal });
119
+ if (choice === 'local') {
120
+ return runLocal({ settings: resolvedSettings, courseSlug, language: lang, files: fileMap(files), entry, signal, onOutput, timeoutMs });
121
+ }
122
+ if (choice === 'browser') {
123
+ return runBrowser(browserPath);
124
+ }
125
+
126
+ // No backend works — surface the most useful error.
127
+ if (localError) throw localError;
128
+ if (browserUsable && isBrowserLanguage(lang)) return runBrowser(browserPath);
129
+ if (!browserUsable && isBrowserLanguage(lang)) {
130
+ const err = new RunnerError('unavailable', `The ${lang} runner needs Web Workers, which this browser does not expose.`);
131
+ err.hint = 'Try a modern browser, or start the local server and use the local runner.';
132
+ throw err;
133
+ }
134
+ throw localToolchainError(lang, getLanguage(lang)?.command);
135
+ }
136
+
137
+ /**
138
+ * @param {string} language
139
+ * @param {string|undefined} command
140
+ */
141
+ function localToolchainError(language, command) {
142
+ const label = getLanguage(language)?.label || language;
143
+ const err = new RunnerError(
144
+ 'unavailable',
145
+ `${label} needs its toolchain installed and the local server running.`
146
+ );
147
+ err.hint = `Start the local server (\`npx ronin-skillforce web\`)${command ? ` and make sure \`${command}\` is on your PATH` : ''}. The local runner writes files to ~/.professor/workspaces/ so you can also open them in your editor. You can still write and save code without running it.`;
148
+ return err;
149
+ }
150
+
151
+ /** @param {Record<string,string>|Array<{path:string,content:string}>} files */
152
+ function fileMap(files) {
153
+ if (!files) return {};
154
+ return Array.isArray(files)
155
+ ? Object.fromEntries(files.map((f) => [f.path, String(f.content ?? '')]))
156
+ : Object.fromEntries(Object.entries(files).map(([k, v]) => [k, String(v ?? '')]));
157
+ }
158
+
159
+ /** Settings without throwing when localStorage is unavailable (worker/node/tests). */
160
+ function safeSettings() {
161
+ try {
162
+ return loadSettings();
163
+ } catch {
164
+ return {};
165
+ }
166
+ }
167
+
168
+ export { checkToolchains, runnerBase };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Language metadata + detection for the code workspace (CODE-02, CODE-04).
3
+ *
4
+ * `backend` is the deliberate split the brief asks for:
5
+ * - 'browser' — runs with no install (JS/TS, Python via Pyodide, SQL via sql.js, HTML/CSS preview)
6
+ * - 'local' — needs the learner's own toolchain through `web/studio-runner.js`
7
+ *
8
+ * This module is pure (no DOM, no imports) so it is unit-testable and safe to use everywhere.
9
+ */
10
+
11
+ /** @typedef {'javascript'|'typescript'|'python'|'sql'|'html'|'css'|'go'|'java'|'rust'|'bash'} LanguageId */
12
+
13
+ /**
14
+ * @typedef {object} LanguageSpec
15
+ * @property {LanguageId} id
16
+ * @property {string} label
17
+ * @property {string[]} extensions lower-case, with the leading dot
18
+ * @property {'browser'|'local'} backend
19
+ * @property {string|null} codeMirror CodeMirror language key (Editor.jsx maps it to a grammar)
20
+ * @property {string} entry default entry filename
21
+ * @property {string} comment line-comment token (used when writing seeds / hints)
22
+ * @property {string} [command] local allowlist command
23
+ */
24
+
25
+ /** @type {Record<LanguageId, LanguageSpec>} */
26
+ export const LANGUAGES = {
27
+ javascript: {
28
+ id: 'javascript',
29
+ label: 'JavaScript',
30
+ extensions: ['.js', '.mjs', '.cjs', '.jsx'],
31
+ backend: 'browser',
32
+ codeMirror: 'javascript',
33
+ entry: 'main.js',
34
+ comment: '//',
35
+ command: 'node',
36
+ },
37
+ typescript: {
38
+ id: 'typescript',
39
+ label: 'TypeScript',
40
+ extensions: ['.ts', '.tsx', '.mts', '.cts'],
41
+ backend: 'browser',
42
+ codeMirror: 'typescript',
43
+ entry: 'main.ts',
44
+ comment: '//',
45
+ },
46
+ python: {
47
+ id: 'python',
48
+ label: 'Python',
49
+ extensions: ['.py', '.pyw'],
50
+ backend: 'browser',
51
+ codeMirror: 'python',
52
+ entry: 'main.py',
53
+ comment: '#',
54
+ command: 'python3',
55
+ },
56
+ sql: {
57
+ id: 'sql',
58
+ label: 'SQL',
59
+ extensions: ['.sql'],
60
+ backend: 'browser',
61
+ codeMirror: 'sql',
62
+ entry: 'main.sql',
63
+ comment: '--',
64
+ command: 'sqlite3',
65
+ },
66
+ html: {
67
+ id: 'html',
68
+ label: 'HTML',
69
+ extensions: ['.html', '.htm'],
70
+ backend: 'browser',
71
+ codeMirror: 'html',
72
+ entry: 'index.html',
73
+ comment: '<!--',
74
+ },
75
+ css: {
76
+ id: 'css',
77
+ label: 'CSS',
78
+ extensions: ['.css'],
79
+ backend: 'browser',
80
+ codeMirror: 'css',
81
+ entry: 'style.css',
82
+ comment: '/*',
83
+ },
84
+ go: {
85
+ id: 'go',
86
+ label: 'Go',
87
+ extensions: ['.go'],
88
+ backend: 'local',
89
+ codeMirror: 'go',
90
+ entry: 'main.go',
91
+ comment: '//',
92
+ command: 'go',
93
+ },
94
+ java: {
95
+ id: 'java',
96
+ label: 'Java',
97
+ extensions: ['.java'],
98
+ backend: 'local',
99
+ codeMirror: 'java',
100
+ entry: 'Main.java',
101
+ comment: '//',
102
+ command: 'javac',
103
+ },
104
+ rust: {
105
+ id: 'rust',
106
+ label: 'Rust',
107
+ extensions: ['.rs'],
108
+ backend: 'local',
109
+ codeMirror: 'rust',
110
+ entry: 'main.rs',
111
+ comment: '//',
112
+ command: 'rustc',
113
+ },
114
+ bash: {
115
+ id: 'bash',
116
+ label: 'Bash',
117
+ extensions: ['.sh', '.bash'],
118
+ backend: 'local',
119
+ codeMirror: null,
120
+ entry: 'main.sh',
121
+ comment: '#',
122
+ command: 'bash',
123
+ },
124
+ };
125
+
126
+ export const LANGUAGE_IDS = /** @type {LanguageId[]} */ (Object.keys(LANGUAGES));
127
+
128
+ /**
129
+ * Aliases seen in fenced-code info strings / course metadata.
130
+ * @type {Record<string, LanguageId>}
131
+ */
132
+ const ALIASES = {
133
+ js: 'javascript',
134
+ javascript: 'javascript',
135
+ mjs: 'javascript',
136
+ node: 'javascript',
137
+ jsx: 'javascript',
138
+ ts: 'typescript',
139
+ typescript: 'typescript',
140
+ tsx: 'typescript',
141
+ py: 'python',
142
+ python: 'python',
143
+ python3: 'python',
144
+ sql: 'sql',
145
+ sqlite: 'sql',
146
+ html: 'html',
147
+ htm: 'html',
148
+ css: 'css',
149
+ scss: 'css',
150
+ go: 'go',
151
+ golang: 'go',
152
+ java: 'java',
153
+ rust: 'rust',
154
+ rs: 'rust',
155
+ sh: 'bash',
156
+ bash: 'bash',
157
+ shell: 'bash',
158
+ };
159
+
160
+ /**
161
+ * Normalise a user/course-supplied language string to a known id.
162
+ * @param {string|null|undefined} value
163
+ * @returns {LanguageId|null}
164
+ */
165
+ export function normalizeLanguage(value) {
166
+ if (!value) return null;
167
+ const key = String(value).trim().toLowerCase().replace(/^language-/, '');
168
+ if (ALIASES[key]) return ALIASES[key];
169
+ if (LANGUAGES[/** @type {LanguageId} */ (key)]) return /** @type {LanguageId} */ (key);
170
+ return null;
171
+ }
172
+
173
+ /**
174
+ * @param {string|null|undefined} path
175
+ * @returns {LanguageId|null}
176
+ */
177
+ export function languageForPath(path) {
178
+ if (!path) return null;
179
+ const match = /\.([a-z0-9]+)$/i.exec(String(path));
180
+ if (!match) return null;
181
+ const ext = `.${match[1].toLowerCase()}`;
182
+ for (const spec of Object.values(LANGUAGES)) {
183
+ if (spec.extensions.includes(ext)) return spec.id;
184
+ }
185
+ return null;
186
+ }
187
+
188
+ /** @param {string} id */
189
+ export function getLanguage(id) {
190
+ return LANGUAGES[/** @type {LanguageId} */ (id)] || null;
191
+ }
192
+
193
+ /** @param {string} id */
194
+ export function isBrowserLanguage(id) {
195
+ return getLanguage(id)?.backend === 'browser';
196
+ }
197
+
198
+ /** @param {string} id */
199
+ export function isLocalLanguage(id) {
200
+ return getLanguage(id)?.backend === 'local';
201
+ }
202
+
203
+ /** @param {string} id */
204
+ export function defaultEntry(id) {
205
+ return getLanguage(id)?.entry || 'main.txt';
206
+ }
207
+
208
+ /**
209
+ * The languages the UI advertises for the "browser" runner.
210
+ * @returns {LanguageSpec[]}
211
+ */
212
+ export function browserLanguages() {
213
+ return Object.values(LANGUAGES).filter((l) => l.backend === 'browser');
214
+ }
215
+
216
+ /**
217
+ * Guess a course's primary language from its kind + lesson text.
218
+ * Programming courses usually name the language in the goal/topics/title.
219
+ * @param {{ kind?: string, title?: string, goal?: string, topics?: string[]|string, language?: string }} course
220
+ * @returns {LanguageId|null}
221
+ */
222
+ export function guessCourseLanguage(course = {}) {
223
+ const explicit = normalizeLanguage(course.language);
224
+ if (explicit) return explicit;
225
+
226
+ const haystack = [
227
+ course.title,
228
+ course.goal,
229
+ Array.isArray(course.topics) ? course.topics.join(' ') : course.topics,
230
+ ]
231
+ .filter(Boolean)
232
+ .join(' ')
233
+ .toLowerCase();
234
+ if (!haystack) return null;
235
+
236
+ // Ordered so more specific names win over substrings ("javascript" before "java").
237
+ const ordered = [
238
+ 'typescript',
239
+ 'javascript',
240
+ 'python',
241
+ 'rust',
242
+ 'golang',
243
+ 'go',
244
+ 'java',
245
+ 'sql',
246
+ 'html',
247
+ 'css',
248
+ 'bash',
249
+ ];
250
+ for (const needle of ordered) {
251
+ const re = new RegExp(`(^|[^a-z])${needle}([^a-z]|$)`);
252
+ if (re.test(haystack)) {
253
+ const id = normalizeLanguage(needle);
254
+ if (id) return id;
255
+ }
256
+ }
257
+ return null;
258
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Local runner client (CODE-04) — talks to `web/studio-runner.js` through the existing local
3
+ * agent bridge (`web/server.js` → `web/studio-api.js`), reusing the same token + CORS + LNA
4
+ * handling as the AI bridge (see ../ai/fetch-local.js).
5
+ *
6
+ * Endpoints (mounted by studio-api.js as `router.use('/run', createRunnerRouter(...))`):
7
+ * GET {bridge}/api/studio/run/toolchains -> { node: true, python3: true, go: false, … }
8
+ * POST {bridge}/api/studio/run -> SSE: {"stdout"}, {"stderr"}, {"exit"}, [DONE]
9
+ *
10
+ * Files are written server-side to `${PROFESSOR_HOME:-~/.professor}/workspaces/<courseSlug>/` so
11
+ * the learner can open the same folder in a real editor.
12
+ */
13
+ import { parseSSE } from '../ai/sse.js';
14
+ import { localFetch, classifyNetworkError, mapHttpError } from '../ai/fetch-local.js';
15
+ import { isAbortError, providerError } from '../ai/errors.js';
16
+ import { DEFAULT_BRIDGE_URL } from '../ai/bridge.js';
17
+ import { assertLoopbackBridgeUrl } from '../settings.js';
18
+
19
+ const RUN_HINT = 'Start the local server (`npx ronin-skillforce web` or `npm start`), then retry. Or pick a language that runs in the browser.';
20
+
21
+ /**
22
+ * @param {object} settings
23
+ * @returns {string} the runner API root (no trailing slash)
24
+ */
25
+ export function runnerBase(settings = {}) {
26
+ const root = String(settings.bridge?.bridgeUrl || settings.bridgeUrl || DEFAULT_BRIDGE_URL).replace(/\/$/, '');
27
+ assertLoopbackBridgeUrl(root);
28
+ return `${root}/api/studio/run`;
29
+ }
30
+
31
+ /**
32
+ * @param {object} settings
33
+ * @returns {{ 'Content-Type': string, 'X-Studio-Token'?: string }}
34
+ */
35
+ function headersFor(settings = {}) {
36
+ const token = settings.bridge?.bridgeToken || settings.bridgeToken;
37
+ return {
38
+ 'Content-Type': 'application/json',
39
+ ...(token ? { 'X-Studio-Token': token } : {}),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Which toolchains are installed on the server.
45
+ * @param {object} settings
46
+ * @param {{ signal?: AbortSignal }} [opts]
47
+ * @returns {Promise<Record<string, boolean>>}
48
+ */
49
+ export async function checkToolchains(settings = {}, { signal } = {}) {
50
+ const url = `${runnerBase(settings)}/toolchains`;
51
+ let res;
52
+ try {
53
+ res = await localFetch(url, { headers: headersFor(settings), signal });
54
+ } catch (err) {
55
+ if (isAbortError(err)) throw providerError('aborted', 'Stopped');
56
+ const mapped = classifyNetworkError(err, { url, what: 'the local runner', notRunningHint: RUN_HINT });
57
+ if (mapped.code === 'not_running') mapped.hint = RUN_HINT;
58
+ throw mapped;
59
+ }
60
+ if (!res.ok) throw await mapHttpError(res, { what: 'the local runner' });
61
+ return res.json();
62
+ }
63
+
64
+ /**
65
+ * @param {object} input
66
+ * @param {object} input.settings
67
+ * @param {string} [input.courseSlug]
68
+ * @param {string} input.language
69
+ * @param {Record<string,string>} input.files
70
+ * @param {string|null} [input.entry]
71
+ * @param {AbortSignal} [input.signal]
72
+ * @param {(chunk:{stream:'stdout'|'stderr',text:string})=>void} [input.onOutput]
73
+ * @param {number} [input.timeoutMs]
74
+ * @returns {Promise<import('./browser.js').RunResult>}
75
+ */
76
+ export async function runLocal({
77
+ settings = {},
78
+ courseSlug = 'workspace',
79
+ language,
80
+ files = {},
81
+ entry = null,
82
+ signal,
83
+ onOutput,
84
+ timeoutMs,
85
+ } = {}) {
86
+ const url = runnerBase(settings);
87
+ const started = Date.now();
88
+ let res;
89
+ try {
90
+ res = await localFetch(url, {
91
+ method: 'POST',
92
+ headers: headersFor(settings),
93
+ body: JSON.stringify({ courseSlug, language, files, entry, timeoutMs }),
94
+ signal,
95
+ });
96
+ } catch (err) {
97
+ if (isAbortError(err)) throw providerError('aborted', 'Stopped');
98
+ const mapped = classifyNetworkError(err, { url, what: 'the local runner', notRunningHint: RUN_HINT });
99
+ if (mapped.code === 'not_running') mapped.hint = RUN_HINT;
100
+ throw mapped;
101
+ }
102
+ if (!res.ok) throw await mapHttpError(res, { what: 'the local runner' });
103
+ if (!res.body) throw providerError('unknown', 'The local runner returned no stream');
104
+
105
+ let stdout = '';
106
+ let stderr = '';
107
+ let exitCode = null;
108
+ try {
109
+ for await (const data of parseSSE(res)) {
110
+ if (data === '[DONE]') break;
111
+ let json;
112
+ try {
113
+ json = JSON.parse(data);
114
+ } catch {
115
+ continue;
116
+ }
117
+ if (typeof json.stdout === 'string') {
118
+ stdout += json.stdout;
119
+ onOutput?.({ stream: 'stdout', text: json.stdout });
120
+ }
121
+ if (typeof json.stderr === 'string') {
122
+ stderr += json.stderr;
123
+ onOutput?.({ stream: 'stderr', text: json.stderr });
124
+ }
125
+ if (json.error) {
126
+ stderr += `${json.error}\n`;
127
+ onOutput?.({ stream: 'stderr', text: `${json.error}\n` });
128
+ }
129
+ if (typeof json.exit === 'number') exitCode = json.exit;
130
+ }
131
+ } catch (err) {
132
+ if (isAbortError(err)) {
133
+ return { stdout, stderr, exitCode, checks: [], durationMs: Date.now() - started, runner: 'local', aborted: true };
134
+ }
135
+ throw err;
136
+ }
137
+
138
+ return { stdout, stderr, exitCode: exitCode ?? 0, checks: [], durationMs: Date.now() - started, runner: 'local' };
139
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Path safety shared by the browser runner and `web/studio-runner.js` (PITFALLS: path traversal).
3
+ *
4
+ * A workspace file is addressed by a *relative* path (e.g. `main.py`, `src/util.py`). Anything
5
+ * absolute, containing a `..` segment, a NUL byte, or a Windows drive prefix is rejected so a
6
+ * crafted lesson/CLI payload can never write outside the workspace root.
7
+ *
8
+ * Pure module — no Node/DOM APIs — so both the SPA and the Express router import the same rules.
9
+ */
10
+
11
+ const WINDOWS_DRIVE = /^[a-zA-Z]:[\\/]/;
12
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
13
+ const BACKSLASH = /\\/;
14
+
15
+ /**
16
+ * @param {unknown} path
17
+ * @returns {boolean} true when `path` is a safe, relative, portable path
18
+ */
19
+ export function isSafeRelPath(path) {
20
+ if (typeof path !== 'string') return false;
21
+ const value = path.trim();
22
+ if (!value) return false;
23
+ if (value.length > 512) return false;
24
+ if (CONTROL_CHARS.test(value)) return false;
25
+ if (value.startsWith('/') || value.startsWith('~')) return false;
26
+ if (WINDOWS_DRIVE.test(value)) return false;
27
+ if (BACKSLASH.test(value)) return false; // enforce forward slashes for portability
28
+ if (value.endsWith('/')) return false;
29
+
30
+ const segments = value.split('/');
31
+ for (const segment of segments) {
32
+ if (segment === '' || segment === '.' || segment === '..') return false;
33
+ }
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * A course slug becomes a single folder name under the workspaces root.
39
+ * @param {unknown} slug
40
+ * @returns {boolean}
41
+ */
42
+ export function isSafeSlug(slug) {
43
+ if (typeof slug !== 'string') return false;
44
+ const value = slug.trim();
45
+ if (!value || value.length > 120) return false;
46
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(value)) return false;
47
+ if (value === '.' || value === '..') return false;
48
+ return true;
49
+ }
50
+
51
+ /**
52
+ * Kebab-case slug from an arbitrary title (used to derive a course folder name).
53
+ * @param {string} title
54
+ * @returns {string}
55
+ */
56
+ export function slugify(title) {
57
+ const out = String(title || '')
58
+ .normalize('NFKD')
59
+ .replace(/[\u0300-\u036f]/g, '')
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]+/g, '-')
62
+ .replace(/^-+|-+$/g, '')
63
+ .slice(0, 80);
64
+ return out || 'workspace';
65
+ }
66
+
67
+ /**
68
+ * Normalise a relative path's separators to forward slashes (does not resolve `..`).
69
+ * @param {string} path
70
+ * @returns {string}
71
+ */
72
+ export function toPosix(path) {
73
+ return String(path || '').replace(/\\/g, '/');
74
+ }
75
+
76
+ /**
77
+ * Resolve `rel` under `root` **and** assert the result stays inside `root`.
78
+ * Uses only string ops so it behaves identically in the browser and in Node.
79
+ * @param {string} root
80
+ * @param {string} rel
81
+ * @returns {string} the joined path (with the root's existing separators)
82
+ * @throws {Error} when `rel` is unsafe
83
+ */
84
+ export function resolveInside(root, rel) {
85
+ if (!isSafeRelPath(rel)) throw new Error(`Unsafe path: ${rel}`);
86
+ const sep = root.includes('\\') ? '\\' : '/';
87
+ const base = root.replace(/[\\/]+$/, '');
88
+ return `${base}${sep}${toPosix(rel).split('/').join(sep)}`;
89
+ }
90
+
91
+ /**
92
+ * Convert a `{ path: content }` map (or `[{path, content}]` list) into a normalised list.
93
+ * @param {Record<string,string>|Array<{path?:string, name?:string, content?:string}>|null|undefined} files
94
+ * @returns {Array<{ path: string, content: string }>}
95
+ */
96
+ export function normalizeFiles(files) {
97
+ if (!files) return [];
98
+ const rows = Array.isArray(files)
99
+ ? files.map((f) => ({ path: f?.path ?? f?.name, content: f?.content ?? '' }))
100
+ : Object.entries(files).map(([path, content]) => ({ path, content }));
101
+ return rows
102
+ .filter((f) => typeof f.path === 'string' && f.path)
103
+ .map((f) => ({ path: toPosix(f.path), content: String(f.content ?? '') }));
104
+ }