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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +225 -2
- package/bin/cli.js +811 -0
- package/bin/commands/check.js +298 -0
- package/bin/commands/create.js +169 -0
- package/bin/commands/doctor.js +392 -0
- package/bin/commands/import.js +557 -0
- package/bin/commands/next.js +125 -0
- package/bin/commands/review.js +207 -0
- package/bin/commands/status.js +114 -0
- package/bin/commands/studio.js +497 -0
- package/bin/commands/trail.js +211 -0
- package/bin/commands/web.js +350 -0
- package/bin/course-reader.js +611 -0
- package/bin/node-version.js +60 -0
- package/bin/platforms/_shared.js +211 -0
- package/bin/platforms/claude.js +104 -0
- package/bin/platforms/cursor.js +49 -0
- package/bin/platforms/gemini.js +65 -0
- package/bin/platforms/opencode.js +95 -0
- package/bin/registry-helpers.js +504 -0
- package/bin/static-server.js +153 -0
- package/bin/studio-store.js +94 -0
- package/bin/sync-shared.sh +5 -0
- package/package.json +91 -4
- package/plugin.json +139 -0
- package/shared/SKILL.md +568 -0
- package/shared/agents/coach.md +225 -0
- package/shared/agents/navigator.md +215 -0
- package/shared/agents/professor.md +372 -0
- package/shared/agents/researcher.md +181 -0
- package/shared/agents/spotter.md +129 -0
- package/shared/commands/professor/archive.md +103 -0
- package/shared/commands/professor/capstone-review.md +79 -0
- package/shared/commands/professor/capstone.md +34 -0
- package/shared/commands/professor/discuss.md +18 -0
- package/shared/commands/professor/done.md +44 -0
- package/shared/commands/professor/export.md +69 -0
- package/shared/commands/professor/hint.md +108 -0
- package/shared/commands/professor/navigator.md +67 -0
- package/shared/commands/professor/new-topic.md +73 -0
- package/shared/commands/professor/next.md +174 -0
- package/shared/commands/professor/note.md +63 -0
- package/shared/commands/professor/onboard.md +95 -0
- package/shared/commands/professor/progress.md +46 -0
- package/shared/commands/professor/publish.md +93 -0
- package/shared/commands/professor/quiz.md +27 -0
- package/shared/commands/professor/recall.md +74 -0
- package/shared/commands/professor/review.md +43 -0
- package/shared/commands/professor/schedule.md +75 -0
- package/shared/commands/professor/skill-export.md +87 -0
- package/shared/commands/professor/spotter.md +76 -0
- package/shared/commands/professor/stuck.md +27 -0
- package/shared/commands/professor/switch.md +50 -0
- package/shared/commands/professor/syllabus.md +9 -0
- package/shared/commands/professor/template-export.md +91 -0
- package/shared/commands/professor/template-import.md +111 -0
- package/shared/commands/professor/worktrees.md +58 -0
- package/shared/hooks/pre-compact.js +232 -0
- package/studio/README.md +313 -0
- package/studio/dist/assets/BetaReportPanel-8NrDLiPm.js +1 -0
- package/studio/dist/assets/Capstone-Cv9kH6LY.js +6 -0
- package/studio/dist/assets/CodeWorkspace-BwCEINWo.js +2 -0
- package/studio/dist/assets/Course-BJ_hafUP.js +1 -0
- package/studio/dist/assets/Editor-CV0mNhlz.js +53 -0
- package/studio/dist/assets/Home-C1ry89ak.js +1 -0
- package/studio/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/studio/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/studio/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/studio/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/studio/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/studio/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/studio/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/studio/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/studio/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/studio/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/studio/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/studio/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/studio/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/studio/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/studio/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/studio/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/studio/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/studio/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/studio/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/studio/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/studio/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/studio/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/studio/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/studio/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/studio/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/studio/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/studio/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/studio/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/studio/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/studio/dist/assets/Lesson-2e9TD9yL.js +2 -0
- package/studio/dist/assets/NewCourse-BZeOg2xn.js +952 -0
- package/studio/dist/assets/Progress-CRc_xEWC.js +1 -0
- package/studio/dist/assets/Review-6QEbHEfk.js +1 -0
- package/studio/dist/assets/Settings-NFnqVQui.js +1 -0
- package/studio/dist/assets/TutorPanel-DXQ6Z3BY.js +123 -0
- package/studio/dist/assets/abnfDiagram-WEYOIA3C-qeSjKlGM.js +1 -0
- package/studio/dist/assets/anthropic-D7NiQCUH.js +15 -0
- package/studio/dist/assets/arc-Y1ARJVZ4.js +1 -0
- package/studio/dist/assets/architecture-WOLXFQ4H-COG-m35l.js +1 -0
- package/studio/dist/assets/architectureDiagram-ZHVYKT53-VO7uKoeD.js +36 -0
- package/studio/dist/assets/array-BifhSqXX.js +1 -0
- package/studio/dist/assets/blockDiagram-RJAE7QA3-BGJF_qiL.js +140 -0
- package/studio/dist/assets/bridge-BTh0AeNX.js +1 -0
- package/studio/dist/assets/c4Diagram-XDPPPE54-C-S10RgU.js +38 -0
- package/studio/dist/assets/capstone-Amn6Hztq.js +135 -0
- package/studio/dist/assets/card-store-BrbrtX9w.js +1 -0
- package/studio/dist/assets/channel-SJiUQu_N.js +1 -0
- package/studio/dist/assets/chunk-2E7WHVMD-fv-iqTyC.js +88 -0
- package/studio/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/studio/dist/assets/chunk-4EMNKR2T-CY9IZrgv.js +1 -0
- package/studio/dist/assets/chunk-55XCU76K-DyTZz7v_.js +1 -0
- package/studio/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/studio/dist/assets/chunk-7S6XSTFO-BNMzTLRW.js +2 -0
- package/studio/dist/assets/chunk-AQDTSKZB-CEYANohH.js +72 -0
- package/studio/dist/assets/chunk-DSNLSHJZ-CDk5EXjH.js +129 -0
- package/studio/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/studio/dist/assets/chunk-HAGFOTED-hNAi1zh_.js +62 -0
- package/studio/dist/assets/chunk-HNE4Q36P-COJnSYoZ.js +1 -0
- package/studio/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/studio/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/studio/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/studio/dist/assets/chunk-NQOKRVIF-BU6KVxRr.js +213 -0
- package/studio/dist/assets/chunk-UHW7PFBP-DYvxL9JG.js +1 -0
- package/studio/dist/assets/chunk-VQ5W62QI-CuRfk65j.js +1 -0
- package/studio/dist/assets/chunk-WQQL4CDK-KaG7x-Mb.js +10 -0
- package/studio/dist/assets/chunk-XXDRQBXY-CnbRkih_.js +1 -0
- package/studio/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/studio/dist/assets/chunk-YZO67HFG-DbuqIZmk.js +1 -0
- package/studio/dist/assets/chunk-ZAUX2LTX-bk4ivUmc.js +1 -0
- package/studio/dist/assets/chunk-ZCHMQSOR-B_LElfwh.js +2 -0
- package/studio/dist/assets/classDiagram-v2-4MYWR3UA-DtCJ6H_X.js +217 -0
- package/studio/dist/assets/cose-bilkent-JH36ORCC-DMBAZFnj.js +1 -0
- package/studio/dist/assets/course-export-DpaOTT3a.js +1 -0
- package/studio/dist/assets/cynefin-EF2NZ3EQ-D9btHJvU.js +1 -0
- package/studio/dist/assets/cynefinDiagram-WF24UDRP-CsFJuv27.js +62 -0
- package/studio/dist/assets/cytoscape.esm-CHBj4VkZ.js +321 -0
- package/studio/dist/assets/dagre-NK4GIZVX-Cg1Duec7.js +4 -0
- package/studio/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/studio/dist/assets/diagram-CBK3MWTC-_KU2m5gd.js +24 -0
- package/studio/dist/assets/diagram-I5XIIV2D-5RaBrwUA.js +24 -0
- package/studio/dist/assets/diagram-ODIL2UCW-C0VKxoD3.js +30 -0
- package/studio/dist/assets/diagram-PFBEJUXE-BaHcZEP2.js +3 -0
- package/studio/dist/assets/diagram-RJA3I4VV-cwSAZ90p.js +41 -0
- package/studio/dist/assets/diagram-TYUNFSNR-BJImxCEN.js +200 -0
- package/studio/dist/assets/dist-CumnGVmJ.js +1 -0
- package/studio/dist/assets/ebnfDiagram-V4P3TTPT-OQQLRXcm.js +1 -0
- package/studio/dist/assets/elk-QONTBCTQ-BJGhQHxQ.js +27 -0
- package/studio/dist/assets/erDiagram-KYQIGQAA-D91EKSep.js +99 -0
- package/studio/dist/assets/error-BTdmRKoB.js +1 -0
- package/studio/dist/assets/errors-BZa_CNHL.js +1 -0
- package/studio/dist/assets/eventmodeling-K75KTNOO-DgdTRgnw.js +1 -0
- package/studio/dist/assets/fetch-local-DG3O3hmQ.js +4 -0
- package/studio/dist/assets/flowDiagram-IUGLLWBU-CVnJYMum.js +1 -0
- package/studio/dist/assets/ganttDiagram-R7GPNTJY-CSYLPWEI.js +292 -0
- package/studio/dist/assets/gitGraph-VSP46ZUC-D6GWaV9y.js +1 -0
- package/studio/dist/assets/gitGraphDiagram-PDYL7XHY-CaV31bvT.js +106 -0
- package/studio/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/studio/dist/assets/index-CVenTTOY.css +2 -0
- package/studio/dist/assets/index-DioXTZVc.js +313 -0
- package/studio/dist/assets/info-OHQRW6UA-BN-x8GoN.js +1 -0
- package/studio/dist/assets/infoDiagram-MVDRKAX5-CIose_cy.js +2 -0
- package/studio/dist/assets/init-C-OQMol4.js +1 -0
- package/studio/dist/assets/ishikawaDiagram-HPEHV2GE-DCihSNBw.js +70 -0
- package/studio/dist/assets/journeyDiagram-BOSHW7O7-Bz3o3RIX.js +139 -0
- package/studio/dist/assets/js.worker-AD7DjksO.js +4 -0
- package/studio/dist/assets/kanban-definition-FNSA4HC5-BUyqvlFE.js +89 -0
- package/studio/dist/assets/katex-B_JcIwqj.js +257 -0
- package/studio/dist/assets/katex-BgsA2lO1.css +1 -0
- package/studio/dist/assets/line-B08T8KlK.js +1 -0
- package/studio/dist/assets/linear-C9iwaMOi.js +1 -0
- package/studio/dist/assets/mermaid-parser.core-BYwreSUy.js +7 -0
- package/studio/dist/assets/mermaid.core-DTO1oDmq.js +44 -0
- package/studio/dist/assets/mindmap-definition-6OY57UG3-BGLwKheY.js +98 -0
- package/studio/dist/assets/node.browser-BISniYn8.js +1 -0
- package/studio/dist/assets/node.browser-HQ5SKBOf.js +1 -0
- package/studio/dist/assets/ollama-De5w84tG.js +1 -0
- package/studio/dist/assets/openai-compatible-Joy1oK3L.js +1 -0
- package/studio/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/studio/dist/assets/packet-JDAUHWVQ-CDIMjHS_.js +1 -0
- package/studio/dist/assets/path-fybaL0A-.js +1 -0
- package/studio/dist/assets/pegDiagram-ECIUJNBX-DWXm4mwS.js +1 -0
- package/studio/dist/assets/pie-XZMESJXO-CY3QQiKZ.js +1 -0
- package/studio/dist/assets/pieDiagram-GNMPY74T-M1aCnI7W.js +39 -0
- package/studio/dist/assets/python.worker-CzTrFdd9.js +24 -0
- package/studio/dist/assets/quadrantDiagram-D57K4MC3-BFFtO7U5.js +7 -0
- package/studio/dist/assets/radar-ABXABTNO-DT0W4_Xn.js +1 -0
- package/studio/dist/assets/railroad-I3PHUGI6-_VCpgZR5.js +1 -0
- package/studio/dist/assets/railroad-abnf-LNEFI6M7-DkSpQ5YY.js +1 -0
- package/studio/dist/assets/railroad-ebnf-DWYD2UWJ-BqWILdGb.js +1 -0
- package/studio/dist/assets/railroad-peg-JR7BVNR7-BB0bQL-Z.js +1 -0
- package/studio/dist/assets/railroadDiagram-Y72NRBL7-CjLKjIdJ.js +1 -0
- package/studio/dist/assets/react-B8SKf8RM.js +1 -0
- package/studio/dist/assets/rehype-katex-C-sKddsH.js +259 -0
- package/studio/dist/assets/remark-math-C-ox67hE.js +5 -0
- package/studio/dist/assets/requirementDiagram-EQYSW5RA-CnwzZqR8.js +84 -0
- package/studio/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/studio/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/studio/dist/assets/sankeyDiagram-SY24JHO5-CPDugTUl.js +40 -0
- package/studio/dist/assets/schemas-BOc2YsOP.js +55 -0
- package/studio/dist/assets/sequenceDiagram-BLEHNHQU-1dLfeU9Y.js +169 -0
- package/studio/dist/assets/settings-DhwuyNPU.js +1 -0
- package/studio/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/studio/dist/assets/spec-nAnsSMkw.js +1 -0
- package/studio/dist/assets/sql-wasm-C1D_RJuy.wasm +0 -0
- package/studio/dist/assets/sql.worker-D7pBHOrq.js +4 -0
- package/studio/dist/assets/src-BWF7jMDv.js +1 -0
- package/studio/dist/assets/stateDiagram-v2-A6IESKWZ-DR7CmEjj.js +291 -0
- package/studio/dist/assets/swimlanes-B3ENRKFQ-BAWB9omQ.js +1 -0
- package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-qeuHru16.js +8 -0
- package/studio/dist/assets/timeline-definition-JXYKGLGB-DNkj8z77.js +120 -0
- package/studio/dist/assets/treeView-D4JQ5SDB-BA-ouhMb.js +1 -0
- package/studio/dist/assets/treemap-SAJKECNS-BrrGxsAb.js +1 -0
- package/studio/dist/assets/useStream-rdsR6A7P.js +1 -0
- package/studio/dist/assets/usecaseDiagram-WPOLBNNX-BB3D0Dt_.js +328 -0
- package/studio/dist/assets/vennDiagram-Q3Y3IDCW-BrtqtKTE.js +34 -0
- package/studio/dist/assets/wardley-7MLQ67FV-BKkemDql.js +1 -0
- package/studio/dist/assets/wardleyDiagram-IVAIDKWA-DFnI3P5z.js +78 -0
- package/studio/dist/assets/xychartDiagram-4URNHDU4-Da0DZG_q.js +7 -0
- package/studio/dist/index.html +20 -0
- package/studio/dist/preview.html +22 -0
- package/studio/migrations/0001_init.sql +188 -0
- package/studio/migrations/0002_external_ids.sql +20 -0
- package/studio/migrations/0003_course_language.sql +12 -0
- package/studio/migrations/0004_capstone.sql +51 -0
- package/studio/migrations/0005_beta_loop.sql +73 -0
- package/studio/migrations/0006_interactive_exercises.sql +49 -0
- package/studio/src/lib/activity.js +92 -0
- package/studio/src/lib/ai/anthropic.js +109 -0
- package/studio/src/lib/ai/bridge.js +126 -0
- package/studio/src/lib/ai/catalog.js +160 -0
- package/studio/src/lib/ai/config.js +70 -0
- package/studio/src/lib/ai/errors.js +95 -0
- package/studio/src/lib/ai/fetch-local.js +104 -0
- package/studio/src/lib/ai/index.js +194 -0
- package/studio/src/lib/ai/ollama.js +136 -0
- package/studio/src/lib/ai/openai-compatible.js +101 -0
- package/studio/src/lib/ai/provider-utils.js +107 -0
- package/studio/src/lib/ai/sse.js +99 -0
- package/studio/src/lib/ai/status.js +92 -0
- package/studio/src/lib/ai/useStream.js +109 -0
- package/studio/src/lib/beta/metrics.js +147 -0
- package/studio/src/lib/beta/report.js +134 -0
- package/studio/src/lib/capstone-evidence.js +758 -0
- package/studio/src/lib/capstone.js +663 -0
- package/studio/src/lib/chat-store.js +128 -0
- package/studio/src/lib/course-export.js +38 -0
- package/studio/src/lib/course.js +187 -0
- package/studio/src/lib/exercises/journal-entry.js +259 -0
- package/studio/src/lib/exercises/placement.js +31 -0
- package/studio/src/lib/exercises/registry.js +19 -0
- package/studio/src/lib/exercises/spec.js +129 -0
- package/studio/src/lib/i18n/detect.js +162 -0
- package/studio/src/lib/i18n/en/app.json +23 -0
- package/studio/src/lib/i18n/en/betaReport.json +55 -0
- package/studio/src/lib/i18n/en/capstone.json +57 -0
- package/studio/src/lib/i18n/en/common.json +25 -0
- package/studio/src/lib/i18n/en/course.json +27 -0
- package/studio/src/lib/i18n/en/data.json +35 -0
- package/studio/src/lib/i18n/en/durability.json +5 -0
- package/studio/src/lib/i18n/en/errors.json +20 -0
- package/studio/src/lib/i18n/en/exercises.json +55 -0
- package/studio/src/lib/i18n/en/externalEvidence.json +76 -0
- package/studio/src/lib/i18n/en/home.json +18 -0
- package/studio/src/lib/i18n/en/lesson.json +33 -0
- package/studio/src/lib/i18n/en/nav.json +8 -0
- package/studio/src/lib/i18n/en/newCourse.json +51 -0
- package/studio/src/lib/i18n/en/onboarding.json +35 -0
- package/studio/src/lib/i18n/en/playwrightTemplate.json +20 -0
- package/studio/src/lib/i18n/en/practice.json +79 -0
- package/studio/src/lib/i18n/en/provider.json +27 -0
- package/studio/src/lib/i18n/en/providerMeta.json +26 -0
- package/studio/src/lib/i18n/en/reflection.json +17 -0
- package/studio/src/lib/i18n/en/revision.json +52 -0
- package/studio/src/lib/i18n/en/runnerNotice.json +14 -0
- package/studio/src/lib/i18n/en/settings.json +12 -0
- package/studio/src/lib/i18n/en/storage.json +49 -0
- package/studio/src/lib/i18n/en/tutor.json +39 -0
- package/studio/src/lib/i18n/en/workspace.json +50 -0
- package/studio/src/lib/i18n/index.js +144 -0
- package/studio/src/lib/i18n/languages.js +93 -0
- package/studio/src/lib/i18n/vi/app.json +23 -0
- package/studio/src/lib/i18n/vi/betaReport.json +55 -0
- package/studio/src/lib/i18n/vi/capstone.json +57 -0
- package/studio/src/lib/i18n/vi/common.json +25 -0
- package/studio/src/lib/i18n/vi/course.json +27 -0
- package/studio/src/lib/i18n/vi/data.json +35 -0
- package/studio/src/lib/i18n/vi/durability.json +5 -0
- package/studio/src/lib/i18n/vi/errors.json +20 -0
- package/studio/src/lib/i18n/vi/exercises.json +55 -0
- package/studio/src/lib/i18n/vi/externalEvidence.json +76 -0
- package/studio/src/lib/i18n/vi/home.json +18 -0
- package/studio/src/lib/i18n/vi/lesson.json +33 -0
- package/studio/src/lib/i18n/vi/nav.json +8 -0
- package/studio/src/lib/i18n/vi/newCourse.json +51 -0
- package/studio/src/lib/i18n/vi/onboarding.json +35 -0
- package/studio/src/lib/i18n/vi/playwrightTemplate.json +20 -0
- package/studio/src/lib/i18n/vi/practice.json +79 -0
- package/studio/src/lib/i18n/vi/provider.json +27 -0
- package/studio/src/lib/i18n/vi/providerMeta.json +26 -0
- package/studio/src/lib/i18n/vi/reflection.json +17 -0
- package/studio/src/lib/i18n/vi/revision.json +52 -0
- package/studio/src/lib/i18n/vi/runnerNotice.json +14 -0
- package/studio/src/lib/i18n/vi/settings.json +12 -0
- package/studio/src/lib/i18n/vi/storage.json +49 -0
- package/studio/src/lib/i18n/vi/tutor.json +39 -0
- package/studio/src/lib/i18n/vi/workspace.json +50 -0
- package/studio/src/lib/ids.js +10 -0
- package/studio/src/lib/json.js +26 -0
- package/studio/src/lib/markdown.js +74 -0
- package/studio/src/lib/prompts/base.js +139 -0
- package/studio/src/lib/prompts/capstone.js +777 -0
- package/studio/src/lib/prompts/course.js +150 -0
- package/studio/src/lib/prompts/lesson.js +275 -0
- package/studio/src/lib/prompts/practice.js +511 -0
- package/studio/src/lib/prompts/tutor.js +509 -0
- package/studio/src/lib/repo/blocked-adapter.js +69 -0
- package/studio/src/lib/repo/events.js +239 -0
- package/studio/src/lib/repo/idb-adapter.js +226 -0
- package/studio/src/lib/repo/index.js +1315 -0
- package/studio/src/lib/repo/memory-adapter.js +115 -0
- package/studio/src/lib/repo/remote-adapter.js +158 -0
- package/studio/src/lib/repo/schema.js +143 -0
- package/studio/src/lib/repo/select-adapter.js +345 -0
- package/studio/src/lib/router.js +71 -0
- package/studio/src/lib/runner/browser.js +244 -0
- package/studio/src/lib/runner/checks.js +248 -0
- package/studio/src/lib/runner/current-code.js +98 -0
- package/studio/src/lib/runner/esm-cjs.js +109 -0
- package/studio/src/lib/runner/hash.js +34 -0
- package/studio/src/lib/runner/history.js +48 -0
- package/studio/src/lib/runner/index.js +168 -0
- package/studio/src/lib/runner/languages.js +258 -0
- package/studio/src/lib/runner/local.js +139 -0
- package/studio/src/lib/runner/path.js +104 -0
- package/studio/src/lib/runner/skeleton.js +315 -0
- package/studio/src/lib/runner/strip-types.js +134 -0
- package/studio/src/lib/settings.js +399 -0
- package/studio/src/lib/srs.js +39 -0
- package/templates/agent/settings.json +6 -0
- package/templates/claude/plugin.json +139 -0
- package/templates/claude/settings.json +15 -0
- package/templates/cursor/professor.mdc +4 -0
- package/templates/cursor/settings.json +18 -0
- package/templates/gemini/GEMINI.md +28 -0
- package/templates/gemini/settings.json +5 -0
- package/templates/opencode/opencode.json +3 -0
- package/templates/researcher/README.md +61 -0
- package/templates/researcher/settings.json +18 -0
- package/web/client/components.json +15 -0
- package/web/client/index.html +12 -0
- package/web/client/package-lock.json +8732 -0
- package/web/client/package.json +39 -0
- package/web/client/postcss.config.js +6 -0
- package/web/client/src/App.jsx +140 -0
- package/web/client/src/api/client.js +357 -0
- package/web/client/src/components/ChatPanel.jsx +417 -0
- package/web/client/src/components/CommandPills.jsx +50 -0
- package/web/client/src/components/DeprecationBanner.jsx +21 -0
- package/web/client/src/components/ExerciseEditor.jsx +164 -0
- package/web/client/src/components/Header.jsx +119 -0
- package/web/client/src/components/LecturePanel.jsx +342 -0
- package/web/client/src/components/MobileTabSwitcher.jsx +51 -0
- package/web/client/src/components/SplitPane.jsx +42 -0
- package/web/client/src/components/ui/alert.jsx +38 -0
- package/web/client/src/components/ui/badge.jsx +24 -0
- package/web/client/src/components/ui/button.jsx +23 -0
- package/web/client/src/components/ui/select.jsx +74 -0
- package/web/client/src/hooks/useSSE.js +152 -0
- package/web/client/src/hooks/useWebSocket.js +98 -0
- package/web/client/src/index.css +23 -0
- package/web/client/src/lib/utils.js +6 -0
- package/web/client/src/main.jsx +12 -0
- package/web/client/src/utils/markdown.js +50 -0
- package/web/client/tailwind.config.js +8 -0
- package/web/client/vite.config.js +21 -0
- package/web/dist/assets/index-C1W0eXIQ.js +294 -0
- package/web/dist/assets/index-C3OUx70T.css +1 -0
- package/web/dist/index.html +13 -0
- package/web/lecture-archive.js +172 -0
- package/web/legacy-server.js +1277 -0
- package/web/package-lock.json +855 -0
- package/web/package.json +10 -0
- package/web/security.js +1016 -0
- package/web/server.js +390 -0
- package/web/studio-api/agents.js +580 -0
- package/web/studio-api/bridge.js +199 -0
- package/web/studio-api/db.js +66 -0
- package/web/studio-api/guards.js +201 -0
- package/web/studio-api/health.js +538 -0
- package/web/studio-api.js +214 -0
- package/web/studio-db.js +380 -0
- package/web/studio-runner.js +568 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner interface (CODE-02, CODE-04) — the single entry point the Workspace UI calls.
|
|
3
|
+
*
|
|
4
|
+
* run({ language, files, entry, tests, signal, onOutput }) -> RunResult
|
|
5
|
+
*
|
|
6
|
+
* Backend selection is deliberate and boring:
|
|
7
|
+
* 1. browser — when the language is supported with no install (JS/TS, Python, SQL, HTML/CSS)
|
|
8
|
+
* 2. local — otherwise, if the learner's local server answers `/toolchains`
|
|
9
|
+
* 3. error — with an actionable hint telling them how to get the toolchain
|
|
10
|
+
*
|
|
11
|
+
* `onOutput` streams `{ stream: 'stdout'|'stderr'|'status', text }` as the run progresses.
|
|
12
|
+
* Nothing here ever writes, reveals or generates a solution — it only executes the learner's code.
|
|
13
|
+
*/
|
|
14
|
+
import { languageForPath, normalizeLanguage, isBrowserLanguage, isLocalLanguage, getLanguage } from './languages.js';
|
|
15
|
+
import { runBrowser, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, RunnerError } from './browser.js';
|
|
16
|
+
import { runLocal, checkToolchains, runnerBase } from './local.js';
|
|
17
|
+
import { loadSettings } from '../settings.js';
|
|
18
|
+
|
|
19
|
+
export { DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES };
|
|
20
|
+
export { isBrowserLanguage, isLocalLanguage };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {import('./browser.js').RunResult} RunResult
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @returns {boolean} whether a Web Worker can be constructed in this environment */
|
|
27
|
+
export function canUseWorkers() {
|
|
28
|
+
return typeof Worker !== 'undefined';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure backend choice — exported so it can be unit-tested without a browser.
|
|
33
|
+
* @param {{ language: string, browserUsable?: boolean, localAvailable?: boolean, preferLocal?: boolean }} input
|
|
34
|
+
* @returns {'browser'|'local'|null}
|
|
35
|
+
*/
|
|
36
|
+
export function selectRunner({ language, browserUsable = true, localAvailable = false, preferLocal = false }) {
|
|
37
|
+
const browser = isBrowserLanguage(language);
|
|
38
|
+
if (preferLocal && localAvailable) return 'local';
|
|
39
|
+
if (browser && browserUsable) return 'browser';
|
|
40
|
+
if (isLocalLanguage(language) && localAvailable) return 'local';
|
|
41
|
+
if (browser && !browserUsable && localAvailable) return 'local';
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The runner the UI should advertise for a language (no network).
|
|
47
|
+
* @param {string|null|undefined} language
|
|
48
|
+
* @param {{ toolchains?: Record<string, boolean>|null }} [opts]
|
|
49
|
+
*/
|
|
50
|
+
export function describeRunner(language, { toolchains = null } = {}) {
|
|
51
|
+
const lang = normalizeLanguage(language);
|
|
52
|
+
const spec = getLanguage(lang);
|
|
53
|
+
if (!spec) return { id: null, label: 'Unknown', kind: null, ready: false };
|
|
54
|
+
const browser = spec.backend === 'browser';
|
|
55
|
+
if (browser) return { id: lang, label: 'Browser', kind: 'browser', ready: true };
|
|
56
|
+
const command = spec.command;
|
|
57
|
+
const ready = Boolean(toolchains && command && toolchains[command]);
|
|
58
|
+
return { id: lang, label: 'Local server', kind: 'local', ready, command };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {object} input
|
|
63
|
+
* @param {string} input.language
|
|
64
|
+
* @param {Record<string,string>|Array<{path:string,content:string}>} input.files
|
|
65
|
+
* @param {string|null} [input.entry]
|
|
66
|
+
* @param {any} [input.tests]
|
|
67
|
+
* @param {AbortSignal} [input.signal]
|
|
68
|
+
* @param {(chunk:{stream:string,text:string})=>void} [input.onOutput]
|
|
69
|
+
* @param {string} [input.courseSlug]
|
|
70
|
+
* @param {object} [input.settings]
|
|
71
|
+
* @param {number} [input.timeoutMs]
|
|
72
|
+
* @param {number} [input.maxOutputBytes]
|
|
73
|
+
* @param {boolean} [input.preferLocal]
|
|
74
|
+
* @returns {Promise<RunResult>}
|
|
75
|
+
*/
|
|
76
|
+
export async function run(input) {
|
|
77
|
+
const {
|
|
78
|
+
language,
|
|
79
|
+
files = {},
|
|
80
|
+
entry = null,
|
|
81
|
+
tests = null,
|
|
82
|
+
signal,
|
|
83
|
+
onOutput,
|
|
84
|
+
courseSlug = 'workspace',
|
|
85
|
+
settings = null,
|
|
86
|
+
timeoutMs,
|
|
87
|
+
maxOutputBytes,
|
|
88
|
+
preferLocal = false,
|
|
89
|
+
} = input || {};
|
|
90
|
+
|
|
91
|
+
const resolvedSettings = settings || safeSettings();
|
|
92
|
+
const lang = normalizeLanguage(language) || languageForPath(entry) || normalizeLanguage(Object.keys(files)[0]) || 'javascript';
|
|
93
|
+
|
|
94
|
+
const browserUsable = canUseWorkers();
|
|
95
|
+
const browserPath = { language: lang, files: fileMap(files), entry, tests, signal, onOutput, timeoutMs, maxOutputBytes };
|
|
96
|
+
|
|
97
|
+
if (isBrowserLanguage(lang) && browserUsable && !preferLocal) {
|
|
98
|
+
return runBrowser(browserPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Local (or browser-as-fallback when a browser language but no workers, or preferLocal).
|
|
102
|
+
let localAvailable = false;
|
|
103
|
+
let localError = null;
|
|
104
|
+
if (!isBrowserLanguage(lang) || preferLocal) {
|
|
105
|
+
try {
|
|
106
|
+
const toolchains = await checkToolchains(resolvedSettings, { signal });
|
|
107
|
+
localAvailable = true;
|
|
108
|
+
const spec = getLanguage(lang);
|
|
109
|
+
if (spec?.command && toolchains && toolchains[spec.command] === false) {
|
|
110
|
+
throw localToolchainError(lang, spec.command);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
localError = err;
|
|
114
|
+
localAvailable = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const choice = selectRunner({ language: lang, browserUsable, localAvailable, preferLocal });
|
|
119
|
+
if (choice === 'local') {
|
|
120
|
+
return runLocal({ settings: resolvedSettings, courseSlug, language: lang, files: fileMap(files), entry, signal, onOutput, timeoutMs });
|
|
121
|
+
}
|
|
122
|
+
if (choice === 'browser') {
|
|
123
|
+
return runBrowser(browserPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// No backend works — surface the most useful error.
|
|
127
|
+
if (localError) throw localError;
|
|
128
|
+
if (browserUsable && isBrowserLanguage(lang)) return runBrowser(browserPath);
|
|
129
|
+
if (!browserUsable && isBrowserLanguage(lang)) {
|
|
130
|
+
const err = new RunnerError('unavailable', `The ${lang} runner needs Web Workers, which this browser does not expose.`);
|
|
131
|
+
err.hint = 'Try a modern browser, or start the local server and use the local runner.';
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
throw localToolchainError(lang, getLanguage(lang)?.command);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} language
|
|
139
|
+
* @param {string|undefined} command
|
|
140
|
+
*/
|
|
141
|
+
function localToolchainError(language, command) {
|
|
142
|
+
const label = getLanguage(language)?.label || language;
|
|
143
|
+
const err = new RunnerError(
|
|
144
|
+
'unavailable',
|
|
145
|
+
`${label} needs its toolchain installed and the local server running.`
|
|
146
|
+
);
|
|
147
|
+
err.hint = `Start the local server (\`npx ronin-skillforce web\`)${command ? ` and make sure \`${command}\` is on your PATH` : ''}. The local runner writes files to ~/.professor/workspaces/ so you can also open them in your editor. You can still write and save code without running it.`;
|
|
148
|
+
return err;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @param {Record<string,string>|Array<{path:string,content:string}>} files */
|
|
152
|
+
function fileMap(files) {
|
|
153
|
+
if (!files) return {};
|
|
154
|
+
return Array.isArray(files)
|
|
155
|
+
? Object.fromEntries(files.map((f) => [f.path, String(f.content ?? '')]))
|
|
156
|
+
: Object.fromEntries(Object.entries(files).map(([k, v]) => [k, String(v ?? '')]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Settings without throwing when localStorage is unavailable (worker/node/tests). */
|
|
160
|
+
function safeSettings() {
|
|
161
|
+
try {
|
|
162
|
+
return loadSettings();
|
|
163
|
+
} catch {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { checkToolchains, runnerBase };
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language metadata + detection for the code workspace (CODE-02, CODE-04).
|
|
3
|
+
*
|
|
4
|
+
* `backend` is the deliberate split the brief asks for:
|
|
5
|
+
* - 'browser' — runs with no install (JS/TS, Python via Pyodide, SQL via sql.js, HTML/CSS preview)
|
|
6
|
+
* - 'local' — needs the learner's own toolchain through `web/studio-runner.js`
|
|
7
|
+
*
|
|
8
|
+
* This module is pure (no DOM, no imports) so it is unit-testable and safe to use everywhere.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @typedef {'javascript'|'typescript'|'python'|'sql'|'html'|'css'|'go'|'java'|'rust'|'bash'} LanguageId */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} LanguageSpec
|
|
15
|
+
* @property {LanguageId} id
|
|
16
|
+
* @property {string} label
|
|
17
|
+
* @property {string[]} extensions lower-case, with the leading dot
|
|
18
|
+
* @property {'browser'|'local'} backend
|
|
19
|
+
* @property {string|null} codeMirror CodeMirror language key (Editor.jsx maps it to a grammar)
|
|
20
|
+
* @property {string} entry default entry filename
|
|
21
|
+
* @property {string} comment line-comment token (used when writing seeds / hints)
|
|
22
|
+
* @property {string} [command] local allowlist command
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** @type {Record<LanguageId, LanguageSpec>} */
|
|
26
|
+
export const LANGUAGES = {
|
|
27
|
+
javascript: {
|
|
28
|
+
id: 'javascript',
|
|
29
|
+
label: 'JavaScript',
|
|
30
|
+
extensions: ['.js', '.mjs', '.cjs', '.jsx'],
|
|
31
|
+
backend: 'browser',
|
|
32
|
+
codeMirror: 'javascript',
|
|
33
|
+
entry: 'main.js',
|
|
34
|
+
comment: '//',
|
|
35
|
+
command: 'node',
|
|
36
|
+
},
|
|
37
|
+
typescript: {
|
|
38
|
+
id: 'typescript',
|
|
39
|
+
label: 'TypeScript',
|
|
40
|
+
extensions: ['.ts', '.tsx', '.mts', '.cts'],
|
|
41
|
+
backend: 'browser',
|
|
42
|
+
codeMirror: 'typescript',
|
|
43
|
+
entry: 'main.ts',
|
|
44
|
+
comment: '//',
|
|
45
|
+
},
|
|
46
|
+
python: {
|
|
47
|
+
id: 'python',
|
|
48
|
+
label: 'Python',
|
|
49
|
+
extensions: ['.py', '.pyw'],
|
|
50
|
+
backend: 'browser',
|
|
51
|
+
codeMirror: 'python',
|
|
52
|
+
entry: 'main.py',
|
|
53
|
+
comment: '#',
|
|
54
|
+
command: 'python3',
|
|
55
|
+
},
|
|
56
|
+
sql: {
|
|
57
|
+
id: 'sql',
|
|
58
|
+
label: 'SQL',
|
|
59
|
+
extensions: ['.sql'],
|
|
60
|
+
backend: 'browser',
|
|
61
|
+
codeMirror: 'sql',
|
|
62
|
+
entry: 'main.sql',
|
|
63
|
+
comment: '--',
|
|
64
|
+
command: 'sqlite3',
|
|
65
|
+
},
|
|
66
|
+
html: {
|
|
67
|
+
id: 'html',
|
|
68
|
+
label: 'HTML',
|
|
69
|
+
extensions: ['.html', '.htm'],
|
|
70
|
+
backend: 'browser',
|
|
71
|
+
codeMirror: 'html',
|
|
72
|
+
entry: 'index.html',
|
|
73
|
+
comment: '<!--',
|
|
74
|
+
},
|
|
75
|
+
css: {
|
|
76
|
+
id: 'css',
|
|
77
|
+
label: 'CSS',
|
|
78
|
+
extensions: ['.css'],
|
|
79
|
+
backend: 'browser',
|
|
80
|
+
codeMirror: 'css',
|
|
81
|
+
entry: 'style.css',
|
|
82
|
+
comment: '/*',
|
|
83
|
+
},
|
|
84
|
+
go: {
|
|
85
|
+
id: 'go',
|
|
86
|
+
label: 'Go',
|
|
87
|
+
extensions: ['.go'],
|
|
88
|
+
backend: 'local',
|
|
89
|
+
codeMirror: 'go',
|
|
90
|
+
entry: 'main.go',
|
|
91
|
+
comment: '//',
|
|
92
|
+
command: 'go',
|
|
93
|
+
},
|
|
94
|
+
java: {
|
|
95
|
+
id: 'java',
|
|
96
|
+
label: 'Java',
|
|
97
|
+
extensions: ['.java'],
|
|
98
|
+
backend: 'local',
|
|
99
|
+
codeMirror: 'java',
|
|
100
|
+
entry: 'Main.java',
|
|
101
|
+
comment: '//',
|
|
102
|
+
command: 'javac',
|
|
103
|
+
},
|
|
104
|
+
rust: {
|
|
105
|
+
id: 'rust',
|
|
106
|
+
label: 'Rust',
|
|
107
|
+
extensions: ['.rs'],
|
|
108
|
+
backend: 'local',
|
|
109
|
+
codeMirror: 'rust',
|
|
110
|
+
entry: 'main.rs',
|
|
111
|
+
comment: '//',
|
|
112
|
+
command: 'rustc',
|
|
113
|
+
},
|
|
114
|
+
bash: {
|
|
115
|
+
id: 'bash',
|
|
116
|
+
label: 'Bash',
|
|
117
|
+
extensions: ['.sh', '.bash'],
|
|
118
|
+
backend: 'local',
|
|
119
|
+
codeMirror: null,
|
|
120
|
+
entry: 'main.sh',
|
|
121
|
+
comment: '#',
|
|
122
|
+
command: 'bash',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const LANGUAGE_IDS = /** @type {LanguageId[]} */ (Object.keys(LANGUAGES));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Aliases seen in fenced-code info strings / course metadata.
|
|
130
|
+
* @type {Record<string, LanguageId>}
|
|
131
|
+
*/
|
|
132
|
+
const ALIASES = {
|
|
133
|
+
js: 'javascript',
|
|
134
|
+
javascript: 'javascript',
|
|
135
|
+
mjs: 'javascript',
|
|
136
|
+
node: 'javascript',
|
|
137
|
+
jsx: 'javascript',
|
|
138
|
+
ts: 'typescript',
|
|
139
|
+
typescript: 'typescript',
|
|
140
|
+
tsx: 'typescript',
|
|
141
|
+
py: 'python',
|
|
142
|
+
python: 'python',
|
|
143
|
+
python3: 'python',
|
|
144
|
+
sql: 'sql',
|
|
145
|
+
sqlite: 'sql',
|
|
146
|
+
html: 'html',
|
|
147
|
+
htm: 'html',
|
|
148
|
+
css: 'css',
|
|
149
|
+
scss: 'css',
|
|
150
|
+
go: 'go',
|
|
151
|
+
golang: 'go',
|
|
152
|
+
java: 'java',
|
|
153
|
+
rust: 'rust',
|
|
154
|
+
rs: 'rust',
|
|
155
|
+
sh: 'bash',
|
|
156
|
+
bash: 'bash',
|
|
157
|
+
shell: 'bash',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Normalise a user/course-supplied language string to a known id.
|
|
162
|
+
* @param {string|null|undefined} value
|
|
163
|
+
* @returns {LanguageId|null}
|
|
164
|
+
*/
|
|
165
|
+
export function normalizeLanguage(value) {
|
|
166
|
+
if (!value) return null;
|
|
167
|
+
const key = String(value).trim().toLowerCase().replace(/^language-/, '');
|
|
168
|
+
if (ALIASES[key]) return ALIASES[key];
|
|
169
|
+
if (LANGUAGES[/** @type {LanguageId} */ (key)]) return /** @type {LanguageId} */ (key);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string|null|undefined} path
|
|
175
|
+
* @returns {LanguageId|null}
|
|
176
|
+
*/
|
|
177
|
+
export function languageForPath(path) {
|
|
178
|
+
if (!path) return null;
|
|
179
|
+
const match = /\.([a-z0-9]+)$/i.exec(String(path));
|
|
180
|
+
if (!match) return null;
|
|
181
|
+
const ext = `.${match[1].toLowerCase()}`;
|
|
182
|
+
for (const spec of Object.values(LANGUAGES)) {
|
|
183
|
+
if (spec.extensions.includes(ext)) return spec.id;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @param {string} id */
|
|
189
|
+
export function getLanguage(id) {
|
|
190
|
+
return LANGUAGES[/** @type {LanguageId} */ (id)] || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {string} id */
|
|
194
|
+
export function isBrowserLanguage(id) {
|
|
195
|
+
return getLanguage(id)?.backend === 'browser';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @param {string} id */
|
|
199
|
+
export function isLocalLanguage(id) {
|
|
200
|
+
return getLanguage(id)?.backend === 'local';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @param {string} id */
|
|
204
|
+
export function defaultEntry(id) {
|
|
205
|
+
return getLanguage(id)?.entry || 'main.txt';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The languages the UI advertises for the "browser" runner.
|
|
210
|
+
* @returns {LanguageSpec[]}
|
|
211
|
+
*/
|
|
212
|
+
export function browserLanguages() {
|
|
213
|
+
return Object.values(LANGUAGES).filter((l) => l.backend === 'browser');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Guess a course's primary language from its kind + lesson text.
|
|
218
|
+
* Programming courses usually name the language in the goal/topics/title.
|
|
219
|
+
* @param {{ kind?: string, title?: string, goal?: string, topics?: string[]|string, language?: string }} course
|
|
220
|
+
* @returns {LanguageId|null}
|
|
221
|
+
*/
|
|
222
|
+
export function guessCourseLanguage(course = {}) {
|
|
223
|
+
const explicit = normalizeLanguage(course.language);
|
|
224
|
+
if (explicit) return explicit;
|
|
225
|
+
|
|
226
|
+
const haystack = [
|
|
227
|
+
course.title,
|
|
228
|
+
course.goal,
|
|
229
|
+
Array.isArray(course.topics) ? course.topics.join(' ') : course.topics,
|
|
230
|
+
]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join(' ')
|
|
233
|
+
.toLowerCase();
|
|
234
|
+
if (!haystack) return null;
|
|
235
|
+
|
|
236
|
+
// Ordered so more specific names win over substrings ("javascript" before "java").
|
|
237
|
+
const ordered = [
|
|
238
|
+
'typescript',
|
|
239
|
+
'javascript',
|
|
240
|
+
'python',
|
|
241
|
+
'rust',
|
|
242
|
+
'golang',
|
|
243
|
+
'go',
|
|
244
|
+
'java',
|
|
245
|
+
'sql',
|
|
246
|
+
'html',
|
|
247
|
+
'css',
|
|
248
|
+
'bash',
|
|
249
|
+
];
|
|
250
|
+
for (const needle of ordered) {
|
|
251
|
+
const re = new RegExp(`(^|[^a-z])${needle}([^a-z]|$)`);
|
|
252
|
+
if (re.test(haystack)) {
|
|
253
|
+
const id = normalizeLanguage(needle);
|
|
254
|
+
if (id) return id;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local runner client (CODE-04) — talks to `web/studio-runner.js` through the existing local
|
|
3
|
+
* agent bridge (`web/server.js` → `web/studio-api.js`), reusing the same token + CORS + LNA
|
|
4
|
+
* handling as the AI bridge (see ../ai/fetch-local.js).
|
|
5
|
+
*
|
|
6
|
+
* Endpoints (mounted by studio-api.js as `router.use('/run', createRunnerRouter(...))`):
|
|
7
|
+
* GET {bridge}/api/studio/run/toolchains -> { node: true, python3: true, go: false, … }
|
|
8
|
+
* POST {bridge}/api/studio/run -> SSE: {"stdout"}, {"stderr"}, {"exit"}, [DONE]
|
|
9
|
+
*
|
|
10
|
+
* Files are written server-side to `${PROFESSOR_HOME:-~/.professor}/workspaces/<courseSlug>/` so
|
|
11
|
+
* the learner can open the same folder in a real editor.
|
|
12
|
+
*/
|
|
13
|
+
import { parseSSE } from '../ai/sse.js';
|
|
14
|
+
import { localFetch, classifyNetworkError, mapHttpError } from '../ai/fetch-local.js';
|
|
15
|
+
import { isAbortError, providerError } from '../ai/errors.js';
|
|
16
|
+
import { DEFAULT_BRIDGE_URL } from '../ai/bridge.js';
|
|
17
|
+
import { assertLoopbackBridgeUrl } from '../settings.js';
|
|
18
|
+
|
|
19
|
+
const RUN_HINT = 'Start the local server (`npx ronin-skillforce web` or `npm start`), then retry. Or pick a language that runs in the browser.';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} settings
|
|
23
|
+
* @returns {string} the runner API root (no trailing slash)
|
|
24
|
+
*/
|
|
25
|
+
export function runnerBase(settings = {}) {
|
|
26
|
+
const root = String(settings.bridge?.bridgeUrl || settings.bridgeUrl || DEFAULT_BRIDGE_URL).replace(/\/$/, '');
|
|
27
|
+
assertLoopbackBridgeUrl(root);
|
|
28
|
+
return `${root}/api/studio/run`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} settings
|
|
33
|
+
* @returns {{ 'Content-Type': string, 'X-Studio-Token'?: string }}
|
|
34
|
+
*/
|
|
35
|
+
function headersFor(settings = {}) {
|
|
36
|
+
const token = settings.bridge?.bridgeToken || settings.bridgeToken;
|
|
37
|
+
return {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...(token ? { 'X-Studio-Token': token } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Which toolchains are installed on the server.
|
|
45
|
+
* @param {object} settings
|
|
46
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
47
|
+
* @returns {Promise<Record<string, boolean>>}
|
|
48
|
+
*/
|
|
49
|
+
export async function checkToolchains(settings = {}, { signal } = {}) {
|
|
50
|
+
const url = `${runnerBase(settings)}/toolchains`;
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await localFetch(url, { headers: headersFor(settings), signal });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (isAbortError(err)) throw providerError('aborted', 'Stopped');
|
|
56
|
+
const mapped = classifyNetworkError(err, { url, what: 'the local runner', notRunningHint: RUN_HINT });
|
|
57
|
+
if (mapped.code === 'not_running') mapped.hint = RUN_HINT;
|
|
58
|
+
throw mapped;
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) throw await mapHttpError(res, { what: 'the local runner' });
|
|
61
|
+
return res.json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} input
|
|
66
|
+
* @param {object} input.settings
|
|
67
|
+
* @param {string} [input.courseSlug]
|
|
68
|
+
* @param {string} input.language
|
|
69
|
+
* @param {Record<string,string>} input.files
|
|
70
|
+
* @param {string|null} [input.entry]
|
|
71
|
+
* @param {AbortSignal} [input.signal]
|
|
72
|
+
* @param {(chunk:{stream:'stdout'|'stderr',text:string})=>void} [input.onOutput]
|
|
73
|
+
* @param {number} [input.timeoutMs]
|
|
74
|
+
* @returns {Promise<import('./browser.js').RunResult>}
|
|
75
|
+
*/
|
|
76
|
+
export async function runLocal({
|
|
77
|
+
settings = {},
|
|
78
|
+
courseSlug = 'workspace',
|
|
79
|
+
language,
|
|
80
|
+
files = {},
|
|
81
|
+
entry = null,
|
|
82
|
+
signal,
|
|
83
|
+
onOutput,
|
|
84
|
+
timeoutMs,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const url = runnerBase(settings);
|
|
87
|
+
const started = Date.now();
|
|
88
|
+
let res;
|
|
89
|
+
try {
|
|
90
|
+
res = await localFetch(url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: headersFor(settings),
|
|
93
|
+
body: JSON.stringify({ courseSlug, language, files, entry, timeoutMs }),
|
|
94
|
+
signal,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (isAbortError(err)) throw providerError('aborted', 'Stopped');
|
|
98
|
+
const mapped = classifyNetworkError(err, { url, what: 'the local runner', notRunningHint: RUN_HINT });
|
|
99
|
+
if (mapped.code === 'not_running') mapped.hint = RUN_HINT;
|
|
100
|
+
throw mapped;
|
|
101
|
+
}
|
|
102
|
+
if (!res.ok) throw await mapHttpError(res, { what: 'the local runner' });
|
|
103
|
+
if (!res.body) throw providerError('unknown', 'The local runner returned no stream');
|
|
104
|
+
|
|
105
|
+
let stdout = '';
|
|
106
|
+
let stderr = '';
|
|
107
|
+
let exitCode = null;
|
|
108
|
+
try {
|
|
109
|
+
for await (const data of parseSSE(res)) {
|
|
110
|
+
if (data === '[DONE]') break;
|
|
111
|
+
let json;
|
|
112
|
+
try {
|
|
113
|
+
json = JSON.parse(data);
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (typeof json.stdout === 'string') {
|
|
118
|
+
stdout += json.stdout;
|
|
119
|
+
onOutput?.({ stream: 'stdout', text: json.stdout });
|
|
120
|
+
}
|
|
121
|
+
if (typeof json.stderr === 'string') {
|
|
122
|
+
stderr += json.stderr;
|
|
123
|
+
onOutput?.({ stream: 'stderr', text: json.stderr });
|
|
124
|
+
}
|
|
125
|
+
if (json.error) {
|
|
126
|
+
stderr += `${json.error}\n`;
|
|
127
|
+
onOutput?.({ stream: 'stderr', text: `${json.error}\n` });
|
|
128
|
+
}
|
|
129
|
+
if (typeof json.exit === 'number') exitCode = json.exit;
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (isAbortError(err)) {
|
|
133
|
+
return { stdout, stderr, exitCode, checks: [], durationMs: Date.now() - started, runner: 'local', aborted: true };
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { stdout, stderr, exitCode: exitCode ?? 0, checks: [], durationMs: Date.now() - started, runner: 'local' };
|
|
139
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path safety shared by the browser runner and `web/studio-runner.js` (PITFALLS: path traversal).
|
|
3
|
+
*
|
|
4
|
+
* A workspace file is addressed by a *relative* path (e.g. `main.py`, `src/util.py`). Anything
|
|
5
|
+
* absolute, containing a `..` segment, a NUL byte, or a Windows drive prefix is rejected so a
|
|
6
|
+
* crafted lesson/CLI payload can never write outside the workspace root.
|
|
7
|
+
*
|
|
8
|
+
* Pure module — no Node/DOM APIs — so both the SPA and the Express router import the same rules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const WINDOWS_DRIVE = /^[a-zA-Z]:[\\/]/;
|
|
12
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
13
|
+
const BACKSLASH = /\\/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} path
|
|
17
|
+
* @returns {boolean} true when `path` is a safe, relative, portable path
|
|
18
|
+
*/
|
|
19
|
+
export function isSafeRelPath(path) {
|
|
20
|
+
if (typeof path !== 'string') return false;
|
|
21
|
+
const value = path.trim();
|
|
22
|
+
if (!value) return false;
|
|
23
|
+
if (value.length > 512) return false;
|
|
24
|
+
if (CONTROL_CHARS.test(value)) return false;
|
|
25
|
+
if (value.startsWith('/') || value.startsWith('~')) return false;
|
|
26
|
+
if (WINDOWS_DRIVE.test(value)) return false;
|
|
27
|
+
if (BACKSLASH.test(value)) return false; // enforce forward slashes for portability
|
|
28
|
+
if (value.endsWith('/')) return false;
|
|
29
|
+
|
|
30
|
+
const segments = value.split('/');
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
if (segment === '' || segment === '.' || segment === '..') return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A course slug becomes a single folder name under the workspaces root.
|
|
39
|
+
* @param {unknown} slug
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isSafeSlug(slug) {
|
|
43
|
+
if (typeof slug !== 'string') return false;
|
|
44
|
+
const value = slug.trim();
|
|
45
|
+
if (!value || value.length > 120) return false;
|
|
46
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(value)) return false;
|
|
47
|
+
if (value === '.' || value === '..') return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Kebab-case slug from an arbitrary title (used to derive a course folder name).
|
|
53
|
+
* @param {string} title
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function slugify(title) {
|
|
57
|
+
const out = String(title || '')
|
|
58
|
+
.normalize('NFKD')
|
|
59
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
62
|
+
.replace(/^-+|-+$/g, '')
|
|
63
|
+
.slice(0, 80);
|
|
64
|
+
return out || 'workspace';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normalise a relative path's separators to forward slashes (does not resolve `..`).
|
|
69
|
+
* @param {string} path
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function toPosix(path) {
|
|
73
|
+
return String(path || '').replace(/\\/g, '/');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve `rel` under `root` **and** assert the result stays inside `root`.
|
|
78
|
+
* Uses only string ops so it behaves identically in the browser and in Node.
|
|
79
|
+
* @param {string} root
|
|
80
|
+
* @param {string} rel
|
|
81
|
+
* @returns {string} the joined path (with the root's existing separators)
|
|
82
|
+
* @throws {Error} when `rel` is unsafe
|
|
83
|
+
*/
|
|
84
|
+
export function resolveInside(root, rel) {
|
|
85
|
+
if (!isSafeRelPath(rel)) throw new Error(`Unsafe path: ${rel}`);
|
|
86
|
+
const sep = root.includes('\\') ? '\\' : '/';
|
|
87
|
+
const base = root.replace(/[\\/]+$/, '');
|
|
88
|
+
return `${base}${sep}${toPosix(rel).split('/').join(sep)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert a `{ path: content }` map (or `[{path, content}]` list) into a normalised list.
|
|
93
|
+
* @param {Record<string,string>|Array<{path?:string, name?:string, content?:string}>|null|undefined} files
|
|
94
|
+
* @returns {Array<{ path: string, content: string }>}
|
|
95
|
+
*/
|
|
96
|
+
export function normalizeFiles(files) {
|
|
97
|
+
if (!files) return [];
|
|
98
|
+
const rows = Array.isArray(files)
|
|
99
|
+
? files.map((f) => ({ path: f?.path ?? f?.name, content: f?.content ?? '' }))
|
|
100
|
+
: Object.entries(files).map(([path, content]) => ({ path, content }));
|
|
101
|
+
return rows
|
|
102
|
+
.filter((f) => typeof f.path === 'string' && f.path)
|
|
103
|
+
.map((f) => ({ path: toPosix(f.path), content: String(f.content ?? '') }));
|
|
104
|
+
}
|