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,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillforce studio` (CLI-05) — one command that brings the whole local platform up:
|
|
3
|
+
*
|
|
4
|
+
* 1. installs `web/` and `studio/` deps if missing, builds `studio/dist` if missing/stale
|
|
5
|
+
* 2. starts the local server (`web/server.js`, production, on `--api-port`) with a pairing token
|
|
6
|
+
* 3. serves `studio/dist` on `--port` (tiny static server, loopback only)
|
|
7
|
+
* 4. waits for `/api/studio/health`, then opens
|
|
8
|
+
* `http://localhost:<port>/#token=…` (plus `&bridge=…` for a non-default API port)
|
|
9
|
+
* (unless `--no-open`)
|
|
10
|
+
*
|
|
11
|
+
* Inherited `CLAUDECODE` / `CLAUDE_CODE_*` env vars are stripped from the child so a nested agent
|
|
12
|
+
* CLI (spawned by the bridge) behaves like a normal terminal.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn, execSync } from 'child_process';
|
|
15
|
+
import { createServer } from 'http';
|
|
16
|
+
import { existsSync, statSync, readdirSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
import { createStaticHandler } from '../static-server.js';
|
|
21
|
+
import { requireStudioNode } from '../node-version.js';
|
|
22
|
+
import * as security from '../../web/security.js';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
export const REPO_ROOT = join(__dirname, '..', '..');
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_PORT = 5180;
|
|
28
|
+
export const DEFAULT_API_PORT = 3000;
|
|
29
|
+
export const HEALTH_PATH = '/api/studio/health';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Usage text for `studio --help`. Kept as a plain string so the CLI can print it without starting
|
|
33
|
+
* or binding anything.
|
|
34
|
+
*/
|
|
35
|
+
export const STUDIO_USAGE = `
|
|
36
|
+
Usage: skillforce studio [options]
|
|
37
|
+
|
|
38
|
+
Start the local bridge server and Professor Studio, then open the browser (Node 22.5+).
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--port <n> Studio port (default ${DEFAULT_PORT})
|
|
42
|
+
--api-port <n> Local bridge server port (default ${DEFAULT_API_PORT})
|
|
43
|
+
--no-open Do not open the browser
|
|
44
|
+
--rotate-token Generate a new pairing token and exit
|
|
45
|
+
-h, --help Show this help and exit
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
/** True when `-h` or `--help` is present. */
|
|
49
|
+
export function wantsHelp(args = []) {
|
|
50
|
+
return args.includes('--help') || args.includes('-h');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse `--port`, `--api-port`, `--no-open` (both `--flag value` and `--flag=value` forms).
|
|
56
|
+
* @param {string[]} [args]
|
|
57
|
+
* @returns {{ port: number, apiPort: number, open: boolean, rotateToken: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
export function parseStudioArgs(args = []) {
|
|
60
|
+
const valueOf = (name, fallback) => {
|
|
61
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
62
|
+
if (eq) return eq.slice(name.length + 1);
|
|
63
|
+
const i = args.indexOf(name);
|
|
64
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('--')) return args[i + 1];
|
|
65
|
+
return fallback;
|
|
66
|
+
};
|
|
67
|
+
const asPort = (raw, fallback) => {
|
|
68
|
+
const n = Number.parseInt(raw, 10);
|
|
69
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : fallback;
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
port: asPort(valueOf('--port', String(DEFAULT_PORT)), DEFAULT_PORT),
|
|
73
|
+
apiPort: asPort(valueOf('--api-port', String(DEFAULT_API_PORT)), DEFAULT_API_PORT),
|
|
74
|
+
open: !args.includes('--no-open'),
|
|
75
|
+
rotateToken: args.includes('--rotate-token'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Newest mtime (ms) of any file under `dir` (recursive), or 0 when it does not exist. */
|
|
80
|
+
export function newestMtime(dir) {
|
|
81
|
+
if (!existsSync(dir)) return 0;
|
|
82
|
+
let newest = 0;
|
|
83
|
+
const walk = (current) => {
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const full = join(current, entry.name);
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
walk(full);
|
|
94
|
+
} else {
|
|
95
|
+
try {
|
|
96
|
+
const m = statSync(full).mtimeMs;
|
|
97
|
+
if (m > newest) newest = m;
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
walk(dir);
|
|
105
|
+
return newest;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* True when `dist/index.html` is missing or older than any source/config file.
|
|
110
|
+
* @param {string} studioDir
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isBuildStale(studioDir) {
|
|
114
|
+
const indexHtml = join(studioDir, 'dist', 'index.html');
|
|
115
|
+
if (!existsSync(indexHtml)) return true;
|
|
116
|
+
let builtAt;
|
|
117
|
+
try {
|
|
118
|
+
builtAt = statSync(indexHtml).mtimeMs;
|
|
119
|
+
} catch {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const inputs = [
|
|
123
|
+
newestMtime(join(studioDir, 'src')),
|
|
124
|
+
...['package.json', 'package-lock.json', 'vite.config.js', 'index.html'].map((f) => {
|
|
125
|
+
const p = join(studioDir, f);
|
|
126
|
+
try {
|
|
127
|
+
return existsSync(p) ? statSync(p).mtimeMs : 0;
|
|
128
|
+
} catch {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
];
|
|
133
|
+
return inputs.some((m) => m > builtAt);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Copy `env`, dropping `CLAUDECODE` and every `CLAUDE_CODE_*` variable.
|
|
138
|
+
* @param {Record<string,string|undefined>} [env]
|
|
139
|
+
* @returns {Record<string,string|undefined>}
|
|
140
|
+
*/
|
|
141
|
+
export function stripAgentEnv(env = process.env) {
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const [key, value] of Object.entries(env)) {
|
|
144
|
+
if (key === 'CLAUDECODE') continue;
|
|
145
|
+
if (key.startsWith('CLAUDE_CODE_')) continue;
|
|
146
|
+
out[key] = value;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Merge the Studio origins we will serve into any pre-existing `STUDIO_ORIGINS`. */
|
|
152
|
+
export function mergeOrigins(existing, port) {
|
|
153
|
+
const set = new Set(
|
|
154
|
+
String(existing || '')
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((s) => s.trim())
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
);
|
|
159
|
+
set.add(`http://localhost:${port}`);
|
|
160
|
+
set.add(`http://127.0.0.1:${port}`);
|
|
161
|
+
return [...set].join(',');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the fragment pairing URL (D-03):
|
|
166
|
+
* <studio-url>/#token=<encoded token>[&bridge=<encoded apiUrl>]
|
|
167
|
+
* The bridge parameter is only present when the API port is not the default 3000. Ports are
|
|
168
|
+
* compared numerically because `parseWebArgs().port` is a string such as `'3000'`.
|
|
169
|
+
*
|
|
170
|
+
* @param {{ studioPort: number|string, apiPort: number|string, token: string }} opts
|
|
171
|
+
* @returns {{ apiUrl: string, studioUrl: string, pairingUrl: string }}
|
|
172
|
+
*/
|
|
173
|
+
export function buildPairingUrl({ studioPort, apiPort, token }) {
|
|
174
|
+
const apiUrl = `http://localhost:${apiPort}`;
|
|
175
|
+
const studioUrl = `http://localhost:${studioPort}`;
|
|
176
|
+
let pairingUrl = `${studioUrl}/#token=${encodeURIComponent(token)}`;
|
|
177
|
+
if (Number(apiPort) !== DEFAULT_API_PORT) {
|
|
178
|
+
pairingUrl += `&bridge=${encodeURIComponent(apiUrl)}`;
|
|
179
|
+
}
|
|
180
|
+
return { apiUrl, studioUrl, pairingUrl };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the pairing token through the shared helper (D-01/D-02). `repoRoot` is still accepted for
|
|
185
|
+
* tests but only supplies the one-time legacy `web/.studio-token` migration path; the token itself
|
|
186
|
+
* always lives under `PROFESSOR_HOME`.
|
|
187
|
+
*
|
|
188
|
+
* @param {{ repoRoot?: string, env?: Record<string,string|undefined>, home?: string }} [opts]
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
export function resolveStudioToken({ repoRoot = REPO_ROOT, env = process.env, home } = {}) {
|
|
192
|
+
return security.ensureStudioToken({
|
|
193
|
+
env,
|
|
194
|
+
home: home || env.PROFESSOR_HOME,
|
|
195
|
+
legacyFile: security.legacyTokenPath(repoRoot),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Poll `/api/studio/health` until it answers 200 with the pairing token.
|
|
201
|
+
* @param {{ url: string, token: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: typeof fetch }} opts
|
|
202
|
+
*/
|
|
203
|
+
export async function waitForHealth({
|
|
204
|
+
url,
|
|
205
|
+
token,
|
|
206
|
+
timeoutMs = 20000,
|
|
207
|
+
intervalMs = 250,
|
|
208
|
+
fetchImpl = globalThis.fetch,
|
|
209
|
+
}) {
|
|
210
|
+
const deadline = Date.now() + timeoutMs;
|
|
211
|
+
let lastError = 'no response';
|
|
212
|
+
while (Date.now() < deadline) {
|
|
213
|
+
try {
|
|
214
|
+
const res = await fetchImpl(url, { headers: { 'X-Studio-Token': token } });
|
|
215
|
+
if (res.ok) return await res.json().catch(() => ({ ok: true }));
|
|
216
|
+
lastError = `HTTP ${res.status}`;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
lastError = err.message;
|
|
219
|
+
}
|
|
220
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`Studio server did not become healthy at ${url} (${lastError})`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Host-validating Studio static server. `bin/static-server.js` itself is not owned by Phase 2, so
|
|
227
|
+
* this thin wrapper rejects a spoofed/missing Host before delegating to its static handler (SEC-03).
|
|
228
|
+
*
|
|
229
|
+
* @param {{ root: string, host?: string, port?: number, extraHosts?: string[] }} opts
|
|
230
|
+
* @returns {Promise<{ server: import('http').Server, url: string, port: number,
|
|
231
|
+
* close: () => Promise<void> }>}
|
|
232
|
+
*/
|
|
233
|
+
export async function createStudioStaticServer({
|
|
234
|
+
root,
|
|
235
|
+
host = '127.0.0.1',
|
|
236
|
+
port = 0,
|
|
237
|
+
extraHosts = [],
|
|
238
|
+
} = {}) {
|
|
239
|
+
const handler = createStaticHandler({ root, spaFallback: true });
|
|
240
|
+
const server = createServer((req, res) => {
|
|
241
|
+
if (!security.isAllowedHost(req.headers?.host, { extraHosts })) {
|
|
242
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
243
|
+
res.end(JSON.stringify({ ok: false, error: 'Forbidden host', code: 'forbidden_host' }));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
handler(req, res);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await new Promise((resolveListen, reject) => {
|
|
250
|
+
server.once('error', reject);
|
|
251
|
+
server.listen(port, host, () => {
|
|
252
|
+
server.removeListener('error', reject);
|
|
253
|
+
resolveListen();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const address = server.address();
|
|
258
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
259
|
+
return {
|
|
260
|
+
server,
|
|
261
|
+
port: actualPort,
|
|
262
|
+
url: `http://localhost:${actualPort}`,
|
|
263
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Open a URL in the OS browser. Never throws. */
|
|
268
|
+
export function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
|
|
269
|
+
try {
|
|
270
|
+
const child =
|
|
271
|
+
platform === 'darwin'
|
|
272
|
+
? spawnImpl('open', [url], { stdio: 'ignore', detached: true })
|
|
273
|
+
: platform === 'win32'
|
|
274
|
+
? spawnImpl('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true })
|
|
275
|
+
: spawnImpl('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
276
|
+
child.on?.('error', () => {});
|
|
277
|
+
child.unref?.();
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** True when `pkg` can be resolved from `dir` (or any ancestor `node_modules`, e.g. the root). */
|
|
285
|
+
function canResolveFrom(dir, pkg) {
|
|
286
|
+
try {
|
|
287
|
+
createRequire(join(dir, 'index.js')).resolve(pkg);
|
|
288
|
+
return true;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Install a package's deps only when they are genuinely missing. `probe` short-circuits the install
|
|
296
|
+
* when the dependency already resolves from an ancestor (the root package's own `node_modules`) —
|
|
297
|
+
* which is the case for the web server once `express`/`cors` are root dependencies.
|
|
298
|
+
*/
|
|
299
|
+
function ensureDeps({ dir, label, spinner, probe }) {
|
|
300
|
+
if (probe && canResolveFrom(dir, probe)) return false;
|
|
301
|
+
if (existsSync(join(dir, 'node_modules'))) return false;
|
|
302
|
+
if (!existsSync(join(dir, 'package.json'))) return false;
|
|
303
|
+
spinner.start(`Installing ${label} dependencies…`);
|
|
304
|
+
try {
|
|
305
|
+
execSync('npm install', { cwd: dir, stdio: 'pipe' });
|
|
306
|
+
spinner.succeed(`${label} dependencies installed.`);
|
|
307
|
+
return true;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
spinner.fail(`${label} dependencies failed.`);
|
|
310
|
+
const message = String(err?.message || err);
|
|
311
|
+
if (err?.code === 'EACCES' || /EACCES|permission denied|read-only/i.test(message)) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Cannot install ${label} dependencies in ${dir} (permission denied). ` +
|
|
314
|
+
'This happens when the package runs from the read-only npx cache. ' +
|
|
315
|
+
`Install the package globally instead (\`npm i -g ronin-skillforce\`) or re-run with a writable cache.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`npm install failed in ${dir}: ${message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Build `studio/dist` from source — but only from a git checkout. An installed package ships a
|
|
324
|
+
* prebuilt `studio/dist` and has no `studio/src`, so we never attempt a build there.
|
|
325
|
+
*/
|
|
326
|
+
function ensureBuild({ studioDir, spinner }) {
|
|
327
|
+
if (!existsSync(join(studioDir, 'src'))) return false;
|
|
328
|
+
if (!isBuildStale(studioDir)) return false;
|
|
329
|
+
spinner.start('Building Studio (studio/dist)…');
|
|
330
|
+
try {
|
|
331
|
+
execSync('npm run build', { cwd: studioDir, stdio: 'pipe' });
|
|
332
|
+
spinner.succeed('Studio built.');
|
|
333
|
+
return true;
|
|
334
|
+
} catch (err) {
|
|
335
|
+
spinner.fail('Studio build failed.');
|
|
336
|
+
throw new Error(`npm run build failed in ${studioDir}: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {{ args?: string[], cwd?: string, chalk?: object, style?: Function, repoRoot?: string,
|
|
342
|
+
* ora?: Function, spawnImpl?: typeof spawn, fetchImpl?: typeof fetch,
|
|
343
|
+
* openImpl?: typeof openBrowser, nodeVersion?: string }} [ctx]
|
|
344
|
+
*/
|
|
345
|
+
export async function run(ctx = {}) {
|
|
346
|
+
const {
|
|
347
|
+
args = [],
|
|
348
|
+
chalk,
|
|
349
|
+
style = (t, m) => m,
|
|
350
|
+
repoRoot = REPO_ROOT,
|
|
351
|
+
spawnImpl = spawn,
|
|
352
|
+
fetchImpl = globalThis.fetch,
|
|
353
|
+
openImpl = openBrowser,
|
|
354
|
+
} = ctx;
|
|
355
|
+
|
|
356
|
+
if (wantsHelp(args)) {
|
|
357
|
+
console.log(STUDIO_USAGE);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
requireStudioNode(ctx.nodeVersion);
|
|
362
|
+
|
|
363
|
+
const ora = ctx.ora || (await import('ora')).default;
|
|
364
|
+
const { port, apiPort, open, rotateToken } = parseStudioArgs(args);
|
|
365
|
+
|
|
366
|
+
const hasStartRequest = args.some((arg) => arg !== '--rotate-token');
|
|
367
|
+
let rotatedToken = null;
|
|
368
|
+
if (rotateToken) {
|
|
369
|
+
try {
|
|
370
|
+
rotatedToken = security.rotateStudioToken({ env: process.env });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err.code === 'env_token_immutable') {
|
|
373
|
+
console.error(style('error', err.message));
|
|
374
|
+
} else {
|
|
375
|
+
console.error(style('error', `Could not rotate token: ${err.message}`));
|
|
376
|
+
}
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
const { pairingUrl } = buildPairingUrl({ studioPort: port, apiPort, token: rotatedToken });
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log(` Pairing token: ${security.maskToken(rotatedToken)}`);
|
|
382
|
+
console.log(` Pairing URL: ${pairingUrl}`);
|
|
383
|
+
console.log('');
|
|
384
|
+
if (!hasStartRequest) return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const webDir = join(repoRoot, 'web');
|
|
388
|
+
const studioDir = join(repoRoot, 'studio');
|
|
389
|
+
const serverEntry = join(webDir, 'server.js');
|
|
390
|
+
const distDir = join(studioDir, 'dist');
|
|
391
|
+
const studioHasSource = existsSync(join(studioDir, 'src'));
|
|
392
|
+
|
|
393
|
+
if (!existsSync(serverEntry)) {
|
|
394
|
+
throw new Error(`Local server not found at ${serverEntry}. Is the package installed fully?`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log('');
|
|
398
|
+
const setupSpinner = ora('Preparing local platform…');
|
|
399
|
+
// `express`/`cors` are root dependencies, so the web server runs without a nested install. This
|
|
400
|
+
// stays as a fallback for a checkout that has `web/package.json` but no resolvable deps.
|
|
401
|
+
ensureDeps({ dir: webDir, label: 'web', spinner: setupSpinner, probe: 'express' });
|
|
402
|
+
if (studioHasSource) {
|
|
403
|
+
ensureDeps({ dir: studioDir, label: 'studio', spinner: setupSpinner });
|
|
404
|
+
}
|
|
405
|
+
ensureBuild({ studioDir, spinner: setupSpinner });
|
|
406
|
+
setupSpinner.stop();
|
|
407
|
+
|
|
408
|
+
if (!existsSync(join(distDir, 'index.html'))) {
|
|
409
|
+
throw new Error(`Studio build missing at ${distDir}. Run: cd ${studioDir} && npm run build`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const token = rotatedToken || resolveStudioToken({ repoRoot });
|
|
413
|
+
const { apiUrl, studioUrl, pairingUrl } = buildPairingUrl({ studioPort: port, apiPort, token });
|
|
414
|
+
|
|
415
|
+
const env = stripAgentEnv(process.env);
|
|
416
|
+
env.NODE_ENV = 'production';
|
|
417
|
+
env.PORT = String(apiPort);
|
|
418
|
+
if (process.env.STUDIO_BRIDGE_TOKEN) {
|
|
419
|
+
env.STUDIO_BRIDGE_TOKEN = process.env.STUDIO_BRIDGE_TOKEN;
|
|
420
|
+
}
|
|
421
|
+
env.STUDIO_ORIGINS = mergeOrigins(process.env.STUDIO_ORIGINS, port);
|
|
422
|
+
|
|
423
|
+
const serverSpinner = ora(`Starting local server on ${apiUrl}…`).start();
|
|
424
|
+
const apiChild = spawnImpl(process.execPath, [serverEntry], {
|
|
425
|
+
cwd: webDir,
|
|
426
|
+
env,
|
|
427
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
let staticServer = null;
|
|
431
|
+
let shuttingDown = false;
|
|
432
|
+
let resolveDone;
|
|
433
|
+
const done = new Promise((resolve) => {
|
|
434
|
+
resolveDone = resolve;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const shutdown = async (code = 0) => {
|
|
438
|
+
if (shuttingDown) return;
|
|
439
|
+
shuttingDown = true;
|
|
440
|
+
try { apiChild.kill('SIGTERM'); } catch { /* already gone */ }
|
|
441
|
+
if (staticServer) {
|
|
442
|
+
try { await staticServer.close(); } catch { /* ignore */ }
|
|
443
|
+
}
|
|
444
|
+
process.off('SIGINT', onSignal);
|
|
445
|
+
process.off('SIGTERM', onSignal);
|
|
446
|
+
resolveDone(code);
|
|
447
|
+
};
|
|
448
|
+
const onSignal = () => {
|
|
449
|
+
console.log('');
|
|
450
|
+
console.log(style('info', 'Stopping Professor Studio…'));
|
|
451
|
+
shutdown(0);
|
|
452
|
+
};
|
|
453
|
+
process.on('SIGINT', onSignal);
|
|
454
|
+
process.on('SIGTERM', onSignal);
|
|
455
|
+
|
|
456
|
+
apiChild.on('error', (err) => {
|
|
457
|
+
serverSpinner.fail('Local server failed to start.');
|
|
458
|
+
console.error(style('error', `Could not start the local server: ${err.message}`));
|
|
459
|
+
shutdown(1);
|
|
460
|
+
});
|
|
461
|
+
apiChild.on('exit', (code) => {
|
|
462
|
+
if (!shuttingDown) {
|
|
463
|
+
serverSpinner.fail(`Local server exited (code ${code ?? 0}).`);
|
|
464
|
+
shutdown(code ?? 1);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const extraHosts = security.resolveExtraHosts({ env: process.env, host: process.env.HOST });
|
|
469
|
+
try {
|
|
470
|
+
staticServer = await createStudioStaticServer({ root: distDir, port, extraHosts });
|
|
471
|
+
} catch (err) {
|
|
472
|
+
serverSpinner.fail('Could not serve Studio.');
|
|
473
|
+
console.error(style('error', `Static server failed on port ${port}: ${err.message}`));
|
|
474
|
+
await shutdown(1);
|
|
475
|
+
return done;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
await waitForHealth({ url: `${apiUrl}${HEALTH_PATH}`, token, fetchImpl });
|
|
480
|
+
serverSpinner.succeed(`Local server ready at ${apiUrl}`);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
serverSpinner.fail('Local server did not become healthy.');
|
|
483
|
+
console.error(style('error', err.message));
|
|
484
|
+
await shutdown(1);
|
|
485
|
+
return done;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log('');
|
|
489
|
+
console.log(style('success', `Professor Studio is running at ${studioUrl}`));
|
|
490
|
+
console.log(` Pairing URL: ${pairingUrl}`);
|
|
491
|
+
console.log(style('info', 'Press Ctrl+C to stop'));
|
|
492
|
+
console.log('');
|
|
493
|
+
|
|
494
|
+
if (open) openImpl(pairingUrl);
|
|
495
|
+
|
|
496
|
+
return done;
|
|
497
|
+
}
|