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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RON-250 — give Coach the real course and keep its review trail on disk.
|
|
3
|
+
*
|
|
4
|
+
* `skillforce context [slug] --json` prints one explicit context object (absolute course
|
|
5
|
+
* path, section, objectives, active exercise, recent trail) so Coach never guesses a path.
|
|
6
|
+
*
|
|
7
|
+
* `skillforce trail <slug> --stdin <<'EOF' {"learner","asked","concept","evidence?","watch?"} EOF`
|
|
8
|
+
* (or the same fields as `--learner ..` flags) appends one review round to `LEARNING-LOG.md` → `## 🗣️ Reasoning Trail` (max 5 rounds per
|
|
9
|
+
* section, oldest dropped), then commits programming course worktrees.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { isAbsolute, join, relative } from 'path';
|
|
13
|
+
import { findCourses, parseLecture, readCourse, todayISO } from '../course-reader.js';
|
|
14
|
+
import { saveCourse } from './create.js';
|
|
15
|
+
import { style } from '../registry-helpers.js';
|
|
16
|
+
|
|
17
|
+
export const MAX_ROUNDS = 5;
|
|
18
|
+
const TRAIL_RE = /^##\s+.*reasoning trail/i;
|
|
19
|
+
const ROUND_RE = /^Round\s+\d+\b/;
|
|
20
|
+
|
|
21
|
+
/** Pick the course by slug; with no slug only a single course is accepted — never guess. */
|
|
22
|
+
export function resolveCourse(cwd, slug) {
|
|
23
|
+
const courses = findCourses(cwd);
|
|
24
|
+
if (slug) {
|
|
25
|
+
const hit = courses.find((c) => c.slug === slug);
|
|
26
|
+
if (hit) return hit;
|
|
27
|
+
throw new Error(`No course "${slug}" under learning/ or courses/. Found: ${courses.map((c) => c.slug).join(', ') || 'none'}.`);
|
|
28
|
+
}
|
|
29
|
+
if (courses.length === 1) return courses[0];
|
|
30
|
+
if (!courses.length) throw new Error('No course found under learning/ or courses/. Run professor:new-topic first.');
|
|
31
|
+
throw new Error(`Several courses found — pass the slug: ${courses.map((c) => c.slug).join(', ')}.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sectionKey = (section) => String(section).trim().split(/\s+/)[0];
|
|
35
|
+
|
|
36
|
+
const sectionHeadRe = (key) =>
|
|
37
|
+
new RegExp(`^###\\s+Section\\s+${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`);
|
|
38
|
+
|
|
39
|
+
/** Lines of `### Section <key>` inside the trail region, or [] when absent. */
|
|
40
|
+
function trailBlock(text, key) {
|
|
41
|
+
const lines = text.split('\n');
|
|
42
|
+
const t = lines.findIndex((l) => TRAIL_RE.test(l));
|
|
43
|
+
if (t === -1) return [];
|
|
44
|
+
const headRe = sectionHeadRe(key);
|
|
45
|
+
let h = -1;
|
|
46
|
+
for (let i = t + 1; i < lines.length && !/^##\s/.test(lines[i]); i++) {
|
|
47
|
+
if (headRe.test(lines[i])) { h = i; break; }
|
|
48
|
+
}
|
|
49
|
+
if (h === -1) return [];
|
|
50
|
+
let end = h + 1;
|
|
51
|
+
while (end < lines.length && !/^#{2,3}\s/.test(lines[end])) end++;
|
|
52
|
+
return lines.slice(h + 1, end);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Split a section block into preface lines (watch flags) and rounds (arrays of lines). */
|
|
56
|
+
function splitRounds(block) {
|
|
57
|
+
const preface = [];
|
|
58
|
+
const rounds = [];
|
|
59
|
+
for (const line of block) {
|
|
60
|
+
if (ROUND_RE.test(line)) rounds.push([line]);
|
|
61
|
+
else if (rounds.length) { if (line.trim()) rounds[rounds.length - 1].push(line); }
|
|
62
|
+
else if (line.trim()) preface.push(line);
|
|
63
|
+
}
|
|
64
|
+
return { preface, rounds };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Explicit context Coach must use. */
|
|
68
|
+
export function buildContext({ cwd, slug }) {
|
|
69
|
+
const found = resolveCourse(cwd, slug);
|
|
70
|
+
const course = readCourse(found.dir, { slug: found.slug });
|
|
71
|
+
const lecturePath = join(found.dir, 'LECTURE.md');
|
|
72
|
+
const lecture = existsSync(lecturePath) ? parseLecture(readFileSync(lecturePath, 'utf-8')) : null;
|
|
73
|
+
const logPath = join(found.dir, 'LEARNING-LOG.md');
|
|
74
|
+
const section = course.currentSection;
|
|
75
|
+
const block = section && existsSync(logPath)
|
|
76
|
+
? trailBlock(readFileSync(logPath, 'utf-8'), String(section.n)) : [];
|
|
77
|
+
const { preface, rounds } = splitRounds(block);
|
|
78
|
+
const exercise = course.activeExercise
|
|
79
|
+
? (isAbsolute(course.activeExercise) ? course.activeExercise : join(found.dir, course.activeExercise))
|
|
80
|
+
: null;
|
|
81
|
+
return {
|
|
82
|
+
slug: found.slug,
|
|
83
|
+
dir: found.dir,
|
|
84
|
+
kind: course.kind,
|
|
85
|
+
title: course.title,
|
|
86
|
+
section: section ? `${section.n} — ${section.title}` : null,
|
|
87
|
+
objectives: lecture?.objectives || [],
|
|
88
|
+
activeExercise: exercise && existsSync(exercise) ? exercise : null,
|
|
89
|
+
learningLog: logPath,
|
|
90
|
+
watch: preface.filter((l) => /watch-this/i.test(l)).map((l) => l.trim()),
|
|
91
|
+
trail: rounds.map((r) => r.join('\n')),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const oneLine = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
96
|
+
|
|
97
|
+
/** Pure: return LEARNING-LOG.md text with one more round for `section`. */
|
|
98
|
+
export function appendRound(text, { slug, section, learner, asked, concept, evidence, watch, date }) {
|
|
99
|
+
let src = text && text.trim()
|
|
100
|
+
? text
|
|
101
|
+
: `---\ncourse: ${slug}\nupdated: ${date}\n---\n\n# Learning Log\n`;
|
|
102
|
+
src = src.replace(/^(---\n[\s\S]*?^updated:\s*).*$/m, `$1${date}`);
|
|
103
|
+
const lines = src.replace(/\n+$/, '').split('\n');
|
|
104
|
+
|
|
105
|
+
let t = lines.findIndex((l) => TRAIL_RE.test(l));
|
|
106
|
+
if (t === -1) {
|
|
107
|
+
const firstH2 = lines.findIndex((l) => /^##\s/.test(l));
|
|
108
|
+
if (firstH2 === -1) lines.push('', '## 🗣️ Reasoning Trail');
|
|
109
|
+
else lines.splice(firstH2, 0, '## 🗣️ Reasoning Trail', '');
|
|
110
|
+
t = lines.findIndex((l) => TRAIL_RE.test(l));
|
|
111
|
+
}
|
|
112
|
+
let regionEnd = t + 1;
|
|
113
|
+
while (regionEnd < lines.length && !/^##\s/.test(lines[regionEnd])) regionEnd++;
|
|
114
|
+
|
|
115
|
+
const key = sectionKey(section);
|
|
116
|
+
const block = trailBlock(lines.join('\n'), key);
|
|
117
|
+
let h = -1;
|
|
118
|
+
for (let i = t + 1; i < regionEnd; i++) {
|
|
119
|
+
if (sectionHeadRe(key).test(lines[i])) { h = i; break; }
|
|
120
|
+
}
|
|
121
|
+
if (h === -1) {
|
|
122
|
+
let at = regionEnd;
|
|
123
|
+
while (at > t + 1 && !lines[at - 1].trim()) at--;
|
|
124
|
+
lines.splice(at, 0, '', `### Section ${section}`);
|
|
125
|
+
h = at + 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { preface, rounds } = splitRounds(block);
|
|
129
|
+
const keep = preface.filter((l) => !(watch && /watch-this/i.test(l)));
|
|
130
|
+
if (watch) keep.push(`⚠️ watch-this: ${oneLine(watch)}`);
|
|
131
|
+
rounds.push([
|
|
132
|
+
`Round 0 — ${date}`,
|
|
133
|
+
` Learner: "${oneLine(learner)}"`,
|
|
134
|
+
` Coach asked: "${oneLine(asked)}"`,
|
|
135
|
+
...(evidence ? [` Evidence: ${oneLine(evidence)}`] : []),
|
|
136
|
+
` Concept: ${oneLine(concept)}`,
|
|
137
|
+
]);
|
|
138
|
+
const kept = rounds.slice(-MAX_ROUNDS).map((r, i) => [r[0].replace(/^Round\s+\d+/, `Round ${i + 1}`), ...r.slice(1)]);
|
|
139
|
+
|
|
140
|
+
const body = ['', ...(keep.length ? [...keep, ''] : []), ...kept.flatMap((r) => [...r, '']).slice(0, -1)];
|
|
141
|
+
lines.splice(h + 1, block.length, ...body, ...(h + 1 + block.length < lines.length ? [''] : []));
|
|
142
|
+
return { text: `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`, rounds: kept.length };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function recordRound({ cwd, slug, section, learner, asked, concept, evidence, watch, date = todayISO() }) {
|
|
146
|
+
if (!slug) throw new Error('Pass the course slug — trail never guesses which course to write.');
|
|
147
|
+
for (const [name, v] of [['--learner', learner], ['--asked', asked], ['--concept', concept]]) {
|
|
148
|
+
if (!oneLine(v)) throw new Error(`Missing ${name} — nothing was recorded.`);
|
|
149
|
+
}
|
|
150
|
+
const ctx = buildContext({ cwd, slug });
|
|
151
|
+
const target = section || ctx.section;
|
|
152
|
+
if (!target) throw new Error('No current section in COURSE.md — pass --section "N — Title".');
|
|
153
|
+
const path = ctx.learningLog;
|
|
154
|
+
const before = existsSync(path) ? readFileSync(path, 'utf-8') : '';
|
|
155
|
+
const { text, rounds } = appendRound(before, { slug, section: target, learner, asked, concept, evidence, watch, date });
|
|
156
|
+
writeFileSync(path, text);
|
|
157
|
+
let saved = false;
|
|
158
|
+
try {
|
|
159
|
+
saved = saveCourse({ cwd, slug, message: `course(${slug}): review round, section ${sectionKey(target)}` }).committed;
|
|
160
|
+
} catch { /* file is written; commit is best-effort (plain folders, missing git identity) */ }
|
|
161
|
+
return { recorded: true, path, section: target, rounds, saved };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function flagValue(args, name) {
|
|
165
|
+
const i = args.indexOf(name);
|
|
166
|
+
return i !== -1 && args[i + 1] !== undefined && !args[i + 1].startsWith('--') ? args[i + 1] : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const VALUE_FLAGS = ['--section', '--learner', '--asked', '--concept', '--evidence', '--watch', '--date'];
|
|
170
|
+
|
|
171
|
+
function positionals(args) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
if (VALUE_FLAGS.includes(args[i])) { i++; continue; }
|
|
175
|
+
if (!args[i].startsWith('--')) out.push(args[i]);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** `context` and `trail` share this module; cli.js passes the command name. */
|
|
181
|
+
export async function run({ args = [], cwd = process.cwd(), command = 'context' } = {}) {
|
|
182
|
+
const slug = positionals(args)[0] || null;
|
|
183
|
+
const json = args.includes('--json');
|
|
184
|
+
try {
|
|
185
|
+
if (command === 'trail') {
|
|
186
|
+
// Agents pass learner text as JSON on stdin (quoted heredoc) so the shell never expands it.
|
|
187
|
+
const fromStdin = args.includes('--stdin') ? JSON.parse(readFileSync(0, 'utf-8')) : {};
|
|
188
|
+
const f = (n) => fromStdin[n.slice(2)] ?? flagValue(args, n);
|
|
189
|
+
const r = recordRound({
|
|
190
|
+
cwd, slug, section: f('--section'), learner: f('--learner'), asked: f('--asked'),
|
|
191
|
+
concept: f('--concept'), evidence: f('--evidence'), watch: f('--watch'), date: f('--date') || undefined,
|
|
192
|
+
});
|
|
193
|
+
if (json) console.log(JSON.stringify(r));
|
|
194
|
+
else {
|
|
195
|
+
console.log(style('success', `Recorded review round ${r.rounds} for Section ${r.section} in ${relative(cwd, r.path)}`
|
|
196
|
+
+ (r.saved ? ' (committed)' : '')));
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const ctx = buildContext({ cwd, slug });
|
|
201
|
+
if (json) { console.log(JSON.stringify(ctx, null, 2)); return; }
|
|
202
|
+
console.log(`${ctx.title} (${ctx.slug}, ${ctx.kind}) — ${ctx.dir}`);
|
|
203
|
+
console.log(`Section: ${ctx.section || '—'}`);
|
|
204
|
+
console.log(`Active exercise: ${ctx.activeExercise || '—'}`);
|
|
205
|
+
console.log(`Trail rounds this section: ${ctx.trail.length}`);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (json) console.log(JSON.stringify({ error: err.message }));
|
|
208
|
+
else console.error(style('error', err.message));
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/commands/web.js — the `skillforce web` command.
|
|
3
|
+
*
|
|
4
|
+
* Studio-only by default: no API-key discovery/prompt and no `.env`, keychain or agent-home scan.
|
|
5
|
+
* Legacy mode is the explicit opt-in (`--legacy` or `PROFESSOR_LEGACY=1`) and keeps the old
|
|
6
|
+
* Anthropic/OpenAI fallback prompt. The server itself owns legacy-dev Vite startup (D-08).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import inquirer from 'inquirer';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import { buildPairingUrl } from './studio.js';
|
|
15
|
+
import * as security from '../../web/security.js';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const REPO_ROOT = join(__dirname, '..', '..');
|
|
19
|
+
const DEFAULT_PORT = '3000';
|
|
20
|
+
|
|
21
|
+
/** Usage text for `web --help`. */
|
|
22
|
+
export const WEB_USAGE = `
|
|
23
|
+
Usage: skillforce web [port] [options]
|
|
24
|
+
|
|
25
|
+
Start the Studio bridge server; --production also serves the built Studio app
|
|
26
|
+
from the same port.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
[port] Port to listen on (default ${DEFAULT_PORT})
|
|
30
|
+
--production Serve the built Studio app (requires an existing Studio build)
|
|
31
|
+
--rotate-token Generate a new pairing token and exit
|
|
32
|
+
--legacy Deprecated: mount the legacy web UI (PROFESSOR_LEGACY=1)
|
|
33
|
+
--cloud Deprecated: legacy only — no effect without --legacy
|
|
34
|
+
-h, --help Show this help and exit
|
|
35
|
+
`.trim();
|
|
36
|
+
|
|
37
|
+
/** True when `-h` or `--help` is present. */
|
|
38
|
+
export function wantsHelp(args = []) {
|
|
39
|
+
const list = Array.isArray(args) ? args : [];
|
|
40
|
+
return list.includes('--help') || list.includes('-h');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/** True when `pkg` resolves from `dir` (or an ancestor `node_modules`, e.g. the package root). */
|
|
45
|
+
function canResolveFrom(dir, pkg) {
|
|
46
|
+
try {
|
|
47
|
+
createRequire(join(dir, 'index.js')).resolve(pkg);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse `web` flags and the single positional port. The port is returned as a **string** (or `null`
|
|
56
|
+
* when absent) so existing callers can pass it through unchanged; unknown flags never become a port.
|
|
57
|
+
*
|
|
58
|
+
* @param {string[]} [args]
|
|
59
|
+
* @returns {{ legacy: boolean, production: boolean, cloud: boolean, rotateToken: boolean,
|
|
60
|
+
* port: string|null }}
|
|
61
|
+
*/
|
|
62
|
+
export function parseWebArgs(args = []) {
|
|
63
|
+
const list = Array.isArray(args) ? args : [];
|
|
64
|
+
let port = null;
|
|
65
|
+
for (const arg of list) {
|
|
66
|
+
if (typeof arg !== 'string' || arg.startsWith('--')) continue;
|
|
67
|
+
if (port === null) port = arg;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
legacy: list.includes('--legacy'),
|
|
71
|
+
production: list.includes('--production'),
|
|
72
|
+
cloud: list.includes('--cloud'),
|
|
73
|
+
rotateToken: list.includes('--rotate-token'),
|
|
74
|
+
port,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* User-facing UI port. Legacy dev is served by Vite on 5173; every other mode serves the UI on the
|
|
80
|
+
* API port (Studio-only or legacy production).
|
|
81
|
+
*
|
|
82
|
+
* @param {{ legacy?: boolean, production?: boolean, port?: string|null }} [opts]
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function resolveWebUiPort({ legacy = false, production = false, port = null } = {}) {
|
|
86
|
+
if (legacy && !production) return '5173';
|
|
87
|
+
return port || DEFAULT_PORT;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {{ args: string[], cwd: string, chalk: any, style: (kind: string, text: string) => string,
|
|
92
|
+
* startServerImpl?: Function }} ctx
|
|
93
|
+
*/
|
|
94
|
+
export async function run({ args = [], style, startServerImpl } = {}) {
|
|
95
|
+
if (wantsHelp(args)) {
|
|
96
|
+
console.log(WEB_USAGE);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { execSync } = await import('child_process');
|
|
101
|
+
const webDir = join(REPO_ROOT, 'web');
|
|
102
|
+
const options = parseWebArgs(args);
|
|
103
|
+
const isProduction = options.production;
|
|
104
|
+
const legacy = options.legacy || process.env.PROFESSOR_LEGACY === '1';
|
|
105
|
+
const cloudOnly = options.cloud;
|
|
106
|
+
|
|
107
|
+
if (!existsSync(webDir)) {
|
|
108
|
+
console.error(style('error', 'Web UI not found. Ensure the package is fully installed.'));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isProduction) process.env.NODE_ENV = 'production';
|
|
113
|
+
if (legacy) process.env.PROFESSOR_LEGACY = '1';
|
|
114
|
+
|
|
115
|
+
const positionalPort =
|
|
116
|
+
options.port || (!isProduction ? process.env.PORT : null) || DEFAULT_PORT;
|
|
117
|
+
|
|
118
|
+
let rotatedToken = null;
|
|
119
|
+
if (options.rotateToken) {
|
|
120
|
+
try {
|
|
121
|
+
rotatedToken = security.rotateStudioToken({ env: process.env });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(
|
|
124
|
+
style('error', err.code === 'env_token_immutable' ? err.message : `Could not rotate token: ${err.message}`)
|
|
125
|
+
);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
const startRequested = options.legacy || options.production || options.cloud || options.port !== null;
|
|
129
|
+
const { pairingUrl } = buildPairingUrl({
|
|
130
|
+
studioPort: resolveWebUiPort({ legacy, production: isProduction, port: positionalPort }),
|
|
131
|
+
apiPort: positionalPort,
|
|
132
|
+
token: rotatedToken,
|
|
133
|
+
});
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(` Pairing token: ${security.maskToken(rotatedToken)}`);
|
|
136
|
+
console.log(` Pairing URL: ${pairingUrl}`);
|
|
137
|
+
console.log('');
|
|
138
|
+
if (!startRequested) return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isProduction) {
|
|
142
|
+
if (legacy) {
|
|
143
|
+
const distPath = join(webDir, 'client', 'dist');
|
|
144
|
+
if (!existsSync(distPath)) {
|
|
145
|
+
console.error(style('warning', 'Production build not found.'));
|
|
146
|
+
console.error(' From repo root: npm run build');
|
|
147
|
+
console.error(' Then: npx ronin-skillforce web --legacy --production');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const studioIndex = join(REPO_ROOT, 'studio', 'dist', 'index.html');
|
|
152
|
+
if (!existsSync(studioIndex)) {
|
|
153
|
+
console.error(style('warning', 'Studio build not found.'));
|
|
154
|
+
console.error(' Run: npx ronin-skillforce studio');
|
|
155
|
+
console.error(' Or: npm run build:studio');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// `express`/`cors` are root dependencies, so an installed package needs no nested install.
|
|
162
|
+
if (!canResolveFrom(webDir, 'express') && !existsSync(join(webDir, 'node_modules'))) {
|
|
163
|
+
const spinner = ora('Installing web dependencies…').start();
|
|
164
|
+
try {
|
|
165
|
+
execSync('npm install', { cwd: webDir, stdio: 'pipe' });
|
|
166
|
+
spinner.succeed('Web dependencies installed.');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
spinner.fail('Web dependencies failed.');
|
|
169
|
+
const message = String(err?.message || err);
|
|
170
|
+
if (err?.code === 'EACCES' || /EACCES|permission denied|read-only/i.test(message)) {
|
|
171
|
+
console.error(style('error',
|
|
172
|
+
'Cannot install web dependencies: the package directory is read-only (npx cache). ' +
|
|
173
|
+
'Install globally (`npm i -g ronin-skillforce`) or run from a git clone.'));
|
|
174
|
+
} else {
|
|
175
|
+
console.error(style('error', `Check your network and try: cd ${webDir} && npm install`));
|
|
176
|
+
}
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mirror web/server.js: the legacy Vite dev server only runs when NODE_ENV is not production,
|
|
182
|
+
// so its client dependencies are only needed then.
|
|
183
|
+
const clientDir = join(webDir, 'client');
|
|
184
|
+
const needsLegacyDevClient = legacy && !isProduction && process.env.NODE_ENV !== 'production';
|
|
185
|
+
if (needsLegacyDevClient && existsSync(clientDir) && !existsSync(join(clientDir, 'node_modules'))) {
|
|
186
|
+
const spinner = ora('Installing client dependencies…').start();
|
|
187
|
+
try {
|
|
188
|
+
execSync('npm install', { cwd: clientDir, stdio: 'pipe' });
|
|
189
|
+
spinner.succeed('Client dependencies installed.');
|
|
190
|
+
} catch {
|
|
191
|
+
spinner.fail('Client dependencies failed.');
|
|
192
|
+
console.error(style('error', `Check your network and try: cd ${clientDir} && npm install`));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function findApiKey() {
|
|
198
|
+
if (process.env.ANTHROPIC_API_KEY) return { key: process.env.ANTHROPIC_API_KEY, source: 'env' };
|
|
199
|
+
if (process.env.CLAUDE_API_KEY) return { key: process.env.CLAUDE_API_KEY, source: 'env' };
|
|
200
|
+
if (process.env.OPENAI_API_KEY) return { key: process.env.OPENAI_API_KEY, source: 'env' };
|
|
201
|
+
|
|
202
|
+
const envFile = join(process.cwd(), '.env');
|
|
203
|
+
if (existsSync(envFile)) {
|
|
204
|
+
try {
|
|
205
|
+
const lines = readFileSync(envFile, 'utf-8').split('\n');
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
const m = line.match(/^(ANTHROPIC_API_KEY|CLAUDE_API_KEY|OPENAI_API_KEY)\s*=\s*(.+)$/);
|
|
208
|
+
if (m) return { key: m[2].trim().replace(/^["']|["']$/g, ''), source: '.env' };
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (process.platform === 'darwin') {
|
|
214
|
+
try {
|
|
215
|
+
const { execSync: _exec } = await import('child_process');
|
|
216
|
+
for (const svc of ['anthropic-api-key', 'Anthropic API Key', 'ANTHROPIC_API_KEY']) {
|
|
217
|
+
try {
|
|
218
|
+
const raw = _exec(`security find-generic-password -s "${svc}" -w`, {
|
|
219
|
+
encoding: 'utf-8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
220
|
+
}).trim();
|
|
221
|
+
if (raw?.startsWith('sk-ant-api')) return { key: raw, source: `keychain:${svc}` };
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const home = process.env.HOME || '';
|
|
228
|
+
for (const p of [
|
|
229
|
+
join(home, '.claude', 'settings.json'),
|
|
230
|
+
join(home, '.anthropic', 'config.json'),
|
|
231
|
+
]) {
|
|
232
|
+
if (existsSync(p)) {
|
|
233
|
+
try {
|
|
234
|
+
const s = JSON.parse(readFileSync(p, 'utf-8'));
|
|
235
|
+
const k = s.ANTHROPIC_API_KEY || s.apiKey || s.api_key;
|
|
236
|
+
if (k) return { key: k, source: p };
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasLocalProvider() {
|
|
244
|
+
for (const cmd of ['claude', 'ollama', 'cursor-agent', 'opencode']) {
|
|
245
|
+
try {
|
|
246
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
247
|
+
return true;
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (legacy) {
|
|
254
|
+
let found = await findApiKey();
|
|
255
|
+
const localProviderAvailable = hasLocalProvider();
|
|
256
|
+
const shouldPromptForApiKey = cloudOnly || !localProviderAvailable;
|
|
257
|
+
if (!found && shouldPromptForApiKey) {
|
|
258
|
+
if (!process.stdin.isTTY) {
|
|
259
|
+
console.log(style('info', 'No API key. Chat disabled.\n'));
|
|
260
|
+
} else {
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(style('info', 'No Anthropic API key detected.'));
|
|
263
|
+
console.log(' Get one at: https://console.anthropic.com/settings/keys');
|
|
264
|
+
console.log(' (Key will be saved to .env if you paste one; otherwise chat will be disabled.)');
|
|
265
|
+
console.log('');
|
|
266
|
+
const { apiKeyInput } = await inquirer.prompt([{
|
|
267
|
+
type: 'input',
|
|
268
|
+
name: 'apiKeyInput',
|
|
269
|
+
message: 'Paste your API key (or press Enter to skip):',
|
|
270
|
+
}]);
|
|
271
|
+
const trimmed = (apiKeyInput || '').trim();
|
|
272
|
+
if (trimmed && trimmed.startsWith('sk-')) {
|
|
273
|
+
const envPath = join(process.cwd(), '.env');
|
|
274
|
+
const line = `ANTHROPIC_API_KEY=${trimmed}\n`;
|
|
275
|
+
try {
|
|
276
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
277
|
+
if (!existing.includes('ANTHROPIC_API_KEY')) {
|
|
278
|
+
writeFileSync(envPath, existing + line);
|
|
279
|
+
console.log(` ${style('success', "Saved to .env — won't ask again.")}\n`);
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
found = { key: trimmed, source: '.env (just saved)' };
|
|
283
|
+
} else {
|
|
284
|
+
console.log(' Skipping. Chat disabled.\n');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const apiKey = found?.key;
|
|
290
|
+
if (apiKey) {
|
|
291
|
+
console.log(style('success', `API key loaded (${found.source}).\n`));
|
|
292
|
+
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
293
|
+
} else if (localProviderAvailable) {
|
|
294
|
+
console.log(style('info', 'No API key found. Local providers will be used when available.\n'));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
process.env.PORT = positionalPort;
|
|
299
|
+
|
|
300
|
+
let defaultCourses = './courses';
|
|
301
|
+
if (existsSync('./courses')) {
|
|
302
|
+
if (readdirSync('./courses').length === 0) defaultCourses = './learning';
|
|
303
|
+
} else if (existsSync('./learning')) {
|
|
304
|
+
defaultCourses = './learning';
|
|
305
|
+
}
|
|
306
|
+
process.env.COURSES_DIR = process.env.COURSES_DIR || defaultCourses;
|
|
307
|
+
|
|
308
|
+
const token =
|
|
309
|
+
rotatedToken ||
|
|
310
|
+
security.ensureStudioToken({
|
|
311
|
+
env: process.env,
|
|
312
|
+
legacyFile: security.legacyTokenPath(REPO_ROOT),
|
|
313
|
+
});
|
|
314
|
+
const expectedUiPort = resolveWebUiPort({
|
|
315
|
+
legacy,
|
|
316
|
+
production: isProduction,
|
|
317
|
+
port: positionalPort,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const mode = isProduction ? 'production' : 'development';
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(style('info', 'Starting Professor Web UI…'));
|
|
323
|
+
console.log(` Mode: ${mode} | Port: ${positionalPort} | Courses: ${process.env.COURSES_DIR}`);
|
|
324
|
+
console.log(` Pairing token: ${security.maskToken(token)}`);
|
|
325
|
+
|
|
326
|
+
const loadServer = startServerImpl || (await import(join(webDir, 'server.js'))).startServer;
|
|
327
|
+
const started = await loadServer({
|
|
328
|
+
legacy,
|
|
329
|
+
port: positionalPort,
|
|
330
|
+
host: process.env.HOST,
|
|
331
|
+
logBanner: false,
|
|
332
|
+
});
|
|
333
|
+
const uiUrl = started?.uiUrl || `http://localhost:${expectedUiPort}`;
|
|
334
|
+
const uiPort = (() => {
|
|
335
|
+
try {
|
|
336
|
+
return new URL(uiUrl).port || expectedUiPort;
|
|
337
|
+
} catch {
|
|
338
|
+
return expectedUiPort;
|
|
339
|
+
}
|
|
340
|
+
})();
|
|
341
|
+
const { pairingUrl } = buildPairingUrl({
|
|
342
|
+
studioPort: uiPort,
|
|
343
|
+
apiPort: positionalPort,
|
|
344
|
+
token,
|
|
345
|
+
});
|
|
346
|
+
console.log(` UI: ${uiUrl}`);
|
|
347
|
+
console.log(` Pairing URL: ${pairingUrl}`);
|
|
348
|
+
console.log('');
|
|
349
|
+
return started;
|
|
350
|
+
}
|