ronin-skillforce 0.0.0-stage â 0.2.0-beta.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 +74 -0
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +258 -2
- package/bin/cli.js +823 -0
- package/bin/commands/archive.js +192 -0
- package/bin/commands/capstone-status.js +82 -0
- package/bin/commands/check.js +298 -0
- package/bin/commands/create.js +226 -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-autostart.js +414 -0
- package/bin/commands/studio-state.js +159 -0
- package/bin/commands/studio.js +831 -0
- package/bin/commands/trail.js +361 -0
- package/bin/commands/web.js +348 -0
- package/bin/course-reader.js +602 -0
- package/bin/node-version.js +60 -0
- package/bin/platforms/_shared.js +337 -0
- package/bin/platforms/claude.js +85 -0
- package/bin/platforms/cursor.js +52 -0
- package/bin/platforms/gemini.js +64 -0
- package/bin/platforms/opencode.js +114 -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 +92 -4
- package/plugin.json +139 -0
- package/shared/SKILL.md +598 -0
- package/shared/agents/coach.md +228 -0
- package/shared/agents/navigator.md +208 -0
- package/shared/agents/professor.md +354 -0
- package/shared/agents/researcher.md +181 -0
- package/shared/agents/spotter.md +128 -0
- package/shared/commands/professor/archive.md +96 -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 +72 -0
- package/shared/commands/professor/export.md +69 -0
- package/shared/commands/professor/hint.md +120 -0
- package/shared/commands/professor/navigator.md +93 -0
- package/shared/commands/professor/new-topic.md +73 -0
- package/shared/commands/professor/next.md +178 -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 +57 -0
- package/shared/commands/professor/schedule.md +75 -0
- package/shared/commands/professor/skill-export.md +86 -0
- package/shared/commands/professor/spotter.md +93 -0
- package/shared/commands/professor/stuck.md +51 -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 +54 -0
- package/shared/hooks/pre-compact.js +221 -0
- package/studio/README.md +313 -0
- package/studio/dist/assets/BetaReportPanel-BJ9HIAnz.js +1 -0
- package/studio/dist/assets/Capstone-CoUU-jP7.js +6 -0
- package/studio/dist/assets/CodeWorkspace-BNrL4qvR.js +2 -0
- package/studio/dist/assets/Course-BMGTXus_.js +1 -0
- package/studio/dist/assets/Editor-Cw9j3cnp.js +53 -0
- package/studio/dist/assets/Home-D2woITq5.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-CYQhrDCZ.js +2 -0
- package/studio/dist/assets/NewCourse-CMpwVRQb.js +952 -0
- package/studio/dist/assets/Progress-2rS19Fur.js +1 -0
- package/studio/dist/assets/Review-SlqK-hjp.js +1 -0
- package/studio/dist/assets/Settings-t42u7n9H.js +1 -0
- package/studio/dist/assets/TutorPanel-DQDWIU7f.js +123 -0
- package/studio/dist/assets/abnfDiagram-WEYOIA3C-jTUmGBuB.js +1 -0
- package/studio/dist/assets/anthropic-iIm2l85r.js +15 -0
- package/studio/dist/assets/arc-Y1ARJVZ4.js +1 -0
- package/studio/dist/assets/architecture-WOLXFQ4H-Ch9Ld_vv.js +1 -0
- package/studio/dist/assets/architectureDiagram-ZHVYKT53-Dy-z6wsy.js +36 -0
- package/studio/dist/assets/array-BifhSqXX.js +1 -0
- package/studio/dist/assets/blockDiagram-RJAE7QA3-2fHVrc6N.js +140 -0
- package/studio/dist/assets/bridge-Cr0q4rLp.js +1 -0
- package/studio/dist/assets/c4Diagram-XDPPPE54-BuuKlG4V.js +38 -0
- package/studio/dist/assets/capstone-DhwBeNqk.js +135 -0
- package/studio/dist/assets/card-store-CVMBc-Pv.js +1 -0
- package/studio/dist/assets/channel-M_JkdHrL.js +1 -0
- package/studio/dist/assets/chunk-2E7WHVMD-lZdQWttk.js +88 -0
- package/studio/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/studio/dist/assets/chunk-4EMNKR2T-C6kbMwke.js +1 -0
- package/studio/dist/assets/chunk-55XCU76K-rkBzkZog.js +1 -0
- package/studio/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/studio/dist/assets/chunk-7S6XSTFO-Skg347QF.js +2 -0
- package/studio/dist/assets/chunk-AQDTSKZB-0pSDBJ23.js +72 -0
- package/studio/dist/assets/chunk-DSNLSHJZ-BaeQDl2i.js +129 -0
- package/studio/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/studio/dist/assets/chunk-HAGFOTED-BEm00juw.js +62 -0
- package/studio/dist/assets/chunk-HNE4Q36P-DyCR-LvS.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-BNJ9uMmB.js +213 -0
- package/studio/dist/assets/chunk-UHW7PFBP-DqJczBLs.js +1 -0
- package/studio/dist/assets/chunk-VQ5W62QI-CVWYSP5Q.js +1 -0
- package/studio/dist/assets/chunk-WQQL4CDK-DJf0Dx05.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-BFUm_JHA.js +1 -0
- package/studio/dist/assets/chunk-ZAUX2LTX-C5ETkkSE.js +1 -0
- package/studio/dist/assets/chunk-ZCHMQSOR-WVX9r9PZ.js +2 -0
- package/studio/dist/assets/classDiagram-v2-4MYWR3UA-CI77SpuR.js +217 -0
- package/studio/dist/assets/cose-bilkent-JH36ORCC-DMBAZFnj.js +1 -0
- package/studio/dist/assets/course-export-C8jSPuda.js +1 -0
- package/studio/dist/assets/cynefin-EF2NZ3EQ-CLffmM25.js +1 -0
- package/studio/dist/assets/cynefinDiagram-WF24UDRP-BvLwocOb.js +62 -0
- package/studio/dist/assets/cytoscape.esm-CHBj4VkZ.js +321 -0
- package/studio/dist/assets/dagre-NK4GIZVX-C2nMAVdh.js +4 -0
- package/studio/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/studio/dist/assets/diagram-CBK3MWTC-Bid-pdax.js +24 -0
- package/studio/dist/assets/diagram-I5XIIV2D-DYJeAvzl.js +24 -0
- package/studio/dist/assets/diagram-ODIL2UCW-B06X2r_F.js +30 -0
- package/studio/dist/assets/diagram-PFBEJUXE-CwY--5Rz.js +3 -0
- package/studio/dist/assets/diagram-RJA3I4VV-4SqhH8zw.js +41 -0
- package/studio/dist/assets/diagram-TYUNFSNR-CrW4AIM9.js +200 -0
- package/studio/dist/assets/dist-CumnGVmJ.js +1 -0
- package/studio/dist/assets/ebnfDiagram-V4P3TTPT-xeoCmH-d.js +1 -0
- package/studio/dist/assets/elk-QONTBCTQ-Dadmz6iS.js +27 -0
- package/studio/dist/assets/erDiagram-KYQIGQAA-CSuH0bbF.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-CciVfx3I.js +1 -0
- package/studio/dist/assets/fetch-local-DG3O3hmQ.js +4 -0
- package/studio/dist/assets/flowDiagram-IUGLLWBU-DgNR54qK.js +1 -0
- package/studio/dist/assets/ganttDiagram-R7GPNTJY-C0wtA-2y.js +292 -0
- package/studio/dist/assets/gitGraph-VSP46ZUC-BXoDzaA3.js +1 -0
- package/studio/dist/assets/gitGraphDiagram-PDYL7XHY-vZBlNRPe.js +106 -0
- package/studio/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/studio/dist/assets/index-CS8XPYbB.js +313 -0
- package/studio/dist/assets/index-CVenTTOY.css +2 -0
- package/studio/dist/assets/info-OHQRW6UA-bs2Qpf7P.js +1 -0
- package/studio/dist/assets/infoDiagram-MVDRKAX5-rPJio6-1.js +2 -0
- package/studio/dist/assets/init-C-OQMol4.js +1 -0
- package/studio/dist/assets/ishikawaDiagram-HPEHV2GE-CXf9XFff.js +70 -0
- package/studio/dist/assets/journeyDiagram-BOSHW7O7-C3MSH79o.js +139 -0
- package/studio/dist/assets/js.worker-AD7DjksO.js +4 -0
- package/studio/dist/assets/kanban-definition-FNSA4HC5-Bdh7SB4n.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-CHkoQXJF.js +1 -0
- package/studio/dist/assets/linear-C9iwaMOi.js +1 -0
- package/studio/dist/assets/mermaid-parser.core-Do1oxD1S.js +7 -0
- package/studio/dist/assets/mermaid.core-DgZUD0sU.js +44 -0
- package/studio/dist/assets/mindmap-definition-6OY57UG3-PUaK6oE_.js +98 -0
- package/studio/dist/assets/node.browser-DkpMze_r.js +1 -0
- package/studio/dist/assets/node.browser-HQ5SKBOf.js +1 -0
- package/studio/dist/assets/ollama-bJNp3jaD.js +1 -0
- package/studio/dist/assets/openai-compatible-0tmAyMlN.js +1 -0
- package/studio/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/studio/dist/assets/packet-JDAUHWVQ-vf2FtqFr.js +1 -0
- package/studio/dist/assets/path-fybaL0A-.js +1 -0
- package/studio/dist/assets/pegDiagram-ECIUJNBX-NEVrgPTY.js +1 -0
- package/studio/dist/assets/pie-XZMESJXO-BIWydVNZ.js +1 -0
- package/studio/dist/assets/pieDiagram-GNMPY74T-BI9_u6rT.js +39 -0
- package/studio/dist/assets/python.worker-CzTrFdd9.js +24 -0
- package/studio/dist/assets/quadrantDiagram-D57K4MC3-ryNpoBC5.js +7 -0
- package/studio/dist/assets/radar-ABXABTNO-D8TjnvBB.js +1 -0
- package/studio/dist/assets/railroad-I3PHUGI6-BmEDV5kf.js +1 -0
- package/studio/dist/assets/railroad-abnf-LNEFI6M7-Cs_sArMc.js +1 -0
- package/studio/dist/assets/railroad-ebnf-DWYD2UWJ-DYuCq46d.js +1 -0
- package/studio/dist/assets/railroad-peg-JR7BVNR7-BcXSgdMy.js +1 -0
- package/studio/dist/assets/railroadDiagram-Y72NRBL7-DBcc8b2c.js +1 -0
- package/studio/dist/assets/react-B8SKf8RM.js +1 -0
- package/studio/dist/assets/rehype-katex-BfUIq5As.js +259 -0
- package/studio/dist/assets/remark-math-XvZOjmDO.js +5 -0
- package/studio/dist/assets/requirementDiagram-EQYSW5RA-CVcBgM1L.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-DjZ81hDs.js +40 -0
- package/studio/dist/assets/schemas-BOc2YsOP.js +55 -0
- package/studio/dist/assets/sequenceDiagram-BLEHNHQU-BNwOa2S7.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-BFWa5lEx.js +291 -0
- package/studio/dist/assets/swimlanes-B3ENRKFQ-LfM_Jg6Z.js +1 -0
- package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-BDmpoiGp.js +8 -0
- package/studio/dist/assets/timeline-definition-JXYKGLGB-BwGzkWgZ.js +120 -0
- package/studio/dist/assets/treeView-D4JQ5SDB-MLk3VuRt.js +1 -0
- package/studio/dist/assets/treemap-SAJKECNS-D8iSjp7L.js +1 -0
- package/studio/dist/assets/useStream-rdsR6A7P.js +1 -0
- package/studio/dist/assets/usecaseDiagram-WPOLBNNX-BqxpaNlT.js +328 -0
- package/studio/dist/assets/vennDiagram-Q3Y3IDCW-CpeUDMhQ.js +34 -0
- package/studio/dist/assets/wardley-7MLQ67FV-tAMbCZAj.js +1 -0
- package/studio/dist/assets/wardleyDiagram-IVAIDKWA-CDJViWzt.js +78 -0
- package/studio/dist/assets/xychartDiagram-4URNHDU4-CwaQrd3_.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 +30 -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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillforce archive <slug> [--json]` â retire a course without losing learner work.
|
|
3
|
+
*
|
|
4
|
+
* - Copies the whole `learning/<slug>/` directory, minus `.git`, to
|
|
5
|
+
* `.course_archive/<slug>[-N]` (an existing archive is never overwritten).
|
|
6
|
+
* - Programming courses keep their commits on branch `archive/<slug>` instead of `-D`.
|
|
7
|
+
* - Plain-folder courses are removed only after the copy is verified.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
10
|
+
import {
|
|
11
|
+
cpSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
lstatSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
} from 'fs';
|
|
18
|
+
import { basename, join, relative } from 'path';
|
|
19
|
+
import { listWorktrees, saveCourse, excludeRepoPath, SLUG_RE } from './create.js';
|
|
20
|
+
import { style } from '../registry-helpers.js';
|
|
21
|
+
|
|
22
|
+
function git(cwd, args) {
|
|
23
|
+
return execFileSync('git', args, {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tryGit(cwd, args) {
|
|
31
|
+
try { return git(cwd, args); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** First non-existing path among `<base>`, `<base>-2`, `<base>-3`, âĻ */
|
|
35
|
+
export function nextFreePath(base) {
|
|
36
|
+
if (!existsSync(base)) return base;
|
|
37
|
+
for (let n = 2; ; n++) {
|
|
38
|
+
const candidate = `${base}-${n}`;
|
|
39
|
+
if (!existsSync(candidate)) return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nextFreeBranch(cwd, base) {
|
|
44
|
+
if (tryGit(cwd, ['rev-parse', '--verify', '-q', `refs/heads/${base}`]) === null) return base;
|
|
45
|
+
for (let n = 2; ; n++) {
|
|
46
|
+
const candidate = `${base}-${n}`;
|
|
47
|
+
const exists = tryGit(cwd, ['rev-parse', '--verify', '-q', `refs/heads/${candidate}`]);
|
|
48
|
+
if (exists === null) return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every entry under `dir`, as paths relative to it, skipping any `.git` entry. */
|
|
53
|
+
function entries(dir, prefix = '') {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const name of readdirSync(dir).sort()) {
|
|
56
|
+
if (name === '.git') continue;
|
|
57
|
+
const rel = prefix ? join(prefix, name) : name;
|
|
58
|
+
const abs = join(dir, name);
|
|
59
|
+
const stat = lstatSync(abs);
|
|
60
|
+
out.push({ rel, stat });
|
|
61
|
+
if (stat.isDirectory()) out.push(...entries(abs, rel));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Throw unless every source entry exists in the copy with the same type and byte size. */
|
|
67
|
+
export function verifyCopy(src, dest) {
|
|
68
|
+
for (const { rel, stat } of entries(src)) {
|
|
69
|
+
const copy = join(dest, rel);
|
|
70
|
+
if (!existsSync(copy)) throw new Error(`Archive copy is missing ${rel}.`);
|
|
71
|
+
const copied = lstatSync(copy);
|
|
72
|
+
if (copied.isDirectory() !== stat.isDirectory()) {
|
|
73
|
+
throw new Error(`Archive copy has the wrong type for ${rel}.`);
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isDirectory() && copied.size !== stat.size) {
|
|
76
|
+
throw new Error(`Archive copy size differs for ${rel}.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Collapse multiline git stderr into one readable line. */
|
|
82
|
+
function detail(err) {
|
|
83
|
+
return (err.stderr || err.message || '').toString().trim().split('\n')[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function archiveProgramming({ root, cwd, slug, courseDir, branch, target }) {
|
|
87
|
+
let archiveBranch = branch;
|
|
88
|
+
if (!branch || !branch.startsWith('archive/')) {
|
|
89
|
+
try {
|
|
90
|
+
saveCourse({ cwd, slug });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(`Could not commit learning/${slug}/ â nothing was removed. ${detail(err)} ` +
|
|
93
|
+
`The copy is at ${relative(cwd, target)}.`);
|
|
94
|
+
}
|
|
95
|
+
const dirty = tryGit(courseDir, ['status', '--porcelain']);
|
|
96
|
+
if (dirty === null) {
|
|
97
|
+
throw new Error(`Could not read the status of learning/${slug}/ â nothing was removed. ` +
|
|
98
|
+
`The copy is at ${relative(cwd, target)}.`);
|
|
99
|
+
}
|
|
100
|
+
if (dirty !== '') {
|
|
101
|
+
throw new Error(`learning/${slug}/ still has uncommitted changes â nothing was removed. ` +
|
|
102
|
+
`The copy is at ${relative(cwd, target)}.`);
|
|
103
|
+
}
|
|
104
|
+
archiveBranch = nextFreeBranch(root, `archive/${slug}`);
|
|
105
|
+
try {
|
|
106
|
+
git(courseDir, ['branch', '-m', archiveBranch]);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new Error(`Could not rename the course branch to ${archiveBranch} â nothing was ` +
|
|
109
|
+
`removed. ${detail(err)} The copy is at ${relative(cwd, target)}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
git(root, ['worktree', 'remove', courseDir]);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new Error(`Could not remove learning/${slug}/ (it is still registered on branch ` +
|
|
117
|
+
`${archiveBranch}). ${detail(err)} The copy is at ${relative(cwd, target)}.`);
|
|
118
|
+
}
|
|
119
|
+
return { archivedTo: relative(cwd, target), branch: archiveBranch, removed: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Archive one course. Throws with a user-facing message on any failure; the course is then
|
|
124
|
+
* still in `learning/<slug>/` unless the error says git removed it.
|
|
125
|
+
* @returns {{ archivedTo: string, branch: string|null, removed: boolean, warning?: string }}
|
|
126
|
+
*/
|
|
127
|
+
export function archiveCourse({ cwd, slug, verify = verifyCopy }) {
|
|
128
|
+
if (!SLUG_RE.test(slug || '')) {
|
|
129
|
+
throw new Error(`Invalid slug ${JSON.stringify(slug)} â use lowercase a-z, 0-9 and single hyphens.`);
|
|
130
|
+
}
|
|
131
|
+
const courseDir = join(cwd, 'learning', slug);
|
|
132
|
+
if (!existsSync(join(courseDir, 'COURSE.md'))) {
|
|
133
|
+
throw new Error(`No course at learning/${slug}/.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let warning;
|
|
137
|
+
const root = tryGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
138
|
+
if (root) {
|
|
139
|
+
try {
|
|
140
|
+
excludeRepoPath(root, cwd, join(cwd, '.course_archive'));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
warning = `Could not add .course_archive/ to git info/exclude: ${detail(err)}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const target = nextFreePath(join(cwd, '.course_archive', slug));
|
|
147
|
+
cpSync(courseDir, target, {
|
|
148
|
+
recursive: true,
|
|
149
|
+
filter: (source) => basename(source) !== '.git',
|
|
150
|
+
});
|
|
151
|
+
verify(courseDir, target);
|
|
152
|
+
|
|
153
|
+
const worktree = root
|
|
154
|
+
? listWorktrees(root).find((tree) => tree.path === realpathSync.native(courseDir))
|
|
155
|
+
: null;
|
|
156
|
+
if (worktree) {
|
|
157
|
+
const result = archiveProgramming({ root, cwd, slug, courseDir, branch: worktree.branch, target });
|
|
158
|
+
return warning ? { ...result, warning } : result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
rmSync(courseDir, { recursive: true });
|
|
162
|
+
const result = { archivedTo: relative(cwd, target), branch: null, removed: true };
|
|
163
|
+
return warning ? { ...result, warning } : result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function positionals(args) {
|
|
167
|
+
return args.filter((arg) => !arg.startsWith('-'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** CLI entry point for `skillforce archive`. */
|
|
171
|
+
export async function run({ args = [], cwd = process.cwd() } = {}) {
|
|
172
|
+
const slug = positionals(args)[0];
|
|
173
|
+
const json = args.includes('--json');
|
|
174
|
+
try {
|
|
175
|
+
const result = archiveCourse({ cwd, slug });
|
|
176
|
+
if (json) {
|
|
177
|
+
console.log(JSON.stringify({
|
|
178
|
+
archivedTo: result.archivedTo,
|
|
179
|
+
branch: result.branch,
|
|
180
|
+
removed: result.removed,
|
|
181
|
+
}));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log(style('success', `Archived learning/${slug}/ â ${result.archivedTo}`));
|
|
185
|
+
if (result.branch) console.log(style('info', `History kept on branch ${result.branch}.`));
|
|
186
|
+
if (result.warning) console.log(style('warning', result.warning));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (json) console.log(JSON.stringify({ error: err.message }));
|
|
189
|
+
else console.error(style('error', err.message));
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { resolveCourse } from './trail.js';
|
|
4
|
+
import { style } from '../registry-helpers.js';
|
|
5
|
+
|
|
6
|
+
const HEADER_RE = /^(\s*[-*]?\s*\*\*Capstone status\*\*\s*:?\s*).*$/mi;
|
|
7
|
+
const EMOJI_RE = /(âŦ|đ|â
|đ)/;
|
|
8
|
+
const LIST_ROW_RE = /^\s*(?:[-*+]\s*)?(âŦ|đ|â
|đ)/;
|
|
9
|
+
|
|
10
|
+
export const CAPSTONE_STATES = {
|
|
11
|
+
locked: { emoji: 'đ', label: 'Locked', header: 'đ Locked (complete all sections to unlock)' },
|
|
12
|
+
active: { emoji: 'đ', label: 'In progress', header: 'đ In progress' },
|
|
13
|
+
done: { emoji: 'â
', label: 'Complete', header: 'â
Complete' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Keep the `**Capstone status**` header and the capstone row telling the same story. */
|
|
17
|
+
export function setCapstoneStatus(text, status) {
|
|
18
|
+
const state = CAPSTONE_STATES[status];
|
|
19
|
+
if (!state) throw new Error(`Unknown capstone status "${status}" â use locked, active or done.`);
|
|
20
|
+
if (!HEADER_RE.test(text)) {
|
|
21
|
+
throw new Error('No **Capstone status** line in COURSE.md â nothing was updated.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines = text.replace(HEADER_RE, (m, prefix) => `${prefix}${state.header}`).split('\n');
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
if (!/capstone/i.test(lines[i])) continue;
|
|
27
|
+
|
|
28
|
+
if (/^\s*\|.*\|\s*$/.test(lines[i])) {
|
|
29
|
+
const cells = lines[i].split('|');
|
|
30
|
+
const at = cells.findIndex((c, idx) =>
|
|
31
|
+
idx > 0 && idx < cells.length - 1 && EMOJI_RE.test(c) && !/capstone/i.test(c));
|
|
32
|
+
if (at === -1) continue;
|
|
33
|
+
cells[at] = ` ${state.emoji} ${state.label} `;
|
|
34
|
+
lines[i] = cells.join('|');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (LIST_ROW_RE.test(lines[i])) {
|
|
39
|
+
lines[i] = lines[i].replace(EMOJI_RE, state.emoji);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function recordCapstoneStatus({ cwd, slug, status }) {
|
|
46
|
+
if (!slug) throw new Error('Pass the course slug â capstone-status never guesses which course to write.');
|
|
47
|
+
if (!status) throw new Error('Pass --set locked|active|done â nothing was updated.');
|
|
48
|
+
const found = resolveCourse(cwd, slug);
|
|
49
|
+
const path = join(found.dir, 'COURSE.md');
|
|
50
|
+
if (!existsSync(path)) throw new Error(`No COURSE.md in ${found.dir}`);
|
|
51
|
+
const next = setCapstoneStatus(readFileSync(path, 'utf-8'), status);
|
|
52
|
+
writeFileSync(path, next);
|
|
53
|
+
return { updated: true, path, section: found.slug, status };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function flagValue(args, name) {
|
|
57
|
+
const i = args.indexOf(name);
|
|
58
|
+
return i !== -1 && args[i + 1] !== undefined && !args[i + 1].startsWith('--') ? args[i + 1] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function positionals(args) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i] === '--set') { i++; continue; }
|
|
65
|
+
if (!args[i].startsWith('--')) out.push(args[i]);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function run({ args = [], cwd = process.cwd() } = {}) {
|
|
71
|
+
const slug = positionals(args)[0] || null;
|
|
72
|
+
const json = args.includes('--json');
|
|
73
|
+
try {
|
|
74
|
+
const r = recordCapstoneStatus({ cwd, slug, status: flagValue(args, '--set') });
|
|
75
|
+
if (json) console.log(JSON.stringify(r));
|
|
76
|
+
else console.log(style('success', `Capstone status set to ${r.status} in ${relative(cwd, r.path)}`));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (json) console.log(JSON.stringify({ error: err.message }));
|
|
79
|
+
else console.error(style('error', err.message));
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -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
|
+
}
|