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,298 @@
1
+ /**
2
+ * `skillforce check [slug]` — run the active exercise's self-checks
3
+ * with the right toolchain. Shows pass/fail per check, never solutions.
4
+ */
5
+ import { appendFileSync, existsSync, readFileSync, statSync } from 'fs';
6
+ import { join, extname } from 'path';
7
+ import { spawn } from 'child_process';
8
+ import inquirer from 'inquirer';
9
+ import {
10
+ findCourses, parseLecture, readCourse, resolveExercise, todayISO,
11
+ } from '../course-reader.js';
12
+ import { resolveActiveCourse } from './next.js';
13
+ import { style } from '../registry-helpers.js';
14
+
15
+ const TIMEOUT_MS = 30000;
16
+
17
+ function flagValue(args, name) {
18
+ const i = args.indexOf(name);
19
+ if (i === -1) return null;
20
+ const next = args[i + 1];
21
+ return next && !next.startsWith('--') ? next : true;
22
+ }
23
+
24
+ function positionals(args) {
25
+ const out = [];
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i] === '--dir') { i++; continue; }
28
+ if (args[i].startsWith('--')) continue;
29
+ out.push(args[i]);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /**
35
+ * Map a file extension to a runner strategy.
36
+ * @param {string} file
37
+ * @returns {{ kind: 'run'|'tsx'|'explain'|'rubric', cmd?: string, args?: string[], note?: string }}
38
+ */
39
+ export function resolveRunner(file) {
40
+ const ext = extname(file).toLowerCase();
41
+ switch (ext) {
42
+ case '.py': return { kind: 'run', cmd: 'python3', args: [file] };
43
+ case '.js':
44
+ case '.mjs': return { kind: 'run', cmd: 'node', args: [file] };
45
+ case '.ts':
46
+ case '.tsx': return { kind: 'tsx', cmd: 'npx', args: ['--yes', 'tsx', file] };
47
+ case '.go': return { kind: 'run', cmd: 'go', args: ['run', file] };
48
+ case '.rs':
49
+ return { kind: 'explain', note: 'Rust: compile with `rustc <file> -o out && ./out`.' };
50
+ case '.sql':
51
+ return {
52
+ kind: 'explain',
53
+ note: 'SQL: no DB is configured — review the expected result in the file.',
54
+ };
55
+ case '.md': return { kind: 'rubric' };
56
+ default:
57
+ return {
58
+ kind: 'explain',
59
+ note: `No CLI runner for "${ext || 'this file'}" — open it and follow its instructions.`,
60
+ };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Parse self-check output into per-check pass/fail results.
66
+ * @param {{ stdout?: string, stderr?: string, code?: number }} result
67
+ * @returns {{ checks: object[], allPass: boolean, code: number, errorLine: string|null }}
68
+ */
69
+ export function parseCheckOutput({ stdout = '', stderr = '', code = 0 } = {}) {
70
+ const checks = [];
71
+ const lines = `${stdout}\n${stderr}`.split('\n');
72
+ for (const line of lines) {
73
+ let m = line.match(/^\s*[✓✔]\s*(.+?)\s*$/);
74
+ if (m) { checks.push({ name: m[1], pass: true }); continue; }
75
+ m = line.match(/^\s*[✗✘×]\s*(.+?)\s*$/);
76
+ if (m) { checks.push({ name: m[1], pass: false }); continue; }
77
+ m = line.match(/^\s*PASS\s*[:—–-]\s*(.+?)\s*$/i);
78
+ if (m) { checks.push({ name: m[1], pass: true }); continue; }
79
+ m = line.match(/^\s*FAIL\s*[:—–-]\s*(.+?)\s*$/i);
80
+ if (m) { checks.push({ name: m[1], pass: false }); continue; }
81
+ }
82
+
83
+ const trimmed = lines.map((l) => l.trim()).filter(Boolean);
84
+ // `console.assert` (JS) and Python `assert` failures: the former does not set
85
+ // a non-zero exit code, so scan the output too.
86
+ const assertRe = /assert(ion)?\s*(error|fail(ed)?)|ERR_ASSERTION/i;
87
+ const assertion = trimmed.find((l) => assertRe.test(l)) || null;
88
+ const errorLine = assertion
89
+ || trimmed.find((l) => /assert|error:/i.test(l))
90
+ || trimmed.find((l) => /error|traceback|exception|not ok/i.test(l))
91
+ || trimmed[0] || null;
92
+
93
+ const allPass = code === 0 && !checks.some((c) => !c.pass) && !assertion;
94
+ return { checks, allPass, code, errorLine, assertionFailed: Boolean(assertion) };
95
+ }
96
+
97
+ function runProcess(cmd, args, cwd) {
98
+ return new Promise((resolve) => {
99
+ let stdout = '';
100
+ let stderr = '';
101
+ let settled = false;
102
+ let child;
103
+ try {
104
+ child = spawn(cmd, args, { cwd, shell: false });
105
+ } catch (err) {
106
+ resolve({ stdout, stderr: err.message, code: 127 });
107
+ return;
108
+ }
109
+
110
+ const finish = (result) => {
111
+ if (settled) return;
112
+ settled = true;
113
+ clearTimeout(timer);
114
+ resolve(result);
115
+ };
116
+ const timer = setTimeout(() => {
117
+ try { child.kill('SIGKILL'); } catch {}
118
+ finish({ stdout, stderr: `${stderr}\nTimed out after 30s`, code: 124 });
119
+ }, TIMEOUT_MS);
120
+
121
+ child.stdout?.on('data', (d) => { stdout += d; });
122
+ child.stderr?.on('data', (d) => { stderr += d; });
123
+ child.on('error', (err) => finish({ stdout, stderr: `${stderr}\n${err.message}`, code: 127 }));
124
+ child.on('close', (code) => finish({ stdout, stderr, code: code ?? 1 }));
125
+ });
126
+ }
127
+
128
+ function whichPath(cmd) {
129
+ const sep = process.platform === 'win32' ? ';' : ':';
130
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
131
+ for (const dir of (process.env.PATH || '').split(sep)) {
132
+ for (const ext of exts) {
133
+ const full = join(dir, cmd + ext);
134
+ try { if (statSync(full).isFile()) return full; } catch {}
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function checklistFrom(filePath, lecture) {
141
+ if (filePath && existsSync(filePath)) {
142
+ const text = readFileSync(filePath, 'utf-8');
143
+ const items = text.split('\n')
144
+ .map((l) => l.match(/^\s*[-*]\s*\[[ xX]\]\s*(.+?)\s*$/))
145
+ .filter(Boolean)
146
+ .map((m) => m[1]);
147
+ if (items.length) return items;
148
+ }
149
+ return lecture?.successCriteria || [];
150
+ }
151
+
152
+ async function runRubric({ course, exerciseFile, lecture }) {
153
+ const items = checklistFrom(exerciseFile, lecture);
154
+
155
+ console.log('');
156
+ console.log(` 📋 Self-assessment: ${course.slug}`);
157
+ if (!items.length) {
158
+ console.log(' No rubric items found in the exercise. Read it and self-assess.');
159
+ console.log('');
160
+ return;
161
+ }
162
+ for (const [i, item] of items.entries()) console.log(` ${i + 1}. ${item}`);
163
+ console.log('');
164
+
165
+ if (!process.stdin.isTTY) {
166
+ console.log(style('info', 'Run this in a terminal to self-assess each item (y/n).'));
167
+ console.log('');
168
+ return;
169
+ }
170
+
171
+ const answers = [];
172
+ for (const item of items) {
173
+ const { done } = await inquirer.prompt([{
174
+ type: 'confirm', name: 'done', message: item, default: true,
175
+ }]);
176
+ answers.push({ item, done });
177
+ }
178
+
179
+ const passed = answers.filter((a) => a.done).length;
180
+ const verdict = passed === answers.length ? 'solid' : '⚠️ needs work';
181
+ const section = course.currentSection
182
+ ? `${course.currentSection.n} — ${course.currentSection.title}`
183
+ : 'General';
184
+ const entry = [
185
+ '',
186
+ `### ${section}`,
187
+ '',
188
+ `- Date: ${todayISO()}`,
189
+ '- Check: self-assessed (CLI check)',
190
+ ...answers.map((a) => `- Q: "${a.item}" → A: ${a.done ? 'yes' : 'no'}`),
191
+ `- Verdict: ${verdict}`,
192
+ '',
193
+ ].join('\n');
194
+
195
+ const logPath = join(course.dir, 'LEARNING-LOG.md');
196
+ const header = existsSync(logPath) ? '' : '# Learning Log\n\n## 📝 Section Entries\n';
197
+ try {
198
+ appendFileSync(logPath, header + entry);
199
+ console.log('');
200
+ const saved = `Self-assessment saved (${passed}/${answers.length} criteria met).`;
201
+ console.log(style('success', saved));
202
+ } catch (err) {
203
+ console.log(style('warning', `Could not save self-assessment: ${err.message}`));
204
+ }
205
+ console.log('');
206
+ }
207
+
208
+ /**
209
+ * @param {{ args: string[], cwd?: string }} ctx
210
+ */
211
+ export async function run({ args = [], cwd = process.cwd() } = {}) {
212
+ const slug = positionals(args)[0] || null;
213
+ const dir = flagValue(args, '--dir');
214
+ const courses = findCourses(cwd, { dir: typeof dir === 'string' ? dir : undefined });
215
+ if (courses.length === 0) {
216
+ console.log(style('info', 'No course found. Start one with /professor:new-topic.'));
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+
221
+ const descriptors = courses.map((c) => readCourse(c.dir, { slug: c.slug }));
222
+ const course = resolveActiveCourse(descriptors, slug);
223
+ if (!course) {
224
+ console.log(style('error', `Course "${slug}" not found.`));
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+
229
+ const lecturePath = join(course.dir, 'LECTURE.md');
230
+ const lecture = existsSync(lecturePath) ? parseLecture(readFileSync(lecturePath, 'utf-8')) : null;
231
+
232
+ const exercise = resolveExercise(course, { lecture });
233
+ if (!exercise.path) {
234
+ const hint = `No active exercise for "${course.slug}". Run professor:next first.`;
235
+ console.log(style('info', hint));
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+ if (!exercise.exists) {
240
+ console.log('');
241
+ console.log(style('error', `Exercise not found: ${exercise.path}`));
242
+ console.log(` Expected at: ${join(course.dir, exercise.path)}`);
243
+ console.log(' Run professor:next to create it.');
244
+ console.log('');
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+
249
+ const exerciseFile = join(course.dir, exercise.path);
250
+ const runner = resolveRunner(exerciseFile);
251
+ console.log('');
252
+ console.log(style('info', `Check: ${course.slug} → ${exercise.path}`));
253
+
254
+ if (runner.kind === 'rubric') {
255
+ await runRubric({ course, exerciseFile, lecture });
256
+ return;
257
+ }
258
+
259
+ if (runner.kind === 'explain') {
260
+ console.log(` ℹ️ ${runner.note}`);
261
+ console.log('');
262
+ return;
263
+ }
264
+
265
+ const tsxAvailable = whichPath('tsx')
266
+ || existsSync(join(course.dir, 'node_modules', '.bin', 'tsx'));
267
+ if (runner.kind === 'tsx' && !tsxAvailable) {
268
+ console.log(' ℹ️ TypeScript needs `tsx` (or `ts-node`) to run from the CLI.');
269
+ console.log(' Install it: npm i -D tsx — or run with `npx tsx <file>`.');
270
+ console.log('');
271
+ return;
272
+ }
273
+
274
+ const result = await runProcess(runner.cmd, runner.args, course.dir);
275
+ const parsed = parseCheckOutput(result);
276
+
277
+ if (parsed.checks.length) {
278
+ for (const check of parsed.checks) {
279
+ const mark = check.pass ? '✓' : '✗';
280
+ const line = ` ${mark} ${check.name}`;
281
+ console.log(check.pass ? line : style('error', line));
282
+ }
283
+ }
284
+
285
+ console.log('');
286
+ if (parsed.allPass) {
287
+ const message = parsed.checks.length
288
+ ? 'All checks passed.'
289
+ : 'Ran cleanly (no explicit checks).';
290
+ console.log(style('success', message));
291
+ } else {
292
+ console.log(style('error', `Check failed (exit ${parsed.code}). First error:`));
293
+ if (parsed.errorLine) console.log(` ${parsed.errorLine}`);
294
+ console.log(' Fix it yourself, then run check again. (No solution given.)');
295
+ process.exitCode = 1;
296
+ }
297
+ console.log('');
298
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * `skillforce create <slug> --kind <kind>` — create the course folder the
3
+ * way `professor:new-topic` promises, so the agent never improvises it (RON-249).
4
+ *
5
+ * - `programming`: a registered git worktree at `learning/<slug>/` on branch
6
+ * `learning/<slug>`, verified after creation; rolled back on any failure.
7
+ * - other kinds: a plain `learning/<slug>/` folder.
8
+ *
9
+ * `skillforce save <slug> [-m msg]` commits the course worktree so its
10
+ * state lives in git history, not only in untracked files.
11
+ */
12
+ import { execFileSync } from 'child_process';
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'fs';
14
+ import { isAbsolute, join, relative } from 'path';
15
+ import { COURSE_KINDS } from '../course-reader.js';
16
+ import { style } from '../registry-helpers.js';
17
+
18
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
19
+
20
+ function git(cwd, args) {
21
+ return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
22
+ }
23
+
24
+ function tryGit(cwd, args) {
25
+ try { return git(cwd, args); } catch { return null; }
26
+ }
27
+
28
+ /** Registered worktrees as `{ path, branch }` (branch without `refs/heads/`). */
29
+ export function listWorktrees(cwd) {
30
+ const out = tryGit(cwd, ['worktree', 'list', '--porcelain']) || '';
31
+ const trees = [];
32
+ for (const block of out.split('\n\n')) {
33
+ const path = block.match(/^worktree (.+)$/m)?.[1];
34
+ if (!path) continue;
35
+ const branch = block.match(/^branch refs\/heads\/(.+)$/m)?.[1] || null;
36
+ trees.push({ path: realpathSync.native(path), branch });
37
+ }
38
+ return trees;
39
+ }
40
+
41
+ function isCourseWorktree(cwd, dir, branch) {
42
+ if (!existsSync(dir)) return false;
43
+ const real = realpathSync.native(dir);
44
+ return listWorktrees(cwd).some((t) => t.path === real && t.branch === branch);
45
+ }
46
+
47
+ /** Keep `learning/` out of the host repo's `git status` without touching tracked files. */
48
+ function excludeLearningDir(root, cwd) {
49
+ const probe = join(cwd, 'learning');
50
+ if (tryGit(root, ['check-ignore', '-q', probe]) !== null) return;
51
+ const commonDir = git(root, ['rev-parse', '--git-common-dir']);
52
+ const infoDir = join(isAbsolute(commonDir) ? commonDir : join(root, commonDir), 'info');
53
+ mkdirSync(infoDir, { recursive: true });
54
+ const excludeFile = join(infoDir, 'exclude');
55
+ const line = `/${relative(root, probe).split('\\').join('/')}/`;
56
+ const current = existsSync(excludeFile) ? readFileSync(excludeFile, 'utf-8') : '';
57
+ if (current.split('\n').includes(line)) return;
58
+ appendFileSync(excludeFile, `${current && !current.endsWith('\n') ? '\n' : ''}${line}\n`);
59
+ }
60
+
61
+ /**
62
+ * Create (or reattach) the course directory. Throws with a user-facing message.
63
+ * @returns {{ slug: string, kind: string, dir: string, branch: string|null, created: boolean }}
64
+ */
65
+ export function createCourse({ cwd, slug, kind }) {
66
+ if (!SLUG_RE.test(slug || '')) {
67
+ throw new Error(`Invalid slug ${JSON.stringify(slug)} — use lowercase a-z, 0-9 and single hyphens.`);
68
+ }
69
+ if (!COURSE_KINDS.includes(kind)) {
70
+ throw new Error(`Invalid kind ${JSON.stringify(kind)} — use one of: ${COURSE_KINDS.join(', ')}.`);
71
+ }
72
+ const dir = join(cwd, 'learning', slug);
73
+
74
+ if (kind !== 'programming') {
75
+ const created = !existsSync(dir);
76
+ mkdirSync(dir, { recursive: true });
77
+ return { slug, kind, dir, branch: null, created };
78
+ }
79
+
80
+ const root = tryGit(cwd, ['rev-parse', '--show-toplevel']);
81
+ if (!root) {
82
+ throw new Error('Programming courses require a git repository. Run `git init` and make a first commit.');
83
+ }
84
+ if (tryGit(root, ['rev-parse', '--verify', '-q', 'HEAD']) === null) {
85
+ throw new Error('This git repository has no commits yet. Make a first commit, then retry.');
86
+ }
87
+
88
+ const branch = `learning/${slug}`;
89
+ if (isCourseWorktree(root, dir, branch)) {
90
+ excludeLearningDir(root, cwd);
91
+ return { slug, kind, dir, branch, created: false };
92
+ }
93
+ if (existsSync(dir)) {
94
+ throw new Error(`${relative(cwd, dir)}/ already exists but is not the ${branch} worktree. ` +
95
+ 'Move it aside or pick another slug — nothing was changed.');
96
+ }
97
+
98
+ const branchExists = tryGit(root, ['rev-parse', '--verify', '-q', `refs/heads/${branch}`]) !== null;
99
+ try {
100
+ // Reattach an existing branch (course restored after its worktree was pruned).
101
+ git(root, ['worktree', 'add', dir, ...(branchExists ? [branch] : ['-b', branch])]);
102
+ if (!isCourseWorktree(root, dir, branch)) throw new Error('worktree is not registered after creation');
103
+ } catch (err) {
104
+ tryGit(root, ['worktree', 'remove', '--force', dir]);
105
+ if (!branchExists) tryGit(root, ['branch', '-D', branch]);
106
+ rmSync(dir, { recursive: true, force: true });
107
+ tryGit(root, ['worktree', 'prune']);
108
+ const detail = (err.stderr || err.message || '').toString().trim();
109
+ throw new Error(`Could not create the course worktree — nothing was kept. ${detail}`);
110
+ }
111
+ excludeLearningDir(root, cwd);
112
+ return { slug, kind, dir, branch, created: true };
113
+ }
114
+
115
+ /**
116
+ * Commit everything in a course worktree. No-op for plain-folder courses.
117
+ * @returns {{ committed: boolean, reason?: string }}
118
+ */
119
+ export function saveCourse({ cwd, slug, message }) {
120
+ if (!SLUG_RE.test(slug || '')) throw new Error(`Invalid slug ${JSON.stringify(slug)}.`);
121
+ const dir = join(cwd, 'learning', slug);
122
+ if (!existsSync(join(dir, 'COURSE.md'))) throw new Error(`No course at learning/${slug}/.`);
123
+ if (!isCourseWorktree(cwd, dir, `learning/${slug}`)) {
124
+ return { committed: false, reason: 'not a course worktree (plain-folder course)' };
125
+ }
126
+ git(dir, ['add', '-A']);
127
+ if (tryGit(dir, ['diff', '--cached', '--quiet']) !== null) {
128
+ return { committed: false, reason: 'nothing to save' };
129
+ }
130
+ git(dir, ['commit', '-q', '--no-verify', '-m', message || `course(${slug}): save progress`]);
131
+ return { committed: true };
132
+ }
133
+
134
+ function flagValue(args, name) {
135
+ const i = args.indexOf(name);
136
+ return i !== -1 && args[i + 1] && !args[i + 1].startsWith('-') ? args[i + 1] : null;
137
+ }
138
+
139
+ function positionals(args) {
140
+ const out = [];
141
+ for (let i = 0; i < args.length; i++) {
142
+ if (['--kind', '-m', '--message'].includes(args[i])) { i++; continue; }
143
+ if (!args[i].startsWith('-')) out.push(args[i]);
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /** `create` and `save` share this module; cli.js passes the command name. */
149
+ export async function run({ args = [], cwd = process.cwd(), command = 'create' } = {}) {
150
+ const slug = positionals(args)[0];
151
+ const json = args.includes('--json');
152
+ try {
153
+ const result = command === 'save'
154
+ ? saveCourse({ cwd, slug, message: flagValue(args, '-m') || flagValue(args, '--message') })
155
+ : createCourse({ cwd, slug, kind: flagValue(args, '--kind') || 'programming' });
156
+ if (json) console.log(JSON.stringify(result));
157
+ else if (command === 'save') {
158
+ console.log(style(result.committed ? 'success' : 'info',
159
+ result.committed ? `Saved learning/${slug}/` : `Not saved: ${result.reason}`));
160
+ } else {
161
+ const where = result.branch ? `git worktree on branch ${result.branch}` : 'plain folder';
162
+ console.log(style('success', `${result.created ? 'Created' : 'Reusing'} learning/${slug}/ (${where})`));
163
+ }
164
+ } catch (err) {
165
+ if (json) console.log(JSON.stringify({ error: err.message }));
166
+ else console.error(style('error', err.message));
167
+ process.exitCode = 1;
168
+ }
169
+ }