@neuralnomads/codenomad 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/dist/api-types.js +1 -0
- package/dist/bin.js +24 -0
- package/dist/config/binaries.js +118 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +68 -0
- package/dist/events/bus.js +32 -0
- package/dist/filesystem/__tests__/search-cache.test.js +40 -0
- package/dist/filesystem/browser.js +238 -0
- package/dist/filesystem/search-cache.js +43 -0
- package/dist/filesystem/search.js +135 -0
- package/dist/index.js +142 -0
- package/dist/launcher.js +149 -0
- package/dist/loader.js +21 -0
- package/dist/logger.js +109 -0
- package/dist/server/http-server.js +224 -0
- package/dist/server/routes/config.js +44 -0
- package/dist/server/routes/events.js +35 -0
- package/dist/server/routes/filesystem.js +19 -0
- package/dist/server/routes/meta.js +3 -0
- package/dist/server/routes/storage.js +52 -0
- package/dist/server/routes/workspaces.js +82 -0
- package/dist/storage/instance-store.js +56 -0
- package/dist/workspaces/manager.js +147 -0
- package/dist/workspaces/runtime.js +168 -0
- package/package.json +38 -0
- package/public/assets/CodeNomad-Icon-bmTWNPXy.png +0 -0
- package/public/assets/abap-BdImnpbu.js +1 -0
- package/public/assets/actionscript-3-CfeIJUat.js +1 -0
- package/public/assets/ada-bCR0ucgS.js +1 -0
- package/public/assets/andromeeda-C-Jbm3Hp.js +1 -0
- package/public/assets/angular-html-CU67Zn6k.js +1 -0
- package/public/assets/angular-ts-BwZT4LLn.js +1 -0
- package/public/assets/apache-Pmp26Uib.js +1 -0
- package/public/assets/apex-DhZLUxFE.js +1 -0
- package/public/assets/apl-dKokRX4l.js +1 -0
- package/public/assets/applescript-Co6uUVPk.js +1 -0
- package/public/assets/ara-BRHolxvo.js +1 -0
- package/public/assets/asciidoc-Dv7Oe6Be.js +1 -0
- package/public/assets/asm-D_Q5rh1f.js +1 -0
- package/public/assets/astro-CbQHKStN.js +1 -0
- package/public/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/public/assets/awk-DMzUqQB5.js +1 -0
- package/public/assets/ayu-dark-Cv9koXgw.js +1 -0
- package/public/assets/ballerina-BFfxhgS-.js +1 -0
- package/public/assets/bat-BkioyH1T.js +1 -0
- package/public/assets/beancount-k_qm7-4y.js +1 -0
- package/public/assets/berry-D08WgyRC.js +1 -0
- package/public/assets/bibtex-CHM0blh-.js +1 -0
- package/public/assets/bicep-Bmn6On1c.js +1 -0
- package/public/assets/blade-DVc8C-J4.js +1 -0
- package/public/assets/bsl-BO_Y6i37.js +1 -0
- package/public/assets/c-BIGW1oBm.js +1 -0
- package/public/assets/cadence-Bv_4Rxtq.js +1 -0
- package/public/assets/cairo-KRGpt6FW.js +1 -0
- package/public/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/public/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/public/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/public/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/public/assets/clarity-D53aC0YG.js +1 -0
- package/public/assets/clojure-P80f7IUj.js +1 -0
- package/public/assets/cmake-D1j8_8rp.js +1 -0
- package/public/assets/cobol-nwyudZeR.js +1 -0
- package/public/assets/codeowners-Bp6g37R7.js +1 -0
- package/public/assets/codeql-DsOJ9woJ.js +1 -0
- package/public/assets/coffee-Ch7k5sss.js +1 -0
- package/public/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/public/assets/coq-DkFqJrB1.js +1 -0
- package/public/assets/cpp-CofmeUqb.js +1 -0
- package/public/assets/crystal-tKQVLTB8.js +1 -0
- package/public/assets/csharp-CX12Zw3r.js +1 -0
- package/public/assets/css-DPfMkruS.js +1 -0
- package/public/assets/csv-fuZLfV_i.js +1 -0
- package/public/assets/cue-D82EKSYY.js +1 -0
- package/public/assets/cypher-COkxafJQ.js +1 -0
- package/public/assets/d-85-TOEBH.js +1 -0
- package/public/assets/dark-plus-eOWES_5F.js +1 -0
- package/public/assets/dart-CF10PKvl.js +1 -0
- package/public/assets/dax-CEL-wOlO.js +1 -0
- package/public/assets/desktop-BmXAJ9_W.js +1 -0
- package/public/assets/diff-D97Zzqfu.js +1 -0
- package/public/assets/docker-BcOcwvcX.js +1 -0
- package/public/assets/dotenv-Da5cRb03.js +1 -0
- package/public/assets/dracula-BzJJZx-M.js +1 -0
- package/public/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/public/assets/dream-maker-BtqSS_iP.js +1 -0
- package/public/assets/edge-BkV0erSs.js +1 -0
- package/public/assets/elixir-CDX3lj18.js +1 -0
- package/public/assets/elm-DbKCFpqz.js +1 -0
- package/public/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/public/assets/erb-BOJIQeun.js +1 -0
- package/public/assets/erlang-DsQrWhSR.js +1 -0
- package/public/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/public/assets/everforest-light-C8M2exoo.js +1 -0
- package/public/assets/fennel-BYunw83y.js +1 -0
- package/public/assets/fish-BvzEVeQv.js +1 -0
- package/public/assets/fluent-C4IJs8-o.js +1 -0
- package/public/assets/fortran-fixed-form-BZjJHVRy.js +1 -0
- package/public/assets/fortran-free-form-D22FLkUw.js +1 -0
- package/public/assets/fsharp-CXgrBDvD.js +1 -0
- package/public/assets/gdresource-B7Tvp0Sc.js +1 -0
- package/public/assets/gdscript-DTMYz4Jt.js +1 -0
- package/public/assets/gdshader-DkwncUOv.js +1 -0
- package/public/assets/genie-D0YGMca9.js +1 -0
- package/public/assets/gherkin-DyxjwDmM.js +1 -0
- package/public/assets/git-commit-F4YmCXRG.js +1 -0
- package/public/assets/git-rebase-r7XF79zn.js +1 -0
- package/public/assets/github-dark-DHJKELXO.js +1 -0
- package/public/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/public/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/public/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/public/assets/github-light-DAi9KRSo.js +1 -0
- package/public/assets/github-light-default-D7oLnXFd.js +1 -0
- package/public/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/public/assets/gleam-BspZqrRM.js +1 -0
- package/public/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/public/assets/glimmer-ts-U6CK756n.js +1 -0
- package/public/assets/glsl-DplSGwfg.js +1 -0
- package/public/assets/gnuplot-DdkO51Og.js +1 -0
- package/public/assets/go-Dn2_MT6a.js +1 -0
- package/public/assets/graphql-ChdNCCLP.js +1 -0
- package/public/assets/groovy-gcz8RCvz.js +1 -0
- package/public/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/public/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/public/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/public/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/public/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/public/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/public/assets/hack-CaT9iCJl.js +1 -0
- package/public/assets/haml-B8DHNrY2.js +1 -0
- package/public/assets/handlebars-BL8al0AC.js +1 -0
- package/public/assets/haskell-Df6bDoY_.js +1 -0
- package/public/assets/haxe-CzTSHFRz.js +1 -0
- package/public/assets/hcl-BWvSN4gD.js +1 -0
- package/public/assets/hjson-D5-asLiD.js +1 -0
- package/public/assets/hlsl-D3lLCCz7.js +1 -0
- package/public/assets/houston-DnULxvSX.js +1 -0
- package/public/assets/html-GMplVEZG.js +1 -0
- package/public/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/public/assets/http-jrhK8wxY.js +1 -0
- package/public/assets/hurl-irOxFIW8.js +1 -0
- package/public/assets/hxml-Bvhsp5Yf.js +1 -0
- package/public/assets/hy-DFXneXwc.js +1 -0
- package/public/assets/imba-DGztddWO.js +1 -0
- package/public/assets/index-CoTu_3WL.js +147 -0
- package/public/assets/index-Cyy59ha1.css +19 -0
- package/public/assets/ini-BEwlwnbL.js +1 -0
- package/public/assets/java-CylS5w8V.js +1 -0
- package/public/assets/javascript-wDzz0qaB.js +1 -0
- package/public/assets/jinja-4LBKfQ-Z.js +1 -0
- package/public/assets/jison-wvAkD_A8.js +1 -0
- package/public/assets/json-Cp-IABpG.js +1 -0
- package/public/assets/json5-C9tS-k6U.js +1 -0
- package/public/assets/jsonc-Des-eS-w.js +1 -0
- package/public/assets/jsonl-DcaNXYhu.js +1 -0
- package/public/assets/jsonnet-DFQXde-d.js +1 -0
- package/public/assets/jssm-C2t-YnRu.js +1 -0
- package/public/assets/jsx-g9-lgVsj.js +1 -0
- package/public/assets/julia-C8NyazO9.js +1 -0
- package/public/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/public/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/public/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/public/assets/kdl-DV7GczEv.js +1 -0
- package/public/assets/kotlin-BdnUsdx6.js +1 -0
- package/public/assets/kusto-BvAqAH-y.js +1 -0
- package/public/assets/laserwave-DUszq2jm.js +1 -0
- package/public/assets/latex-BUKiar2Z.js +1 -0
- package/public/assets/lean-DP1Csr6i.js +1 -0
- package/public/assets/less-B1dDrJ26.js +1 -0
- package/public/assets/light-plus-B7mTdjB0.js +1 -0
- package/public/assets/liquid-DYVedYrR.js +1 -0
- package/public/assets/llvm-BtvRca6l.js +1 -0
- package/public/assets/log-2UxHyX5q.js +1 -0
- package/public/assets/logo-BtOb2qkB.js +1 -0
- package/public/assets/lua-BbnMAYS6.js +1 -0
- package/public/assets/luau-CXu1NL6O.js +1 -0
- package/public/assets/make-CHLpvVh8.js +1 -0
- package/public/assets/markdown-Cvjx9yec.js +1 -0
- package/public/assets/marko-CPi9NSCl.js +1 -0
- package/public/assets/material-theme-D5KoaKCx.js +1 -0
- package/public/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/public/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/public/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/public/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/public/assets/matlab-D7o27uSR.js +1 -0
- package/public/assets/mdc-DUICxH0z.js +1 -0
- package/public/assets/mdx-Cmh6b_Ma.js +1 -0
- package/public/assets/mermaid-DKYwYmdq.js +1 -0
- package/public/assets/min-dark-CafNBF8u.js +1 -0
- package/public/assets/min-light-CTRr51gU.js +1 -0
- package/public/assets/mipsasm-CKIfxQSi.js +1 -0
- package/public/assets/mojo-1DNp92w6.js +1 -0
- package/public/assets/monokai-D4h5O-jR.js +1 -0
- package/public/assets/move-Bu9oaDYs.js +1 -0
- package/public/assets/narrat-DRg8JJMk.js +1 -0
- package/public/assets/nextflow-CUEJCptM.js +1 -0
- package/public/assets/nginx-DknmC5AR.js +1 -0
- package/public/assets/night-owl-C39BiMTA.js +1 -0
- package/public/assets/nim-CVrawwO9.js +1 -0
- package/public/assets/nix-BbRYJGeE.js +1 -0
- package/public/assets/nord-Ddv68eIx.js +1 -0
- package/public/assets/nushell-C-sUppwS.js +1 -0
- package/public/assets/objective-c-DXmwc3jG.js +1 -0
- package/public/assets/objective-cpp-CLxacb5B.js +1 -0
- package/public/assets/ocaml-C0hk2d4L.js +1 -0
- package/public/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/public/assets/one-light-PoHY5YXO.js +1 -0
- package/public/assets/pascal-D93ZcfNL.js +1 -0
- package/public/assets/perl-C0TMdlhV.js +1 -0
- package/public/assets/php-CDn_0X-4.js +1 -0
- package/public/assets/pkl-u5AG7uiY.js +1 -0
- package/public/assets/plastic-3e1v2bzS.js +1 -0
- package/public/assets/plsql-ChMvpjG-.js +1 -0
- package/public/assets/po-BTJTHyun.js +1 -0
- package/public/assets/poimandres-CS3Unz2-.js +1 -0
- package/public/assets/polar-C0HS_06l.js +1 -0
- package/public/assets/postcss-CXtECtnM.js +1 -0
- package/public/assets/powerquery-CEu0bR-o.js +1 -0
- package/public/assets/powershell-Dpen1YoG.js +1 -0
- package/public/assets/prisma-Dd19v3D-.js +1 -0
- package/public/assets/prolog-CbFg5uaA.js +1 -0
- package/public/assets/proto-DyJlTyXw.js +1 -0
- package/public/assets/pug-CGlum2m_.js +1 -0
- package/public/assets/puppet-BMWR74SV.js +1 -0
- package/public/assets/purescript-CklMAg4u.js +1 -0
- package/public/assets/python-B6aJPvgy.js +1 -0
- package/public/assets/qml-3beO22l8.js +1 -0
- package/public/assets/qmldir-C8lEn-DE.js +1 -0
- package/public/assets/qss-IeuSbFQv.js +1 -0
- package/public/assets/r-DiinP2Uv.js +1 -0
- package/public/assets/racket-BqYA7rlc.js +1 -0
- package/public/assets/raku-DXvB9xmW.js +1 -0
- package/public/assets/razor-WgofotgN.js +1 -0
- package/public/assets/red-bN70gL4F.js +1 -0
- package/public/assets/reg-C-SQnVFl.js +1 -0
- package/public/assets/regexp-CDVJQ6XC.js +1 -0
- package/public/assets/rel-C3B-1QV4.js +1 -0
- package/public/assets/riscv-BM1_JUlF.js +1 -0
- package/public/assets/rose-pine-BHrmToEH.js +1 -0
- package/public/assets/rose-pine-dawn-CnK8MTSM.js +1 -0
- package/public/assets/rose-pine-moon-NleAzG8P.js +1 -0
- package/public/assets/rosmsg-BJDFO7_C.js +1 -0
- package/public/assets/rst-B0xPkSld.js +1 -0
- package/public/assets/ruby-BvKwtOVI.js +1 -0
- package/public/assets/rust-B1yitclQ.js +1 -0
- package/public/assets/sas-cz2c8ADy.js +1 -0
- package/public/assets/sass-Cj5Yp3dK.js +1 -0
- package/public/assets/scala-C151Ov-r.js +1 -0
- package/public/assets/scheme-C98Dy4si.js +1 -0
- package/public/assets/scss-OYdSNvt2.js +1 -0
- package/public/assets/sdbl-DVxCFoDh.js +1 -0
- package/public/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/public/assets/shellscript-Yzrsuije.js +1 -0
- package/public/assets/shellsession-BADoaaVG.js +1 -0
- package/public/assets/slack-dark-BthQWCQV.js +1 -0
- package/public/assets/slack-ochin-DqwNpetd.js +1 -0
- package/public/assets/smalltalk-BERRCDM3.js +1 -0
- package/public/assets/snazzy-light-Bw305WKR.js +1 -0
- package/public/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/public/assets/solarized-light-L9t79GZl.js +1 -0
- package/public/assets/solidity-BbcW6ACK.js +1 -0
- package/public/assets/soy-Brmx7dQM.js +1 -0
- package/public/assets/sparql-rVzFXLq3.js +1 -0
- package/public/assets/splunk-BtCnVYZw.js +1 -0
- package/public/assets/sql-BLtJtn59.js +1 -0
- package/public/assets/ssh-config-_ykCGR6B.js +1 -0
- package/public/assets/stata-BH5u7GGu.js +1 -0
- package/public/assets/stylus-BEDo0Tqx.js +1 -0
- package/public/assets/svelte-3Dk4HxPD.js +1 -0
- package/public/assets/swift-Dg5xB15N.js +1 -0
- package/public/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/public/assets/system-verilog-CnnmHF94.js +1 -0
- package/public/assets/systemd-4A_iFExJ.js +1 -0
- package/public/assets/talonscript-CkByrt1z.js +1 -0
- package/public/assets/tasl-QIJgUcNo.js +1 -0
- package/public/assets/tcl-dwOrl1Do.js +1 -0
- package/public/assets/templ-W15q3VgB.js +1 -0
- package/public/assets/terraform-BETggiCN.js +1 -0
- package/public/assets/tex-Cppo0RY3.js +1 -0
- package/public/assets/tokyo-night-hegEt444.js +1 -0
- package/public/assets/toml-vGWfd6FD.js +1 -0
- package/public/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/public/assets/tsv-B_m7g4N7.js +1 -0
- package/public/assets/tsx-COt5Ahok.js +1 -0
- package/public/assets/turtle-BsS91CYL.js +1 -0
- package/public/assets/twig-CO9l9SDP.js +1 -0
- package/public/assets/typescript-BPQ3VLAy.js +1 -0
- package/public/assets/typespec-Df68jz8_.js +1 -0
- package/public/assets/typst-DHCkPAjA.js +1 -0
- package/public/assets/v-BcVCzyr7.js +1 -0
- package/public/assets/vala-CsfeWuGM.js +1 -0
- package/public/assets/vb-D17OF-Vu.js +1 -0
- package/public/assets/verilog-BQ8w6xss.js +1 -0
- package/public/assets/vesper-DU1UobuO.js +1 -0
- package/public/assets/vhdl-CeAyd5Ju.js +1 -0
- package/public/assets/viml-CJc9bBzg.js +1 -0
- package/public/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/public/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/public/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/public/assets/vue-CCoi5OLL.js +1 -0
- package/public/assets/vue-html-DAAvJJDi.js +1 -0
- package/public/assets/vue-vine-_Ih-lPRR.js +1 -0
- package/public/assets/vyper-CDx5xZoG.js +1 -0
- package/public/assets/wasm-CG6Dc4jp.js +1 -0
- package/public/assets/wasm-MzD3tlZU.js +1 -0
- package/public/assets/wenyan-BV7otONQ.js +1 -0
- package/public/assets/wgsl-Dx-B1_4e.js +1 -0
- package/public/assets/wikitext-BhOHFoWU.js +1 -0
- package/public/assets/wit-5i3qLPDT.js +1 -0
- package/public/assets/wolfram-lXgVvXCa.js +1 -0
- package/public/assets/xml-sdJ4AIDG.js +1 -0
- package/public/assets/xsl-CtQFsRM5.js +1 -0
- package/public/assets/yaml-Buea-lGh.js +1 -0
- package/public/assets/zenscript-DVFEvuxE.js +1 -0
- package/public/assets/zig-VOosw3JB.js +1 -0
- package/public/index.html +44 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import cors from "@fastify/cors";
|
|
3
|
+
import fastifyStatic from "@fastify/static";
|
|
4
|
+
import replyFrom from "@fastify/reply-from";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fetch } from "undici";
|
|
8
|
+
import { registerWorkspaceRoutes } from "./routes/workspaces";
|
|
9
|
+
import { registerConfigRoutes } from "./routes/config";
|
|
10
|
+
import { registerFilesystemRoutes } from "./routes/filesystem";
|
|
11
|
+
import { registerMetaRoutes } from "./routes/meta";
|
|
12
|
+
import { registerEventRoutes } from "./routes/events";
|
|
13
|
+
import { registerStorageRoutes } from "./routes/storage";
|
|
14
|
+
export function createHttpServer(deps) {
|
|
15
|
+
const app = Fastify({ logger: false });
|
|
16
|
+
const proxyLogger = deps.logger.child({ component: "proxy" });
|
|
17
|
+
const sseClients = new Set();
|
|
18
|
+
const registerSseClient = (cleanup) => {
|
|
19
|
+
sseClients.add(cleanup);
|
|
20
|
+
return () => sseClients.delete(cleanup);
|
|
21
|
+
};
|
|
22
|
+
const closeSseClients = () => {
|
|
23
|
+
for (const cleanup of Array.from(sseClients)) {
|
|
24
|
+
cleanup();
|
|
25
|
+
}
|
|
26
|
+
sseClients.clear();
|
|
27
|
+
};
|
|
28
|
+
app.register(cors, {
|
|
29
|
+
origin: true,
|
|
30
|
+
credentials: true,
|
|
31
|
+
});
|
|
32
|
+
app.register(replyFrom, {
|
|
33
|
+
contentTypesToEncode: [],
|
|
34
|
+
});
|
|
35
|
+
registerWorkspaceRoutes(app, { workspaceManager: deps.workspaceManager });
|
|
36
|
+
registerConfigRoutes(app, { configStore: deps.configStore, binaryRegistry: deps.binaryRegistry });
|
|
37
|
+
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser });
|
|
38
|
+
registerMetaRoutes(app, { serverMeta: deps.serverMeta });
|
|
39
|
+
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient });
|
|
40
|
+
registerStorageRoutes(app, {
|
|
41
|
+
instanceStore: deps.instanceStore,
|
|
42
|
+
eventBus: deps.eventBus,
|
|
43
|
+
workspaceManager: deps.workspaceManager,
|
|
44
|
+
});
|
|
45
|
+
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger });
|
|
46
|
+
if (deps.uiDevServerUrl) {
|
|
47
|
+
setupDevProxy(app, deps.uiDevServerUrl);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
setupStaticUi(app, deps.uiStaticDir);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
instance: app,
|
|
54
|
+
start: async () => {
|
|
55
|
+
const addressInfo = await app.listen({ port: deps.port, host: deps.host });
|
|
56
|
+
let actualPort = deps.port;
|
|
57
|
+
if (typeof addressInfo === "string") {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = new URL(addressInfo);
|
|
60
|
+
actualPort = Number(parsed.port) || deps.port;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
actualPort = deps.port;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const address = app.server.address();
|
|
68
|
+
if (typeof address === "object" && address) {
|
|
69
|
+
actualPort = address.port;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host;
|
|
73
|
+
const serverUrl = `http://${displayHost}:${actualPort}`;
|
|
74
|
+
deps.serverMeta.httpBaseUrl = serverUrl;
|
|
75
|
+
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening");
|
|
76
|
+
console.log(`CodeNomad Server is ready at ${serverUrl}`);
|
|
77
|
+
return { port: actualPort, url: serverUrl, displayHost };
|
|
78
|
+
},
|
|
79
|
+
stop: () => {
|
|
80
|
+
closeSseClients();
|
|
81
|
+
return app.close();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function registerInstanceProxyRoutes(app, deps) {
|
|
86
|
+
app.register(async (instance) => {
|
|
87
|
+
instance.removeAllContentTypeParsers();
|
|
88
|
+
instance.addContentTypeParser("*", (req, body, done) => done(null, body));
|
|
89
|
+
const proxyBaseHandler = async (request, reply) => {
|
|
90
|
+
await proxyWorkspaceRequest({
|
|
91
|
+
request,
|
|
92
|
+
reply,
|
|
93
|
+
workspaceManager: deps.workspaceManager,
|
|
94
|
+
pathSuffix: "",
|
|
95
|
+
logger: deps.logger,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const proxyWildcardHandler = async (request, reply) => {
|
|
99
|
+
await proxyWorkspaceRequest({
|
|
100
|
+
request,
|
|
101
|
+
reply,
|
|
102
|
+
workspaceManager: deps.workspaceManager,
|
|
103
|
+
pathSuffix: request.params["*"] ?? "",
|
|
104
|
+
logger: deps.logger,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
instance.all("/workspaces/:id/instance", proxyBaseHandler);
|
|
108
|
+
instance.all("/workspaces/:id/instance/*", proxyWildcardHandler);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const INSTANCE_PROXY_HOST = "127.0.0.1";
|
|
112
|
+
async function proxyWorkspaceRequest(args) {
|
|
113
|
+
const { request, reply, workspaceManager, logger } = args;
|
|
114
|
+
const workspaceId = request.params.id;
|
|
115
|
+
const workspace = workspaceManager.get(workspaceId);
|
|
116
|
+
if (!workspace) {
|
|
117
|
+
reply.code(404).send({ error: "Workspace not found" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const port = workspaceManager.getInstancePort(workspaceId);
|
|
121
|
+
if (!port) {
|
|
122
|
+
reply.code(502).send({ error: "Workspace instance is not ready" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const normalizedSuffix = normalizeInstanceSuffix(args.pathSuffix);
|
|
126
|
+
const queryIndex = (request.raw.url ?? "").indexOf("?");
|
|
127
|
+
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : "";
|
|
128
|
+
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`;
|
|
129
|
+
return reply.from(targetUrl, {
|
|
130
|
+
onError: (proxyReply, { error }) => {
|
|
131
|
+
logger.error({ err: error, workspaceId, targetUrl }, "Failed to proxy workspace request");
|
|
132
|
+
if (!proxyReply.sent) {
|
|
133
|
+
proxyReply.code(502).send({ error: "Workspace instance proxy failed" });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function normalizeInstanceSuffix(pathSuffix) {
|
|
139
|
+
if (!pathSuffix || pathSuffix === "/") {
|
|
140
|
+
return "/";
|
|
141
|
+
}
|
|
142
|
+
const trimmed = pathSuffix.replace(/^\/+/, "");
|
|
143
|
+
return trimmed.length === 0 ? "/" : `/${trimmed}`;
|
|
144
|
+
}
|
|
145
|
+
function setupStaticUi(app, uiDir) {
|
|
146
|
+
if (!uiDir) {
|
|
147
|
+
app.log.warn("UI static directory not provided; API endpoints only");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!fs.existsSync(uiDir)) {
|
|
151
|
+
app.log.warn({ uiDir }, "UI static directory missing; API endpoints only");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
app.register(fastifyStatic, {
|
|
155
|
+
root: uiDir,
|
|
156
|
+
prefix: "/",
|
|
157
|
+
decorateReply: false,
|
|
158
|
+
});
|
|
159
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
160
|
+
app.setNotFoundHandler((request, reply) => {
|
|
161
|
+
const url = request.raw.url ?? "";
|
|
162
|
+
if (isApiRequest(url)) {
|
|
163
|
+
reply.code(404).send({ message: "Not Found" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (fs.existsSync(indexPath)) {
|
|
167
|
+
reply.type("text/html").send(fs.readFileSync(indexPath, "utf-8"));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
reply.code(404).send({ message: "UI bundle missing" });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function setupDevProxy(app, upstreamBase) {
|
|
175
|
+
app.log.info({ upstreamBase }, "Proxying UI requests to development server");
|
|
176
|
+
app.setNotFoundHandler((request, reply) => {
|
|
177
|
+
const url = request.raw.url ?? "";
|
|
178
|
+
if (isApiRequest(url)) {
|
|
179
|
+
reply.code(404).send({ message: "Not Found" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
void proxyToDevServer(request, reply, upstreamBase);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function proxyToDevServer(request, reply, upstreamBase) {
|
|
186
|
+
try {
|
|
187
|
+
const targetUrl = new URL(request.raw.url ?? "/", upstreamBase);
|
|
188
|
+
const response = await fetch(targetUrl, {
|
|
189
|
+
method: request.method,
|
|
190
|
+
headers: buildProxyHeaders(request.headers),
|
|
191
|
+
});
|
|
192
|
+
response.headers.forEach((value, key) => {
|
|
193
|
+
reply.header(key, value);
|
|
194
|
+
});
|
|
195
|
+
reply.code(response.status);
|
|
196
|
+
if (!response.body || request.method === "HEAD") {
|
|
197
|
+
reply.send();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
201
|
+
reply.send(buffer);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
request.log.error({ err: error }, "Failed to proxy UI request to dev server");
|
|
205
|
+
if (!reply.sent) {
|
|
206
|
+
reply.code(502).send("UI dev server is unavailable");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function isApiRequest(rawUrl) {
|
|
211
|
+
if (!rawUrl)
|
|
212
|
+
return false;
|
|
213
|
+
const pathname = rawUrl.split("?")[0] ?? "";
|
|
214
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
215
|
+
}
|
|
216
|
+
function buildProxyHeaders(headers) {
|
|
217
|
+
const result = {};
|
|
218
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
219
|
+
if (!value || key.toLowerCase() === "host")
|
|
220
|
+
continue;
|
|
221
|
+
result[key] = Array.isArray(value) ? value.join(",") : value;
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ConfigFileSchema } from "../../config/schema";
|
|
3
|
+
const BinaryCreateSchema = z.object({
|
|
4
|
+
path: z.string(),
|
|
5
|
+
label: z.string().optional(),
|
|
6
|
+
makeDefault: z.boolean().optional(),
|
|
7
|
+
});
|
|
8
|
+
const BinaryUpdateSchema = z.object({
|
|
9
|
+
label: z.string().optional(),
|
|
10
|
+
makeDefault: z.boolean().optional(),
|
|
11
|
+
});
|
|
12
|
+
const BinaryValidateSchema = z.object({
|
|
13
|
+
path: z.string(),
|
|
14
|
+
});
|
|
15
|
+
export function registerConfigRoutes(app, deps) {
|
|
16
|
+
app.get("/api/config/app", async () => deps.configStore.get());
|
|
17
|
+
app.put("/api/config/app", async (request) => {
|
|
18
|
+
const body = ConfigFileSchema.parse(request.body ?? {});
|
|
19
|
+
deps.configStore.replace(body);
|
|
20
|
+
return deps.configStore.get();
|
|
21
|
+
});
|
|
22
|
+
app.get("/api/config/binaries", async () => {
|
|
23
|
+
return { binaries: deps.binaryRegistry.list() };
|
|
24
|
+
});
|
|
25
|
+
app.post("/api/config/binaries", async (request, reply) => {
|
|
26
|
+
const body = BinaryCreateSchema.parse(request.body ?? {});
|
|
27
|
+
const binary = deps.binaryRegistry.create(body);
|
|
28
|
+
reply.code(201);
|
|
29
|
+
return { binary };
|
|
30
|
+
});
|
|
31
|
+
app.patch("/api/config/binaries/:id", async (request) => {
|
|
32
|
+
const body = BinaryUpdateSchema.parse(request.body ?? {});
|
|
33
|
+
const binary = deps.binaryRegistry.update(request.params.id, body);
|
|
34
|
+
return { binary };
|
|
35
|
+
});
|
|
36
|
+
app.delete("/api/config/binaries/:id", async (request, reply) => {
|
|
37
|
+
deps.binaryRegistry.remove(request.params.id);
|
|
38
|
+
reply.code(204);
|
|
39
|
+
});
|
|
40
|
+
app.post("/api/config/binaries/validate", async (request) => {
|
|
41
|
+
const body = BinaryValidateSchema.parse(request.body ?? {});
|
|
42
|
+
return deps.binaryRegistry.validatePath(body.path);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function registerEventRoutes(app, deps) {
|
|
2
|
+
app.get("/api/events", (request, reply) => {
|
|
3
|
+
const origin = request.headers.origin ?? "*";
|
|
4
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", origin);
|
|
5
|
+
reply.raw.setHeader("Access-Control-Allow-Credentials", "true");
|
|
6
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
7
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
8
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
9
|
+
reply.raw.flushHeaders?.();
|
|
10
|
+
reply.hijack();
|
|
11
|
+
const send = (event) => {
|
|
12
|
+
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
13
|
+
};
|
|
14
|
+
const unsubscribe = deps.eventBus.onEvent(send);
|
|
15
|
+
const heartbeat = setInterval(() => {
|
|
16
|
+
reply.raw.write(`:hb ${Date.now()}\n\n`);
|
|
17
|
+
}, 15000);
|
|
18
|
+
let closed = false;
|
|
19
|
+
const close = () => {
|
|
20
|
+
if (closed)
|
|
21
|
+
return;
|
|
22
|
+
closed = true;
|
|
23
|
+
clearInterval(heartbeat);
|
|
24
|
+
unsubscribe();
|
|
25
|
+
reply.raw.end?.();
|
|
26
|
+
};
|
|
27
|
+
const unregister = deps.registerClient(close);
|
|
28
|
+
const handleClose = () => {
|
|
29
|
+
close();
|
|
30
|
+
unregister();
|
|
31
|
+
};
|
|
32
|
+
request.raw.on("close", handleClose);
|
|
33
|
+
request.raw.on("error", handleClose);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const FilesystemQuerySchema = z.object({
|
|
3
|
+
path: z.string().optional(),
|
|
4
|
+
includeFiles: z.coerce.boolean().optional(),
|
|
5
|
+
});
|
|
6
|
+
export function registerFilesystemRoutes(app, deps) {
|
|
7
|
+
app.get("/api/filesystem", async (request, reply) => {
|
|
8
|
+
const query = FilesystemQuerySchema.parse(request.query ?? {});
|
|
9
|
+
try {
|
|
10
|
+
return deps.fileSystemBrowser.browse(query.path, {
|
|
11
|
+
includeFiles: query.includeFiles,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
reply.code(400);
|
|
16
|
+
return { error: error.message };
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ModelPreferenceSchema } from "../../config/schema";
|
|
3
|
+
const InstanceDataSchema = z.object({
|
|
4
|
+
messageHistory: z.array(z.string()).default([]),
|
|
5
|
+
agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}),
|
|
6
|
+
});
|
|
7
|
+
const EMPTY_INSTANCE_DATA = {
|
|
8
|
+
messageHistory: [],
|
|
9
|
+
agentModelSelections: {},
|
|
10
|
+
};
|
|
11
|
+
export function registerStorageRoutes(app, deps) {
|
|
12
|
+
const resolveStorageKey = (instanceId) => {
|
|
13
|
+
const workspace = deps.workspaceManager.get(instanceId);
|
|
14
|
+
return workspace?.path ?? instanceId;
|
|
15
|
+
};
|
|
16
|
+
app.get("/api/storage/instances/:id", async (request, reply) => {
|
|
17
|
+
try {
|
|
18
|
+
const storageId = resolveStorageKey(request.params.id);
|
|
19
|
+
const data = await deps.instanceStore.read(storageId);
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
reply.code(500);
|
|
24
|
+
return { error: error instanceof Error ? error.message : "Failed to read instance data" };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
app.put("/api/storage/instances/:id", async (request, reply) => {
|
|
28
|
+
try {
|
|
29
|
+
const body = InstanceDataSchema.parse(request.body ?? {});
|
|
30
|
+
const storageId = resolveStorageKey(request.params.id);
|
|
31
|
+
await deps.instanceStore.write(storageId, body);
|
|
32
|
+
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: body });
|
|
33
|
+
reply.code(204);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
reply.code(400);
|
|
37
|
+
return { error: error instanceof Error ? error.message : "Failed to save instance data" };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
app.delete("/api/storage/instances/:id", async (request, reply) => {
|
|
41
|
+
try {
|
|
42
|
+
const storageId = resolveStorageKey(request.params.id);
|
|
43
|
+
await deps.instanceStore.delete(storageId);
|
|
44
|
+
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: EMPTY_INSTANCE_DATA });
|
|
45
|
+
reply.code(204);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
reply.code(500);
|
|
49
|
+
return { error: error instanceof Error ? error.message : "Failed to delete instance data" };
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const WorkspaceCreateSchema = z.object({
|
|
3
|
+
path: z.string(),
|
|
4
|
+
name: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
const WorkspaceFilesQuerySchema = z.object({
|
|
7
|
+
path: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
const WorkspaceFileContentQuerySchema = z.object({
|
|
10
|
+
path: z.string(),
|
|
11
|
+
});
|
|
12
|
+
const WorkspaceFileSearchQuerySchema = z.object({
|
|
13
|
+
q: z.string().trim().min(1, "Query is required"),
|
|
14
|
+
limit: z.coerce.number().int().positive().max(200).optional(),
|
|
15
|
+
type: z.enum(["all", "file", "directory"]).optional(),
|
|
16
|
+
refresh: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.transform((value) => (value === undefined ? undefined : value === "true")),
|
|
20
|
+
});
|
|
21
|
+
export function registerWorkspaceRoutes(app, deps) {
|
|
22
|
+
app.get("/api/workspaces", async () => {
|
|
23
|
+
return deps.workspaceManager.list();
|
|
24
|
+
});
|
|
25
|
+
app.post("/api/workspaces", async (request, reply) => {
|
|
26
|
+
const body = WorkspaceCreateSchema.parse(request.body ?? {});
|
|
27
|
+
const workspace = await deps.workspaceManager.create(body.path, body.name);
|
|
28
|
+
reply.code(201);
|
|
29
|
+
return workspace;
|
|
30
|
+
});
|
|
31
|
+
app.get("/api/workspaces/:id", async (request, reply) => {
|
|
32
|
+
const workspace = deps.workspaceManager.get(request.params.id);
|
|
33
|
+
if (!workspace) {
|
|
34
|
+
reply.code(404);
|
|
35
|
+
return { error: "Workspace not found" };
|
|
36
|
+
}
|
|
37
|
+
return workspace;
|
|
38
|
+
});
|
|
39
|
+
app.delete("/api/workspaces/:id", async (request, reply) => {
|
|
40
|
+
await deps.workspaceManager.delete(request.params.id);
|
|
41
|
+
reply.code(204);
|
|
42
|
+
});
|
|
43
|
+
app.get("/api/workspaces/:id/files", async (request, reply) => {
|
|
44
|
+
try {
|
|
45
|
+
const query = WorkspaceFilesQuerySchema.parse(request.query ?? {});
|
|
46
|
+
return deps.workspaceManager.listFiles(request.params.id, query.path ?? ".");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return handleWorkspaceError(error, reply);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
app.get("/api/workspaces/:id/files/search", async (request, reply) => {
|
|
53
|
+
try {
|
|
54
|
+
const query = WorkspaceFileSearchQuerySchema.parse(request.query ?? {});
|
|
55
|
+
return deps.workspaceManager.searchFiles(request.params.id, query.q, {
|
|
56
|
+
limit: query.limit,
|
|
57
|
+
type: query.type,
|
|
58
|
+
refresh: query.refresh,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return handleWorkspaceError(error, reply);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
app.get("/api/workspaces/:id/files/content", async (request, reply) => {
|
|
66
|
+
try {
|
|
67
|
+
const query = WorkspaceFileContentQuerySchema.parse(request.query ?? {});
|
|
68
|
+
return deps.workspaceManager.readFile(request.params.id, query.path);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return handleWorkspaceError(error, reply);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function handleWorkspaceError(error, reply) {
|
|
76
|
+
if (error instanceof Error && error.message === "Workspace not found") {
|
|
77
|
+
reply.code(404);
|
|
78
|
+
return { error: "Workspace not found" };
|
|
79
|
+
}
|
|
80
|
+
reply.code(400);
|
|
81
|
+
return { error: error instanceof Error ? error.message : "Unable to fulfill request" };
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { promises as fsp } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
const DEFAULT_INSTANCE_DATA = {
|
|
6
|
+
messageHistory: [],
|
|
7
|
+
agentModelSelections: {},
|
|
8
|
+
};
|
|
9
|
+
export class InstanceStore {
|
|
10
|
+
constructor(baseDir = path.join(os.homedir(), ".config", "codenomad", "instances")) {
|
|
11
|
+
this.instancesDir = baseDir;
|
|
12
|
+
fs.mkdirSync(this.instancesDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
async read(id) {
|
|
15
|
+
try {
|
|
16
|
+
const filePath = this.resolvePath(id);
|
|
17
|
+
const content = await fsp.readFile(filePath, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(content);
|
|
19
|
+
return { ...DEFAULT_INSTANCE_DATA, ...parsed };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
23
|
+
return DEFAULT_INSTANCE_DATA;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async write(id, data) {
|
|
29
|
+
const filePath = this.resolvePath(id);
|
|
30
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
31
|
+
await fsp.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
async delete(id) {
|
|
34
|
+
try {
|
|
35
|
+
const filePath = this.resolvePath(id);
|
|
36
|
+
await fsp.unlink(filePath);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT") {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
resolvePath(id) {
|
|
45
|
+
const filename = this.sanitizeId(id);
|
|
46
|
+
return path.join(this.instancesDir, `${filename}.json`);
|
|
47
|
+
}
|
|
48
|
+
sanitizeId(id) {
|
|
49
|
+
return id
|
|
50
|
+
.replace(/[\\/]/g, "_")
|
|
51
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_")
|
|
52
|
+
.replace(/_{2,}/g, "_")
|
|
53
|
+
.replace(/^_|_$/g, "")
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { FileSystemBrowser } from "../filesystem/browser";
|
|
3
|
+
import { searchWorkspaceFiles } from "../filesystem/search";
|
|
4
|
+
import { clearWorkspaceSearchCache } from "../filesystem/search-cache";
|
|
5
|
+
import { WorkspaceRuntime } from "./runtime";
|
|
6
|
+
export class WorkspaceManager {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.workspaces = new Map();
|
|
10
|
+
this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger);
|
|
11
|
+
}
|
|
12
|
+
list() {
|
|
13
|
+
return Array.from(this.workspaces.values());
|
|
14
|
+
}
|
|
15
|
+
get(id) {
|
|
16
|
+
return this.workspaces.get(id);
|
|
17
|
+
}
|
|
18
|
+
getInstancePort(id) {
|
|
19
|
+
return this.workspaces.get(id)?.port;
|
|
20
|
+
}
|
|
21
|
+
listFiles(workspaceId, relativePath = ".") {
|
|
22
|
+
const workspace = this.requireWorkspace(workspaceId);
|
|
23
|
+
const browser = new FileSystemBrowser({ rootDir: workspace.path });
|
|
24
|
+
return browser.list(relativePath);
|
|
25
|
+
}
|
|
26
|
+
searchFiles(workspaceId, query, options) {
|
|
27
|
+
const workspace = this.requireWorkspace(workspaceId);
|
|
28
|
+
return searchWorkspaceFiles(workspace.path, query, options);
|
|
29
|
+
}
|
|
30
|
+
readFile(workspaceId, relativePath) {
|
|
31
|
+
const workspace = this.requireWorkspace(workspaceId);
|
|
32
|
+
const browser = new FileSystemBrowser({ rootDir: workspace.path });
|
|
33
|
+
const contents = browser.readFile(relativePath);
|
|
34
|
+
return {
|
|
35
|
+
workspaceId,
|
|
36
|
+
relativePath,
|
|
37
|
+
contents,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async create(folder, name) {
|
|
41
|
+
const id = `${Date.now().toString(36)}`;
|
|
42
|
+
const binary = this.options.binaryRegistry.resolveDefault();
|
|
43
|
+
const workspacePath = path.isAbsolute(folder) ? folder : path.resolve(this.options.rootDir, folder);
|
|
44
|
+
clearWorkspaceSearchCache(workspacePath);
|
|
45
|
+
this.options.logger.info({ workspaceId: id, folder: workspacePath, binary: binary.path }, "Creating workspace");
|
|
46
|
+
const proxyPath = `/workspaces/${id}/instance`;
|
|
47
|
+
const descriptor = {
|
|
48
|
+
id,
|
|
49
|
+
path: workspacePath,
|
|
50
|
+
name,
|
|
51
|
+
status: "starting",
|
|
52
|
+
proxyPath,
|
|
53
|
+
binaryId: binary.id,
|
|
54
|
+
binaryLabel: binary.label,
|
|
55
|
+
binaryVersion: binary.version,
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
this.workspaces.set(id, descriptor);
|
|
60
|
+
this.options.eventBus.publish({ type: "workspace.created", workspace: descriptor });
|
|
61
|
+
const environment = this.options.configStore.get().preferences.environmentVariables ?? {};
|
|
62
|
+
try {
|
|
63
|
+
const { pid, port } = await this.runtime.launch({
|
|
64
|
+
workspaceId: id,
|
|
65
|
+
folder: workspacePath,
|
|
66
|
+
binaryPath: binary.path,
|
|
67
|
+
environment,
|
|
68
|
+
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
|
69
|
+
});
|
|
70
|
+
descriptor.pid = pid;
|
|
71
|
+
descriptor.port = port;
|
|
72
|
+
descriptor.status = "ready";
|
|
73
|
+
descriptor.updatedAt = new Date().toISOString();
|
|
74
|
+
this.options.eventBus.publish({ type: "workspace.started", workspace: descriptor });
|
|
75
|
+
this.options.logger.info({ workspaceId: id, port }, "Workspace ready");
|
|
76
|
+
return descriptor;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
descriptor.status = "error";
|
|
80
|
+
descriptor.error = error instanceof Error ? error.message : String(error);
|
|
81
|
+
descriptor.updatedAt = new Date().toISOString();
|
|
82
|
+
this.options.eventBus.publish({ type: "workspace.error", workspace: descriptor });
|
|
83
|
+
this.options.logger.error({ workspaceId: id, err: error }, "Workspace failed to start");
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async delete(id) {
|
|
88
|
+
const workspace = this.workspaces.get(id);
|
|
89
|
+
if (!workspace)
|
|
90
|
+
return undefined;
|
|
91
|
+
this.options.logger.info({ workspaceId: id }, "Stopping workspace");
|
|
92
|
+
const wasRunning = Boolean(workspace.pid);
|
|
93
|
+
if (wasRunning) {
|
|
94
|
+
await this.runtime.stop(id).catch((error) => {
|
|
95
|
+
this.options.logger.warn({ workspaceId: id, err: error }, "Failed to stop workspace process cleanly");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
this.workspaces.delete(id);
|
|
99
|
+
clearWorkspaceSearchCache(workspace.path);
|
|
100
|
+
if (!wasRunning) {
|
|
101
|
+
this.options.eventBus.publish({ type: "workspace.stopped", workspaceId: id });
|
|
102
|
+
}
|
|
103
|
+
return workspace;
|
|
104
|
+
}
|
|
105
|
+
async shutdown() {
|
|
106
|
+
this.options.logger.info("Shutting down all workspaces");
|
|
107
|
+
for (const [id, workspace] of this.workspaces) {
|
|
108
|
+
if (workspace.pid) {
|
|
109
|
+
this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown");
|
|
110
|
+
await this.runtime.stop(id).catch((error) => {
|
|
111
|
+
this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown");
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.options.logger.debug({ workspaceId: id }, "Workspace already stopped");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.workspaces.clear();
|
|
119
|
+
this.options.logger.info("All workspaces cleared");
|
|
120
|
+
}
|
|
121
|
+
requireWorkspace(id) {
|
|
122
|
+
const workspace = this.workspaces.get(id);
|
|
123
|
+
if (!workspace) {
|
|
124
|
+
throw new Error("Workspace not found");
|
|
125
|
+
}
|
|
126
|
+
return workspace;
|
|
127
|
+
}
|
|
128
|
+
handleProcessExit(workspaceId, info) {
|
|
129
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
130
|
+
if (!workspace)
|
|
131
|
+
return;
|
|
132
|
+
this.options.logger.info({ workspaceId, ...info }, "Workspace process exited");
|
|
133
|
+
workspace.pid = undefined;
|
|
134
|
+
workspace.port = undefined;
|
|
135
|
+
workspace.updatedAt = new Date().toISOString();
|
|
136
|
+
if (info.requested || info.code === 0) {
|
|
137
|
+
workspace.status = "stopped";
|
|
138
|
+
workspace.error = undefined;
|
|
139
|
+
this.options.eventBus.publish({ type: "workspace.stopped", workspaceId });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
workspace.status = "error";
|
|
143
|
+
workspace.error = `Process exited with code ${info.code}`;
|
|
144
|
+
this.options.eventBus.publish({ type: "workspace.error", workspace });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|