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,557 @@
1
+ /**
2
+ * `skillforce import [slug|--all] [--dry-run]` (CLI-07) — bring CLI courses into Studio.
3
+ *
4
+ * Reads `learning/{slug}/` (primary) and legacy `courses/{slug}/` through `course-reader.js` and
5
+ * writes them into the shared Studio SQLite DB. Everything goes through the repository or the same
6
+ * StorageAdapter, so Studio sees identical rows:
7
+ *
8
+ * COURSE.md → courses (title, level, kind, goal) + lessons (sections, status, dates)
9
+ * lectures/NN-*.md → lesson_versions (ordered)
10
+ * LECTURE.md → lesson_versions of the active lesson (appended, never overwritten)
11
+ * SCHEDULE.md → cards (due dates preserved)
12
+ * LEARNING-LOG.md → quiz_attempts / reflections where they map, else notes
13
+ *
14
+ * Idempotent: `source: 'cli'` plus a stable `external_id` (`cli:<slug>`, `cli:<slug>:<n>`, …) means
15
+ * re-running updates instead of duplicating. `--dry-run` prints the plan and writes nothing.
16
+ */
17
+ import { existsSync, readFileSync, readdirSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { findCourses, readCourse } from '../course-reader.js';
20
+ import { requireStudioNode } from '../node-version.js';
21
+ import { newCardState } from '../../studio/src/lib/srs.js';
22
+ import {
23
+ useAdapter,
24
+ createCourse,
25
+ updateCourse,
26
+ updateLesson,
27
+ addLessonVersion,
28
+ saveNote,
29
+ saveReflection,
30
+ saveQuizAttempt,
31
+ logActivity,
32
+ getCapstone,
33
+ createCapstone,
34
+ createCapstoneSubmission,
35
+ } from '../../studio/src/lib/repo/index.js';
36
+ import { parseRubricFromMarkdown } from '../../studio/src/lib/prompts/capstone.js';
37
+
38
+ const nowIso = () => new Date().toISOString();
39
+ const dateIso = (date) => (date ? `${date}T00:00:00.000Z` : nowIso());
40
+ const norm = (s) => String(s || '').trim().toLowerCase();
41
+
42
+ /** `**Goal**: …` from COURSE.md (not otherwise parsed by course-reader). */
43
+ export function readCourseGoal(dir) {
44
+ try {
45
+ const content = readFileSync(join(dir, 'COURSE.md'), 'utf-8');
46
+ const m = content.match(/^\s*[-*]?\s*\*\*Goal\*\*\s*:?\s*(.*?)\s*$/mi);
47
+ return m ? m[1].trim() : '';
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Read `CAPSTONE.md` (immutable brief; rubric parsed if present, else empty) and `COMPLETION.md`
55
+ * (frontmatter verdict + summary) for a course (CAP-05). Missing files yield null.
56
+ * @param {string} dir
57
+ * @param {string} slug
58
+ */
59
+ export function readCapstonePlan(dir, slug) {
60
+ const capstonePath = join(dir, 'CAPSTONE.md');
61
+ const capstone = existsSync(capstonePath)
62
+ ? {
63
+ brief_md: readFileSync(capstonePath, 'utf-8'),
64
+ externalId: `cli:${slug}:capstone`,
65
+ }
66
+ : null;
67
+ if (capstone) capstone.rubric = parseRubricFromMarkdown(capstone.brief_md);
68
+
69
+ const completionPath = join(dir, 'COMPLETION.md');
70
+ let completion = null;
71
+ if (existsSync(completionPath)) {
72
+ const raw = readFileSync(completionPath, 'utf-8');
73
+ const meta = parseFrontmatter(raw);
74
+ completion = {
75
+ content_md: meta.capstone_summary || stripFrontmatter(raw),
76
+ verdict: 'course_complete',
77
+ completed: meta.completed || null,
78
+ externalId: `cli:${slug}:completion`,
79
+ };
80
+ }
81
+ return { capstone, completion };
82
+ }
83
+
84
+ /** Parse simple `key: value` YAML frontmatter (no nesting — this file is flat). */
85
+ function parseFrontmatter(text) {
86
+ const match = String(text || '').match(/^---\s*\n([\s\S]*?)\n---/);
87
+ if (!match) return {};
88
+ const out = {};
89
+ for (const line of match[1].split('\n')) {
90
+ const kv = line.match(/^\s*([A-Za-z0-9_]+)\s*:\s*(.*)\s*$/);
91
+ if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, '').trim();
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function stripFrontmatter(text) {
97
+ return String(text || '').replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '').trim();
98
+ }
99
+
100
+ /** Archived lectures (sorted) + the current LECTURE.md, each with a stable external id. */
101
+ export function readLectureContents(course) {
102
+ const slug = course.slug;
103
+ const out = [];
104
+ const lecturesDir = join(course.dir, 'lectures');
105
+ if (existsSync(lecturesDir)) {
106
+ let files = [];
107
+ try { files = readdirSync(lecturesDir).filter((f) => f.endsWith('.md')).sort(); } catch { files = []; }
108
+ for (const filename of files) {
109
+ out.push({
110
+ filename,
111
+ current: false,
112
+ content_md: readFileSync(join(lecturesDir, filename), 'utf-8'),
113
+ externalId: `cli:${slug}:lecture:${filename}`,
114
+ });
115
+ }
116
+ }
117
+ const lecturePath = join(course.dir, 'LECTURE.md');
118
+ if (existsSync(lecturePath)) {
119
+ out.push({
120
+ filename: 'LECTURE.md',
121
+ current: true,
122
+ content_md: readFileSync(lecturePath, 'utf-8'),
123
+ externalId: `cli:${slug}:lecture:current`,
124
+ });
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Turn a parsed course into a deterministic import plan. Pure apart from reading lecture files.
131
+ * @param {object} course output of readCourse
132
+ * @param {{ now?: number }} [opts]
133
+ * @returns {object}
134
+ */
135
+ export function planCourseImport(course, { now = Date.now() } = {}) {
136
+ const slug = course.slug;
137
+ const courseId = `cli-course-${slug}`;
138
+ const goal = readCourseGoal(course.dir);
139
+ const lectures = readLectureContents(course);
140
+
141
+ const sectionList = course.sections.length
142
+ ? course.sections
143
+ : lectures.length
144
+ ? [{ n: '1', title: course.title, status: 'active', completedAt: null }]
145
+ : [];
146
+
147
+ const lessons = sectionList.map((s, i) => ({
148
+ n: s.n,
149
+ position: i + 1,
150
+ id: `cli-lesson-${slug}-${i + 1}`,
151
+ externalId: `cli:${slug}:${s.n ?? i + 1}`,
152
+ title: s.title,
153
+ status: s.status === 'active' ? 'active' : s.status === 'done' ? 'done' : 'todo',
154
+ completed_at: s.completedAt || null,
155
+ lectures: [],
156
+ }));
157
+
158
+ const assignLesson = (item) => {
159
+ if (!lessons.length) return null;
160
+ if (item.current) {
161
+ const n = course.currentSection?.n;
162
+ return lessons.find((l) => String(l.n) === String(n)) || lessons[lessons.length - 1];
163
+ }
164
+ const m = item.filename.match(/^(\d+)/);
165
+ if (m) {
166
+ const padded = m[1].padStart(2, '0');
167
+ const byNumber = lessons.find((l) => String(l.n).padStart(2, '0') === padded);
168
+ if (byNumber) return byNumber;
169
+ const byPosition = lessons[Number.parseInt(m[1], 10) - 1];
170
+ if (byPosition) return byPosition;
171
+ }
172
+ return lessons[0];
173
+ };
174
+
175
+ for (const lecture of lectures) {
176
+ const lesson = assignLesson(lecture);
177
+ if (lesson) lesson.lectures.push(lecture);
178
+ }
179
+
180
+ const cards = (course.cards || []).map((c, i) => ({
181
+ id: `cli-card-${slug}-${i + 1}`,
182
+ externalId: `cli:${slug}:card:${i + 1}`,
183
+ front: c.front,
184
+ back: '',
185
+ due_at: c.due ? `${c.due}T00:00:00.000Z` : new Date(now).toISOString(),
186
+ }));
187
+
188
+ const findLesson = (section) => {
189
+ const target = norm(section);
190
+ if (!target) return null;
191
+ return (
192
+ lessons.find((l) => norm(l.title) === target) ||
193
+ lessons.find((l) => target.includes(norm(l.title))) ||
194
+ null
195
+ );
196
+ };
197
+
198
+ const logs = (course.log || []).map((entry, i) => {
199
+ const verdict = entry.verdict || '';
200
+ const lesson = findLesson(entry.section);
201
+ const scoreMatch = verdict.match(/(\d+)\s*\/\s*(\d+)/);
202
+ let kind = 'note';
203
+ if (lesson && scoreMatch) kind = 'quiz';
204
+ else if (lesson && verdict) kind = 'reflection';
205
+ return {
206
+ index: i,
207
+ date: entry.date,
208
+ section: entry.section,
209
+ verdict,
210
+ lessonId: lesson ? lesson.id : null,
211
+ kind,
212
+ score: scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null,
213
+ total: scoreMatch ? Number.parseInt(scoreMatch[2], 10) : null,
214
+ externalId: `cli:${slug}:log:${i + 1}`,
215
+ };
216
+ });
217
+
218
+ const { capstone, completion } = readCapstonePlan(course.dir, slug);
219
+
220
+ return {
221
+ course: {
222
+ id: courseId,
223
+ externalId: `cli:${slug}`,
224
+ slug,
225
+ title: course.title,
226
+ level: course.level || null,
227
+ kind: course.kind,
228
+ goal,
229
+ },
230
+ lessons,
231
+ cards,
232
+ logs,
233
+ capstone,
234
+ completion,
235
+ versionCount: lessons.reduce((n, l) => n + l.lectures.length, 0),
236
+ };
237
+ }
238
+
239
+ /** Aggregate counters for one course (also used by --dry-run). */
240
+ function emptyStats() {
241
+ return { lessons: 0, versions: 0, cards: 0, quiz: 0, reflections: 0, notes: 0, capstones: 0, completions: 0 };
242
+ }
243
+
244
+ async function existingExternalIds(store, table, where) {
245
+ const rows = await store.list(table, where ? { where } : {});
246
+ return new Set(rows.map((r) => r.external_id).filter(Boolean));
247
+ }
248
+
249
+ /**
250
+ * Write one plan through the repository/adapter. Returns counts of what was (or would be) applied.
251
+ * @param {object} plan
252
+ * @param {object} store
253
+ * @param {{ dryRun?: boolean }} [opts]
254
+ */
255
+ export async function applyCourseImport(plan, store, { dryRun = false } = {}) {
256
+ const stats = emptyStats();
257
+ stats.lessons = plan.lessons.length;
258
+ stats.versions = plan.versionCount;
259
+ stats.cards = plan.cards.length;
260
+ stats.capstones = plan.capstone ? 1 : 0;
261
+ stats.completions = plan.completion ? 1 : 0;
262
+ for (const log of plan.logs) {
263
+ if (log.kind === 'quiz') stats.quiz += 1;
264
+ else if (log.kind === 'reflection') stats.reflections += 1;
265
+ else stats.notes += 1;
266
+ }
267
+ if (dryRun) return stats;
268
+
269
+ // Repository calls (createCourse, addLessonVersion, saveNote, …) must target this store.
270
+ useAdapter(store);
271
+
272
+ const { course } = plan;
273
+ const existingCourse = await store.get('courses', course.id);
274
+ if (existingCourse) {
275
+ await updateCourse(course.id, {
276
+ title: course.title,
277
+ level: course.level,
278
+ kind: course.kind,
279
+ goal: course.goal,
280
+ source: 'cli',
281
+ });
282
+ } else {
283
+ await createCourse({
284
+ id: course.id,
285
+ title: course.title,
286
+ level: course.level,
287
+ kind: course.kind,
288
+ goal: course.goal,
289
+ source: 'cli',
290
+ });
291
+ }
292
+ const courseRow = await store.get('courses', course.id);
293
+ if (courseRow && courseRow.external_id !== course.externalId) {
294
+ await store.put('courses', { ...courseRow, external_id: course.externalId });
295
+ }
296
+
297
+ for (const lesson of plan.lessons) {
298
+ const prev = await store.get('lessons', lesson.id);
299
+ if (prev) {
300
+ await updateLesson(lesson.id, {
301
+ title: lesson.title,
302
+ status: lesson.status,
303
+ completed_at: lesson.completed_at,
304
+ position: lesson.position,
305
+ });
306
+ const row = await store.get('lessons', lesson.id);
307
+ if (row && row.external_id !== lesson.externalId) {
308
+ await store.put('lessons', { ...row, external_id: lesson.externalId });
309
+ }
310
+ } else {
311
+ const now = nowIso();
312
+ await store.put('lessons', {
313
+ id: lesson.id,
314
+ owner_id: 'local',
315
+ course_id: course.id,
316
+ position: lesson.position,
317
+ title: lesson.title,
318
+ summary: '',
319
+ status: lesson.status,
320
+ completed_at: lesson.completed_at,
321
+ current_version_id: null,
322
+ quiz_seed: null,
323
+ external_id: lesson.externalId,
324
+ created_at: now,
325
+ updated_at: now,
326
+ deleted_at: null,
327
+ });
328
+ }
329
+
330
+ const seen = await existingExternalIds(store, 'lesson_versions', { lesson_id: lesson.id });
331
+ for (const lecture of lesson.lectures) {
332
+ if (seen.has(lecture.externalId)) continue;
333
+ const version = await addLessonVersion(lesson.id, {
334
+ content_md: lecture.content_md,
335
+ model: 'cli-import',
336
+ provider: 'cli',
337
+ });
338
+ await store.put('lesson_versions', { ...version, external_id: lecture.externalId });
339
+ }
340
+ }
341
+
342
+ const state = newCardState();
343
+ for (const card of plan.cards) {
344
+ if (await store.get('cards', card.id)) continue; // never clobber a graded card
345
+ const now = nowIso();
346
+ await store.put('cards', {
347
+ id: card.id,
348
+ owner_id: 'local',
349
+ course_id: course.id,
350
+ lesson_id: null,
351
+ front: card.front,
352
+ back: card.back,
353
+ ease: state.ease,
354
+ interval_days: state.interval,
355
+ reps: state.reps,
356
+ lapses: state.lapses,
357
+ due_at: card.due_at,
358
+ external_id: card.externalId,
359
+ created_at: now,
360
+ updated_at: now,
361
+ deleted_at: null,
362
+ });
363
+ }
364
+
365
+ await applyLogs(plan, store);
366
+ await applyCapstone(plan, store);
367
+
368
+ await logActivity('import', course.id);
369
+ return stats;
370
+ }
371
+
372
+ /**
373
+ * Import `CAPSTONE.md` into `capstones` (immutable — an existing capstone is kept) and
374
+ * `COMPLETION.md` into a `course_complete` submission. Idempotent via `external_id`.
375
+ * @param {object} plan
376
+ * @param {object} store
377
+ */
378
+ async function applyCapstone(plan, store) {
379
+ const { course } = plan;
380
+ if (!plan.capstone && !plan.completion) return;
381
+
382
+ let capstone = null;
383
+ if (plan.capstone) {
384
+ capstone = await createCapstone({
385
+ courseId: course.id,
386
+ kind: course.kind,
387
+ brief_md: plan.capstone.brief_md,
388
+ rubric: plan.capstone.rubric || [],
389
+ externalId: plan.capstone.externalId,
390
+ });
391
+ }
392
+
393
+ if (!plan.completion) return;
394
+ if (!capstone) capstone = await getCapstone(course.id);
395
+ if (!capstone) {
396
+ // A COMPLETION.md without a brief: create an (empty-brief) capstone so the submission can attach.
397
+ capstone = await createCapstone({
398
+ courseId: course.id,
399
+ kind: course.kind,
400
+ brief_md: '',
401
+ rubric: [],
402
+ externalId: `cli:${course.slug}:capstone`,
403
+ });
404
+ }
405
+
406
+ const submission = await createCapstoneSubmission({
407
+ capstoneId: capstone.id,
408
+ content_md: plan.completion.content_md || '',
409
+ verdict: plan.completion.verdict,
410
+ externalId: plan.completion.externalId,
411
+ });
412
+ if (plan.completion.completed && submission?.id) {
413
+ const at = dateIso(plan.completion.completed);
414
+ await store.put('capstone_submissions', { ...submission, created_at: at, updated_at: at });
415
+ }
416
+
417
+ const courseRow = await store.get('courses', course.id);
418
+ if (courseRow && courseRow.status !== 'completed') {
419
+ await updateCourse(course.id, { status: 'completed' });
420
+ }
421
+ }
422
+
423
+ async function applyLogs(plan, store) {
424
+ const notes = await store.list('notes', { where: { course_id: plan.course.id } });
425
+ const noteSet = new Set(notes.map((n) => n.content_md));
426
+ const reflectionsByLesson = new Map();
427
+ const quizByLesson = new Map();
428
+
429
+ for (const log of plan.logs) {
430
+ const created_at = dateIso(log.date);
431
+ if (log.kind === 'quiz') {
432
+ const list = quizByLesson.get(log.lessonId) || (await store.list('quiz_attempts', { where: { lesson_id: log.lessonId } }));
433
+ quizByLesson.set(log.lessonId, list);
434
+ const signature = `${log.score}|${log.total}|${created_at}`;
435
+ if (list.some((a) => `${a.score}|${a.total}|${a.created_at}` === signature)) continue;
436
+ const row = await saveQuizAttempt(log.lessonId, { score: log.score, total: log.total });
437
+ await store.put('quiz_attempts', { ...row, created_at, updated_at: created_at });
438
+ } else if (log.kind === 'reflection') {
439
+ const list = reflectionsByLesson.get(log.lessonId) || (await store.list('reflections', { where: { lesson_id: log.lessonId } }));
440
+ reflectionsByLesson.set(log.lessonId, list);
441
+ if (list.some((r) => r.note === log.verdict && r.hardest === log.section)) continue;
442
+ const row = await saveReflection(log.lessonId, { hardest: log.section, note: log.verdict });
443
+ await store.put('reflections', { ...row, created_at, updated_at: created_at });
444
+ } else {
445
+ const content_md = formatLogNote(log);
446
+ if (noteSet.has(content_md)) continue;
447
+ const row = await saveNote({ courseId: plan.course.id, lessonId: log.lessonId, content_md });
448
+ await store.put('notes', { ...row, created_at, updated_at: created_at });
449
+ noteSet.add(content_md);
450
+ }
451
+ }
452
+ }
453
+
454
+ /** @param {object} log */
455
+ export function formatLogNote(log) {
456
+ const heading = [log.date, log.section].filter(Boolean).join(' · ');
457
+ return heading ? `**${heading}**\n${log.verdict}` : log.verdict;
458
+ }
459
+
460
+ /**
461
+ * Import one course from disk into Studio storage. Shared by `import` and `install` so both write
462
+ * identical rows and stay idempotent through the same stable external ids.
463
+ * @param {string} cwd
464
+ * @param {string} slug
465
+ * @param {{ store?: object }} [opts] reuse an open store instead of opening the Studio DB
466
+ * @returns {Promise<{ course: object, plan: object, stats: object }|null>} null when the course is absent
467
+ */
468
+ export async function importCourse(cwd, slug, { store = null } = {}) {
469
+ const found = findCourses(cwd).find((c) => c.slug === slug);
470
+ if (!found) return null;
471
+
472
+ const ownStore = !store;
473
+ // `studio-store` (→ `web/studio-db.js`) imports `node:sqlite`; callers check the Node floor first.
474
+ const activeStore = store || (await (await import('../studio-store.js')).installStudioStore());
475
+ try {
476
+ const course = readCourse(found.dir, { slug: found.slug });
477
+ const plan = planCourseImport(course);
478
+ const stats = await applyCourseImport(plan, activeStore);
479
+ return { course, plan, stats };
480
+ } finally {
481
+ if (ownStore && activeStore.close) activeStore.close();
482
+ }
483
+ }
484
+
485
+ /**
486
+ * @param {{ args?: string[], cwd?: string, chalk?: object, style?: Function, store?: object }} [ctx]
487
+ */
488
+ export async function run(ctx = {}) {
489
+ requireStudioNode(ctx.nodeVersion);
490
+ const { args = [], cwd = process.cwd(), chalk, style = (t, m) => m } = ctx;
491
+ const dryRun = args.includes('--dry-run');
492
+ const slug = args.find((a) => !a.startsWith('--')) || null;
493
+ const all = args.includes('--all') || !slug;
494
+
495
+ const courses = findCourses(cwd);
496
+ const selected = slug ? courses.filter((c) => c.slug === slug) : courses;
497
+
498
+ if (slug && selected.length === 0) {
499
+ console.error(style('error', `No course "${slug}" found in learning/ or courses/.`));
500
+ process.exitCode = 1;
501
+ return { courses: 0, error: `not found: ${slug}` };
502
+ }
503
+
504
+ if (selected.length === 0) {
505
+ console.log('');
506
+ console.log(style('info', 'No courses found in learning/ or courses/.'));
507
+ console.log('');
508
+ return { courses: 0 };
509
+ }
510
+
511
+ const ownStore = !ctx.store;
512
+ // `studio-store` (→ `web/studio-db.js`) imports `node:sqlite`, which only exists on Node 22.5+.
513
+ // Import it lazily so the version guard above runs first and prints a helpful message.
514
+ const store = ctx.store || (await (await import('../studio-store.js')).installStudioStore());
515
+ useAdapter(store);
516
+
517
+ const totals = emptyStats();
518
+ try {
519
+ for (const found of selected) {
520
+ const course = readCourse(found.dir, { slug: found.slug });
521
+ const plan = planCourseImport(course);
522
+ const stats = await applyCourseImport(plan, store, { dryRun });
523
+
524
+ for (const key of Object.keys(totals)) totals[key] += stats[key];
525
+
526
+ const verb = dryRun ? 'would import' : 'imported';
527
+ console.log('');
528
+ console.log(chalk ? chalk.bold(course.slug) : course.slug);
529
+ console.log(` ${course.title} (${course.kind}${course.level ? `, ${course.level}` : ''})`);
530
+ console.log(` ${verb}: ${stats.lessons} lessons · ${stats.versions} lecture versions · ${stats.cards} cards`);
531
+ console.log(` ${stats.quiz} quiz attempts · ${stats.reflections} reflections · ${stats.notes} notes`);
532
+ if (stats.capstones || stats.completions) {
533
+ console.log(` ${stats.capstones} capstone · ${stats.completions} completion(s)`);
534
+ }
535
+
536
+ if (dryRun) {
537
+ for (const lesson of plan.lessons) {
538
+ console.log(` lesson ${lesson.position}. ${lesson.title} [${lesson.status}]${lesson.lectures.length ? ` ← ${lesson.lectures.length} version(s)` : ''}`);
539
+ }
540
+ }
541
+ }
542
+
543
+ console.log('');
544
+ const verb = dryRun ? 'Would import' : 'Imported';
545
+ console.log(
546
+ style('success',
547
+ `${verb} ${selected.length} course${selected.length === 1 ? '' : 's'}: ` +
548
+ `${totals.lessons} lessons, ${totals.versions} versions, ${totals.cards} cards, ` +
549
+ `${totals.quiz + totals.reflections + totals.notes} log entries, ` +
550
+ `${totals.capstones} capstones, ${totals.completions} completions.`)
551
+ );
552
+ console.log('');
553
+ return { courses: selected.length, totals, dryRun, all };
554
+ } finally {
555
+ if (ownStore && store.close) store.close();
556
+ }
557
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `skillforce next [slug]` — show the active lecture summary and the
3
+ * agent command to continue. Never prints solutions.
4
+ */
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { findCourses, parseLecture, readCourse, resolveExercise } from '../course-reader.js';
8
+ import { detectAgents } from './doctor.js';
9
+ import { style } from '../registry-helpers.js';
10
+
11
+ const AGENT_COMMANDS = {
12
+ claude: '/professor:next',
13
+ opencode: '/professor:next',
14
+ gemini: '/professor:next',
15
+ cursor: 'professor:next',
16
+ };
17
+
18
+ function flagValue(args, name) {
19
+ const i = args.indexOf(name);
20
+ if (i === -1) return null;
21
+ const next = args[i + 1];
22
+ return next && !next.startsWith('--') ? next : true;
23
+ }
24
+
25
+ function positionals(args) {
26
+ const out = [];
27
+ for (let i = 0; i < args.length; i++) {
28
+ if (args[i] === '--dir') { i++; continue; }
29
+ if (args[i].startsWith('--')) continue;
30
+ out.push(args[i]);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Resolve which course `next` should show.
37
+ * @param {object[]} courses
38
+ * @param {string|null} slug
39
+ * @returns {object|null}
40
+ */
41
+ export function resolveActiveCourse(courses, slug) {
42
+ if (slug) return courses.find((c) => c.slug === slug) || null;
43
+ const active = courses.filter((c) => c.currentSection && c.currentSection.status === 'active');
44
+ const pool = active.length ? active : courses;
45
+ const byRecency = (a, b) => (b.lastActive || '').localeCompare(a.lastActive || '');
46
+ return [...pool].sort(byRecency)[0] || null;
47
+ }
48
+
49
+ /**
50
+ * @param {{ args: string[], cwd?: string, chalk?: object }} ctx
51
+ */
52
+ export async function run({ args = [], cwd = process.cwd() } = {}) {
53
+ const slug = positionals(args)[0] || null;
54
+ const dir = flagValue(args, '--dir');
55
+ const courses = findCourses(cwd, { dir: typeof dir === 'string' ? dir : undefined });
56
+
57
+ if (courses.length === 0) {
58
+ console.log('');
59
+ console.log(style('info', 'No course found. Start one with /professor:new-topic.'));
60
+ console.log('');
61
+ return;
62
+ }
63
+
64
+ const descriptors = courses.map((c) => readCourse(c.dir, { slug: c.slug }));
65
+ const chosen = resolveActiveCourse(descriptors, slug);
66
+
67
+ if (!chosen) {
68
+ console.log(style('error', `Course "${slug}" not found.`));
69
+ console.log(`Available: ${courses.map((c) => c.slug).join(', ')}`);
70
+ return;
71
+ }
72
+
73
+ const lecturePath = join(chosen.dir, 'LECTURE.md');
74
+ const lecture = existsSync(lecturePath)
75
+ ? parseLecture(readFileSync(lecturePath, 'utf-8'))
76
+ : null;
77
+
78
+ const section = chosen.currentSection;
79
+ console.log('');
80
+ const meta = `${chosen.kind}${chosen.level ? `, ${chosen.level}` : ''}`;
81
+ console.log(style('info', `Course: ${chosen.title} (${meta})`));
82
+
83
+ if (!section) {
84
+ console.log(' All sections are done. It may be capstone time 🏗️');
85
+ console.log('');
86
+ return;
87
+ }
88
+
89
+ const heading = lecture && lecture.title ? lecture.title : section.title;
90
+ console.log(` Section ${section.n}: ${heading}`);
91
+ console.log(` Status: ${section.status === 'active' ? '🔄 In progress' : '⬜ Not started'}`);
92
+ console.log('');
93
+
94
+ if (lecture?.objectives?.length) {
95
+ console.log(' 🎯 Objectives');
96
+ for (const objective of lecture.objectives) console.log(` - ${objective}`);
97
+ console.log('');
98
+ }
99
+
100
+ if (lecture?.explanation) {
101
+ console.log(' 🧠 Explanation (first paragraph)');
102
+ console.log(` ${lecture.explanation}`);
103
+ console.log('');
104
+ }
105
+
106
+ const exercise = resolveExercise(chosen, { lecture });
107
+ if (exercise.path) {
108
+ const pending = exercise.exists ? '' : ' (not created yet — run professor:next)';
109
+ console.log(` 🛠️ Exercise: ${exercise.path}${pending}`);
110
+ if (lecture?.successCriteria?.length) {
111
+ console.log(' Success criteria:');
112
+ for (const criterion of lecture.successCriteria) console.log(` - [ ] ${criterion}`);
113
+ }
114
+ console.log('');
115
+ }
116
+
117
+ const agents = await detectAgents();
118
+ const list = agents.length ? agents : ['claude'];
119
+ console.log(' Continue in your agent:');
120
+ for (const agent of list) {
121
+ const command = AGENT_COMMANDS[agent] || '/professor:next';
122
+ console.log(` ${agent.padEnd(9)} ${command}`);
123
+ }
124
+ console.log('');
125
+ }