diff --git a/bun.lock b/bun.lock
index 5bac150a7..80edff981 100644
--- a/bun.lock
+++ b/bun.lock
@@ -54,8 +54,10 @@
"@storybook/test-runner": "^0.23.0",
"@testing-library/react": "^16.3.0",
"@types/bun": "^1.2.23",
+ "@types/cors": "^2.8.19",
"@types/diff": "^8.0.0",
"@types/escape-html": "^1.0.4",
+ "@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.2",
@@ -63,12 +65,14 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/write-file-atomic": "^4.0.3",
+ "@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"@typescript/native-preview": "^7.0.0-dev.20251014.1",
"@vitejs/plugin-react": "^4.0.0",
"babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^8.2.0",
+ "cors": "^2.8.5",
"dotenv": "^17.2.3",
"electron": "^38.2.1",
"electron-builder": "^24.6.0",
@@ -77,6 +81,7 @@
"eslint": "^9.36.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
+ "express": "^5.1.0",
"jest": "^30.1.3",
"playwright": "^1.56.0",
"prettier": "^3.6.2",
@@ -88,6 +93,7 @@
"vite": "^4.4.0",
"vite-plugin-svgr": "^4.5.0",
"vite-plugin-top-level-await": "^1.6.0",
+ "ws": "^8.18.3",
},
},
},
@@ -612,10 +618,16 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
+ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
+
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -690,6 +702,10 @@
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+ "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
+
+ "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
+
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
@@ -700,6 +716,8 @@
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
+ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
@@ -724,6 +742,8 @@
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
+ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
+
"@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -736,6 +756,10 @@
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
+ "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
"@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="],
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
@@ -744,6 +768,10 @@
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
+ "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
+
+ "@types/serve-static": ["@types/serve-static@1.15.9", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA=="],
+
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
@@ -758,6 +786,8 @@
"@types/write-file-atomic": ["@types/write-file-atomic@4.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-qdo+vZRchyJIHNeuI1nrpsLw+hnkgqP/8mlaN6Wle/NKhydHmUN9l4p3ZE8yP90AJNJW4uB8HQhedb4f1vNayQ=="],
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
@@ -854,6 +884,8 @@
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -966,6 +998,8 @@
"bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="],
+ "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
+
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -994,6 +1028,8 @@
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
@@ -1082,12 +1118,22 @@
"config-file-ts": ["config-file-ts@0.2.6", "", { "dependencies": { "glob": "^10.3.10", "typescript": "^5.3.3" } }, "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w=="],
+ "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
+ "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
+
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
@@ -1220,6 +1266,8 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
@@ -1272,6 +1320,8 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@38.3.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-Wij4AzX4SAV0X/ktq+NrWrp5piTCSS8F6YWh1KAcG+QRtNzyns9XLKERP68nFHIwfprhxF2YCN2uj7nx9DaeJw=="],
@@ -1294,6 +1344,8 @@
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -1358,6 +1410,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -1372,6 +1426,8 @@
"expect-playwright": ["expect-playwright@0.8.0", "", {}, "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg=="],
+ "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
+
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -1402,6 +1458,8 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+ "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
+
"find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="],
"find-file-up": ["find-file-up@0.1.3", "", { "dependencies": { "fs-exists-sync": "^0.1.0", "resolve-dir": "^0.1.0" } }, "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A=="],
@@ -1426,6 +1484,10 @@
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
"fromentries": ["fromentries@1.3.2", "", {}, "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
@@ -1554,6 +1616,8 @@
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
+ "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
+
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
@@ -1592,6 +1656,8 @@
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
@@ -1648,6 +1714,8 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@@ -1920,8 +1988,12 @@
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
"memoizerific": ["memoizerific@1.11.3", "", { "dependencies": { "map-or-similar": "^1.5.0" } }, "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog=="],
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@@ -1990,9 +2062,9 @@
"mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
- "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
- "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+ "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
@@ -2022,6 +2094,8 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
@@ -2056,6 +2130,8 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
@@ -2100,6 +2176,8 @@
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -2112,6 +2190,8 @@
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -2174,6 +2254,8 @@
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
@@ -2184,6 +2266,8 @@
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
+ "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
+
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="],
@@ -2192,6 +2276,10 @@
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
+
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"react-compiler-runtime": ["react-compiler-runtime@1.0.0", "", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } }, "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w=="],
@@ -2294,6 +2382,8 @@
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
@@ -2302,7 +2392,7 @@
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
- "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
@@ -2320,8 +2410,12 @@
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
+ "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
+
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
+ "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
+
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
@@ -2332,6 +2426,8 @@
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@@ -2382,6 +2478,8 @@
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"storybook": ["storybook@8.6.14", "", { "dependencies": { "@storybook/core": "8.6.14" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": { "sb": "./bin/index.cjs", "storybook": "./bin/index.cjs", "getstorybook": "./bin/index.cjs" } }, "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw=="],
@@ -2462,6 +2560,8 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@@ -2488,6 +2588,8 @@
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
@@ -2532,6 +2634,8 @@
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
"unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@@ -2556,6 +2660,8 @@
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -2878,6 +2984,8 @@
"foreground-child/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"glob/foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
@@ -2908,6 +3016,8 @@
"htmlparser2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+ "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
+
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"istanbul-lib-processinfo/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
@@ -3062,12 +3172,16 @@
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+ "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
+
"react-docgen/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"read-config-file/dotenv": ["dotenv@9.0.2", "", {}, "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg=="],
"readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
+ "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -3096,6 +3210,8 @@
"string-length/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+ "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
@@ -3358,6 +3474,8 @@
"find-process/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"istanbul-lib-report/make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 41855ff3d..65045f056 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -329,7 +329,7 @@ export default defineConfig([
"src/config.ts",
"src/debug/**/*.ts",
"src/git.ts",
- "src/main.ts",
+ "src/main-desktop.ts",
"src/config.test.ts",
"src/services/gitService.ts",
"src/services/log.ts",
diff --git a/package.json b/package.json
index bf82e642c..20341bd8f 100644
--- a/package.json
+++ b/package.json
@@ -83,8 +83,10 @@
"@storybook/test-runner": "^0.23.0",
"@testing-library/react": "^16.3.0",
"@types/bun": "^1.2.23",
+ "@types/cors": "^2.8.19",
"@types/diff": "^8.0.0",
"@types/escape-html": "^1.0.4",
+ "@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.2",
@@ -92,12 +94,14 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/write-file-atomic": "^4.0.3",
+ "@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"@typescript/native-preview": "^7.0.0-dev.20251014.1",
"@vitejs/plugin-react": "^4.0.0",
"babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^8.2.0",
+ "cors": "^2.8.5",
"dotenv": "^17.2.3",
"electron": "^38.2.1",
"electron-builder": "^24.6.0",
@@ -106,6 +110,7 @@
"eslint": "^9.36.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
+ "express": "^5.1.0",
"jest": "^30.1.3",
"playwright": "^1.56.0",
"prettier": "^3.6.2",
@@ -116,7 +121,8 @@
"typescript-eslint": "^8.45.0",
"vite": "^4.4.0",
"vite-plugin-svgr": "^4.5.0",
- "vite-plugin-top-level-await": "^1.6.0"
+ "vite-plugin-top-level-await": "^1.6.0",
+ "ws": "^8.18.3"
},
"build": {
"appId": "com.cmux.app",
diff --git a/src/App.tsx b/src/App.tsx
index 45c382397..2370b60c8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import type { WorkspaceSelection } from "./components/ProjectSidebar";
import type { FrontendWorkspaceMetadata } from "./types/workspace";
import { LeftSidebar } from "./components/LeftSidebar";
import NewWorkspaceModal from "./components/NewWorkspaceModal";
+import { DirectorySelectModal } from "./components/DirectorySelectModal";
import { AIView } from "./components/AIView";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { usePersistedState, updatePersistedState } from "./hooks/usePersistedState";
@@ -870,6 +871,7 @@ function AppInner() {
onAdd={handleCreateWorkspace}
/>
)}
+
>
);
diff --git a/src/browser/api.ts b/src/browser/api.ts
new file mode 100644
index 000000000..e0b0e8648
--- /dev/null
+++ b/src/browser/api.ts
@@ -0,0 +1,279 @@
+/**
+ * Browser API client. Used when running cmux in server mode.
+ */
+import { IPC_CHANNELS, getChatChannel } from "@/constants/ipc-constants";
+import type { IPCApi } from "@/types/ipc";
+
+const API_BASE = window.location.origin;
+const WS_BASE = API_BASE.replace("http://", "ws://").replace("https://", "wss://");
+
+interface InvokeResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+}
+
+// Helper function to invoke IPC handlers via HTTP
+async function invokeIPC(channel: string, ...args: unknown[]): Promise {
+ const response = await fetch(`${API_BASE}/ipc/${encodeURIComponent(channel)}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ args }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = (await response.json()) as InvokeResponse;
+
+ if (!result.success) {
+ throw new Error(result.error ?? "Unknown error");
+ }
+
+ return result.data as T;
+}
+
+// WebSocket connection manager
+class WebSocketManager {
+ private ws: WebSocket | null = null;
+ private reconnectTimer: ReturnType | null = null;
+ private messageHandlers = new Map void>>();
+ private channelWorkspaceIds = new Map(); // Track workspaceId for each channel
+ private isConnecting = false;
+ private shouldReconnect = true;
+
+ connect(): void {
+ if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
+ return;
+ }
+
+ this.isConnecting = true;
+ this.ws = new WebSocket(`${WS_BASE}/ws`);
+
+ this.ws.onopen = () => {
+ console.log("WebSocket connected");
+ this.isConnecting = false;
+
+ // Resubscribe to all channels with their workspace IDs
+ for (const channel of this.messageHandlers.keys()) {
+ const workspaceId = this.channelWorkspaceIds.get(channel);
+ this.subscribe(channel, workspaceId);
+ }
+ };
+
+ this.ws.onmessage = (event) => {
+ try {
+ const parsed = JSON.parse(event.data as string) as { channel: string; args: unknown[] };
+ const { channel, args } = parsed;
+ const handlers = this.messageHandlers.get(channel);
+ if (handlers && args.length > 0) {
+ handlers.forEach((handler) => handler(args[0]));
+ }
+ } catch (error) {
+ console.error("Error handling WebSocket message:", error);
+ }
+ };
+
+ this.ws.onerror = (error) => {
+ console.error("WebSocket error:", error);
+ this.isConnecting = false;
+ };
+
+ this.ws.onclose = () => {
+ console.log("WebSocket disconnected");
+ this.isConnecting = false;
+ this.ws = null;
+
+ // Attempt to reconnect after a delay
+ if (this.shouldReconnect) {
+ this.reconnectTimer = setTimeout(() => this.connect(), 2000);
+ }
+ };
+ }
+
+ subscribe(channel: string, workspaceId?: string): void {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ if (channel.startsWith(IPC_CHANNELS.WORKSPACE_CHAT_PREFIX)) {
+ console.log(
+ `[WebSocketManager] Subscribing to workspace chat for workspaceId: ${workspaceId ?? "undefined"}`
+ );
+ this.ws.send(
+ JSON.stringify({
+ type: "subscribe",
+ channel: "workspace:chat",
+ workspaceId,
+ })
+ );
+ } else if (channel === IPC_CHANNELS.WORKSPACE_METADATA) {
+ this.ws.send(
+ JSON.stringify({
+ type: "subscribe",
+ channel: "workspace:metadata",
+ })
+ );
+ }
+ }
+ }
+
+ unsubscribe(channel: string, workspaceId?: string): void {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ if (channel.startsWith(IPC_CHANNELS.WORKSPACE_CHAT_PREFIX)) {
+ this.ws.send(
+ JSON.stringify({
+ type: "unsubscribe",
+ channel: "workspace:chat",
+ workspaceId,
+ })
+ );
+ } else if (channel === IPC_CHANNELS.WORKSPACE_METADATA) {
+ this.ws.send(
+ JSON.stringify({
+ type: "unsubscribe",
+ channel: "workspace:metadata",
+ })
+ );
+ }
+ }
+ }
+
+ on(channel: string, handler: (data: unknown) => void, workspaceId?: string): () => void {
+ if (!this.messageHandlers.has(channel)) {
+ this.messageHandlers.set(channel, new Set());
+ // Store workspaceId for this channel (needed for reconnection)
+ if (workspaceId) {
+ this.channelWorkspaceIds.set(channel, workspaceId);
+ }
+ this.connect();
+ this.subscribe(channel, workspaceId);
+ }
+
+ const handlers = this.messageHandlers.get(channel)!;
+ handlers.add(handler);
+
+ // Return unsubscribe function
+ return () => {
+ handlers.delete(handler);
+ if (handlers.size === 0) {
+ this.messageHandlers.delete(channel);
+ this.channelWorkspaceIds.delete(channel);
+ this.unsubscribe(channel, workspaceId);
+ }
+ };
+ }
+
+ disconnect(): void {
+ this.shouldReconnect = false;
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = null;
+ }
+ if (this.ws) {
+ this.ws.close();
+ this.ws = null;
+ }
+ }
+}
+
+const wsManager = new WebSocketManager();
+
+// Directory selection via custom event (for browser mode)
+interface DirectorySelectEvent extends CustomEvent {
+ detail: {
+ resolve: (path: string | null) => void;
+ };
+}
+
+function requestDirectorySelection(): Promise {
+ return new Promise((resolve) => {
+ const event = new CustomEvent("directory-select-request", {
+ detail: { resolve },
+ }) as DirectorySelectEvent;
+ window.dispatchEvent(event);
+ });
+}
+
+// Create the Web API implementation
+const webApi: IPCApi = {
+ dialog: {
+ selectDirectory: requestDirectorySelection,
+ },
+ providers: {
+ setProviderConfig: (provider, keyPath, value) =>
+ invokeIPC(IPC_CHANNELS.PROVIDERS_SET_CONFIG, provider, keyPath, value),
+ list: () => invokeIPC(IPC_CHANNELS.PROVIDERS_LIST),
+ },
+ projects: {
+ create: (projectPath) => invokeIPC(IPC_CHANNELS.PROJECT_CREATE, projectPath),
+ remove: (projectPath) => invokeIPC(IPC_CHANNELS.PROJECT_REMOVE, projectPath),
+ list: () => invokeIPC(IPC_CHANNELS.PROJECT_LIST),
+ listBranches: (projectPath) => invokeIPC(IPC_CHANNELS.PROJECT_LIST_BRANCHES, projectPath),
+ secrets: {
+ get: (projectPath) => invokeIPC(IPC_CHANNELS.PROJECT_SECRETS_GET, projectPath),
+ update: (projectPath, secrets) =>
+ invokeIPC(IPC_CHANNELS.PROJECT_SECRETS_UPDATE, projectPath, secrets),
+ },
+ },
+ workspace: {
+ list: () => invokeIPC(IPC_CHANNELS.WORKSPACE_LIST),
+ create: (projectPath, branchName, trunkBranch) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_CREATE, projectPath, branchName, trunkBranch),
+ remove: (workspaceId, options) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, options),
+ rename: (workspaceId, newName) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_RENAME, workspaceId, newName),
+ fork: (sourceWorkspaceId, newName) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_FORK, sourceWorkspaceId, newName),
+ sendMessage: (workspaceId, message, options) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_SEND_MESSAGE, workspaceId, message, options),
+ resumeStream: (workspaceId, options) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_RESUME_STREAM, workspaceId, options),
+ interruptStream: (workspaceId, options) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_INTERRUPT_STREAM, workspaceId, options),
+ truncateHistory: (workspaceId, percentage) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_TRUNCATE_HISTORY, workspaceId, percentage),
+ replaceChatHistory: (workspaceId, summaryMessage) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_REPLACE_HISTORY, workspaceId, summaryMessage),
+ getInfo: (workspaceId) => invokeIPC(IPC_CHANNELS.WORKSPACE_GET_INFO, workspaceId),
+ executeBash: (workspaceId, script, options) =>
+ invokeIPC(IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, workspaceId, script, options),
+ openTerminal: (workspacePath) => invokeIPC(IPC_CHANNELS.WORKSPACE_OPEN_TERMINAL, workspacePath),
+
+ onChat: (workspaceId, callback) => {
+ const channel = getChatChannel(workspaceId);
+ return wsManager.on(channel, callback as (data: unknown) => void, workspaceId);
+ },
+
+ onMetadata: (callback) => {
+ return wsManager.on(IPC_CHANNELS.WORKSPACE_METADATA, callback as (data: unknown) => void);
+ },
+ },
+ window: {
+ setTitle: (title) => {
+ document.title = title;
+ return Promise.resolve();
+ },
+ },
+ update: {
+ check: () => invokeIPC(IPC_CHANNELS.UPDATE_CHECK),
+ download: () => invokeIPC(IPC_CHANNELS.UPDATE_DOWNLOAD),
+ install: () => {
+ // Install is a one-way call that doesn't wait for response
+ void invokeIPC(IPC_CHANNELS.UPDATE_INSTALL);
+ },
+ onStatus: (callback) => {
+ return wsManager.on(IPC_CHANNELS.UPDATE_STATUS, callback as (data: unknown) => void);
+ },
+ },
+};
+
+if (typeof window.api === "undefined") {
+ // @ts-expect-error - Assigning to window.api which is not in TypeScript types
+ window.api = webApi;
+}
+
+window.addEventListener("beforeunload", () => {
+ wsManager.disconnect();
+});
diff --git a/src/components/DirectorySelectModal.tsx b/src/components/DirectorySelectModal.tsx
new file mode 100644
index 000000000..503142ba3
--- /dev/null
+++ b/src/components/DirectorySelectModal.tsx
@@ -0,0 +1,122 @@
+import React, { useState, useCallback, useEffect, useRef } from "react";
+import styled from "@emotion/styled";
+import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
+
+const InputField = styled.input`
+ width: 100%;
+ padding: 8px 12px;
+ background: #2d2d2d;
+ border: 1px solid #444;
+ border-radius: 4px;
+ color: #fff;
+ font-size: 14px;
+ font-family: var(--font-monospace);
+ margin-bottom: 20px;
+
+ &:focus {
+ outline: none;
+ border-color: #007acc;
+ }
+
+ &::placeholder {
+ color: #888;
+ }
+`;
+
+const ErrorText = styled.div`
+ color: var(--color-error);
+ font-size: 12px;
+ margin-top: -12px;
+ margin-bottom: 12px;
+`;
+
+/**
+ * Self-contained directory selection modal for browser mode.
+ *
+ * Listens for 'directory-select-request' custom events and displays
+ * a modal for the user to enter a directory path. The promise from
+ * the event is resolved with the selected path or null if cancelled.
+ */
+export const DirectorySelectModal: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [path, setPath] = useState("");
+ const [error, setError] = useState("");
+ const resolveRef = useRef<((path: string | null) => void) | null>(null);
+
+ // Listen for directory selection requests
+ useEffect(() => {
+ const handleDirectorySelectRequest = (e: Event) => {
+ const customEvent = e as CustomEvent<{
+ resolve: (path: string | null) => void;
+ }>;
+
+ resolveRef.current = customEvent.detail.resolve;
+ setPath("");
+ setError("");
+ setIsOpen(true);
+ };
+
+ window.addEventListener("directory-select-request", handleDirectorySelectRequest);
+ return () => {
+ window.removeEventListener("directory-select-request", handleDirectorySelectRequest);
+ };
+ }, []);
+
+ const handleCancel = useCallback(() => {
+ if (resolveRef.current) {
+ resolveRef.current(null);
+ resolveRef.current = null;
+ }
+ setIsOpen(false);
+ }, []);
+
+ const handleSelect = useCallback(() => {
+ const trimmedPath = path.trim();
+ if (!trimmedPath) {
+ setError("Please enter a directory path");
+ return;
+ }
+
+ if (resolveRef.current) {
+ resolveRef.current(trimmedPath);
+ resolveRef.current = null;
+ }
+ setIsOpen(false);
+ }, [path]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleSelect();
+ }
+ },
+ [handleSelect]
+ );
+
+ return (
+
+ {
+ setPath(e.target.value);
+ setError("");
+ }}
+ onKeyDown={handleKeyDown}
+ placeholder="/home/user/projects/my-project"
+ autoFocus
+ />
+ {error && {error}}
+
+ Cancel
+ Select
+
+
+ );
+};
diff --git a/src/main-desktop.ts b/src/main-desktop.ts
new file mode 100644
index 000000000..58ed794dc
--- /dev/null
+++ b/src/main-desktop.ts
@@ -0,0 +1,540 @@
+// Enable source map support for better error stack traces in production
+import "source-map-support/register";
+import "disposablestack/auto";
+
+import type { MenuItemConstructorOptions } from "electron";
+import {
+ app,
+ BrowserWindow,
+ ipcMain as electronIpcMain,
+ Menu,
+ shell,
+ dialog,
+ screen,
+} from "electron";
+import * as fs from "fs";
+import * as path from "path";
+import type { Config } from "./config";
+import type { IpcMain } from "./services/ipcMain";
+import { VERSION } from "./version";
+import type { loadTokenizerModules } from "./utils/main/tokenizer";
+import { IPC_CHANNELS } from "./constants/ipc-constants";
+import { log } from "./services/log";
+import { parseDebugUpdater } from "./utils/env";
+
+// React DevTools for development profiling
+// Using require() instead of import since it's dev-only and conditionally loaded
+interface Extension {
+ name: string;
+ id: string;
+}
+
+type ExtensionInstaller = (
+ ext: { id: string },
+ options?: { loadExtensionOptions?: { allowFileAccess?: boolean } }
+) => Promise;
+
+let installExtension: ExtensionInstaller | null = null;
+let REACT_DEVELOPER_TOOLS: { id: string } | null = null;
+
+if (!app.isPackaged) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const devtools = require("electron-devtools-installer") as {
+ default: ExtensionInstaller;
+ REACT_DEVELOPER_TOOLS: { id: string };
+ };
+ installExtension = devtools.default;
+ REACT_DEVELOPER_TOOLS = devtools.REACT_DEVELOPER_TOOLS;
+ } catch (error) {
+ console.log("React DevTools not available:", error);
+ }
+}
+
+// IMPORTANT: Lazy-load heavy dependencies to maintain fast startup time
+//
+// To keep startup time under 4s, avoid importing AI SDK packages at the top level.
+// These files MUST use dynamic import():
+// - main.ts, config.ts, preload.ts (startup-critical)
+//
+// ✅ GOOD: const { createAnthropic } = await import("@ai-sdk/anthropic");
+// ❌ BAD: import { createAnthropic } from "@ai-sdk/anthropic";
+//
+// Enforcement: scripts/check_eager_imports.sh validates this in CI
+//
+// Lazy-load Config and IpcMain to avoid loading heavy AI SDK dependencies at startup
+// These will be loaded on-demand when createWindow() is called
+let config: Config | null = null;
+let ipcMain: IpcMain | null = null;
+let loadTokenizerModulesFn: typeof loadTokenizerModules | null = null;
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+let updaterService: typeof import("./services/updater").UpdaterService.prototype | null = null;
+const isE2ETest = process.env.CMUX_E2E === "1";
+const forceDistLoad = process.env.CMUX_E2E_LOAD_DIST === "1";
+
+if (isE2ETest) {
+ // For e2e tests, use a test-specific userData directory
+ // Note: We can't use config.rootDir here because config isn't loaded yet
+ // However, we must respect CMUX_TEST_ROOT to maintain test isolation
+ const testRoot = process.env.CMUX_TEST_ROOT ?? path.join(process.env.HOME ?? "~", ".cmux");
+ const e2eUserData = path.join(testRoot, "user-data");
+ try {
+ fs.mkdirSync(e2eUserData, { recursive: true });
+ app.setPath("userData", e2eUserData);
+ console.log("Using test userData directory:", e2eUserData);
+ } catch (error) {
+ console.warn("Failed to prepare test userData directory:", error);
+ }
+}
+
+const devServerPort = process.env.CMUX_DEVSERVER_PORT ?? "5173";
+
+console.log(
+ `Cmux starting - version: ${(VERSION as { git?: string; buildTime?: string }).git ?? "(dev)"} (built: ${(VERSION as { git?: string; buildTime?: string }).buildTime ?? "dev-mode"})`
+);
+console.log("Main process starting...");
+
+// Debug: abort immediately if CMUX_DEBUG_START_TIME is set
+// This is used to measure baseline startup time without full initialization
+if (process.env.CMUX_DEBUG_START_TIME === "1") {
+ console.log("CMUX_DEBUG_START_TIME is set - aborting immediately");
+ process.exit(0);
+}
+
+// Global error handlers for better error reporting
+process.on("uncaughtException", (error) => {
+ console.error("Uncaught Exception:", error);
+ console.error("Stack:", error.stack);
+
+ // Show error dialog in production
+ if (app.isPackaged) {
+ dialog.showErrorBox(
+ "Application Error",
+ `An unexpected error occurred:\n\n${error.message}\n\nStack trace:\n${error.stack ?? "No stack trace available"}`
+ );
+ }
+});
+
+process.on("unhandledRejection", (reason, promise) => {
+ console.error("Unhandled Rejection at:", promise);
+ console.error("Reason:", reason);
+
+ if (app.isPackaged) {
+ const message = reason instanceof Error ? reason.message : String(reason);
+ const stack = reason instanceof Error ? reason.stack : undefined;
+ dialog.showErrorBox(
+ "Unhandled Promise Rejection",
+ `An unhandled promise rejection occurred:\n\n${message}\n\nStack trace:\n${stack ?? "No stack trace available"}`
+ );
+ }
+});
+
+// Single instance lock
+const gotTheLock = app.requestSingleInstanceLock();
+console.log("Single instance lock acquired:", gotTheLock);
+
+if (!gotTheLock) {
+ // Another instance is already running, quit this one
+ console.log("Another instance is already running, quitting...");
+ app.quit();
+} else {
+ // This is the primary instance
+ console.log("This is the primary instance");
+ app.on("second-instance", () => {
+ // Someone tried to run a second instance, focus our window instead
+ console.log("Second instance attempted to start");
+ if (mainWindow) {
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ mainWindow.focus();
+ }
+ });
+}
+
+let mainWindow: BrowserWindow | null = null;
+let splashWindow: BrowserWindow | null = null;
+
+/**
+ * Format timestamp as HH:MM:SS.mmm for readable logging
+ */
+function timestamp(): string {
+ const now = new Date();
+ const hours = String(now.getHours()).padStart(2, "0");
+ const minutes = String(now.getMinutes()).padStart(2, "0");
+ const seconds = String(now.getSeconds()).padStart(2, "0");
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
+ return `${hours}:${minutes}:${seconds}.${ms}`;
+}
+
+function createMenu() {
+ const template: MenuItemConstructorOptions[] = [
+ {
+ label: "Edit",
+ submenu: [
+ { role: "undo" },
+ { role: "redo" },
+ { type: "separator" },
+ { role: "cut" },
+ { role: "copy" },
+ { role: "paste" },
+ { role: "selectAll" },
+ ],
+ },
+ {
+ label: "View",
+ submenu: [
+ // Reload without Ctrl+R shortcut (reserved for Code Review refresh)
+ {
+ label: "Reload",
+ click: (_item, focusedWindow) => {
+ if (focusedWindow && "reload" in focusedWindow) {
+ (focusedWindow as BrowserWindow).reload();
+ }
+ },
+ },
+ { role: "forceReload" },
+ { role: "toggleDevTools" },
+ { type: "separator" },
+ { role: "resetZoom" },
+ { role: "zoomIn" },
+ { role: "zoomOut" },
+ { type: "separator" },
+ { role: "togglefullscreen" },
+ ],
+ },
+ {
+ label: "Window",
+ submenu: [{ role: "minimize" }, { role: "close" }],
+ },
+ ];
+
+ if (process.platform === "darwin") {
+ template.unshift({
+ label: app.getName(),
+ submenu: [
+ { role: "about" },
+ { type: "separator" },
+ { role: "services", submenu: [] },
+ { type: "separator" },
+ { role: "hide" },
+ { role: "hideOthers" },
+ { role: "unhide" },
+ { type: "separator" },
+ { role: "quit" },
+ ],
+ });
+ }
+
+ const menu = Menu.buildFromTemplate(template);
+ Menu.setApplicationMenu(menu);
+}
+
+/**
+ * Create and show splash screen - instant visual feedback (<100ms)
+ *
+ * Shows a lightweight native window with static HTML while services load.
+ * No IPC, no React, no heavy dependencies - just immediate user feedback.
+ */
+async function showSplashScreen() {
+ const startTime = Date.now();
+ console.log(`[${timestamp()}] Showing splash screen...`);
+
+ splashWindow = new BrowserWindow({
+ width: 400,
+ height: 300,
+ frame: false,
+ transparent: false,
+ backgroundColor: "#1f1f1f", // Match splash HTML background (hsl(0 0% 12%)) - prevents white flash
+ alwaysOnTop: true,
+ center: true,
+ resizable: false,
+ show: false, // Don't show until HTML is loaded
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ },
+ });
+
+ // Wait for splash HTML to load
+ await splashWindow.loadFile(path.join(__dirname, "splash.html"));
+
+ // Wait for the window to actually be shown and rendered before continuing
+ // This ensures the splash is visible before we block the event loop with heavy work
+ await new Promise((resolve) => {
+ splashWindow!.once("show", () => {
+ const loadTime = Date.now() - startTime;
+ console.log(`[${timestamp()}] Splash screen shown (${loadTime}ms)`);
+ // Give one more event loop tick for the window to actually paint
+ setImmediate(resolve);
+ });
+ splashWindow!.show();
+ });
+
+ splashWindow.on("closed", () => {
+ console.log(`[${timestamp()}] Splash screen closed event`);
+ splashWindow = null;
+ });
+}
+
+/**
+ * Close splash screen
+ */
+function closeSplashScreen() {
+ if (splashWindow) {
+ console.log(`[${timestamp()}] Closing splash screen...`);
+ splashWindow.close();
+ splashWindow = null;
+ }
+}
+
+/**
+ * Load backend services (Config, IpcMain, AI SDK, tokenizer)
+ *
+ * Heavy initialization (~100ms) happens here while splash is visible.
+ * Note: Spinner may freeze briefly during this phase. This is acceptable since
+ * the splash still provides visual feedback that the app is loading.
+ */
+async function loadServices(): Promise {
+ if (config && ipcMain && loadTokenizerModulesFn) return; // Already loaded
+
+ const startTime = Date.now();
+ console.log(`[${timestamp()}] Loading services...`);
+
+ /* eslint-disable no-restricted-syntax */
+ // Dynamic imports are justified here for performance:
+ // - IpcMain transitively imports the entire AI SDK (ai, @ai-sdk/anthropic, etc.)
+ // - These are large modules (~100ms load time) that would block splash from appearing
+ // - Loading happens once, then cached
+ const [
+ { Config: ConfigClass },
+ { IpcMain: IpcMainClass },
+ { loadTokenizerModules: loadTokenizerFn },
+ { UpdaterService: UpdaterServiceClass },
+ ] = await Promise.all([
+ import("./config"),
+ import("./services/ipcMain"),
+ import("./utils/main/tokenizer"),
+ import("./services/updater"),
+ ]);
+ /* eslint-enable no-restricted-syntax */
+ config = new ConfigClass();
+ ipcMain = new IpcMainClass(config);
+ loadTokenizerModulesFn = loadTokenizerFn;
+
+ // Initialize updater service in packaged builds or when DEBUG_UPDATER is set
+ const debugConfig = parseDebugUpdater(process.env.DEBUG_UPDATER);
+
+ if (app.isPackaged || debugConfig.enabled) {
+ updaterService = new UpdaterServiceClass();
+ const debugInfo = debugConfig.fakeVersion
+ ? `debug with fake version ${debugConfig.fakeVersion}`
+ : `debug enabled`;
+ console.log(
+ `[${timestamp()}] Updater service initialized (packaged: ${app.isPackaged}, ${debugConfig.enabled ? debugInfo : ""})`
+ );
+ } else {
+ console.log(
+ `[${timestamp()}] Updater service disabled in dev mode (set DEBUG_UPDATER=1 or DEBUG_UPDATER= to enable)`
+ );
+ }
+
+ const loadTime = Date.now() - startTime;
+ console.log(`[${timestamp()}] Services loaded in ${loadTime}ms`);
+}
+
+function createWindow() {
+ if (!ipcMain) {
+ throw new Error("Services must be loaded before creating window");
+ }
+
+ // Calculate window size based on screen dimensions (80% of available space)
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width: screenWidth, height: screenHeight } = primaryDisplay.workArea;
+
+ const windowWidth = Math.max(1200, Math.floor(screenWidth * 0.8));
+ const windowHeight = Math.max(800, Math.floor(screenHeight * 0.8));
+
+ mainWindow = new BrowserWindow({
+ width: windowWidth,
+ height: windowHeight,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, "preload.js"),
+ },
+ title: "cmux - coder multiplexer",
+ // Hide menu bar on Linux by default (like VS Code)
+ // User can press Alt to toggle it
+ autoHideMenuBar: process.platform === "linux",
+ show: false, // Don't show until ready-to-show event
+ });
+
+ // Register IPC handlers with the main window
+ ipcMain.register(electronIpcMain, mainWindow);
+
+ // Register updater IPC handlers (available in both dev and prod)
+ electronIpcMain.handle(IPC_CHANNELS.UPDATE_CHECK, () => {
+ // Note: log interface already includes timestamp and file location
+ log.debug(`UPDATE_CHECK called (updaterService: ${updaterService ? "available" : "null"})`);
+ if (!updaterService) {
+ // Send "idle" status if updater not initialized (dev mode without DEBUG_UPDATER)
+ if (mainWindow) {
+ mainWindow.webContents.send(IPC_CHANNELS.UPDATE_STATUS, {
+ type: "idle" as const,
+ });
+ }
+ return;
+ }
+ log.debug("Calling updaterService.checkForUpdates()");
+ updaterService.checkForUpdates();
+ });
+
+ electronIpcMain.handle(IPC_CHANNELS.UPDATE_DOWNLOAD, async () => {
+ if (!updaterService) throw new Error("Updater not available in development");
+ await updaterService.downloadUpdate();
+ });
+
+ electronIpcMain.handle(IPC_CHANNELS.UPDATE_INSTALL, () => {
+ if (!updaterService) throw new Error("Updater not available in development");
+ updaterService.installUpdate();
+ });
+
+ // Handle status subscription requests
+ // Note: React StrictMode in dev causes components to mount twice, resulting in duplicate calls
+ electronIpcMain.on(IPC_CHANNELS.UPDATE_STATUS_SUBSCRIBE, () => {
+ log.debug("UPDATE_STATUS_SUBSCRIBE called");
+ if (!mainWindow) return;
+ const status = updaterService ? updaterService.getStatus() : { type: "idle" };
+ log.debug("Sending current status to renderer:", status);
+ mainWindow.webContents.send(IPC_CHANNELS.UPDATE_STATUS, status);
+ });
+
+ // Set up updater service with the main window (only in production)
+ if (updaterService) {
+ updaterService.setMainWindow(mainWindow);
+ // Note: Checks are initiated by frontend to respect telemetry preference
+ }
+
+ // Show window once it's ready and close splash
+ mainWindow.once("ready-to-show", () => {
+ console.log(`[${timestamp()}] Main window ready to show`);
+ mainWindow?.show();
+ closeSplashScreen();
+ });
+
+ // Open all external links in default browser
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ void shell.openExternal(url);
+ return { action: "deny" };
+ });
+
+ mainWindow.webContents.on("will-navigate", (event, url) => {
+ const currentOrigin = new URL(mainWindow!.webContents.getURL()).origin;
+ const targetOrigin = new URL(url).origin;
+ // Prevent navigation away from app origin, open externally instead
+ if (targetOrigin !== currentOrigin) {
+ event.preventDefault();
+ void shell.openExternal(url);
+ }
+ });
+
+ // Load from dev server in development, built files in production
+ // app.isPackaged is true when running from a built .app/.exe, false in development
+ if ((isE2ETest && !forceDistLoad) || (!app.isPackaged && !forceDistLoad)) {
+ // Development mode: load from vite dev server
+ const devHost = process.env.CMUX_DEVSERVER_HOST ?? "127.0.0.1";
+ void mainWindow.loadURL(`http://${devHost}:${devServerPort}`);
+ if (!isE2ETest) {
+ mainWindow.webContents.once("did-finish-load", () => {
+ mainWindow?.webContents.openDevTools();
+ });
+ }
+ } else {
+ // Production mode: load built files
+ void mainWindow.loadFile(path.join(__dirname, "index.html"));
+ }
+
+ mainWindow.on("closed", () => {
+ mainWindow = null;
+ });
+}
+
+// Only setup app handlers if we got the lock
+if (gotTheLock) {
+ void app.whenReady().then(async () => {
+ try {
+ console.log("App ready, creating window...");
+
+ // Install React DevTools in development
+ if (!app.isPackaged && installExtension && REACT_DEVELOPER_TOOLS) {
+ try {
+ const extension = await installExtension(REACT_DEVELOPER_TOOLS, {
+ loadExtensionOptions: { allowFileAccess: true },
+ });
+ console.log(`✅ React DevTools installed: ${extension.name} (id: ${extension.id})`);
+ } catch (err) {
+ console.log("❌ Error installing React DevTools:", err);
+ }
+ }
+
+ createMenu();
+
+ // Three-phase startup:
+ // 1. Show splash immediately (<100ms) and wait for it to load
+ // 2. Load services while splash visible (fast - ~100ms)
+ // 3. Create window and start loading content (splash stays visible)
+ // 4. When window ready-to-show: close splash, show main window
+ //
+ // Skip splash in E2E tests to avoid app.firstWindow() grabbing the wrong window
+ if (!isE2ETest) {
+ await showSplashScreen(); // Wait for splash to actually load
+ }
+ await loadServices();
+ createWindow();
+ // Note: splash closes in ready-to-show event handler
+
+ // Start loading tokenizer modules in background after window is created
+ // This ensures accurate token counts for first API calls (especially in e2e tests)
+ // Loading happens asynchronously and won't block the UI
+ if (loadTokenizerModulesFn) {
+ void loadTokenizerModulesFn().then(() => {
+ console.log(`[${timestamp()}] Tokenizer modules loaded`);
+ });
+ }
+ // No need to auto-start workspaces anymore - they start on demand
+ } catch (error) {
+ console.error(`[${timestamp()}] Startup failed:`, error);
+
+ closeSplashScreen();
+
+ // Show error dialog to user
+ const errorMessage =
+ error instanceof Error ? `${error.message}\n\n${error.stack ?? ""}` : String(error);
+
+ dialog.showErrorBox(
+ "Startup Failed",
+ `The application failed to start:\n\n${errorMessage}\n\nPlease check the console for details.`
+ );
+
+ // Quit after showing error
+ app.quit();
+ }
+ });
+
+ app.on("window-all-closed", () => {
+ if (process.platform !== "darwin") {
+ app.quit();
+ }
+ });
+
+ app.on("activate", () => {
+ // Only create window if app is ready and no window exists
+ // This prevents "Cannot create BrowserWindow before app is ready" error
+ if (app.isReady() && mainWindow === null) {
+ void (async () => {
+ await showSplashScreen();
+ await loadServices();
+ createWindow();
+ })();
+ }
+ });
+}
diff --git a/src/main-server.ts b/src/main-server.ts
new file mode 100644
index 000000000..75bfe86a4
--- /dev/null
+++ b/src/main-server.ts
@@ -0,0 +1,237 @@
+/**
+ * HTTP/WebSocket Server for cmux
+ * Allows accessing cmux backend from mobile devices
+ */
+import { Config } from "./config";
+import { IPC_CHANNELS } from "@/constants/ipc-constants";
+import { IpcMain } from "./services/ipcMain";
+import cors from "cors";
+import type { BrowserWindow, IpcMain as ElectronIpcMain } from "electron";
+import express from "express";
+import * as http from "http";
+import * as path from "path";
+import type { RawData } from "ws";
+import { WebSocket, WebSocketServer } from "ws";
+
+// Mock Electron's ipcMain for HTTP
+class HttpIpcMainAdapter {
+ private handlers = new Map Promise>();
+ private listeners = new Map void>>();
+
+ constructor(private readonly app: express.Application) {}
+
+ handle(channel: string, handler: (event: unknown, ...args: unknown[]) => Promise): void {
+ this.handlers.set(channel, handler);
+
+ // Create HTTP endpoint for this handler
+ this.app.post(`/ipc/${encodeURIComponent(channel)}`, async (req, res) => {
+ try {
+ const body = req.body as { args?: unknown[] };
+ const args: unknown[] = body.args ?? [];
+ const result = await handler(null, ...args);
+
+ // If handler returns an error result object, unwrap it and send as error response
+ // This ensures webApi.ts will throw with the proper error message
+ if (
+ result &&
+ typeof result === "object" &&
+ "success" in result &&
+ result.success === false
+ ) {
+ const errorMessage =
+ "error" in result && typeof result.error === "string" ? result.error : "Unknown error";
+ // Return 200 with error structure so webApi can throw with the detailed message
+ res.json({ success: false, error: errorMessage });
+ return;
+ }
+
+ res.json({ success: true, data: result });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`Error in handler ${channel}:`, error);
+ res.json({ success: false, error: message });
+ }
+ });
+ }
+
+ on(channel: string, handler: (event: unknown, ...args: unknown[]) => void): void {
+ if (!this.listeners.has(channel)) {
+ this.listeners.set(channel, []);
+ }
+ this.listeners.get(channel)!.push(handler);
+ }
+
+ send(channel: string, ...args: unknown[]): void {
+ const handlers = this.listeners.get(channel);
+ if (handlers) {
+ handlers.forEach((handler) => handler(null, ...args));
+ }
+ }
+}
+
+type Clients = Map; metadataSubscription: boolean }>;
+
+// Mock BrowserWindow for events
+class MockBrowserWindow {
+ constructor(private readonly clients: Clients) {}
+
+ webContents = {
+ send: (channel: string, ...args: unknown[]) => {
+ // Broadcast to all WebSocket clients
+ const message = JSON.stringify({ channel, args });
+ this.clients.forEach((clientInfo, client) => {
+ if (client.readyState !== WebSocket.OPEN) {
+ return;
+ }
+ // Only send to clients subscribed to this channel
+ if (channel === IPC_CHANNELS.WORKSPACE_METADATA && clientInfo.metadataSubscription) {
+ client.send(message);
+ } else if (channel.startsWith(IPC_CHANNELS.WORKSPACE_CHAT_PREFIX)) {
+ // Extract workspace ID from channel
+ const workspaceId = channel.replace(IPC_CHANNELS.WORKSPACE_CHAT_PREFIX, "");
+ if (clientInfo.chatSubscriptions.has(workspaceId)) {
+ client.send(message);
+ }
+ } else {
+ // Send other channels to all clients
+ client.send(message);
+ }
+ });
+ },
+ };
+}
+
+const app = express();
+
+// Enable CORS for all routes
+app.use(cors());
+app.use(express.json({ limit: "50mb" }));
+
+// Initialize config and IPC service
+const config = new Config();
+const ipcMainService = new IpcMain(config);
+
+// Track WebSocket clients and their subscriptions
+const clients: Clients = new Map();
+
+const mockWindow = new MockBrowserWindow(clients);
+const httpIpcMain = new HttpIpcMainAdapter(app);
+
+// Register IPC handlers
+ipcMainService.register(
+ httpIpcMain as unknown as ElectronIpcMain,
+ mockWindow as unknown as BrowserWindow
+);
+
+// Serve static files from dist directory (built renderer)
+app.use(express.static(path.join(__dirname, ".")));
+
+// Health check endpoint
+app.get("/health", (req, res) => {
+ res.json({ status: "ok" });
+});
+
+// Fallback to index.html for SPA routes (use middleware instead of deprecated wildcard)
+app.use((req, res, next) => {
+ if (!req.path.startsWith("/ipc") && !req.path.startsWith("/ws")) {
+ res.sendFile(path.join(__dirname, "."));
+ } else {
+ next();
+ }
+});
+
+// Create HTTP server
+const server = http.createServer(app);
+
+// Create WebSocket server
+const wss = new WebSocketServer({ server, path: "/ws" });
+
+wss.on("connection", (ws) => {
+ console.log("Client connected");
+
+ // Initialize client tracking
+ clients.set(ws, {
+ chatSubscriptions: new Set(),
+ metadataSubscription: false,
+ });
+
+ ws.on("message", (rawData: RawData) => {
+ try {
+ // WebSocket data can be Buffer, ArrayBuffer, or string - convert to string
+ let dataStr: string;
+ if (typeof rawData === "string") {
+ dataStr = rawData;
+ } else if (Buffer.isBuffer(rawData)) {
+ dataStr = rawData.toString("utf-8");
+ } else if (rawData instanceof ArrayBuffer) {
+ dataStr = Buffer.from(rawData).toString("utf-8");
+ } else {
+ // Array of Buffers
+ dataStr = Buffer.concat(rawData as Buffer[]).toString("utf-8");
+ }
+ const message = JSON.parse(dataStr) as {
+ type: string;
+ channel: string;
+ workspaceId?: string;
+ };
+ const { type, channel, workspaceId } = message;
+
+ const clientInfo = clients.get(ws);
+ if (!clientInfo) return;
+
+ if (type === "subscribe") {
+ if (channel === "workspace:chat" && workspaceId) {
+ console.log(`[WS] Client subscribed to workspace chat: ${workspaceId}`);
+ clientInfo.chatSubscriptions.add(workspaceId);
+ console.log(
+ `[WS] Subscription added. Current subscriptions:`,
+ Array.from(clientInfo.chatSubscriptions)
+ );
+
+ // Send subscription acknowledgment through IPC system
+ console.log(`[WS] Triggering workspace:chat:subscribe handler for ${workspaceId}`);
+ httpIpcMain.send("workspace:chat:subscribe", workspaceId);
+ } else if (channel === "workspace:metadata") {
+ console.log("[WS] Client subscribed to workspace metadata");
+ clientInfo.metadataSubscription = true;
+
+ // Send subscription acknowledgment
+ httpIpcMain.send("workspace:metadata:subscribe");
+ }
+ } else if (type === "unsubscribe") {
+ if (channel === "workspace:chat" && workspaceId) {
+ console.log(`Client unsubscribed from workspace chat: ${workspaceId}`);
+ clientInfo.chatSubscriptions.delete(workspaceId);
+
+ // Send unsubscription acknowledgment
+ httpIpcMain.send("workspace:chat:unsubscribe", workspaceId);
+ } else if (channel === "workspace:metadata") {
+ console.log("Client unsubscribed from workspace metadata");
+ clientInfo.metadataSubscription = false;
+
+ // Send unsubscription acknowledgment
+ httpIpcMain.send("workspace:metadata:unsubscribe");
+ }
+ } else if (type === "invoke") {
+ // Handle direct IPC invocations over WebSocket (for streaming responses)
+ // This is not currently used but could be useful for future enhancements
+ console.log(`WebSocket invoke: ${channel}`);
+ }
+ } catch (error) {
+ console.error("Error handling WebSocket message:", error);
+ }
+ });
+
+ ws.on("close", () => {
+ console.log("Client disconnected");
+ clients.delete(ws);
+ });
+
+ ws.on("error", (error) => {
+ console.error("WebSocket error:", error);
+ });
+});
+
+server.listen(3000, () => {
+ console.log("Server is running on port 3000");
+});
diff --git a/src/main.ts b/src/main.ts
index 58ed794dc..603e0dced 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,540 +1,13 @@
-// Enable source map support for better error stack traces in production
-import "source-map-support/register";
-import "disposablestack/auto";
-
-import type { MenuItemConstructorOptions } from "electron";
-import {
- app,
- BrowserWindow,
- ipcMain as electronIpcMain,
- Menu,
- shell,
- dialog,
- screen,
-} from "electron";
-import * as fs from "fs";
-import * as path from "path";
-import type { Config } from "./config";
-import type { IpcMain } from "./services/ipcMain";
-import { VERSION } from "./version";
-import type { loadTokenizerModules } from "./utils/main/tokenizer";
-import { IPC_CHANNELS } from "./constants/ipc-constants";
-import { log } from "./services/log";
-import { parseDebugUpdater } from "./utils/env";
-
-// React DevTools for development profiling
-// Using require() instead of import since it's dev-only and conditionally loaded
-interface Extension {
- name: string;
- id: string;
-}
-
-type ExtensionInstaller = (
- ext: { id: string },
- options?: { loadExtensionOptions?: { allowFileAccess?: boolean } }
-) => Promise;
-
-let installExtension: ExtensionInstaller | null = null;
-let REACT_DEVELOPER_TOOLS: { id: string } | null = null;
-
-if (!app.isPackaged) {
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const devtools = require("electron-devtools-installer") as {
- default: ExtensionInstaller;
- REACT_DEVELOPER_TOOLS: { id: string };
- };
- installExtension = devtools.default;
- REACT_DEVELOPER_TOOLS = devtools.REACT_DEVELOPER_TOOLS;
- } catch (error) {
- console.log("React DevTools not available:", error);
- }
-}
-
-// IMPORTANT: Lazy-load heavy dependencies to maintain fast startup time
-//
-// To keep startup time under 4s, avoid importing AI SDK packages at the top level.
-// These files MUST use dynamic import():
-// - main.ts, config.ts, preload.ts (startup-critical)
-//
-// ✅ GOOD: const { createAnthropic } = await import("@ai-sdk/anthropic");
-// ❌ BAD: import { createAnthropic } from "@ai-sdk/anthropic";
-//
-// Enforcement: scripts/check_eager_imports.sh validates this in CI
-//
-// Lazy-load Config and IpcMain to avoid loading heavy AI SDK dependencies at startup
-// These will be loaded on-demand when createWindow() is called
-let config: Config | null = null;
-let ipcMain: IpcMain | null = null;
-let loadTokenizerModulesFn: typeof loadTokenizerModules | null = null;
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
-let updaterService: typeof import("./services/updater").UpdaterService.prototype | null = null;
-const isE2ETest = process.env.CMUX_E2E === "1";
-const forceDistLoad = process.env.CMUX_E2E_LOAD_DIST === "1";
-
-if (isE2ETest) {
- // For e2e tests, use a test-specific userData directory
- // Note: We can't use config.rootDir here because config isn't loaded yet
- // However, we must respect CMUX_TEST_ROOT to maintain test isolation
- const testRoot = process.env.CMUX_TEST_ROOT ?? path.join(process.env.HOME ?? "~", ".cmux");
- const e2eUserData = path.join(testRoot, "user-data");
- try {
- fs.mkdirSync(e2eUserData, { recursive: true });
- app.setPath("userData", e2eUserData);
- console.log("Using test userData directory:", e2eUserData);
- } catch (error) {
- console.warn("Failed to prepare test userData directory:", error);
- }
-}
-
-const devServerPort = process.env.CMUX_DEVSERVER_PORT ?? "5173";
-
-console.log(
- `Cmux starting - version: ${(VERSION as { git?: string; buildTime?: string }).git ?? "(dev)"} (built: ${(VERSION as { git?: string; buildTime?: string }).buildTime ?? "dev-mode"})`
-);
-console.log("Main process starting...");
-
-// Debug: abort immediately if CMUX_DEBUG_START_TIME is set
-// This is used to measure baseline startup time without full initialization
-if (process.env.CMUX_DEBUG_START_TIME === "1") {
- console.log("CMUX_DEBUG_START_TIME is set - aborting immediately");
- process.exit(0);
-}
-
-// Global error handlers for better error reporting
-process.on("uncaughtException", (error) => {
- console.error("Uncaught Exception:", error);
- console.error("Stack:", error.stack);
-
- // Show error dialog in production
- if (app.isPackaged) {
- dialog.showErrorBox(
- "Application Error",
- `An unexpected error occurred:\n\n${error.message}\n\nStack trace:\n${error.stack ?? "No stack trace available"}`
- );
- }
-});
-
-process.on("unhandledRejection", (reason, promise) => {
- console.error("Unhandled Rejection at:", promise);
- console.error("Reason:", reason);
-
- if (app.isPackaged) {
- const message = reason instanceof Error ? reason.message : String(reason);
- const stack = reason instanceof Error ? reason.stack : undefined;
- dialog.showErrorBox(
- "Unhandled Promise Rejection",
- `An unhandled promise rejection occurred:\n\n${message}\n\nStack trace:\n${stack ?? "No stack trace available"}`
- );
- }
-});
-
-// Single instance lock
-const gotTheLock = app.requestSingleInstanceLock();
-console.log("Single instance lock acquired:", gotTheLock);
-
-if (!gotTheLock) {
- // Another instance is already running, quit this one
- console.log("Another instance is already running, quitting...");
- app.quit();
-} else {
- // This is the primary instance
- console.log("This is the primary instance");
- app.on("second-instance", () => {
- // Someone tried to run a second instance, focus our window instead
- console.log("Second instance attempted to start");
- if (mainWindow) {
- if (mainWindow.isMinimized()) mainWindow.restore();
- mainWindow.focus();
- }
- });
-}
-
-let mainWindow: BrowserWindow | null = null;
-let splashWindow: BrowserWindow | null = null;
-
-/**
- * Format timestamp as HH:MM:SS.mmm for readable logging
- */
-function timestamp(): string {
- const now = new Date();
- const hours = String(now.getHours()).padStart(2, "0");
- const minutes = String(now.getMinutes()).padStart(2, "0");
- const seconds = String(now.getSeconds()).padStart(2, "0");
- const ms = String(now.getMilliseconds()).padStart(3, "0");
- return `${hours}:${minutes}:${seconds}.${ms}`;
-}
-
-function createMenu() {
- const template: MenuItemConstructorOptions[] = [
- {
- label: "Edit",
- submenu: [
- { role: "undo" },
- { role: "redo" },
- { type: "separator" },
- { role: "cut" },
- { role: "copy" },
- { role: "paste" },
- { role: "selectAll" },
- ],
- },
- {
- label: "View",
- submenu: [
- // Reload without Ctrl+R shortcut (reserved for Code Review refresh)
- {
- label: "Reload",
- click: (_item, focusedWindow) => {
- if (focusedWindow && "reload" in focusedWindow) {
- (focusedWindow as BrowserWindow).reload();
- }
- },
- },
- { role: "forceReload" },
- { role: "toggleDevTools" },
- { type: "separator" },
- { role: "resetZoom" },
- { role: "zoomIn" },
- { role: "zoomOut" },
- { type: "separator" },
- { role: "togglefullscreen" },
- ],
- },
- {
- label: "Window",
- submenu: [{ role: "minimize" }, { role: "close" }],
- },
- ];
-
- if (process.platform === "darwin") {
- template.unshift({
- label: app.getName(),
- submenu: [
- { role: "about" },
- { type: "separator" },
- { role: "services", submenu: [] },
- { type: "separator" },
- { role: "hide" },
- { role: "hideOthers" },
- { role: "unhide" },
- { type: "separator" },
- { role: "quit" },
- ],
- });
- }
-
- const menu = Menu.buildFromTemplate(template);
- Menu.setApplicationMenu(menu);
-}
-
-/**
- * Create and show splash screen - instant visual feedback (<100ms)
- *
- * Shows a lightweight native window with static HTML while services load.
- * No IPC, no React, no heavy dependencies - just immediate user feedback.
- */
-async function showSplashScreen() {
- const startTime = Date.now();
- console.log(`[${timestamp()}] Showing splash screen...`);
-
- splashWindow = new BrowserWindow({
- width: 400,
- height: 300,
- frame: false,
- transparent: false,
- backgroundColor: "#1f1f1f", // Match splash HTML background (hsl(0 0% 12%)) - prevents white flash
- alwaysOnTop: true,
- center: true,
- resizable: false,
- show: false, // Don't show until HTML is loaded
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- },
- });
-
- // Wait for splash HTML to load
- await splashWindow.loadFile(path.join(__dirname, "splash.html"));
-
- // Wait for the window to actually be shown and rendered before continuing
- // This ensures the splash is visible before we block the event loop with heavy work
- await new Promise((resolve) => {
- splashWindow!.once("show", () => {
- const loadTime = Date.now() - startTime;
- console.log(`[${timestamp()}] Splash screen shown (${loadTime}ms)`);
- // Give one more event loop tick for the window to actually paint
- setImmediate(resolve);
- });
- splashWindow!.show();
- });
-
- splashWindow.on("closed", () => {
- console.log(`[${timestamp()}] Splash screen closed event`);
- splashWindow = null;
- });
-}
-
-/**
- * Close splash screen
- */
-function closeSplashScreen() {
- if (splashWindow) {
- console.log(`[${timestamp()}] Closing splash screen...`);
- splashWindow.close();
- splashWindow = null;
- }
-}
-
/**
- * Load backend services (Config, IpcMain, AI SDK, tokenizer)
- *
- * Heavy initialization (~100ms) happens here while splash is visible.
- * Note: Spinner may freeze briefly during this phase. This is acceptable since
- * the splash still provides visual feedback that the app is loading.
+ * The main CLI entrypoint for cmux.
*/
-async function loadServices(): Promise {
- if (config && ipcMain && loadTokenizerModulesFn) return; // Already loaded
-
- const startTime = Date.now();
- console.log(`[${timestamp()}] Loading services...`);
-
- /* eslint-disable no-restricted-syntax */
- // Dynamic imports are justified here for performance:
- // - IpcMain transitively imports the entire AI SDK (ai, @ai-sdk/anthropic, etc.)
- // - These are large modules (~100ms load time) that would block splash from appearing
- // - Loading happens once, then cached
- const [
- { Config: ConfigClass },
- { IpcMain: IpcMainClass },
- { loadTokenizerModules: loadTokenizerFn },
- { UpdaterService: UpdaterServiceClass },
- ] = await Promise.all([
- import("./config"),
- import("./services/ipcMain"),
- import("./utils/main/tokenizer"),
- import("./services/updater"),
- ]);
- /* eslint-enable no-restricted-syntax */
- config = new ConfigClass();
- ipcMain = new IpcMainClass(config);
- loadTokenizerModulesFn = loadTokenizerFn;
-
- // Initialize updater service in packaged builds or when DEBUG_UPDATER is set
- const debugConfig = parseDebugUpdater(process.env.DEBUG_UPDATER);
-
- if (app.isPackaged || debugConfig.enabled) {
- updaterService = new UpdaterServiceClass();
- const debugInfo = debugConfig.fakeVersion
- ? `debug with fake version ${debugConfig.fakeVersion}`
- : `debug enabled`;
- console.log(
- `[${timestamp()}] Updater service initialized (packaged: ${app.isPackaged}, ${debugConfig.enabled ? debugInfo : ""})`
- );
- } else {
- console.log(
- `[${timestamp()}] Updater service disabled in dev mode (set DEBUG_UPDATER=1 or DEBUG_UPDATER= to enable)`
- );
- }
-
- const loadTime = Date.now() - startTime;
- console.log(`[${timestamp()}] Services loaded in ${loadTime}ms`);
-}
-
-function createWindow() {
- if (!ipcMain) {
- throw new Error("Services must be loaded before creating window");
- }
-
- // Calculate window size based on screen dimensions (80% of available space)
- const primaryDisplay = screen.getPrimaryDisplay();
- const { width: screenWidth, height: screenHeight } = primaryDisplay.workArea;
-
- const windowWidth = Math.max(1200, Math.floor(screenWidth * 0.8));
- const windowHeight = Math.max(800, Math.floor(screenHeight * 0.8));
-
- mainWindow = new BrowserWindow({
- width: windowWidth,
- height: windowHeight,
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- preload: path.join(__dirname, "preload.js"),
- },
- title: "cmux - coder multiplexer",
- // Hide menu bar on Linux by default (like VS Code)
- // User can press Alt to toggle it
- autoHideMenuBar: process.platform === "linux",
- show: false, // Don't show until ready-to-show event
- });
-
- // Register IPC handlers with the main window
- ipcMain.register(electronIpcMain, mainWindow);
-
- // Register updater IPC handlers (available in both dev and prod)
- electronIpcMain.handle(IPC_CHANNELS.UPDATE_CHECK, () => {
- // Note: log interface already includes timestamp and file location
- log.debug(`UPDATE_CHECK called (updaterService: ${updaterService ? "available" : "null"})`);
- if (!updaterService) {
- // Send "idle" status if updater not initialized (dev mode without DEBUG_UPDATER)
- if (mainWindow) {
- mainWindow.webContents.send(IPC_CHANNELS.UPDATE_STATUS, {
- type: "idle" as const,
- });
- }
- return;
- }
- log.debug("Calling updaterService.checkForUpdates()");
- updaterService.checkForUpdates();
- });
-
- electronIpcMain.handle(IPC_CHANNELS.UPDATE_DOWNLOAD, async () => {
- if (!updaterService) throw new Error("Updater not available in development");
- await updaterService.downloadUpdate();
- });
- electronIpcMain.handle(IPC_CHANNELS.UPDATE_INSTALL, () => {
- if (!updaterService) throw new Error("Updater not available in development");
- updaterService.installUpdate();
- });
+const isServer = process.argv.length > 2 && process.argv[2] === "server";
- // Handle status subscription requests
- // Note: React StrictMode in dev causes components to mount twice, resulting in duplicate calls
- electronIpcMain.on(IPC_CHANNELS.UPDATE_STATUS_SUBSCRIBE, () => {
- log.debug("UPDATE_STATUS_SUBSCRIBE called");
- if (!mainWindow) return;
- const status = updaterService ? updaterService.getStatus() : { type: "idle" };
- log.debug("Sending current status to renderer:", status);
- mainWindow.webContents.send(IPC_CHANNELS.UPDATE_STATUS, status);
- });
-
- // Set up updater service with the main window (only in production)
- if (updaterService) {
- updaterService.setMainWindow(mainWindow);
- // Note: Checks are initiated by frontend to respect telemetry preference
- }
-
- // Show window once it's ready and close splash
- mainWindow.once("ready-to-show", () => {
- console.log(`[${timestamp()}] Main window ready to show`);
- mainWindow?.show();
- closeSplashScreen();
- });
-
- // Open all external links in default browser
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- void shell.openExternal(url);
- return { action: "deny" };
- });
-
- mainWindow.webContents.on("will-navigate", (event, url) => {
- const currentOrigin = new URL(mainWindow!.webContents.getURL()).origin;
- const targetOrigin = new URL(url).origin;
- // Prevent navigation away from app origin, open externally instead
- if (targetOrigin !== currentOrigin) {
- event.preventDefault();
- void shell.openExternal(url);
- }
- });
-
- // Load from dev server in development, built files in production
- // app.isPackaged is true when running from a built .app/.exe, false in development
- if ((isE2ETest && !forceDistLoad) || (!app.isPackaged && !forceDistLoad)) {
- // Development mode: load from vite dev server
- const devHost = process.env.CMUX_DEVSERVER_HOST ?? "127.0.0.1";
- void mainWindow.loadURL(`http://${devHost}:${devServerPort}`);
- if (!isE2ETest) {
- mainWindow.webContents.once("did-finish-load", () => {
- mainWindow?.webContents.openDevTools();
- });
- }
- } else {
- // Production mode: load built files
- void mainWindow.loadFile(path.join(__dirname, "index.html"));
- }
-
- mainWindow.on("closed", () => {
- mainWindow = null;
- });
-}
-
-// Only setup app handlers if we got the lock
-if (gotTheLock) {
- void app.whenReady().then(async () => {
- try {
- console.log("App ready, creating window...");
-
- // Install React DevTools in development
- if (!app.isPackaged && installExtension && REACT_DEVELOPER_TOOLS) {
- try {
- const extension = await installExtension(REACT_DEVELOPER_TOOLS, {
- loadExtensionOptions: { allowFileAccess: true },
- });
- console.log(`✅ React DevTools installed: ${extension.name} (id: ${extension.id})`);
- } catch (err) {
- console.log("❌ Error installing React DevTools:", err);
- }
- }
-
- createMenu();
-
- // Three-phase startup:
- // 1. Show splash immediately (<100ms) and wait for it to load
- // 2. Load services while splash visible (fast - ~100ms)
- // 3. Create window and start loading content (splash stays visible)
- // 4. When window ready-to-show: close splash, show main window
- //
- // Skip splash in E2E tests to avoid app.firstWindow() grabbing the wrong window
- if (!isE2ETest) {
- await showSplashScreen(); // Wait for splash to actually load
- }
- await loadServices();
- createWindow();
- // Note: splash closes in ready-to-show event handler
-
- // Start loading tokenizer modules in background after window is created
- // This ensures accurate token counts for first API calls (especially in e2e tests)
- // Loading happens asynchronously and won't block the UI
- if (loadTokenizerModulesFn) {
- void loadTokenizerModulesFn().then(() => {
- console.log(`[${timestamp()}] Tokenizer modules loaded`);
- });
- }
- // No need to auto-start workspaces anymore - they start on demand
- } catch (error) {
- console.error(`[${timestamp()}] Startup failed:`, error);
-
- closeSplashScreen();
-
- // Show error dialog to user
- const errorMessage =
- error instanceof Error ? `${error.message}\n\n${error.stack ?? ""}` : String(error);
-
- dialog.showErrorBox(
- "Startup Failed",
- `The application failed to start:\n\n${errorMessage}\n\nPlease check the console for details.`
- );
-
- // Quit after showing error
- app.quit();
- }
- });
-
- app.on("window-all-closed", () => {
- if (process.platform !== "darwin") {
- app.quit();
- }
- });
-
- app.on("activate", () => {
- // Only create window if app is ready and no window exists
- // This prevents "Cannot create BrowserWindow before app is ready" error
- if (app.isReady() && mainWindow === null) {
- void (async () => {
- await showSplashScreen();
- await loadServices();
- createWindow();
- })();
- }
- });
+if (isServer) {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ require("./main-server");
+} else {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ require("./main-desktop");
}
diff --git a/src/main.tsx b/src/main.tsx
index bdd70af2d..fba387313 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,6 +3,10 @@ import ReactDOM from "react-dom/client";
import App from "./App";
import { initTelemetry, trackAppStarted } from "./telemetry";
+// Shims the `window.api` object with the browser API.
+// This occurs if we are not running in Electron.
+import "./browser/api";
+
// Initialize telemetry on app startup
initTelemetry();
trackAppStarted();
diff --git a/tsconfig.main.json b/tsconfig.main.json
index d913052f7..064488c22 100644
--- a/tsconfig.main.json
+++ b/tsconfig.main.json
@@ -6,6 +6,13 @@
"noEmit": false,
"sourceMap": true
},
- "include": ["src/main.ts", "src/constants/**/*", "src/types/**/*.d.ts"],
+ "include": [
+ "src/main.ts",
+ "src/main-server.ts",
+ "src/main-desktop.ts",
+ "src/constants/**/*",
+ "src/web/**/*",
+ "src/types/**/*.d.ts"
+ ],
"exclude": ["src/App.tsx", "src/main.tsx"]
}