Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41cbd393bd | |||
| 8aa53ed3a8 | |||
| 65ecc8bfa4 | |||
| fcfde9f862 | |||
| f349617427 | |||
| d7a0a5d076 | |||
| 83191055bd | |||
| ab786a7861 | |||
| 6911033c1a | |||
| c13c137e24 | |||
| 1b0024d78f | |||
| 570baf83ab |
@@ -35,3 +35,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
PLAN.md
|
PLAN.md
|
||||||
|
|
||||||
|
excali-box-data/
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
:80 {
|
:80 {
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
|
|
||||||
|
@mcp path /mcp
|
||||||
|
handle @mcp {
|
||||||
|
reverse_proxy 127.0.0.1:3001
|
||||||
|
}
|
||||||
|
|
||||||
@api path /api/*
|
@api path /api/*
|
||||||
handle @api {
|
handle @api {
|
||||||
reverse_proxy 127.0.0.1:3000
|
reverse_proxy 127.0.0.1:3000
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HOST=127.0.0.1
|
ENV HOST=127.0.0.1
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
ENV MCP_HOST=127.0.0.1
|
||||||
|
ENV MCP_PORT=3001
|
||||||
ENV DATABASE_PATH=/data/excalidraw.sqlite
|
ENV DATABASE_PATH=/data/excalidraw.sqlite
|
||||||
|
|
||||||
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
|
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
|
||||||
COPY --from=build /app/dist/public /srv/public
|
COPY --from=build /app/dist/public /srv/public
|
||||||
COPY --from=build /app/dist/server /app/dist/server
|
COPY --from=build /app/dist/server /app/dist/server
|
||||||
|
COPY --from=build /app/dist/mcp /app/dist/mcp
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Images are published to:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/ruinivist/excalidraw-box:latest
|
docker pull ghcr.io/ruinivist/excalidraw-box:latest
|
||||||
docker run --rm -p 127.0.0.1:3000:80 -v "$PWD/excali-box-data:/data" ghcr.io/ruinivist/excalidraw-box:latest
|
docker run --rm -p 127.0.0.1:3000:80 -e PUBLIC_BASE_URL=http://localhost:3000 -v "$PWD/excali-box-data:/data" ghcr.io/ruinivist/excalidraw-box:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
@@ -38,7 +38,7 @@ bun run typecheck
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t excali .
|
docker build -t excali .
|
||||||
docker run --rm -p 127.0.0.1:3000:80 -v "$PWD/excali-box-data:/data" excali
|
docker run --rm -p 127.0.0.1:3000:80 -e PUBLIC_BASE_URL=http://localhost:3000 -v "$PWD/excali-box-data:/data" excali
|
||||||
```
|
```
|
||||||
|
|
||||||
This will make the data persistent in the `excali-box-data` folder in the current directory, with Caddy serving the browser build on `localhost:3000`
|
This will make the data persistent in the `excali-box-data` folder in the current directory, with Caddy serving the browser build on `localhost:3000`. `PUBLIC_BASE_URL` is required so MCP tools can return drawing URLs with the public browser origin. You would want it to be the same url you use to access the app in the browser - in case you reverse proxy or use a tailscale alias etc.
|
||||||
|
|||||||
@@ -6,8 +6,12 @@
|
|||||||
"name": "excali",
|
"name": "excali",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "^0.18.1",
|
"@excalidraw/excalidraw": "^0.18.1",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"fast-json-patch": "^3.1.1",
|
||||||
|
"mcp-handler": "^1.1.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
@@ -52,12 +56,16 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
"@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
|
"@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
|
||||||
|
|
||||||
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
|
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
||||||
@@ -108,6 +116,18 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
|
||||||
|
|
||||||
|
"@redis/bloom": ["@redis/bloom@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="],
|
||||||
|
|
||||||
|
"@redis/client": ["@redis/client@1.6.1", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw=="],
|
||||||
|
|
||||||
|
"@redis/graph": ["@redis/graph@1.1.1", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="],
|
||||||
|
|
||||||
|
"@redis/json": ["@redis/json@1.0.7", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="],
|
||||||
|
|
||||||
|
"@redis/search": ["@redis/search@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="],
|
||||||
|
|
||||||
|
"@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
"@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": ["@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=="],
|
||||||
@@ -184,20 +204,36 @@
|
|||||||
|
|
||||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
"browser-fs-access": ["browser-fs-access@0.29.1", "", {}, "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="],
|
"browser-fs-access": ["browser-fs-access@0.29.1", "", {}, "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
"canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="],
|
"canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
|
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
|
||||||
|
|
||||||
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
||||||
@@ -206,7 +242,19 @@
|
|||||||
|
|
||||||
"clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="],
|
"clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="],
|
||||||
|
|
||||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||||
|
|
||||||
@@ -292,33 +340,91 @@
|
|||||||
|
|
||||||
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
"dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
|
"dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||||
|
|
||||||
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
||||||
|
|
||||||
"es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="],
|
"es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
|
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
|
|
||||||
|
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
||||||
|
|
||||||
|
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.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-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-json-patch": ["fast-json-patch@3.1.1", "", {}, "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@2.1.1", "", { "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-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
"fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="],
|
"fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
|
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
|
||||||
|
|
||||||
|
"generic-pool": ["generic-pool@3.9.0", "", {}, "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"glur": ["glur@1.1.2", "", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="],
|
"glur": ["glur@1.1.2", "", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
"image-blob-reduce": ["image-blob-reduce@3.0.1", "", { "dependencies": { "pica": "^7.1.0" } }, "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q=="],
|
"image-blob-reduce": ["image-blob-reduce@3.0.1", "", { "dependencies": { "pica": "^7.1.0" } }, "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q=="],
|
||||||
|
|
||||||
@@ -330,6 +436,10 @@
|
|||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
|
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
@@ -338,12 +448,20 @@
|
|||||||
|
|
||||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
||||||
|
|
||||||
"jotai": ["jotai@2.11.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ=="],
|
"jotai": ["jotai@2.11.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ=="],
|
||||||
|
|
||||||
"jotai-scope": ["jotai-scope@0.7.2", "", { "peerDependencies": { "jotai": ">=2.9.2", "react": ">=17.0.0" } }, "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg=="],
|
"jotai-scope": ["jotai-scope@0.7.2", "", { "peerDependencies": { "jotai": ">=2.9.2", "react": ">=17.0.0" } }, "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
"katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
"katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
||||||
|
|
||||||
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||||
@@ -360,32 +478,60 @@
|
|||||||
|
|
||||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mcp-handler": ["mcp-handler@1.1.0", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^11.1.0", "redis": "^4.6.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "1.26.0", "next": ">=13.0.0" }, "optionalPeers": ["next"], "bin": { "mcp-adapter": "dist/cli/index.js", "mcp-handler": "dist/cli/index.js", "create-mcp-route": "dist/cli/index.js" } }, "sha512-MVCES7g18gcoZy+R/3v5nadkUMzMAWdos8jRl6DyljOKvd2/ZKDmwlCjL6zp4vo+7FeCXOYL1uWinHWlkKAAUg=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
|
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"multimath": ["multimath@2.0.0", "", { "dependencies": { "glur": "^1.1.2", "object-assign": "^4.1.1" } }, "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g=="],
|
"multimath": ["multimath@2.0.0", "", { "dependencies": { "glur": "^1.1.2", "object-assign": "^4.1.1" } }, "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.3", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w=="],
|
"nanoid": ["nanoid@3.3.3", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
|
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
|
||||||
|
|
||||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
"pako": ["pako@2.0.3", "", {}, "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="],
|
"pako": ["pako@2.0.3", "", {}, "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
"perfect-freehand": ["perfect-freehand@1.2.0", "", {}, "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw=="],
|
"perfect-freehand": ["perfect-freehand@1.2.0", "", {}, "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw=="],
|
||||||
|
|
||||||
"pica": ["pica@7.1.1", "", { "dependencies": { "glur": "^1.1.2", "inherits": "^2.0.3", "multimath": "^2.0.0", "object-assign": "^4.1.1", "webworkify": "^1.5.0" } }, "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ=="],
|
"pica": ["pica@7.1.1", "", { "dependencies": { "glur": "^1.1.2", "inherits": "^2.0.3", "multimath": "^2.0.0", "object-assign": "^4.1.1", "webworkify": "^1.5.0" } }, "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
|
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
"png-chunk-text": ["png-chunk-text@1.0.0", "", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="],
|
"png-chunk-text": ["png-chunk-text@1.0.0", "", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="],
|
||||||
|
|
||||||
"png-chunks-encode": ["png-chunks-encode@1.0.0", "", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="],
|
"png-chunks-encode": ["png-chunks-encode@1.0.0", "", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="],
|
||||||
@@ -396,8 +542,16 @@
|
|||||||
|
|
||||||
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"pwacompat": ["pwacompat@2.0.17", "", {}, "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w=="],
|
"pwacompat": ["pwacompat@2.0.17", "", {}, "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||||
@@ -410,10 +564,16 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||||
|
|
||||||
"roughjs": ["roughjs@4.6.4", "", { "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-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw=="],
|
"roughjs": ["roughjs@4.6.4", "", { "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-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
@@ -422,30 +582,52 @@
|
|||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
|
"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-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=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
"sliced": ["sliced@1.0.1", "", {}, "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="],
|
"sliced": ["sliced@1.0.1", "", {}, "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
"stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
|
"tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
|
||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"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=="],
|
||||||
|
|
||||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
@@ -454,6 +636,8 @@
|
|||||||
|
|
||||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||||
@@ -470,6 +654,14 @@
|
|||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
||||||
|
|
||||||
"@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
|
"@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
|
||||||
@@ -524,10 +716,14 @@
|
|||||||
|
|
||||||
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
|
"d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||||
|
|
||||||
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||||
|
|
||||||
|
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"mermaid/@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
"mermaid/@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||||
|
|
||||||
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
||||||
@@ -538,6 +734,8 @@
|
|||||||
|
|
||||||
"roughjs/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
"roughjs/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||||
|
|
||||||
|
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
|
"@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="],
|
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="],
|
||||||
|
|||||||
+16
-6
@@ -1,11 +1,12 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
bun_pid=""
|
app_pid=""
|
||||||
|
mcp_pid=""
|
||||||
caddy_pid=""
|
caddy_pid=""
|
||||||
|
|
||||||
stop_children() {
|
stop_children() {
|
||||||
for pid in "$bun_pid" "$caddy_pid"; do
|
for pid in "$app_pid" "$mcp_pid" "$caddy_pid"; do
|
||||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
kill "$pid" 2>/dev/null || true
|
kill "$pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
@@ -15,7 +16,10 @@ stop_children() {
|
|||||||
trap 'stop_children' INT TERM
|
trap 'stop_children' INT TERM
|
||||||
|
|
||||||
bun /app/dist/server/server.js &
|
bun /app/dist/server/server.js &
|
||||||
bun_pid=$!
|
app_pid=$!
|
||||||
|
|
||||||
|
bun /app/dist/mcp/mcp-server.js &
|
||||||
|
mcp_pid=$!
|
||||||
|
|
||||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
|
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
|
||||||
caddy_pid=$!
|
caddy_pid=$!
|
||||||
@@ -23,8 +27,13 @@ caddy_pid=$!
|
|||||||
exit_code=0
|
exit_code=0
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
if ! kill -0 "$bun_pid" 2>/dev/null; then
|
if ! kill -0 "$app_pid" 2>/dev/null; then
|
||||||
wait "$bun_pid" || exit_code=$?
|
wait "$app_pid" || exit_code=$?
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! kill -0 "$mcp_pid" 2>/dev/null; then
|
||||||
|
wait "$mcp_pid" || exit_code=$?
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -37,7 +46,8 @@ while :; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
stop_children
|
stop_children
|
||||||
wait "$bun_pid" 2>/dev/null || true
|
wait "$app_pid" 2>/dev/null || true
|
||||||
|
wait "$mcp_pid" 2>/dev/null || true
|
||||||
wait "$caddy_pid" 2>/dev/null || true
|
wait "$caddy_pid" 2>/dev/null || true
|
||||||
|
|
||||||
exit "$exit_code"
|
exit "$exit_code"
|
||||||
|
|||||||
+10
-5
@@ -3,18 +3,23 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/dev-server.tsx",
|
"dev": "bun --hot src/server/dev-server.tsx",
|
||||||
"build": "rm -rf dist && bun run build:client && bun run build:server",
|
"build": "rm -rf dist && bun run build:client && bun run build:server && bun run build:mcp",
|
||||||
"build:client": "bun build --target=browser --production --outdir ./dist/public ./src/index.html",
|
"build:client": "bun build --target=browser --production --outdir ./dist/public ./src/client/index.html",
|
||||||
"build:server": "mkdir -p ./dist/server && bun build --target=bun --production --outfile ./dist/server/server.js ./src/server.tsx",
|
"build:server": "mkdir -p ./dist/server && bun build --target=bun --production --outfile ./dist/server/server.js ./src/server/http-server.tsx",
|
||||||
|
"build:mcp": "mkdir -p ./dist/mcp && bun build --target=bun --production --outfile ./dist/mcp/mcp-server.js ./src/mcp/server.ts",
|
||||||
"start": "DATABASE_PATH=./data/excalidraw.sqlite bun ./dist/server/server.js",
|
"start": "DATABASE_PATH=./data/excalidraw.sqlite bun ./dist/server/server.js",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "^0.18.1",
|
"@excalidraw/excalidraw": "^0.18.1",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"fast-json-patch": "^3.1.1",
|
||||||
|
"mcp-handler": "^1.1.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
|
|||||||
-512
@@ -1,512 +0,0 @@
|
|||||||
import "../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { DEFAULT_TITLE, type DrawingMeta, type ScenePayload } from "./shared";
|
|
||||||
|
|
||||||
type DrawingResponse = DrawingMeta & ScenePayload;
|
|
||||||
|
|
||||||
const EXCALIDRAW_THEME_TOKEN_MAP = [
|
|
||||||
["--island-bg-color", "--app-island-bg"],
|
|
||||||
["--sidebar-bg-color", "--app-sidebar-bg"],
|
|
||||||
["--sidebar-border-color", "--app-sidebar-border"],
|
|
||||||
["--sidebar-shadow", "--app-sidebar-shadow"],
|
|
||||||
["--color-surface-lowest", "--app-surface-lowest"],
|
|
||||||
["--color-surface-low", "--app-surface-low"],
|
|
||||||
["--color-surface-primary-container", "--app-selected-bg"],
|
|
||||||
["--color-on-surface", "--app-text-color"],
|
|
||||||
["--color-on-primary-container", "--app-selected-text-color"],
|
|
||||||
["--color-disabled", "--app-disabled-color"],
|
|
||||||
["--input-bg-color", "--app-input-bg"],
|
|
||||||
["--input-border-color", "--app-input-border"],
|
|
||||||
["--input-label-color", "--app-input-color"],
|
|
||||||
["--default-border-color", "--app-button-border"],
|
|
||||||
["--button-hover-bg", "--app-button-hover-bg"],
|
|
||||||
["--button-active-bg", "--app-button-active-bg"],
|
|
||||||
["--button-active-border", "--app-button-active-border"],
|
|
||||||
["--overlay-bg-color", "--app-overlay-bg"],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function pathToId(pathname = window.location.pathname): string | null {
|
|
||||||
const match = pathname.match(/^\/d\/([^/]+)$/);
|
|
||||||
return match?.[1] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortDrawings(drawings: DrawingMeta[]): DrawingMeta[] {
|
|
||||||
return [...drawings].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceMeta(drawings: DrawingMeta[], drawing: DrawingMeta): DrawingMeta[] {
|
|
||||||
const filtered = drawings.filter((item) => item.id !== drawing.id);
|
|
||||||
return sortDrawings([drawing, ...filtered]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sceneToInitialData(scene: ScenePayload): ExcalidrawInitialDataState {
|
|
||||||
return scene as ExcalidrawInitialDataState;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sceneFromEditor(
|
|
||||||
elements: readonly unknown[],
|
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles,
|
|
||||||
): ScenePayload {
|
|
||||||
return {
|
|
||||||
elements: [...elements],
|
|
||||||
appState: appState as unknown as Record<string, unknown>,
|
|
||||||
files: files as unknown as Record<string, unknown>,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...(init?.body ? { "content-type": "application/json" } : {}),
|
|
||||||
...init?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = (await response.json()) as T & { error?: string };
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(body.error ?? `Request failed: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerIcon() {
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
|
|
||||||
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
||||||
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function App() {
|
|
||||||
const [drawings, setDrawings] = useState<DrawingMeta[]>([]);
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(pathToId());
|
|
||||||
const [activeTitle, setActiveTitle] = useState(DEFAULT_TITLE);
|
|
||||||
const [scene, setScene] = useState<ScenePayload | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
||||||
|
|
||||||
const currentIdRef = useRef<string | null>(activeId);
|
|
||||||
const currentTitleRef = useRef(activeTitle);
|
|
||||||
const latestSceneRef = useRef<ScenePayload | null>(null);
|
|
||||||
const ignoreChangeRef = useRef(false);
|
|
||||||
const appShellRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const timeoutRef = useRef<number | null>(null);
|
|
||||||
const inFlightSaveRef = useRef<Promise<void> | null>(null);
|
|
||||||
const loadVersionRef = useRef(0);
|
|
||||||
const themeSyncFrameRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const activeDrawing = useMemo(
|
|
||||||
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
|
|
||||||
[activeId, drawings],
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshList = useCallback(async () => {
|
|
||||||
const list = await requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" });
|
|
||||||
setDrawings(sortDrawings(list));
|
|
||||||
return list;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const saveScene = useCallback(async () => {
|
|
||||||
const drawingId = currentIdRef.current;
|
|
||||||
const nextScene = latestSceneRef.current;
|
|
||||||
if (!drawingId || !nextScene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(nextScene),
|
|
||||||
})
|
|
||||||
.then((body) => {
|
|
||||||
setDrawings((current) => replaceMeta(current, body.drawing));
|
|
||||||
})
|
|
||||||
.catch((saveError: unknown) => {
|
|
||||||
setError(saveError instanceof Error ? saveError.message : "Save failed");
|
|
||||||
throw saveError;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
inFlightSaveRef.current = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
inFlightSaveRef.current = promise;
|
|
||||||
await promise;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const flushPendingSave = useCallback(async () => {
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
timeoutRef.current = null;
|
|
||||||
await saveScene();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inFlightSaveRef.current) {
|
|
||||||
await inFlightSaveRef.current;
|
|
||||||
}
|
|
||||||
}, [saveScene]);
|
|
||||||
|
|
||||||
const scheduleSave = useCallback(() => {
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeoutRef.current = window.setTimeout(() => {
|
|
||||||
timeoutRef.current = null;
|
|
||||||
void saveScene();
|
|
||||||
}, 1500);
|
|
||||||
}, [saveScene]);
|
|
||||||
|
|
||||||
const loadDrawing = useCallback(
|
|
||||||
async (drawingId: string) => {
|
|
||||||
const version = ++loadVersionRef.current;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [list, drawing] = await Promise.all([
|
|
||||||
requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" }),
|
|
||||||
requestJson<DrawingResponse>(`/api/drawings/${drawingId}`, { method: "GET" }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (version !== loadVersionRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextScene = {
|
|
||||||
elements: drawing.elements,
|
|
||||||
appState: drawing.appState,
|
|
||||||
files: drawing.files,
|
|
||||||
};
|
|
||||||
|
|
||||||
ignoreChangeRef.current = true;
|
|
||||||
currentIdRef.current = drawing.id;
|
|
||||||
currentTitleRef.current = drawing.title;
|
|
||||||
latestSceneRef.current = nextScene;
|
|
||||||
|
|
||||||
setDrawings(sortDrawings(list));
|
|
||||||
setActiveId(drawing.id);
|
|
||||||
setActiveTitle(drawing.title);
|
|
||||||
setScene(nextScene);
|
|
||||||
} catch (loadError) {
|
|
||||||
const list = await refreshList();
|
|
||||||
if (list.length === 0) {
|
|
||||||
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
|
|
||||||
window.history.replaceState(null, "", `/d/${created.id}`);
|
|
||||||
await loadDrawing(created.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallback = list[0];
|
|
||||||
if (fallback && fallback.id !== drawingId) {
|
|
||||||
window.history.replaceState(null, "", `/d/${fallback.id}`);
|
|
||||||
await loadDrawing(fallback.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(loadError instanceof Error ? loadError.message : "Failed to load drawing");
|
|
||||||
} finally {
|
|
||||||
if (version === loadVersionRef.current) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[refreshList],
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateToDrawing = useCallback(
|
|
||||||
async (drawingId: string, options?: { replace?: boolean; flush?: boolean }) => {
|
|
||||||
const replace = options?.replace ?? false;
|
|
||||||
const flush = options?.flush ?? true;
|
|
||||||
|
|
||||||
if (drawingId === currentIdRef.current && latestSceneRef.current) {
|
|
||||||
if (replace) {
|
|
||||||
window.history.replaceState(null, "", `/d/${drawingId}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flush) {
|
|
||||||
await flushPendingSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
window.history.replaceState(null, "", `/d/${drawingId}`);
|
|
||||||
} else {
|
|
||||||
window.history.pushState(null, "", `/d/${drawingId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadDrawing(drawingId);
|
|
||||||
},
|
|
||||||
[flushPendingSave, loadDrawing],
|
|
||||||
);
|
|
||||||
|
|
||||||
const createDrawing = useCallback(async () => {
|
|
||||||
await flushPendingSave();
|
|
||||||
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
|
|
||||||
await navigateToDrawing(created.id, { flush: false });
|
|
||||||
}, [flushPendingSave, navigateToDrawing]);
|
|
||||||
|
|
||||||
const deleteDrawing = useCallback(
|
|
||||||
async (drawingId: string) => {
|
|
||||||
if (!window.confirm("Delete this drawing?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawingId === currentIdRef.current) {
|
|
||||||
await flushPendingSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await requestJson<{ ok: true; nextId: string | null }>(`/api/drawings/${drawingId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (drawingId === currentIdRef.current && response.nextId) {
|
|
||||||
await navigateToDrawing(response.nextId, { replace: true, flush: false });
|
|
||||||
} else {
|
|
||||||
await refreshList();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[flushPendingSave, navigateToDrawing, refreshList],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTitleSubmit = useCallback(async () => {
|
|
||||||
const drawingId = currentIdRef.current;
|
|
||||||
if (!drawingId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({ title: currentTitleRef.current }),
|
|
||||||
});
|
|
||||||
|
|
||||||
currentTitleRef.current = body.drawing.title;
|
|
||||||
setActiveTitle(body.drawing.title);
|
|
||||||
setDrawings((current) => replaceMeta(current, body.drawing));
|
|
||||||
} catch (renameError) {
|
|
||||||
setError(renameError instanceof Error ? renameError.message : "Rename failed");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const syncThemeTokens = useCallback(() => {
|
|
||||||
const appShell = appShellRef.current;
|
|
||||||
const excalidrawRoot = appShell?.querySelector<HTMLElement>(".excalidraw");
|
|
||||||
if (!appShell || !excalidrawRoot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStyles = window.getComputedStyle(excalidrawRoot);
|
|
||||||
for (const [sourceToken, targetToken] of EXCALIDRAW_THEME_TOKEN_MAP) {
|
|
||||||
const value = computedStyles.getPropertyValue(sourceToken).trim();
|
|
||||||
if (value) {
|
|
||||||
appShell.style.setProperty(targetToken, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scheduleThemeTokenSync = useCallback(() => {
|
|
||||||
if (themeSyncFrameRef.current !== null) {
|
|
||||||
window.cancelAnimationFrame(themeSyncFrameRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
themeSyncFrameRef.current = window.requestAnimationFrame(() => {
|
|
||||||
themeSyncFrameRef.current = null;
|
|
||||||
syncThemeTokens();
|
|
||||||
});
|
|
||||||
}, [syncThemeTokens]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentPathId = pathToId();
|
|
||||||
if (currentPathId) {
|
|
||||||
void loadDrawing(currentPathId);
|
|
||||||
} else {
|
|
||||||
window.location.replace("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPopState = () => {
|
|
||||||
const nextId = pathToId();
|
|
||||||
if (nextId) {
|
|
||||||
void navigateToDrawing(nextId, { replace: true, flush: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("popstate", onPopState);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("popstate", onPopState);
|
|
||||||
};
|
|
||||||
}, [loadDrawing, navigateToDrawing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (themeSyncFrameRef.current !== null) {
|
|
||||||
window.cancelAnimationFrame(themeSyncFrameRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!scene || loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleThemeTokenSync();
|
|
||||||
}, [activeId, loading, scene, scheduleThemeTokenSync]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSidebarOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
setIsSidebarOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
};
|
|
||||||
}, [isSidebarOpen]);
|
|
||||||
|
|
||||||
const openSidebar = useCallback(() => {
|
|
||||||
setIsSidebarOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeSidebar = useCallback(() => {
|
|
||||||
setIsSidebarOpen(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSelectDrawing = useCallback(
|
|
||||||
async (drawingId: string) => {
|
|
||||||
setIsSidebarOpen(false);
|
|
||||||
await navigateToDrawing(drawingId);
|
|
||||||
},
|
|
||||||
[navigateToDrawing],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCreateDrawing = useCallback(async () => {
|
|
||||||
setIsSidebarOpen(false);
|
|
||||||
await createDrawing();
|
|
||||||
}, [createDrawing]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="app-shell" ref={appShellRef}>
|
|
||||||
<main className="editor-shell">
|
|
||||||
<div className="app-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary-button app-actions-toggle"
|
|
||||||
onClick={openSidebar}
|
|
||||||
aria-label="Open drawings"
|
|
||||||
>
|
|
||||||
<DrawerIcon />
|
|
||||||
<span className="sr-only">Open drawings</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading || !scene ? (
|
|
||||||
<div className="editor-loading">{error ?? "Loading..."}</div>
|
|
||||||
) : (
|
|
||||||
<div className="editor-frame">
|
|
||||||
<Excalidraw
|
|
||||||
key={activeDrawing?.id ?? activeId}
|
|
||||||
initialData={sceneToInitialData(scene)}
|
|
||||||
onChange={(elements, appState, files) => {
|
|
||||||
const nextScene = sceneFromEditor(elements, appState, files);
|
|
||||||
latestSceneRef.current = nextScene;
|
|
||||||
scheduleThemeTokenSync();
|
|
||||||
|
|
||||||
if (ignoreChangeRef.current) {
|
|
||||||
ignoreChangeRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleSave();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
<div
|
|
||||||
className={isSidebarOpen ? "sidebar-backdrop sidebar-backdrop-open" : "sidebar-backdrop"}
|
|
||||||
onClick={closeSidebar}
|
|
||||||
aria-hidden={!isSidebarOpen}
|
|
||||||
/>
|
|
||||||
<aside className={isSidebarOpen ? "sidebar sidebar-open" : "sidebar"} aria-hidden={!isSidebarOpen}>
|
|
||||||
<div className="sidebar-header">
|
|
||||||
<h1>excali-box</h1>
|
|
||||||
<div className="sidebar-actions">
|
|
||||||
<button type="button" className="primary-button" onClick={() => void handleCreateDrawing()}>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
<button type="button" className="icon-button" onClick={closeSidebar} aria-label="Close drawings">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="drawing-list">
|
|
||||||
{drawings.map((drawing) => (
|
|
||||||
<div
|
|
||||||
key={drawing.id}
|
|
||||||
className={drawing.id === activeId ? "drawing-item drawing-item-active" : "drawing-item"}
|
|
||||||
>
|
|
||||||
{drawing.id === activeId ? (
|
|
||||||
<div className="drawing-link">
|
|
||||||
<input
|
|
||||||
className="title-input"
|
|
||||||
value={activeTitle}
|
|
||||||
onChange={(event) => {
|
|
||||||
currentTitleRef.current = event.target.value;
|
|
||||||
setActiveTitle(event.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={() => void handleTitleSubmit()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="drawing-link"
|
|
||||||
onClick={() => void handleSelectDrawing(drawing.id)}
|
|
||||||
>
|
|
||||||
<span className="drawing-title">{drawing.title}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
onClick={() => void deleteDrawing(drawing.id)}
|
|
||||||
aria-label={`Delete ${drawing.title}`}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { mkdtempSync, rmSync } from "node:fs";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { createApi } from "./api";
|
|
||||||
import { createDrawingStore } from "./db";
|
|
||||||
|
|
||||||
function withApi() {
|
|
||||||
const dir = mkdtempSync(join(tmpdir(), "excali-api-"));
|
|
||||||
const store = createDrawingStore(join(dir, "test.sqlite"));
|
|
||||||
const api = createApi(store);
|
|
||||||
|
|
||||||
return {
|
|
||||||
api,
|
|
||||||
cleanup() {
|
|
||||||
store.close();
|
|
||||||
rmSync(dir, { recursive: true, force: true });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("api", () => {
|
|
||||||
test("creates a drawing with dark theme by default", async () => {
|
|
||||||
const { api, cleanup } = withApi();
|
|
||||||
|
|
||||||
const created = await api.createDrawing().json();
|
|
||||||
|
|
||||||
expect(created.appState.theme).toBe("dark");
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 400 for invalid JSON", async () => {
|
|
||||||
const { api, cleanup } = withApi();
|
|
||||||
const drawing = await api.createDrawing().json();
|
|
||||||
|
|
||||||
const response = await api.updateDrawing(
|
|
||||||
Object.assign(
|
|
||||||
new Request(`http://local/api/drawings/${drawing.id}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: "{",
|
|
||||||
}),
|
|
||||||
{ params: { id: drawing.id } },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 404 for missing drawing", () => {
|
|
||||||
const { api, cleanup } = withApi();
|
|
||||||
|
|
||||||
const response = api.getDrawing(
|
|
||||||
Object.assign(new Request("http://local/api/drawings/missing"), { params: { id: "missing" } }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("updates a drawing scene", async () => {
|
|
||||||
const { api, cleanup } = withApi();
|
|
||||||
const created = await api.createDrawing().json();
|
|
||||||
|
|
||||||
const response = await api.updateDrawing(
|
|
||||||
Object.assign(
|
|
||||||
new Request(`http://local/api/drawings/${created.id}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({
|
|
||||||
elements: [{ id: "shape" }],
|
|
||||||
appState: {},
|
|
||||||
files: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
{ params: { id: created.id } },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { DrawingSidebar, DrawingsToggle } from "./components/DrawingSidebar";
|
||||||
|
import { EditorCanvas } from "./components/EditorCanvas";
|
||||||
|
import { useDrawingSession } from "./hooks/useDrawingSession";
|
||||||
|
import { useThemeTokenSync } from "./hooks/useThemeTokenSync";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const appShellRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const scheduleThemeTokenSync = useThemeTokenSync(appShellRef);
|
||||||
|
const {
|
||||||
|
drawings,
|
||||||
|
activeId,
|
||||||
|
activeTitle,
|
||||||
|
scene,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
toastMessage,
|
||||||
|
editorReloadNonce,
|
||||||
|
setActiveTitle,
|
||||||
|
submitTitle,
|
||||||
|
handleSceneChange,
|
||||||
|
selectDrawing,
|
||||||
|
createDrawing,
|
||||||
|
deleteDrawing,
|
||||||
|
} = useDrawingSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scene || loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleThemeTokenSync();
|
||||||
|
}, [activeId, loading, scene, scheduleThemeTokenSync]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSidebarOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, [isSidebarOpen]);
|
||||||
|
|
||||||
|
const openSidebar = useCallback(() => {
|
||||||
|
setIsSidebarOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeSidebar = useCallback(() => {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectDrawing = useCallback(
|
||||||
|
(drawingId: string) => {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
void selectDrawing(drawingId);
|
||||||
|
},
|
||||||
|
[selectDrawing],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateDrawing = useCallback(() => {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
void createDrawing();
|
||||||
|
}, [createDrawing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell" ref={appShellRef}>
|
||||||
|
{toastMessage && (
|
||||||
|
<div className="toast-overlay" aria-live="polite">
|
||||||
|
{toastMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<main className="editor-shell">
|
||||||
|
<div className="app-actions">
|
||||||
|
<DrawingsToggle onClick={openSidebar} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditorCanvas
|
||||||
|
activeId={activeId}
|
||||||
|
scene={scene}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
editorReloadNonce={editorReloadNonce}
|
||||||
|
onSceneChange={handleSceneChange}
|
||||||
|
onEditorActivity={scheduleThemeTokenSync}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
<DrawingSidebar
|
||||||
|
open={isSidebarOpen}
|
||||||
|
drawings={drawings}
|
||||||
|
activeId={activeId}
|
||||||
|
activeTitle={activeTitle}
|
||||||
|
onClose={closeSidebar}
|
||||||
|
onCreate={handleCreateDrawing}
|
||||||
|
onSelect={handleSelectDrawing}
|
||||||
|
onDelete={(drawingId) => void deleteDrawing(drawingId)}
|
||||||
|
onTitleChange={setActiveTitle}
|
||||||
|
onTitleSubmit={() => void submitTitle()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { type DrawingMeta } from "../../core/shared";
|
||||||
|
|
||||||
|
type DrawingSidebarProps = {
|
||||||
|
open: boolean;
|
||||||
|
drawings: DrawingMeta[];
|
||||||
|
activeId: string | null;
|
||||||
|
activeTitle: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
onSelect: (drawingId: string) => void;
|
||||||
|
onDelete: (drawingId: string) => void;
|
||||||
|
onTitleChange: (title: string) => void;
|
||||||
|
onTitleSubmit: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DrawerIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
|
||||||
|
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||||
|
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawingsToggle({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-button app-actions-toggle"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label="Open drawings"
|
||||||
|
>
|
||||||
|
<DrawerIcon />
|
||||||
|
<span className="sr-only">Open drawings</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrawingSidebar({
|
||||||
|
open,
|
||||||
|
drawings,
|
||||||
|
activeId,
|
||||||
|
activeTitle,
|
||||||
|
onClose,
|
||||||
|
onCreate,
|
||||||
|
onSelect,
|
||||||
|
onDelete,
|
||||||
|
onTitleChange,
|
||||||
|
onTitleSubmit,
|
||||||
|
}: DrawingSidebarProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={open ? "sidebar-backdrop sidebar-backdrop-open" : "sidebar-backdrop"}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-hidden={!open}
|
||||||
|
/>
|
||||||
|
<aside className={open ? "sidebar sidebar-open" : "sidebar"} aria-hidden={!open}>
|
||||||
|
<div className="sidebar-header">
|
||||||
|
<h1>excali-box</h1>
|
||||||
|
<div className="sidebar-actions">
|
||||||
|
<button type="button" className="primary-button" onClick={onCreate}>
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
<button type="button" className="icon-button" onClick={onClose} aria-label="Close drawings">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="drawing-list">
|
||||||
|
{drawings.map((drawing) => (
|
||||||
|
<div
|
||||||
|
key={drawing.id}
|
||||||
|
className={drawing.id === activeId ? "drawing-item drawing-item-active" : "drawing-item"}
|
||||||
|
>
|
||||||
|
{drawing.id === activeId ? (
|
||||||
|
<div className="drawing-link">
|
||||||
|
<input
|
||||||
|
className="title-input"
|
||||||
|
value={activeTitle}
|
||||||
|
onChange={(event) => onTitleChange(event.target.value)}
|
||||||
|
onBlur={onTitleSubmit}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="drawing-link" onClick={() => onSelect(drawing.id)}>
|
||||||
|
<span className="drawing-title">{drawing.title}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
onClick={() => onDelete(drawing.id)}
|
||||||
|
aria-label={`Delete ${drawing.title}`}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import "../../../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
BinaryFiles,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import { type ScenePayload } from "../../core/shared";
|
||||||
|
|
||||||
|
type EditorCanvasProps = {
|
||||||
|
activeId: string | null;
|
||||||
|
scene: ScenePayload | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
editorReloadNonce: number;
|
||||||
|
onSceneChange: (scene: ScenePayload) => void;
|
||||||
|
onEditorActivity: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sceneToInitialData(scene: ScenePayload): ExcalidrawInitialDataState {
|
||||||
|
return scene as ExcalidrawInitialDataState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sceneFromEditor(
|
||||||
|
elements: readonly unknown[],
|
||||||
|
appState: AppState,
|
||||||
|
files: BinaryFiles,
|
||||||
|
): ScenePayload {
|
||||||
|
return {
|
||||||
|
elements: [...elements],
|
||||||
|
appState: appState as unknown as Record<string, unknown>,
|
||||||
|
files: files as unknown as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditorCanvas({
|
||||||
|
activeId,
|
||||||
|
scene,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
editorReloadNonce,
|
||||||
|
onSceneChange,
|
||||||
|
onEditorActivity,
|
||||||
|
}: EditorCanvasProps) {
|
||||||
|
if (loading || !scene) {
|
||||||
|
return <div className="editor-loading">{error ?? "Loading..."}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-frame">
|
||||||
|
<Excalidraw
|
||||||
|
key={`${activeId}:${editorReloadNonce}`}
|
||||||
|
initialData={sceneToInitialData(scene)}
|
||||||
|
onChange={(elements, appState, files) => {
|
||||||
|
onEditorActivity();
|
||||||
|
onSceneChange(sceneFromEditor(elements, appState, files));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { DEFAULT_TITLE, type DrawingMeta, type ScenePayload } from "../../core/shared";
|
||||||
|
|
||||||
|
type DrawingResponse = DrawingMeta & ScenePayload;
|
||||||
|
type ApiErrorBody = { ok: false; error?: string; drawing?: DrawingMeta };
|
||||||
|
type UpdateResponse = { ok: true; drawing: DrawingMeta };
|
||||||
|
|
||||||
|
class RequestError extends Error {
|
||||||
|
constructor(
|
||||||
|
readonly status: number,
|
||||||
|
readonly body: ApiErrorBody,
|
||||||
|
) {
|
||||||
|
super(body.error ?? `Request failed: ${status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToId(pathname = window.location.pathname): string | null {
|
||||||
|
const match = pathname.match(/^\/d\/([^/]+)$/);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortDrawings(drawings: DrawingMeta[]): DrawingMeta[] {
|
||||||
|
return [...drawings].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceMeta(drawings: DrawingMeta[], drawing: DrawingMeta): DrawingMeta[] {
|
||||||
|
const filtered = drawings.filter((item) => item.id !== drawing.id);
|
||||||
|
return sortDrawings([drawing, ...filtered]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...(init?.body ? { "content-type": "application/json" } : {}),
|
||||||
|
...init?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = (await response.json()) as T & ApiErrorBody;
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new RequestError(response.status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDrawingList() {
|
||||||
|
return requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConflictError(error: unknown): error is RequestError & { body: ApiErrorBody & { drawing: DrawingMeta } } {
|
||||||
|
return error instanceof RequestError && error.status === 409 && error.body.drawing !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sceneFromDrawing(drawing: DrawingResponse): ScenePayload {
|
||||||
|
return {
|
||||||
|
elements: drawing.elements,
|
||||||
|
appState: drawing.appState,
|
||||||
|
files: drawing.files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDrawingSession() {
|
||||||
|
const [drawings, setDrawings] = useState<DrawingMeta[]>([]);
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(pathToId());
|
||||||
|
const [activeTitle, setActiveTitleState] = useState(DEFAULT_TITLE);
|
||||||
|
const [scene, setScene] = useState<ScenePayload | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||||
|
const [editorReloadNonce, setEditorReloadNonce] = useState(0);
|
||||||
|
|
||||||
|
const currentIdRef = useRef<string | null>(activeId);
|
||||||
|
const currentTitleRef = useRef(activeTitle);
|
||||||
|
const latestSceneRef = useRef<ScenePayload | null>(null);
|
||||||
|
const ignoreChangeRef = useRef(false);
|
||||||
|
const saveTimeoutRef = useRef<number | null>(null);
|
||||||
|
const toastTimeoutRef = useRef<number | null>(null);
|
||||||
|
const inFlightSaveRef = useRef<Promise<void> | null>(null);
|
||||||
|
const loadVersionRef = useRef(0);
|
||||||
|
const loadedRevisionRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const clearPendingSaveTimeout = useCallback(() => {
|
||||||
|
if (saveTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
saveTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showToast = useCallback((message: string | null, timeoutMs?: number) => {
|
||||||
|
if (toastTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(toastTimeoutRef.current);
|
||||||
|
toastTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToastMessage(message);
|
||||||
|
if (message !== null && timeoutMs !== undefined) {
|
||||||
|
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), timeoutMs);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshList = useCallback(async () => {
|
||||||
|
const list = await fetchDrawingList();
|
||||||
|
setDrawings(sortDrawings(list));
|
||||||
|
return list;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadDrawing = useCallback(
|
||||||
|
async (drawingId: string) => {
|
||||||
|
const version = ++loadVersionRef.current;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [list, drawing] = await Promise.all([
|
||||||
|
requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" }),
|
||||||
|
requestJson<DrawingResponse>(`/api/drawings/${drawingId}`, { method: "GET" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (version !== loadVersionRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextScene = sceneFromDrawing(drawing);
|
||||||
|
|
||||||
|
ignoreChangeRef.current = true;
|
||||||
|
currentIdRef.current = drawing.id;
|
||||||
|
currentTitleRef.current = drawing.title;
|
||||||
|
latestSceneRef.current = nextScene;
|
||||||
|
loadedRevisionRef.current = drawing.revision;
|
||||||
|
|
||||||
|
setDrawings(replaceMeta(list, drawing));
|
||||||
|
setActiveId(drawing.id);
|
||||||
|
setActiveTitleState(drawing.title);
|
||||||
|
setScene(nextScene);
|
||||||
|
setEditorReloadNonce((current) => current + 1);
|
||||||
|
} catch (loadError) {
|
||||||
|
if (version !== loadVersionRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load drawing");
|
||||||
|
} finally {
|
||||||
|
if (version === loadVersionRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveScene = useCallback(
|
||||||
|
async (isManual = false) => {
|
||||||
|
const drawingId = currentIdRef.current;
|
||||||
|
const nextScene = latestSceneRef.current;
|
||||||
|
const expectedRevision = loadedRevisionRef.current;
|
||||||
|
if (!drawingId || !nextScene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (isManual) {
|
||||||
|
showToast("Saving...");
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = requestJson<UpdateResponse>(`/api/drawings/${drawingId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ ...nextScene, expectedRevision }),
|
||||||
|
})
|
||||||
|
.then((body) => {
|
||||||
|
setDrawings((current) => replaceMeta(current, body.drawing));
|
||||||
|
if (currentIdRef.current === drawingId) {
|
||||||
|
loadedRevisionRef.current = body.drawing.revision;
|
||||||
|
}
|
||||||
|
if (isManual) {
|
||||||
|
showToast("Saved", 2000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((saveError: unknown) => {
|
||||||
|
if (isConflictError(saveError)) {
|
||||||
|
clearPendingSaveTimeout();
|
||||||
|
setError(null);
|
||||||
|
setDrawings((current) => replaceMeta(current, saveError.body.drawing));
|
||||||
|
if (isManual) {
|
||||||
|
showToast("Reloading latest...");
|
||||||
|
}
|
||||||
|
return loadDrawing(drawingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(saveError instanceof Error ? saveError.message : "Save failed");
|
||||||
|
if (isManual) {
|
||||||
|
showToast("Save failed", 2000);
|
||||||
|
}
|
||||||
|
throw saveError;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inFlightSaveRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
inFlightSaveRef.current = promise;
|
||||||
|
await promise;
|
||||||
|
},
|
||||||
|
[clearPendingSaveTimeout, loadDrawing, showToast],
|
||||||
|
);
|
||||||
|
|
||||||
|
const flushPendingSave = useCallback(async () => {
|
||||||
|
if (saveTimeoutRef.current !== null) {
|
||||||
|
clearPendingSaveTimeout();
|
||||||
|
await saveScene();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFlightSaveRef.current) {
|
||||||
|
await inFlightSaveRef.current;
|
||||||
|
}
|
||||||
|
}, [clearPendingSaveTimeout, saveScene]);
|
||||||
|
|
||||||
|
const triggerManualSave = useCallback(async () => {
|
||||||
|
if (saveTimeoutRef.current !== null) {
|
||||||
|
clearPendingSaveTimeout();
|
||||||
|
}
|
||||||
|
if (inFlightSaveRef.current) {
|
||||||
|
await inFlightSaveRef.current;
|
||||||
|
}
|
||||||
|
await saveScene(true);
|
||||||
|
}, [clearPendingSaveTimeout, saveScene]);
|
||||||
|
|
||||||
|
const scheduleSave = useCallback(() => {
|
||||||
|
if (saveTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTimeoutRef.current = window.setTimeout(() => {
|
||||||
|
saveTimeoutRef.current = null;
|
||||||
|
void saveScene();
|
||||||
|
}, 30000);
|
||||||
|
}, [saveScene]);
|
||||||
|
|
||||||
|
const navigateToDrawing = useCallback(
|
||||||
|
async (drawingId: string, options?: { replace?: boolean; flush?: boolean }) => {
|
||||||
|
const replace = options?.replace ?? false;
|
||||||
|
const flush = options?.flush ?? true;
|
||||||
|
|
||||||
|
if (drawingId === currentIdRef.current && latestSceneRef.current) {
|
||||||
|
if (replace) {
|
||||||
|
window.history.replaceState(null, "", `/d/${drawingId}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flush) {
|
||||||
|
await flushPendingSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replace) {
|
||||||
|
window.history.replaceState(null, "", `/d/${drawingId}`);
|
||||||
|
} else {
|
||||||
|
window.history.pushState(null, "", `/d/${drawingId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadDrawing(drawingId);
|
||||||
|
},
|
||||||
|
[flushPendingSave, loadDrawing],
|
||||||
|
);
|
||||||
|
|
||||||
|
const createDrawing = useCallback(async () => {
|
||||||
|
await flushPendingSave();
|
||||||
|
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
|
||||||
|
await navigateToDrawing(created.id, { flush: false });
|
||||||
|
}, [flushPendingSave, navigateToDrawing]);
|
||||||
|
|
||||||
|
const deleteDrawing = useCallback(
|
||||||
|
async (drawingId: string) => {
|
||||||
|
if (!window.confirm("Delete this drawing?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawingId === currentIdRef.current) {
|
||||||
|
await flushPendingSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await requestJson<{ ok: true; nextId: string | null }>(`/api/drawings/${drawingId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (drawingId === currentIdRef.current && response.nextId) {
|
||||||
|
await navigateToDrawing(response.nextId, { replace: true, flush: false });
|
||||||
|
} else {
|
||||||
|
await refreshList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[flushPendingSave, navigateToDrawing, refreshList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setActiveTitle = useCallback((title: string) => {
|
||||||
|
currentTitleRef.current = title;
|
||||||
|
setActiveTitleState(title);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submitTitle = useCallback(async () => {
|
||||||
|
const drawingId = currentIdRef.current;
|
||||||
|
if (!drawingId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await requestJson<UpdateResponse>(`/api/drawings/${drawingId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: currentTitleRef.current,
|
||||||
|
expectedRevision: loadedRevisionRef.current,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTitleRef.current = body.drawing.title;
|
||||||
|
loadedRevisionRef.current = body.drawing.revision;
|
||||||
|
setActiveTitleState(body.drawing.title);
|
||||||
|
setDrawings((current) => replaceMeta(current, body.drawing));
|
||||||
|
} catch (renameError) {
|
||||||
|
if (isConflictError(renameError)) {
|
||||||
|
clearPendingSaveTimeout();
|
||||||
|
setDrawings((current) => replaceMeta(current, renameError.body.drawing));
|
||||||
|
await loadDrawing(drawingId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(renameError instanceof Error ? renameError.message : "Rename failed");
|
||||||
|
}
|
||||||
|
}, [clearPendingSaveTimeout, loadDrawing]);
|
||||||
|
|
||||||
|
const handleSceneChange = useCallback(
|
||||||
|
(nextScene: ScenePayload) => {
|
||||||
|
latestSceneRef.current = nextScene;
|
||||||
|
|
||||||
|
if (ignoreChangeRef.current) {
|
||||||
|
ignoreChangeRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleSave();
|
||||||
|
},
|
||||||
|
[scheduleSave],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectDrawing = useCallback(
|
||||||
|
async (drawingId: string) => {
|
||||||
|
await navigateToDrawing(drawingId);
|
||||||
|
},
|
||||||
|
[navigateToDrawing],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void triggerManualSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
};
|
||||||
|
}, [triggerManualSave]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPathId = pathToId();
|
||||||
|
if (currentPathId) {
|
||||||
|
void loadDrawing(currentPathId);
|
||||||
|
} else {
|
||||||
|
window.location.replace("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPopState = () => {
|
||||||
|
const nextId = pathToId();
|
||||||
|
if (nextId) {
|
||||||
|
void navigateToDrawing(nextId, { replace: true, flush: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", onPopState);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", onPopState);
|
||||||
|
};
|
||||||
|
}, [loadDrawing, navigateToDrawing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkForExternalChanges = async () => {
|
||||||
|
try {
|
||||||
|
const list = await refreshList();
|
||||||
|
const drawingId = currentIdRef.current;
|
||||||
|
const loadedRevision = loadedRevisionRef.current;
|
||||||
|
const serverDrawing = drawingId ? list.find((drawing) => drawing.id === drawingId) : null;
|
||||||
|
|
||||||
|
if (serverDrawing && loadedRevision !== null && serverDrawing.revision > loadedRevision) {
|
||||||
|
clearPendingSaveTimeout();
|
||||||
|
await loadDrawing(serverDrawing.id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Focus checks only discover external edits; direct save/load requests report failures.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
void checkForExternalChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
void checkForExternalChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("focus", handleFocus);
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("focus", handleFocus);
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [clearPendingSaveTimeout, loadDrawing, refreshList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (saveTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (toastTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(toastTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawings,
|
||||||
|
activeId,
|
||||||
|
activeTitle,
|
||||||
|
scene,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
toastMessage,
|
||||||
|
editorReloadNonce,
|
||||||
|
setActiveTitle,
|
||||||
|
submitTitle,
|
||||||
|
handleSceneChange,
|
||||||
|
selectDrawing,
|
||||||
|
createDrawing,
|
||||||
|
deleteDrawing,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useCallback, useEffect, useRef, type RefObject } from "react";
|
||||||
|
|
||||||
|
const EXCALIDRAW_THEME_TOKEN_MAP = [
|
||||||
|
["--island-bg-color", "--app-island-bg"],
|
||||||
|
["--sidebar-bg-color", "--app-sidebar-bg"],
|
||||||
|
["--sidebar-border-color", "--app-sidebar-border"],
|
||||||
|
["--sidebar-shadow", "--app-sidebar-shadow"],
|
||||||
|
["--color-surface-lowest", "--app-surface-lowest"],
|
||||||
|
["--color-surface-low", "--app-surface-low"],
|
||||||
|
["--color-surface-primary-container", "--app-selected-bg"],
|
||||||
|
["--color-on-surface", "--app-text-color"],
|
||||||
|
["--color-on-primary-container", "--app-selected-text-color"],
|
||||||
|
["--color-disabled", "--app-disabled-color"],
|
||||||
|
["--input-bg-color", "--app-input-bg"],
|
||||||
|
["--input-border-color", "--app-input-border"],
|
||||||
|
["--input-label-color", "--app-input-color"],
|
||||||
|
["--default-border-color", "--app-button-border"],
|
||||||
|
["--button-hover-bg", "--app-button-hover-bg"],
|
||||||
|
["--button-active-bg", "--app-button-active-bg"],
|
||||||
|
["--button-active-border", "--app-button-active-border"],
|
||||||
|
["--overlay-bg-color", "--app-overlay-bg"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function useThemeTokenSync(appShellRef: RefObject<HTMLDivElement | null>) {
|
||||||
|
const themeSyncFrameRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const syncThemeTokens = useCallback(() => {
|
||||||
|
const appShell = appShellRef.current;
|
||||||
|
const excalidrawRoot = appShell?.querySelector<HTMLElement>(".excalidraw");
|
||||||
|
if (!appShell || !excalidrawRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedStyles = window.getComputedStyle(excalidrawRoot);
|
||||||
|
for (const [sourceToken, targetToken] of EXCALIDRAW_THEME_TOKEN_MAP) {
|
||||||
|
const value = computedStyles.getPropertyValue(sourceToken).trim();
|
||||||
|
if (value) {
|
||||||
|
appShell.style.setProperty(targetToken, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [appShellRef]);
|
||||||
|
|
||||||
|
const scheduleThemeTokenSync = useCallback(() => {
|
||||||
|
if (themeSyncFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(themeSyncFrameRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
themeSyncFrameRef.current = window.requestAnimationFrame(() => {
|
||||||
|
themeSyncFrameRef.current = null;
|
||||||
|
syncThemeTokens();
|
||||||
|
});
|
||||||
|
}, [syncThemeTokens]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (themeSyncFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(themeSyncFrameRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return scheduleThemeTokenSync;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- to resolve the hs and css from root -->
|
<!-- to resolve the hs and css from root -->
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<title>excali-box</title>
|
<title>excali-box</title>
|
||||||
<script type="module" src="./client.tsx"></script>
|
<script type="module" src="./main.tsx"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
@@ -253,6 +253,22 @@ input {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--app-overlay-bg);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: var(--app-text-color);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: var(--app-sidebar-font-size);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid var(--app-sidebar-border);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-frame,
|
.editor-frame,
|
||||||
.editor-loading {
|
.editor-loading {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -87,11 +87,3 @@ export function parseJsonBody<T = unknown>(text: string): T {
|
|||||||
export function parseSceneText(text: string): ScenePayload {
|
export function parseSceneText(text: string): ScenePayload {
|
||||||
return normalizeScene(parseJsonBody(text));
|
return normalizeScene(parseJsonBody(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coerceStoredScene(input: unknown): ScenePayload {
|
|
||||||
try {
|
|
||||||
return normalizeScene(input);
|
|
||||||
} catch {
|
|
||||||
return emptyScene();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ export type DrawingMeta = {
|
|||||||
title: string;
|
title: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
revision: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DrawingRecord = DrawingMeta & {
|
export type DrawingRecord = DrawingMeta & {
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test";
|
|
||||||
import { mkdtempSync, rmSync } from "node:fs";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { createDrawingStore } from "./db";
|
|
||||||
|
|
||||||
const cleanup: string[] = [];
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
while (cleanup.length > 0) {
|
|
||||||
const dir = cleanup.pop();
|
|
||||||
if (dir) {
|
|
||||||
rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function createTempStore() {
|
|
||||||
const dir = mkdtempSync(join(tmpdir(), "excali-"));
|
|
||||||
cleanup.push(dir);
|
|
||||||
return createDrawingStore(join(dir, "test.sqlite"));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("drawing store", () => {
|
|
||||||
test("creates, updates, lists, and deletes drawings", () => {
|
|
||||||
const store = createTempStore();
|
|
||||||
const created = store.createDrawing();
|
|
||||||
|
|
||||||
expect(created.title).toBe("Untitled");
|
|
||||||
expect(created.scene.appState.theme).toBe("dark");
|
|
||||||
expect(store.listDrawings()).toHaveLength(1);
|
|
||||||
|
|
||||||
const updated = store.updateDrawing(created.id, {
|
|
||||||
title: "Flow",
|
|
||||||
scene: {
|
|
||||||
elements: [{ id: "one" }],
|
|
||||||
appState: { gridSize: 20 },
|
|
||||||
files: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updated?.title).toBe("Flow");
|
|
||||||
expect(updated?.scene.elements).toHaveLength(1);
|
|
||||||
|
|
||||||
const removed = store.deleteDrawing(created.id);
|
|
||||||
expect(removed.deleted).toBe(true);
|
|
||||||
expect(store.listDrawings()).toHaveLength(1);
|
|
||||||
expect(removed.next?.id).not.toBe(created.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { type ScenePayload } from "../core/shared";
|
||||||
|
import { createDrawingStore, type DrawingStore } from "../server/db";
|
||||||
|
import { createMcpToolHandlers, resolvePublicBaseUrl } from "./server";
|
||||||
|
|
||||||
|
const cleanup: Array<{ dir: string; store: DrawingStore }> = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (cleanup.length > 0) {
|
||||||
|
const item = cleanup.pop();
|
||||||
|
if (item) {
|
||||||
|
item.store.close();
|
||||||
|
rmSync(item.dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createTempTools() {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "excali-mcp-"));
|
||||||
|
const store = createDrawingStore(join(dir, "test.sqlite"));
|
||||||
|
cleanup.push({ dir, store });
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
tools: createMcpToolHandlers(store, "http://example.test"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function testScene(): ScenePayload {
|
||||||
|
return {
|
||||||
|
elements: [
|
||||||
|
{ id: "one", type: "rectangle", x: 10, y: 20 },
|
||||||
|
{ id: "two", type: "text", text: "hello" },
|
||||||
|
],
|
||||||
|
appState: { theme: "dark" },
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("mcp tool handlers", () => {
|
||||||
|
test("PUBLIC_BASE_URL is required", () => {
|
||||||
|
expect(() => resolvePublicBaseUrl("")).toThrow("PUBLIC_BASE_URL is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_drawing writes an explicit scene to a temp SQLite DB", () => {
|
||||||
|
const { store, tools } = createTempTools();
|
||||||
|
const scene = testScene();
|
||||||
|
|
||||||
|
const result = tools.create_drawing({ title: "explicit", scene });
|
||||||
|
const drawing = store.getDrawing(result.id);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: result.id,
|
||||||
|
title: "explicit",
|
||||||
|
revision: 1,
|
||||||
|
url: `http://example.test/d/${result.id}`,
|
||||||
|
});
|
||||||
|
expect(drawing?.revision).toBe(1);
|
||||||
|
expect(drawing?.scene).toEqual(scene);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get_drawing returns raw scene plus URL", () => {
|
||||||
|
const { tools } = createTempTools();
|
||||||
|
const scene = testScene();
|
||||||
|
const result = tools.create_drawing({ title: "read", scene });
|
||||||
|
|
||||||
|
const drawing = tools.get_drawing({ id: result.id });
|
||||||
|
|
||||||
|
expect(drawing).toMatchObject({
|
||||||
|
id: result.id,
|
||||||
|
title: "read",
|
||||||
|
revision: 1,
|
||||||
|
url: `http://example.test/d/${result.id}`,
|
||||||
|
scene,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replace_drawing updates the existing drawing", () => {
|
||||||
|
const { store, tools } = createTempTools();
|
||||||
|
const result = tools.create_drawing({ title: "replace", scene: testScene() });
|
||||||
|
const nextScene: ScenePayload = {
|
||||||
|
elements: [{ id: "manual", type: "rectangle" }],
|
||||||
|
appState: { theme: "dark" },
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaced = tools.replace_drawing({ id: result.id, title: "replaced", scene: nextScene });
|
||||||
|
|
||||||
|
expect(store.listDrawings()).toHaveLength(1);
|
||||||
|
expect(replaced.revision).toBe(2);
|
||||||
|
expect(store.getDrawing(result.id)?.title).toBe("replaced");
|
||||||
|
expect(store.getDrawing(result.id)?.revision).toBe(2);
|
||||||
|
expect(store.getDrawing(result.id)?.scene).toEqual(nextScene);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replace_drawing keeps the revision when the scene is already stored", () => {
|
||||||
|
const { store, tools } = createTempTools();
|
||||||
|
const scene = testScene();
|
||||||
|
const result = tools.create_drawing({ title: "replace noop", scene });
|
||||||
|
|
||||||
|
const replaced = tools.replace_drawing({ id: result.id, scene });
|
||||||
|
|
||||||
|
expect(replaced.revision).toBe(1);
|
||||||
|
expect(store.getDrawing(result.id)?.revision).toBe(1);
|
||||||
|
expect(store.getDrawing(result.id)?.scene).toEqual(scene);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("patch_drawing replaces an element property and removes an element", () => {
|
||||||
|
const { store, tools } = createTempTools();
|
||||||
|
const result = tools.create_drawing({ title: "patch", scene: testScene() });
|
||||||
|
|
||||||
|
const patched = tools.patch_drawing({
|
||||||
|
id: result.id,
|
||||||
|
patch: [
|
||||||
|
{ op: "replace", path: "/elements/0/x", value: 42 },
|
||||||
|
{ op: "remove", path: "/elements/1" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(patched.revision).toBe(2);
|
||||||
|
expect(store.getDrawing(result.id)?.revision).toBe(2);
|
||||||
|
expect(store.getDrawing(result.id)?.scene.elements).toEqual([{ id: "one", type: "rectangle", x: 42, y: 20 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing drawing IDs return tool errors", () => {
|
||||||
|
const { tools } = createTempTools();
|
||||||
|
|
||||||
|
expect(() => tools.get_drawing({ id: "missing" })).toThrow("Drawing not found: missing");
|
||||||
|
expect(() =>
|
||||||
|
tools.replace_drawing({
|
||||||
|
id: "missing",
|
||||||
|
scene: testScene(),
|
||||||
|
}),
|
||||||
|
).toThrow("Drawing not found: missing");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { applyPatch, type Operation } from "fast-json-patch";
|
||||||
|
import { createMcpHandler } from "mcp-handler";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { normalizeScene, normalizeTitle } from "../core/scene";
|
||||||
|
import { type ScenePayload } from "../core/shared";
|
||||||
|
import { createDrawingStore, type DrawingStore } from "../server/db";
|
||||||
|
|
||||||
|
const sceneSchema = z.object({
|
||||||
|
elements: z.array(z.unknown()),
|
||||||
|
appState: z.record(z.string(), z.unknown()),
|
||||||
|
files: z.record(z.string(), z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const patchOperationSchema = z.object({
|
||||||
|
op: z.enum(["add", "remove", "replace", "move", "copy", "test"]),
|
||||||
|
path: z.string(),
|
||||||
|
from: z.string().optional(),
|
||||||
|
value: z.unknown().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const idInput = z.object({ id: z.string().min(1) });
|
||||||
|
const createDrawingInput = z.object({ title: z.string(), scene: sceneSchema });
|
||||||
|
const replaceDrawingInput = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
title: z.string().optional(),
|
||||||
|
scene: sceneSchema,
|
||||||
|
});
|
||||||
|
const patchDrawingInput = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
title: z.string().optional(),
|
||||||
|
patch: z.array(patchOperationSchema).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ToolResult = {
|
||||||
|
content: Array<{ type: "text"; text: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function trimTrailingSlash(url: string): string {
|
||||||
|
return url.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePublicBaseUrl(raw = process.env.PUBLIC_BASE_URL): string {
|
||||||
|
const value = raw?.trim();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("PUBLIC_BASE_URL is required for the MCP server");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
if (url.protocol === "http:" || url.protocol === "https:") {
|
||||||
|
return trimTrailingSlash(url.toString());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("PUBLIC_BASE_URL must be an absolute http(s) URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawingUrl(baseUrl: string, id: string): string {
|
||||||
|
return `${trimTrailingSlash(baseUrl)}/d/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonText(data: unknown): ToolResult {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notFound(id: string): never {
|
||||||
|
throw new Error(`Drawing not found: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutationResult(publicBaseUrl: string, drawing: { id: string; title: string; revision: number }) {
|
||||||
|
return {
|
||||||
|
id: drawing.id,
|
||||||
|
title: drawing.title,
|
||||||
|
revision: drawing.revision,
|
||||||
|
url: drawingUrl(publicBaseUrl, drawing.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExistingDrawing(
|
||||||
|
store: DrawingStore,
|
||||||
|
id: string,
|
||||||
|
update: { scene?: ScenePayload; title?: string },
|
||||||
|
) {
|
||||||
|
const result = store.updateDrawing(id, update);
|
||||||
|
if (!result.ok) {
|
||||||
|
return notFound(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.drawing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyScenePatch(scene: ScenePayload, patch: Operation[]): ScenePayload {
|
||||||
|
try {
|
||||||
|
const result = applyPatch(structuredClone(scene), patch, true, true, true);
|
||||||
|
return normalizeScene(result.newDocument);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error instanceof Error ? `Invalid JSON Patch: ${error.message}` : "Invalid JSON Patch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMcpToolHandlers(store: DrawingStore, publicBaseUrl: string) {
|
||||||
|
return {
|
||||||
|
list_drawings() {
|
||||||
|
return store.listDrawings();
|
||||||
|
},
|
||||||
|
|
||||||
|
get_drawing(input: unknown) {
|
||||||
|
const { id } = idInput.parse(input);
|
||||||
|
const drawing = store.getDrawing(id) ?? notFound(id);
|
||||||
|
return {
|
||||||
|
id: drawing.id,
|
||||||
|
title: drawing.title,
|
||||||
|
createdAt: drawing.createdAt,
|
||||||
|
updatedAt: drawing.updatedAt,
|
||||||
|
revision: drawing.revision,
|
||||||
|
url: drawingUrl(publicBaseUrl, drawing.id),
|
||||||
|
scene: drawing.scene,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
create_drawing(input: unknown) {
|
||||||
|
const { title, scene } = createDrawingInput.parse(input);
|
||||||
|
const created = store.createDrawing(normalizeTitle(title));
|
||||||
|
const updated = updateExistingDrawing(store, created.id, {
|
||||||
|
title,
|
||||||
|
scene: normalizeScene(scene),
|
||||||
|
});
|
||||||
|
return mutationResult(publicBaseUrl, updated);
|
||||||
|
},
|
||||||
|
|
||||||
|
replace_drawing(input: unknown) {
|
||||||
|
const { id, title, scene } = replaceDrawingInput.parse(input);
|
||||||
|
const updated = updateExistingDrawing(store, id, {
|
||||||
|
title,
|
||||||
|
scene: normalizeScene(scene),
|
||||||
|
});
|
||||||
|
return mutationResult(publicBaseUrl, updated);
|
||||||
|
},
|
||||||
|
|
||||||
|
patch_drawing(input: unknown) {
|
||||||
|
const { id, title, patch } = patchDrawingInput.parse(input);
|
||||||
|
const drawing = store.getDrawing(id) ?? notFound(id);
|
||||||
|
const updated = updateExistingDrawing(store, id, {
|
||||||
|
title,
|
||||||
|
scene: applyScenePatch(drawing.scene, patch as Operation[]),
|
||||||
|
});
|
||||||
|
return mutationResult(publicBaseUrl, updated);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExcaliMcpHandler(store: DrawingStore, publicBaseUrl: string) {
|
||||||
|
const tools = createMcpToolHandlers(store, publicBaseUrl);
|
||||||
|
|
||||||
|
return createMcpHandler(
|
||||||
|
(server) => {
|
||||||
|
server.registerTool(
|
||||||
|
"list_drawings",
|
||||||
|
{
|
||||||
|
title: "List Drawings",
|
||||||
|
description: "Returns drawing metadata ordered like the app list.",
|
||||||
|
},
|
||||||
|
async () => jsonText(tools.list_drawings()),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"get_drawing",
|
||||||
|
{
|
||||||
|
title: "Get Drawing",
|
||||||
|
description: "Returns drawing metadata, browser URL, and raw ScenePayload.",
|
||||||
|
inputSchema: idInput.shape,
|
||||||
|
},
|
||||||
|
async (input) => jsonText(tools.get_drawing(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"create_drawing",
|
||||||
|
{
|
||||||
|
title: "Create Drawing",
|
||||||
|
description: "Creates a drawing from an explicit Excalidraw ScenePayload.",
|
||||||
|
inputSchema: createDrawingInput.shape,
|
||||||
|
},
|
||||||
|
async (input) => jsonText(tools.create_drawing(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"replace_drawing",
|
||||||
|
{
|
||||||
|
title: "Replace Drawing",
|
||||||
|
description: "Replaces a drawing scene and optionally its title.",
|
||||||
|
inputSchema: replaceDrawingInput.shape,
|
||||||
|
},
|
||||||
|
async (input) => jsonText(tools.replace_drawing(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"patch_drawing",
|
||||||
|
{
|
||||||
|
title: "Patch Drawing",
|
||||||
|
description: "Applies RFC 6902 JSON Patch operations to a drawing scene and optionally updates its title.",
|
||||||
|
inputSchema: patchDrawingInput.shape,
|
||||||
|
},
|
||||||
|
async (input) => jsonText(tools.patch_drawing(input)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverInfo: {
|
||||||
|
name: "excali",
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
basePath: "",
|
||||||
|
disableSse: true,
|
||||||
|
maxDuration: 60,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMcpServer() {
|
||||||
|
const store = createDrawingStore(process.env.DATABASE_PATH);
|
||||||
|
const handler = createExcaliMcpHandler(store, resolvePublicBaseUrl());
|
||||||
|
|
||||||
|
return {
|
||||||
|
port: Number(process.env.MCP_PORT ?? "3001"),
|
||||||
|
hostname: process.env.MCP_HOST ?? "127.0.0.1",
|
||||||
|
fetch(request: Request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname !== "/mcp") {
|
||||||
|
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(request);
|
||||||
|
},
|
||||||
|
} satisfies Parameters<typeof Bun.serve>[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const server = Bun.serve(createMcpServer());
|
||||||
|
console.log(`MCP listening on http://${server.hostname}:${server.port}/mcp`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createApi } from "./api";
|
||||||
|
import { createDrawingStore } from "./db";
|
||||||
|
|
||||||
|
function withApi() {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "excali-api-"));
|
||||||
|
const store = createDrawingStore(join(dir, "test.sqlite"));
|
||||||
|
const api = createApi(store);
|
||||||
|
|
||||||
|
return {
|
||||||
|
api,
|
||||||
|
cleanup() {
|
||||||
|
store.close();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("api", () => {
|
||||||
|
test("creates a drawing with dark theme by default", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
|
||||||
|
const created = await api.createDrawing().json();
|
||||||
|
|
||||||
|
expect(created.appState.theme).toBe("dark");
|
||||||
|
expect(created.revision).toBe(0);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 for invalid JSON", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
const drawing = await api.createDrawing().json();
|
||||||
|
|
||||||
|
const response = await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${drawing.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: "{",
|
||||||
|
}),
|
||||||
|
{ params: { id: drawing.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 404 for missing drawing", () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
|
||||||
|
const response = api.getDrawing(
|
||||||
|
Object.assign(new Request("http://local/api/drawings/missing"), { params: { id: "missing" } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates a drawing scene", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
const created = await api.createDrawing().json();
|
||||||
|
|
||||||
|
const response = await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
elements: [{ id: "shape" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
expectedRevision: created.revision,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.drawing.revision).toBe(1);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not increment revisions for equivalent normalized scenes", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
const created = await api.createDrawing().json();
|
||||||
|
|
||||||
|
await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
elements: [{ id: "shape" }],
|
||||||
|
appState: { gridSize: 20 },
|
||||||
|
files: {},
|
||||||
|
expectedRevision: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
elements: [{ id: "shape" }],
|
||||||
|
appState: { gridSize: 20, selectedElementIds: { shape: true } },
|
||||||
|
files: {},
|
||||||
|
expectedRevision: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.drawing.revision).toBe(1);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates scene and title with a matching expected revision", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
const created = await api.createDrawing().json();
|
||||||
|
|
||||||
|
const response = await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: "Synced",
|
||||||
|
elements: [{ id: "shape" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
expectedRevision: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.drawing.title).toBe("Synced");
|
||||||
|
expect(body.drawing.revision).toBe(1);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 409 for stale expected revisions without overwriting", async () => {
|
||||||
|
const { api, cleanup } = withApi();
|
||||||
|
const created = await api.createDrawing().json();
|
||||||
|
|
||||||
|
await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
elements: [{ id: "server" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
expectedRevision: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const conflict = await api.updateDrawing(
|
||||||
|
Object.assign(
|
||||||
|
new Request(`http://local/api/drawings/${created.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
elements: [{ id: "stale" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
expectedRevision: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ params: { id: created.id } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(conflict.status).toBe(409);
|
||||||
|
const body = await conflict.json();
|
||||||
|
expect(body).toEqual({
|
||||||
|
ok: false,
|
||||||
|
error: "Drawing changed externally",
|
||||||
|
drawing: {
|
||||||
|
id: created.id,
|
||||||
|
title: created.title,
|
||||||
|
createdAt: body.drawing.createdAt,
|
||||||
|
updatedAt: body.drawing.updatedAt,
|
||||||
|
revision: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stored = await api.getDrawing(
|
||||||
|
Object.assign(new Request(`http://local/api/drawings/${created.id}`), { params: { id: created.id } }),
|
||||||
|
).json();
|
||||||
|
expect(stored.elements).toEqual([{ id: "server" }]);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type DrawingStore } from "./db";
|
import { type DrawingStore } from "./db";
|
||||||
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "./scene";
|
import { type DrawingMeta } from "../core/shared";
|
||||||
|
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "../core/scene";
|
||||||
|
|
||||||
type RouteRequest = Request & {
|
type RouteRequest = Request & {
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
@@ -14,10 +15,37 @@ function errorResponse(error: unknown): Response {
|
|||||||
return json({ ok: false, error: error.message }, { status: error.status });
|
return json({ ok: false, error: error.message }, { status: error.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
return json({ ok: false, error: "Request aborted" }, { status: 499 });
|
||||||
|
}
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return json({ ok: false, error: "Internal server error" }, { status: 500 });
|
return json({ ok: false, error: "Internal server error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toMeta(drawing: DrawingMeta): DrawingMeta {
|
||||||
|
return {
|
||||||
|
id: drawing.id,
|
||||||
|
title: drawing.title,
|
||||||
|
createdAt: drawing.createdAt,
|
||||||
|
updatedAt: drawing.updatedAt,
|
||||||
|
revision: drawing.revision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExpectedRevision(raw: Record<string, unknown>): number | undefined {
|
||||||
|
if (!Object.hasOwn(raw, "expectedRevision")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = raw.expectedRevision;
|
||||||
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
||||||
|
throw new HttpError(400, "expectedRevision must be a non-negative integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function createApi(store: DrawingStore) {
|
export function createApi(store: DrawingStore) {
|
||||||
return {
|
return {
|
||||||
health() {
|
health() {
|
||||||
@@ -32,10 +60,7 @@ export function createApi(store: DrawingStore) {
|
|||||||
const drawing = store.createDrawing();
|
const drawing = store.createDrawing();
|
||||||
return json(
|
return json(
|
||||||
{
|
{
|
||||||
id: drawing.id,
|
...toMeta(drawing),
|
||||||
title: drawing.title,
|
|
||||||
createdAt: drawing.createdAt,
|
|
||||||
updatedAt: drawing.updatedAt,
|
|
||||||
...drawing.scene,
|
...drawing.scene,
|
||||||
},
|
},
|
||||||
{ status: 201 },
|
{ status: 201 },
|
||||||
@@ -43,20 +68,21 @@ export function createApi(store: DrawingStore) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getDrawing(request: RouteRequest) {
|
getDrawing(request: RouteRequest) {
|
||||||
const id = request.params?.id;
|
try {
|
||||||
const drawing = id ? store.getDrawing(id) : null;
|
const id = request.params?.id;
|
||||||
|
const drawing = id ? store.getDrawing(id) : null;
|
||||||
|
|
||||||
if (!drawing) {
|
if (!drawing) {
|
||||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({
|
||||||
|
...toMeta(drawing),
|
||||||
|
...drawing.scene,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({
|
|
||||||
id: drawing.id,
|
|
||||||
title: drawing.title,
|
|
||||||
createdAt: drawing.createdAt,
|
|
||||||
updatedAt: drawing.updatedAt,
|
|
||||||
...drawing.scene,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateDrawing(request: RouteRequest) {
|
async updateDrawing(request: RouteRequest) {
|
||||||
@@ -73,29 +99,38 @@ export function createApi(store: DrawingStore) {
|
|||||||
Object.hasOwn(raw, "elements") ||
|
Object.hasOwn(raw, "elements") ||
|
||||||
Object.hasOwn(raw, "appState") ||
|
Object.hasOwn(raw, "appState") ||
|
||||||
Object.hasOwn(raw, "files");
|
Object.hasOwn(raw, "files");
|
||||||
|
const expectedRevision = parseExpectedRevision(raw);
|
||||||
|
|
||||||
if (!hasTitle && !hasScene) {
|
if (!hasTitle && !hasScene) {
|
||||||
throw new HttpError(400, "Nothing to update");
|
throw new HttpError(400, "Nothing to update");
|
||||||
}
|
}
|
||||||
|
|
||||||
const scene = hasScene ? parseSceneText(text) : undefined;
|
const scene = hasScene ? parseSceneText(text) : undefined;
|
||||||
const updated = store.updateDrawing(id, {
|
const result = store.updateDrawing(id, {
|
||||||
scene,
|
scene,
|
||||||
title: hasTitle ? normalizeTitle(raw.title) : undefined,
|
title: hasTitle ? normalizeTitle(raw.title) : undefined,
|
||||||
|
expectedRevision,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updated) {
|
if (!result.ok && result.reason === "not_found") {
|
||||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!result.ok && result.reason === "conflict") {
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: "Drawing changed externally",
|
||||||
|
drawing: toMeta(result.drawing),
|
||||||
|
},
|
||||||
|
{ status: 409 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = result.drawing;
|
||||||
return json({
|
return json({
|
||||||
ok: true,
|
ok: true,
|
||||||
drawing: {
|
drawing: toMeta(updated),
|
||||||
id: updated.id,
|
|
||||||
title: updated.title,
|
|
||||||
createdAt: updated.createdAt,
|
|
||||||
updatedAt: updated.updatedAt,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error);
|
return errorResponse(error);
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createDrawingStore } from "./db";
|
||||||
|
|
||||||
|
const cleanup: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (cleanup.length > 0) {
|
||||||
|
const dir = cleanup.pop();
|
||||||
|
if (dir) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createTempStore() {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "excali-"));
|
||||||
|
cleanup.push(dir);
|
||||||
|
return createDrawingStore(join(dir, "test.sqlite"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("drawing store", () => {
|
||||||
|
test("creates, updates, lists, and deletes drawings", () => {
|
||||||
|
const store = createTempStore();
|
||||||
|
const created = store.createDrawing();
|
||||||
|
|
||||||
|
expect(created.title).toBe("Untitled");
|
||||||
|
expect(created.revision).toBe(0);
|
||||||
|
expect(created.scene.appState.theme).toBe("dark");
|
||||||
|
expect(store.listDrawings()).toHaveLength(1);
|
||||||
|
expect(store.listDrawings()[0]?.revision).toBe(0);
|
||||||
|
|
||||||
|
const updated = store.updateDrawing(created.id, {
|
||||||
|
title: "Flow",
|
||||||
|
scene: {
|
||||||
|
elements: [{ id: "one" }],
|
||||||
|
appState: { gridSize: 20 },
|
||||||
|
files: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated.ok).toBe(true);
|
||||||
|
expect(updated.ok ? updated.drawing.title : null).toBe("Flow");
|
||||||
|
expect(updated.ok ? updated.drawing.revision : null).toBe(1);
|
||||||
|
expect(updated.ok ? updated.drawing.scene.elements : []).toHaveLength(1);
|
||||||
|
|
||||||
|
const removed = store.deleteDrawing(created.id);
|
||||||
|
expect(removed.deleted).toBe(true);
|
||||||
|
expect(store.listDrawings()).toHaveLength(1);
|
||||||
|
expect(removed.next?.id).not.toBe(created.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects stale expected revisions without overwriting the scene", () => {
|
||||||
|
const store = createTempStore();
|
||||||
|
const created = store.createDrawing();
|
||||||
|
|
||||||
|
const first = store.updateDrawing(created.id, {
|
||||||
|
expectedRevision: 0,
|
||||||
|
scene: {
|
||||||
|
elements: [{ id: "first" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first.ok).toBe(true);
|
||||||
|
|
||||||
|
const stale = store.updateDrawing(created.id, {
|
||||||
|
expectedRevision: 0,
|
||||||
|
scene: {
|
||||||
|
elements: [{ id: "stale" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stale.ok).toBe(false);
|
||||||
|
expect(stale.ok ? null : stale.reason).toBe("conflict");
|
||||||
|
expect(store.getDrawing(created.id)?.revision).toBe(1);
|
||||||
|
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "first" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the revision unchanged when the scene is already stored", () => {
|
||||||
|
const store = createTempStore();
|
||||||
|
const created = store.createDrawing();
|
||||||
|
const scene = {
|
||||||
|
elements: [{ id: "same" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = store.updateDrawing(created.id, { scene });
|
||||||
|
expect(first.ok ? first.drawing.revision : null).toBe(1);
|
||||||
|
|
||||||
|
const repeated = store.updateDrawing(created.id, { scene });
|
||||||
|
expect(repeated.ok).toBe(true);
|
||||||
|
expect(repeated.ok ? repeated.drawing.revision : null).toBe(1);
|
||||||
|
expect(store.getDrawing(created.id)?.revision).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts stale expected revisions when the scene is already stored", () => {
|
||||||
|
const store = createTempStore();
|
||||||
|
const created = store.createDrawing();
|
||||||
|
const scene = {
|
||||||
|
elements: [{ id: "same" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = store.updateDrawing(created.id, { expectedRevision: 0, scene });
|
||||||
|
expect(first.ok ? first.drawing.revision : null).toBe(1);
|
||||||
|
|
||||||
|
const repeated = store.updateDrawing(created.id, { expectedRevision: 0, scene });
|
||||||
|
expect(repeated.ok).toBe(true);
|
||||||
|
expect(repeated.ok ? repeated.drawing.revision : null).toBe(1);
|
||||||
|
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "same" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("increments the revision when the title changes but the scene does not", () => {
|
||||||
|
const store = createTempStore();
|
||||||
|
const created = store.createDrawing();
|
||||||
|
const scene = {
|
||||||
|
elements: [{ id: "same" }],
|
||||||
|
appState: {},
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = store.updateDrawing(created.id, { expectedRevision: 0, scene });
|
||||||
|
expect(first.ok ? first.drawing.revision : null).toBe(1);
|
||||||
|
|
||||||
|
const renamed = store.updateDrawing(created.id, { expectedRevision: 1, title: "Renamed", scene });
|
||||||
|
expect(renamed.ok).toBe(true);
|
||||||
|
expect(renamed.ok ? renamed.drawing.title : null).toBe("Renamed");
|
||||||
|
expect(renamed.ok ? renamed.drawing.revision : null).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,8 +2,8 @@ import { Database } from "bun:sqlite";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "./shared";
|
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "../core/shared";
|
||||||
import { coerceStoredScene, emptyScene, normalizeTitle } from "./scene";
|
import { emptyScene, normalizeScene, normalizeTitle } from "../core/scene";
|
||||||
|
|
||||||
type DrawingRow = {
|
type DrawingRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,10 +11,16 @@ type DrawingRow = {
|
|||||||
data: string;
|
data: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
revision: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DrawingMetaRow = Omit<DrawingRow, "data">;
|
type DrawingMetaRow = Omit<DrawingRow, "data">;
|
||||||
|
|
||||||
|
export type DrawingUpdateResult =
|
||||||
|
| { ok: true; drawing: DrawingRecord }
|
||||||
|
| { ok: false; reason: "not_found" }
|
||||||
|
| { ok: false; reason: "conflict"; drawing: DrawingRecord };
|
||||||
|
|
||||||
export type DrawingStore = ReturnType<typeof createDrawingStore>;
|
export type DrawingStore = ReturnType<typeof createDrawingStore>;
|
||||||
|
|
||||||
function createId(): string {
|
function createId(): string {
|
||||||
@@ -44,6 +50,7 @@ function readMeta(row: DrawingMetaRow | null | undefined): DrawingMeta | null {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
|
revision: row.revision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +59,17 @@ function readRow(row: DrawingRow | null | undefined): DrawingRecord | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scene: ScenePayload;
|
||||||
|
try {
|
||||||
|
scene = normalizeScene(JSON.parse(row.data));
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown scene parsing error";
|
||||||
|
throw new Error(`Invalid stored scene for drawing ${row.id}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...readMeta(row)!,
|
...readMeta(row)!,
|
||||||
scene: coerceStoredScene(JSON.parse(row.data)),
|
scene,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +85,8 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
data TEXT NOT NULL,
|
data TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS drawings_updated_at_idx
|
CREATE INDEX IF NOT EXISTS drawings_updated_at_idx
|
||||||
@@ -82,7 +98,8 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
created_at AS createdAt,
|
created_at AS createdAt,
|
||||||
updated_at AS updatedAt
|
updated_at AS updatedAt,
|
||||||
|
revision
|
||||||
FROM drawings
|
FROM drawings
|
||||||
ORDER BY updated_at DESC, created_at DESC
|
ORDER BY updated_at DESC, created_at DESC
|
||||||
`);
|
`);
|
||||||
@@ -93,7 +110,8 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
title,
|
title,
|
||||||
data,
|
data,
|
||||||
created_at AS createdAt,
|
created_at AS createdAt,
|
||||||
updated_at AS updatedAt
|
updated_at AS updatedAt,
|
||||||
|
revision
|
||||||
FROM drawings
|
FROM drawings
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
`);
|
`);
|
||||||
@@ -105,22 +123,40 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
|
|
||||||
const updateSceneQuery = db.query(`
|
const updateSceneQuery = db.query(`
|
||||||
UPDATE drawings
|
UPDATE drawings
|
||||||
SET data = ?2, updated_at = CURRENT_TIMESTAMP
|
SET data = ?2, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const updateTitleQuery = db.query(`
|
const updateTitleQuery = db.query(`
|
||||||
UPDATE drawings
|
UPDATE drawings
|
||||||
SET title = ?2, updated_at = CURRENT_TIMESTAMP
|
SET title = ?2, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const updateBothQuery = db.query(`
|
const updateBothQuery = db.query(`
|
||||||
UPDATE drawings
|
UPDATE drawings
|
||||||
SET title = ?2, data = ?3, updated_at = CURRENT_TIMESTAMP
|
SET title = ?2, data = ?3, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const updateSceneExpectedQuery = db.query(`
|
||||||
|
UPDATE drawings
|
||||||
|
SET data = ?2, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
|
WHERE id = ?1 AND revision = ?3
|
||||||
|
`);
|
||||||
|
|
||||||
|
const updateTitleExpectedQuery = db.query(`
|
||||||
|
UPDATE drawings
|
||||||
|
SET title = ?2, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
|
WHERE id = ?1 AND revision = ?3
|
||||||
|
`);
|
||||||
|
|
||||||
|
const updateBothExpectedQuery = db.query(`
|
||||||
|
UPDATE drawings
|
||||||
|
SET title = ?2, data = ?3, updated_at = CURRENT_TIMESTAMP, revision = revision + 1
|
||||||
|
WHERE id = ?1 AND revision = ?4
|
||||||
|
`);
|
||||||
|
|
||||||
const deleteQuery = db.query(`
|
const deleteQuery = db.query(`
|
||||||
DELETE FROM drawings
|
DELETE FROM drawings
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
@@ -148,28 +184,64 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
return getLatestDrawing() ?? createDrawing();
|
return getLatestDrawing() ?? createDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrawing(id: string, update: { scene?: ScenePayload; title?: string }): DrawingRecord | null {
|
function updateDrawing(
|
||||||
const existing = getDrawing(id);
|
id: string,
|
||||||
if (!existing) {
|
update: { scene?: ScenePayload; title?: string; expectedRevision?: number },
|
||||||
return null;
|
): DrawingUpdateResult {
|
||||||
}
|
|
||||||
|
|
||||||
const hasScene = update.scene !== undefined;
|
const hasScene = update.scene !== undefined;
|
||||||
const hasTitle = update.title !== undefined;
|
const hasTitle = update.title !== undefined;
|
||||||
|
const existingRow = getQuery.get(id) as DrawingRow | null | undefined;
|
||||||
|
|
||||||
|
if (!existingRow) {
|
||||||
|
return { ok: false, reason: "not_found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = readRow(existingRow)!;
|
||||||
if (!hasScene && !hasTitle) {
|
if (!hasScene && !hasTitle) {
|
||||||
return existing;
|
return { ok: true, drawing: existing };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasScene && hasTitle) {
|
const nextTitle = hasTitle ? normalizeTitle(update.title) : existingRow.title;
|
||||||
updateBothQuery.run(id, normalizeTitle(update.title), JSON.stringify(update.scene));
|
const nextScene = hasScene ? JSON.stringify(update.scene) : existingRow.data;
|
||||||
} else if (hasScene) {
|
const titleChanged = nextTitle !== existingRow.title;
|
||||||
updateSceneQuery.run(id, JSON.stringify(update.scene));
|
const sceneChanged = nextScene !== existingRow.data;
|
||||||
|
|
||||||
|
if (!titleChanged && !sceneChanged) {
|
||||||
|
return { ok: true, drawing: existing };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.expectedRevision !== undefined && update.expectedRevision !== existingRow.revision) {
|
||||||
|
return { ok: false, reason: "conflict", drawing: existing };
|
||||||
|
}
|
||||||
|
|
||||||
|
let changes: number;
|
||||||
|
if (sceneChanged && titleChanged) {
|
||||||
|
changes =
|
||||||
|
update.expectedRevision === undefined
|
||||||
|
? Number(updateBothQuery.run(id, nextTitle, nextScene).changes)
|
||||||
|
: Number(updateBothExpectedQuery.run(id, nextTitle, nextScene, update.expectedRevision).changes);
|
||||||
|
} else if (sceneChanged) {
|
||||||
|
changes =
|
||||||
|
update.expectedRevision === undefined
|
||||||
|
? Number(updateSceneQuery.run(id, nextScene).changes)
|
||||||
|
: Number(updateSceneExpectedQuery.run(id, nextScene, update.expectedRevision).changes);
|
||||||
} else {
|
} else {
|
||||||
updateTitleQuery.run(id, normalizeTitle(update.title));
|
changes =
|
||||||
|
update.expectedRevision === undefined
|
||||||
|
? Number(updateTitleQuery.run(id, nextTitle).changes)
|
||||||
|
: Number(updateTitleExpectedQuery.run(id, nextTitle, update.expectedRevision).changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDrawing(id);
|
const drawing = getDrawing(id);
|
||||||
|
if (changes > 0 && drawing) {
|
||||||
|
return { ok: true, drawing };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drawing) {
|
||||||
|
return { ok: false, reason: "not_found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, reason: "conflict", drawing };
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDrawing(id: string): { deleted: boolean; next: DrawingMeta | null } {
|
function deleteDrawing(id: string): { deleted: boolean; next: DrawingMeta | null } {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import index from "./index.html";
|
import index from "../client/index.html";
|
||||||
import { createServer } from "./server";
|
import { createServer } from "./http-server";
|
||||||
|
|
||||||
export function createDevServer() {
|
export function createDevServer() {
|
||||||
const server = createServer();
|
const server = createServer();
|
||||||
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { createDrawingStore, type DrawingStore } from "./db";
|
import { createDrawingStore, type DrawingStore } from "./db";
|
||||||
import { createServer } from "./server";
|
import { createServer } from "./http-server";
|
||||||
|
|
||||||
const cleanup: string[] = [];
|
const cleanup: string[] = [];
|
||||||
const stores: DrawingStore[] = [];
|
const stores: DrawingStore[] = [];
|
||||||
Reference in New Issue
Block a user