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