Compare commits
114 Commits
e2b-test
...
fix/path-o
Author | SHA1 | Date | |
---|---|---|---|
2c058b259a | |||
5817b2ea48 | |||
6845e1fef9 | |||
f38919d6cf | |||
7aaa920815 | |||
6bfff62513 | |||
0b7cc51c6e | |||
a353863523 | |||
2f88ff6d58 | |||
48731848dd | |||
0509716f34 | |||
3fcfe5a3dc | |||
06118e98e9 | |||
4ebd6dea96 | |||
6e8eee246f | |||
8921cd83bb | |||
45097e0f20 | |||
62e6d64a52 | |||
982a6edc26 | |||
300de1f03a | |||
02deea9c93 | |||
653142dd1d | |||
0c6b2b0dfb | |||
31d74ddc2d | |||
62311faf51 | |||
208d17879f | |||
0067dc8c0c | |||
ec24e64b17 | |||
4fe749daf2 | |||
b8398cc4c2 | |||
0e4649b2c9 | |||
d74205c909 | |||
fa998d9069 | |||
b01934bd20 | |||
a1990a189c | |||
bf79893dfa | |||
47324f15bf | |||
f5b04f9f49 | |||
7149925539 | |||
169319de14 | |||
665e36603f | |||
0679f99bb7 | |||
2dbdf51fd3 | |||
2065814aaa | |||
1502047bf2 | |||
7b2ed21288 | |||
bbd47db467 | |||
2da60ff4e4 | |||
f4a84bd4b6 | |||
a15c1f15f5 | |||
ae7ff3f46b | |||
171a9ce3c6 | |||
f1a65106b0 | |||
7559e9804f | |||
5132850cb0 | |||
5726cecb22 | |||
6b761cc490 | |||
c674c0cab6 | |||
08c131b52d | |||
618c1e81b1 | |||
c2f4f0b6ff | |||
98da0487e4 | |||
71004c61b2 | |||
170bb45143 | |||
cd59b19ac7 | |||
61235551d3 | |||
86db64a83b | |||
d4c65ad1a3 | |||
aac602d9db | |||
2eb2c4c39b | |||
e8a3944b9e | |||
d0a9c8548c | |||
6c615f1a4f | |||
6a31161c0a | |||
a74f7bf71a | |||
2e68b0b537 | |||
02ea851fb7 | |||
7ed19188d4 | |||
74a4352323 | |||
870783940d | |||
051bf1164a | |||
deb32352fb | |||
de4923ec1e | |||
769f52816f | |||
49ca13a6c8 | |||
dead84ac4d | |||
478a332a2e | |||
2163b1dfb7 | |||
8c3e40975e | |||
62a3d6d8f7 | |||
08d562ee54 | |||
db1410f587 | |||
7a80734c25 | |||
0a21cb2637 | |||
7dd67f72d8 | |||
5bf264b807 | |||
6f6926a621 | |||
1c860bd4d9 | |||
c5247a2aaa | |||
94df975842 | |||
2fbabbd403 | |||
9f0b6a8fdc | |||
c262fb2a31 | |||
ed709210e3 | |||
97c8598717 | |||
9ec59bc781 | |||
687416e6e9 | |||
006c5cea66 | |||
869ae6c148 | |||
7353e88567 | |||
a0fb905a04 | |||
0df074924f | |||
e5b320d1c5 | |||
b561f1e962 |
@ -29,7 +29,9 @@ npm run dev
|
|||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
The backend consists of a primary Express and Socket.io server, and 3 Cloudflare Workers microservices for the D1 database, R2 storage, and Workers AI. The D1 database also contains a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to the R2 storage worker.
|
The backend consists of a primary Express and Socket.io server, and 3 Cloudflare Workers microservices for the D1 database, R2 storage, and Workers AI. The D1 database also contains a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to the R2 storage worker. Each open sandbox instantiates a secure Linux sandboxes on E2B, which is used for the terminal and live preview.
|
||||||
|
|
||||||
|
You will need to make an account on [E2B](https://e2b.dev/) to get an API key.
|
||||||
|
|
||||||
#### Socket.io server
|
#### Socket.io server
|
||||||
|
|
||||||
@ -181,3 +183,4 @@ It should be in the form `category(scope or module): message` in your commit mes
|
|||||||
- [Express](https://expressjs.com/)
|
- [Express](https://expressjs.com/)
|
||||||
- [Socket.io](https://socket.io/)
|
- [Socket.io](https://socket.io/)
|
||||||
- [Drizzle ORM](https://orm.drizzle.team/)
|
- [Drizzle ORM](https://orm.drizzle.team/)
|
||||||
|
- [E2B](https://e2b.dev/)
|
||||||
|
216
backend/ai/package-lock.json
generated
216
backend/ai/package-lock.json
generated
@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "ai",
|
"name": "ai",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.27.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vitest-pool-workers": "^0.1.0",
|
"@cloudflare/vitest-pool-workers": "^0.1.0",
|
||||||
"@cloudflare/workers-types": "^4.20240512.0",
|
"@cloudflare/workers-types": "^4.20240512.0",
|
||||||
@ -15,6 +18,28 @@
|
|||||||
"wrangler": "^3.0.0"
|
"wrangler": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-Q6gOx4fyHQ+NCSaVeXEKFZfoFWCR3ctUA+sK5oGB7RKUkzUvK64aYM7v1T9ekJKwn8TwRq6IGjqS31n9PbjCIA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/node-fetch": "^2.6.4",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"agentkeepalive": "^4.2.1",
|
||||||
|
"form-data-encoder": "1.7.2",
|
||||||
|
"formdata-node": "^4.3.2",
|
||||||
|
"node-fetch": "^2.6.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||||
|
"version": "18.19.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz",
|
||||||
|
"integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cloudflare/kv-asset-handler": {
|
"node_modules/@cloudflare/kv-asset-handler": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz",
|
||||||
@ -861,11 +886,19 @@
|
|||||||
"version": "20.12.11",
|
"version": "20.12.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
||||||
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-fetch": {
|
||||||
|
"version": "2.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||||
|
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node-forge": {
|
"node_modules/@types/node-forge": {
|
||||||
"version": "1.3.11",
|
"version": "1.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
|
||||||
@ -944,6 +977,17 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.3",
|
"version": "8.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||||
@ -965,6 +1009,17 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agentkeepalive": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
|
||||||
|
"dependencies": {
|
||||||
|
"humanize-ms": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
@ -1008,6 +1063,11 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -1141,6 +1201,17 @@
|
|||||||
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
|
||||||
@ -1214,6 +1285,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/devalue": {
|
"node_modules/devalue": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
|
||||||
@ -1287,6 +1366,14 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
@ -1334,6 +1421,36 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data-encoder": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
|
||||||
|
},
|
||||||
|
"node_modules/formdata-node": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "1.0.0",
|
||||||
|
"web-streams-polyfill": "4.0.0-beta.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -1452,6 +1569,14 @@
|
|||||||
"node": ">=16.17.0"
|
"node": ">=16.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/humanize-ms": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@ -1610,6 +1735,25 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-fn": {
|
"node_modules/mimic-fn": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
@ -1675,8 +1819,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/mustache": {
|
"node_modules/mustache": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
@ -1705,6 +1848,43 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
@ -2222,6 +2402,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
"node_modules/ts-json-schema-generator": {
|
"node_modules/ts-json-schema-generator": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz",
|
||||||
@ -2292,8 +2477,7 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.2.11",
|
"version": "5.2.11",
|
||||||
@ -2827,6 +3011,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "4.0.0-beta.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||||
|
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -15,5 +15,8 @@
|
|||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vitest": "1.3.0",
|
"vitest": "1.3.0",
|
||||||
"wrangler": "^3.0.0"
|
"wrangler": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.27.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,77 @@
|
|||||||
|
import { Anthropic } from "@anthropic-ai/sdk";
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
AI: any
|
ANTHROPIC_API_KEY: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request, env): Promise<Response> {
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
if (request.method !== "GET") {
|
if (request.method !== "GET") {
|
||||||
return new Response("Method Not Allowed", { status: 405 })
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url);
|
||||||
const fileName = url.searchParams.get("fileName")
|
// const fileName = url.searchParams.get("fileName");
|
||||||
const instructions = url.searchParams.get("instructions")
|
// const line = url.searchParams.get("line");
|
||||||
const line = url.searchParams.get("line")
|
const instructions = url.searchParams.get("instructions");
|
||||||
const code = url.searchParams.get("code")
|
const code = url.searchParams.get("code");
|
||||||
|
|
||||||
const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
|
const prompt = `
|
||||||
messages: [
|
Make the following changes to the code below:
|
||||||
{
|
- ${instructions}
|
||||||
role: "system",
|
|
||||||
content:
|
|
||||||
"You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is CORRECT, efficient, and follows best practices. You may generate multiple lines of code if necessary. When you generate code, you should ONLY return the code, and nothing else. You MUST NOT include backticks in the code you generate.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `The file is called ${fileName}.`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `Here are my instructions on what to generate: ${instructions}.`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
|
|
||||||
|
|
||||||
${code}`,
|
Return the complete code chunk. Do not refer to other code files. Do not add code before or after the chunk. Start your reponse with \`\`\`, and end with \`\`\`. Do not include any other text.
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(response))
|
\`\`\`
|
||||||
|
${code}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
console.log(prompt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
|
||||||
|
|
||||||
|
interface TextBlock {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolUseBlock {
|
||||||
|
type: "tool_use";
|
||||||
|
tool_use: {
|
||||||
|
// Add properties if needed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContentBlock = TextBlock | ToolUseBlock;
|
||||||
|
|
||||||
|
function getTextContent(content: ContentBlock[]): string {
|
||||||
|
for (const block of content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
return block.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "No text content found";
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await anthropic.messages.create({
|
||||||
|
model: "claude-3-5-sonnet-20240620",
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = response.content as ContentBlock[];
|
||||||
|
const textBlockContent = getTextContent(message);
|
||||||
|
|
||||||
|
const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/;
|
||||||
|
const match = textBlockContent.match(pattern);
|
||||||
|
|
||||||
|
const codeContent = match ? match[1] : "Error: Could not extract code.";
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ "response": codeContent }))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return new Response("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
} satisfies ExportedHandler<Env>
|
};
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
# e2b template build --name "terminal"
|
|
||||||
|
|
||||||
# Use a Debian-based base image
|
|
||||||
FROM ubuntu:22.04
|
|
||||||
|
|
||||||
# Install dependencies and customize sandbox
|
|
||||||
RUN apt update \
|
|
||||||
&& apt install -y sudo
|
|
||||||
|
|
||||||
# Install xterm
|
|
||||||
RUN apt update \
|
|
||||||
&& apt install -y xterm
|
|
||||||
|
|
||||||
RUN apt update \
|
|
||||||
&& apt install -y tmux screen
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
RUN apt clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
@ -1,14 +0,0 @@
|
|||||||
# This is a config for E2B sandbox template.
|
|
||||||
# You can use 'template_id' (ne8xtb57tq5xw9vwhdyc) or 'template_name (terminal) from this config to spawn a sandbox:
|
|
||||||
|
|
||||||
# Python SDK
|
|
||||||
# from e2b import Sandbox
|
|
||||||
# sandbox = Sandbox(template='terminal')
|
|
||||||
|
|
||||||
# JS SDK
|
|
||||||
# import { Sandbox } from 'e2b'
|
|
||||||
# const sandbox = await Sandbox.create({ template: 'terminal' })
|
|
||||||
|
|
||||||
dockerfile = "e2b.Dockerfile"
|
|
||||||
template_name = "terminal"
|
|
||||||
template_id = "ne8xtb57tq5xw9vwhdyc"
|
|
@ -101,7 +101,7 @@ export default {
|
|||||||
return success
|
return success
|
||||||
} else if (method === "PUT") {
|
} else if (method === "PUT") {
|
||||||
const initSchema = z.object({
|
const initSchema = z.object({
|
||||||
type: z.enum(["react", "node"]),
|
type: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
@ -110,8 +110,13 @@ export default {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { type, name, userId, visibility } = initSchema.parse(body)
|
const { type, name, userId, visibility } = initSchema.parse(body)
|
||||||
|
|
||||||
const allSandboxes = await db.select().from(sandbox).all()
|
const userSandboxes = await db
|
||||||
if (allSandboxes.length >= 8) {
|
.select()
|
||||||
|
.from(sandbox)
|
||||||
|
.where(eq(sandbox.userId, userId))
|
||||||
|
.all()
|
||||||
|
|
||||||
|
if (userSandboxes.length >= 8) {
|
||||||
return new Response("You reached the maximum # of sandboxes.", {
|
return new Response("You reached the maximum # of sandboxes.", {
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
|
@ -26,7 +26,7 @@ export const sandbox = sqliteTable("sandbox", {
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.unique(),
|
.unique(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
type: text("type", { enum: ["react", "node"] }).notNull(),
|
type: text("type").notNull(),
|
||||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }),
|
createdAt: integer("createdAt", { mode: "timestamp_ms" }),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
# Set WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
|
# Set WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
|
||||||
# Set DATABASE_WORKER_URL and STORAGE_WORKER_URL after deploying the workers.
|
# Set DATABASE_WORKER_URL and STORAGE_WORKER_URL after deploying the workers.
|
||||||
|
# DOKKU_HOST and DOKKU_USERNAME are used to authenticate via SSH with the Dokku server
|
||||||
|
# DOKKU_KEY is the path to an SSH (.pem) key on the local machine
|
||||||
|
|
||||||
PORT=4000
|
PORT=4000
|
||||||
WORKERS_KEY=
|
WORKERS_KEY=
|
||||||
DATABASE_WORKER_URL=
|
DATABASE_WORKER_URL=
|
||||||
STORAGE_WORKER_URL=
|
STORAGE_WORKER_URL=
|
||||||
|
E2B_API_KEY=
|
||||||
|
DOKKU_HOST=
|
||||||
|
DOKKU_USERNAME=
|
||||||
|
DOKKU_KEY=
|
646
backend/server/package-lock.json
generated
646
backend/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,20 +11,22 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@e2b/code-interpreter": "^0.0.7",
|
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"e2b": "^0.16.2-beta.47",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"node-pty": "^1.0.0",
|
|
||||||
"rate-limiter-flexible": "^5.0.3",
|
"rate-limiter-flexible": "^5.0.3",
|
||||||
|
"simple-git": "^3.25.0",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
|
"ssh2": "^1.15.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/ssh2": "^1.15.0",
|
||||||
"nodemon": "^3.1.0",
|
"nodemon": "^3.1.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
|
37
backend/server/src/DokkuClient.ts
Normal file
37
backend/server/src/DokkuClient.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { SSHSocketClient, SSHConfig } from "./SSHSocketClient"
|
||||||
|
|
||||||
|
export interface DokkuResponse {
|
||||||
|
ok: boolean;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DokkuClient extends SSHSocketClient {
|
||||||
|
|
||||||
|
constructor(config: SSHConfig) {
|
||||||
|
super(
|
||||||
|
config,
|
||||||
|
"/var/run/dokku-daemon/dokku-daemon.sock"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendCommand(command: string): Promise<DokkuResponse> {
|
||||||
|
try {
|
||||||
|
const response = await this.sendData(command);
|
||||||
|
|
||||||
|
if (typeof response !== "string") {
|
||||||
|
throw new Error("Received data is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(response);
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to send command: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listApps(): Promise<string[]> {
|
||||||
|
const response = await this.sendCommand("apps:list");
|
||||||
|
return response.output.split("\n").slice(1); // Split by newline and ignore the first line (header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSHConfig };
|
90
backend/server/src/SSHSocketClient.ts
Normal file
90
backend/server/src/SSHSocketClient.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Client } from "ssh2";
|
||||||
|
|
||||||
|
export interface SSHConfig {
|
||||||
|
host: string;
|
||||||
|
port?: number;
|
||||||
|
username: string;
|
||||||
|
privateKey: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SSHSocketClient {
|
||||||
|
private conn: Client;
|
||||||
|
private config: SSHConfig;
|
||||||
|
private socketPath: string;
|
||||||
|
private isConnected: boolean = false;
|
||||||
|
|
||||||
|
constructor(config: SSHConfig, socketPath: string) {
|
||||||
|
this.conn = new Client();
|
||||||
|
this.config = { ...config, port: 22};
|
||||||
|
this.socketPath = socketPath;
|
||||||
|
|
||||||
|
this.setupTerminationHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupTerminationHandlers() {
|
||||||
|
process.on("SIGINT", this.closeConnection.bind(this));
|
||||||
|
process.on("SIGTERM", this.closeConnection.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeConnection() {
|
||||||
|
console.log("Closing SSH connection...");
|
||||||
|
this.conn.end();
|
||||||
|
this.isConnected = false;
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.conn
|
||||||
|
.on("ready", () => {
|
||||||
|
console.log("SSH connection established");
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
|
this.isConnected = false;
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
.on("close", () => {
|
||||||
|
console.log("SSH connection closed");
|
||||||
|
this.isConnected = false;
|
||||||
|
})
|
||||||
|
.connect(this.config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendData(data: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
reject(new Error("SSH connection is not established"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conn.exec(
|
||||||
|
`echo "${data}" | nc -U ${this.socketPath}`,
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream
|
||||||
|
.on("close", (code: number, signal: string) => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Stream closed with code ${code} and signal ${signal}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on("data", (data: Buffer) => {
|
||||||
|
resolve(data.toString());
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data: Buffer) => {
|
||||||
|
reject(new Error(data.toString()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
82
backend/server/src/SecureGitClient.ts
Normal file
82
backend/server/src/SecureGitClient.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import simpleGit, { SimpleGit } from "simple-git";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
|
export type FileData = {
|
||||||
|
id: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SecureGitClient {
|
||||||
|
private gitUrl: string;
|
||||||
|
private sshKeyPath: string;
|
||||||
|
|
||||||
|
constructor(gitUrl: string, sshKeyPath: string) {
|
||||||
|
this.gitUrl = gitUrl;
|
||||||
|
this.sshKeyPath = sshKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushFiles(fileData: FileData[], repository: string): Promise<void> {
|
||||||
|
let tempDir: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a temporary directory
|
||||||
|
tempDir = fs.mkdtempSync(path.posix.join(os.tmpdir(), 'git-push-'));
|
||||||
|
console.log(`Temporary directory created: ${tempDir}`);
|
||||||
|
|
||||||
|
// Write files to the temporary directory
|
||||||
|
console.log(`Writing ${fileData.length} files.`);
|
||||||
|
for (const { id, data } of fileData) {
|
||||||
|
const filePath = path.posix.join(tempDir, id);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the simple-git instance with the temporary directory and custom SSH command
|
||||||
|
const git: SimpleGit = simpleGit(tempDir, {
|
||||||
|
config: [
|
||||||
|
'core.sshCommand=ssh -i ' + this.sshKeyPath + ' -o IdentitiesOnly=yes'
|
||||||
|
]
|
||||||
|
}).outputHandler((_command, stdout, stderr) => {
|
||||||
|
stdout.pipe(process.stdout);
|
||||||
|
stderr.pipe(process.stderr);
|
||||||
|
});;
|
||||||
|
|
||||||
|
// Initialize a new Git repository
|
||||||
|
await git.init();
|
||||||
|
|
||||||
|
// Add remote repository
|
||||||
|
await git.addRemote("origin", `${this.gitUrl}:${repository}`);
|
||||||
|
|
||||||
|
// Add files to the repository
|
||||||
|
for (const {id, data} of fileData) {
|
||||||
|
await git.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
await git.commit("Add files.");
|
||||||
|
|
||||||
|
// Push the changes to the remote repository
|
||||||
|
await git.push("origin", "master", {'--force': null});
|
||||||
|
|
||||||
|
console.log("Files successfully pushed to the repository");
|
||||||
|
|
||||||
|
if (tempDir) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
console.log(`Temporary directory removed: ${tempDir}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (tempDir) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
console.log(`Temporary directory removed: ${tempDir}`);
|
||||||
|
}
|
||||||
|
console.error("Error pushing files to the repository:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
backend/server/src/Terminal.ts
Normal file
67
backend/server/src/Terminal.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Sandbox, ProcessHandle } from "e2b";
|
||||||
|
|
||||||
|
// Terminal class to manage a pseudo-terminal (PTY) in a sandbox environment
|
||||||
|
export class Terminal {
|
||||||
|
private pty: ProcessHandle | undefined; // Holds the PTY process handle
|
||||||
|
private sandbox: Sandbox; // Reference to the sandbox environment
|
||||||
|
|
||||||
|
// Constructor initializes the Terminal with a sandbox
|
||||||
|
constructor(sandbox: Sandbox) {
|
||||||
|
this.sandbox = sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the terminal with specified rows, columns, and data handler
|
||||||
|
async init({
|
||||||
|
rows = 20,
|
||||||
|
cols = 80,
|
||||||
|
onData,
|
||||||
|
}: {
|
||||||
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
|
onData: (responseData: string) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
// Create a new PTY process
|
||||||
|
this.pty = await this.sandbox.pty.create({
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
timeout: 0,
|
||||||
|
onData: (data: Uint8Array) => {
|
||||||
|
onData(new TextDecoder().decode(data)); // Convert received data to string and pass to handler
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data to the terminal
|
||||||
|
async sendData(data: string) {
|
||||||
|
if (this.pty) {
|
||||||
|
await this.sandbox.pty.sendInput(this.pty.pid, new TextEncoder().encode(data));
|
||||||
|
} else {
|
||||||
|
console.log("Cannot send data because pty is not initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the terminal
|
||||||
|
async resize(size: { cols: number; rows: number }): Promise<void> {
|
||||||
|
if (this.pty) {
|
||||||
|
await this.sandbox.pty.resize(this.pty.pid, size);
|
||||||
|
} else {
|
||||||
|
console.log("Cannot resize terminal because pty is not initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the terminal, killing the PTY process and stopping the input stream
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.pty) {
|
||||||
|
await this.pty.kill();
|
||||||
|
} else {
|
||||||
|
console.log("Cannot kill pty because it is not initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage example:
|
||||||
|
// const terminal = new Terminal(sandbox);
|
||||||
|
// await terminal.init();
|
||||||
|
// terminal.sendData('ls -la');
|
||||||
|
// await terminal.resize({ cols: 100, rows: 30 });
|
||||||
|
// await terminal.close();
|
177
backend/server/src/fileoperations.ts
Normal file
177
backend/server/src/fileoperations.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import {
|
||||||
|
R2FileBody,
|
||||||
|
R2Files,
|
||||||
|
Sandbox,
|
||||||
|
TFile,
|
||||||
|
TFileData,
|
||||||
|
TFolder,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const getSandboxFiles = async (id: string) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.STORAGE_WORKER_URL}/api?sandboxId=${id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data: R2Files = await res.json();
|
||||||
|
|
||||||
|
const paths = data.objects.map((obj) => obj.key);
|
||||||
|
const processedFiles = await processFiles(paths, id);
|
||||||
|
return processedFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFolder = async (folderId: string) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.STORAGE_WORKER_URL}/api?folderId=${folderId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data: R2Files = await res.json();
|
||||||
|
|
||||||
|
return data.objects.map((obj) => obj.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFiles = async (paths: string[], id: string) => {
|
||||||
|
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] };
|
||||||
|
const fileData: TFileData[] = [];
|
||||||
|
|
||||||
|
paths.forEach((path) => {
|
||||||
|
const allParts = path.split("/");
|
||||||
|
if (allParts[1] !== id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = allParts.slice(2);
|
||||||
|
let current: TFolder = root;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const isFile = i === parts.length - 1 && part.length;
|
||||||
|
const existing = current.children.find((child) => child.name === part);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (!isFile) {
|
||||||
|
current = existing as TFolder;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isFile) {
|
||||||
|
const file: TFile = { id: path, type: "file", name: part };
|
||||||
|
current.children.push(file);
|
||||||
|
fileData.push({ id: path, data: "" });
|
||||||
|
} else {
|
||||||
|
const folder: TFolder = {
|
||||||
|
// id: path, // todo: wrong id. for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css
|
||||||
|
id: `projects/${id}/${parts.slice(0, i + 1).join("/")}`,
|
||||||
|
type: "folder",
|
||||||
|
name: part,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
current.children.push(folder);
|
||||||
|
current = folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
fileData.map(async (file) => {
|
||||||
|
const data = await fetchFileContent(file.id);
|
||||||
|
file.data = data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: root.children,
|
||||||
|
fileData,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFileContent = async (fileId: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const fileRes = await fetch(
|
||||||
|
`${process.env.STORAGE_WORKER_URL}/api?fileId=${fileId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return await fileRes.text();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("ERROR fetching file:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFile = async (fileId: string) => {
|
||||||
|
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fileId }),
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renameFile = async (
|
||||||
|
fileId: string,
|
||||||
|
newFileId: string,
|
||||||
|
data: string
|
||||||
|
) => {
|
||||||
|
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fileId, newFileId, data }),
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveFile = async (fileId: string, data: string) => {
|
||||||
|
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/save`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fileId, data }),
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteFile = async (fileId: string) => {
|
||||||
|
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fileId }),
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProjectSize = async (id: string) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.STORAGE_WORKER_URL}/api/size?sandboxId=${id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (await res.json()).size;
|
||||||
|
};
|
@ -1,11 +1,12 @@
|
|||||||
import fs from "fs";
|
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express, { Express } from "express";
|
import express, { Express } from "express";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
|
import { DokkuClient } from "./DokkuClient";
|
||||||
|
import { SecureGitClient, FileData } from "./SecureGitClient";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { User } from "./types";
|
import { User } from "./types";
|
||||||
@ -17,8 +18,13 @@ import {
|
|||||||
getSandboxFiles,
|
getSandboxFiles,
|
||||||
renameFile,
|
renameFile,
|
||||||
saveFile,
|
saveFile,
|
||||||
} from "./utils";
|
} from "./fileoperations";
|
||||||
import { Sandbox, Process, ProcessMessage } from "e2b";
|
import { LockManager } from "./utils";
|
||||||
|
|
||||||
|
import { Sandbox, Filesystem } from "e2b";
|
||||||
|
|
||||||
|
import { Terminal } from "./Terminal"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MAX_BODY_SIZE,
|
MAX_BODY_SIZE,
|
||||||
createFileRL,
|
createFileRL,
|
||||||
@ -43,14 +49,21 @@ const io = new Server(httpServer, {
|
|||||||
let inactivityTimeout: NodeJS.Timeout | null = null;
|
let inactivityTimeout: NodeJS.Timeout | null = null;
|
||||||
let isOwnerConnected = false;
|
let isOwnerConnected = false;
|
||||||
|
|
||||||
const terminals: {
|
const containers: Record<string, Sandbox> = {};
|
||||||
[id: string]: Process;
|
const connections: Record<string, number> = {};
|
||||||
} = {};
|
const terminals: Record<string, Terminal> = {};
|
||||||
const containers: {
|
|
||||||
[id: string]: Sandbox;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const dirName = path.join(__dirname, "..");
|
const dirName = "/home/user";
|
||||||
|
|
||||||
|
const moveFile = async (
|
||||||
|
filesystem: Filesystem,
|
||||||
|
filePath: string,
|
||||||
|
newFilePath: string
|
||||||
|
) => {
|
||||||
|
const fileContents = await filesystem.read(filePath);
|
||||||
|
await filesystem.write(newFilePath, fileContents);
|
||||||
|
await filesystem.remove(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
const handshakeSchema = z.object({
|
const handshakeSchema = z.object({
|
||||||
@ -103,33 +116,29 @@ io.use(async (socket, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
class LockManager {
|
|
||||||
private locks: { [key: string]: Promise<any> };
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.locks = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquireLock<T>(key: string, task: () => Promise<T>): Promise<T> {
|
|
||||||
if (!this.locks[key]) {
|
|
||||||
this.locks[key] = new Promise<T>(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const result = await task();
|
|
||||||
resolve(result);
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
} finally {
|
|
||||||
delete this.locks[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await this.locks[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockManager = new LockManager();
|
const lockManager = new LockManager();
|
||||||
|
|
||||||
|
if (!process.env.DOKKU_HOST) console.error('Environment variable DOKKU_HOST is not defined');
|
||||||
|
if (!process.env.DOKKU_USERNAME) console.error('Environment variable DOKKU_USERNAME is not defined');
|
||||||
|
if (!process.env.DOKKU_KEY) console.error('Environment variable DOKKU_KEY is not defined');
|
||||||
|
|
||||||
|
const client =
|
||||||
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
||||||
|
? new DokkuClient({
|
||||||
|
host: process.env.DOKKU_HOST,
|
||||||
|
username: process.env.DOKKU_USERNAME,
|
||||||
|
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
client?.connect();
|
||||||
|
|
||||||
|
const git = process.env.DOKKU_HOST && process.env.DOKKU_KEY ? new SecureGitClient(
|
||||||
|
`dokku@${process.env.DOKKU_HOST}`,
|
||||||
|
process.env.DOKKU_KEY
|
||||||
|
) : null;
|
||||||
|
|
||||||
io.on("connection", async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
|
try {
|
||||||
if (inactivityTimeout) clearTimeout(inactivityTimeout);
|
if (inactivityTimeout) clearTimeout(inactivityTimeout);
|
||||||
|
|
||||||
const data = socket.data as {
|
const data = socket.data as {
|
||||||
@ -140,6 +149,7 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
if (data.isOwner) {
|
if (data.isOwner) {
|
||||||
isOwnerConnected = true;
|
isOwnerConnected = true;
|
||||||
|
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1;
|
||||||
} else {
|
} else {
|
||||||
if (!isOwnerConnected) {
|
if (!isOwnerConnected) {
|
||||||
socket.emit("disableAccess", "The sandbox owner is not connected.");
|
socket.emit("disableAccess", "The sandbox owner is not connected.");
|
||||||
@ -148,86 +158,185 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
if (!containers[data.sandboxId]) {
|
try {
|
||||||
console.log("Creating container ", data.sandboxId);
|
// Start a new container if the container doesn't exist or it timed out.
|
||||||
containers[data.sandboxId] = await Sandbox.create({
|
if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) {
|
||||||
template: "terminal",
|
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200_000 });
|
||||||
});
|
console.log("Created container ", data.sandboxId);
|
||||||
console.log("Created.");
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Error creating container ${data.sandboxId}:`, e);
|
||||||
|
io.emit("error", `Error: container creation. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Change the owner of the project directory to user
|
||||||
|
const fixPermissions = async () => {
|
||||||
|
await containers[data.sandboxId].commands.run(
|
||||||
|
`sudo chown -R user "${path.posix.join(dirName, "projects", data.sandboxId)}"`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy all files from the project to the container
|
||||||
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
const sandboxFiles = await getSandboxFiles(data.sandboxId);
|
||||||
sandboxFiles.fileData.forEach((file) => {
|
const containerFiles = containers[data.sandboxId].files;
|
||||||
const filePath = path.join(dirName, file.id);
|
const promises = sandboxFiles.fileData.map(async (file) => {
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
try {
|
||||||
fs.writeFile(filePath, file.data, function (err) {
|
const filePath = path.posix.join(dirName, file.id);
|
||||||
if (err) throw err;
|
const parentDirectory = path.dirname(filePath);
|
||||||
});
|
if (!containerFiles.exists(parentDirectory)) {
|
||||||
|
await containerFiles.makeDir(parentDirectory);
|
||||||
|
}
|
||||||
|
await containerFiles.write(filePath, file.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Failed to create file: " + e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
fixPermissions();
|
||||||
|
|
||||||
socket.emit("loaded", sandboxFiles.files);
|
socket.emit("loaded", sandboxFiles.files);
|
||||||
|
|
||||||
socket.on("getFile", (fileId: string, callback) => {
|
socket.on("getFile", (fileId: string, callback) => {
|
||||||
|
console.log(fileId);
|
||||||
|
try {
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
callback(file.data);
|
callback(file.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error getting file:", e);
|
||||||
|
io.emit("error", `Error: get file. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getFolder", async (folderId: string, callback) => {
|
socket.on("getFolder", async (folderId: string, callback) => {
|
||||||
|
try {
|
||||||
const files = await getFolder(folderId);
|
const files = await getFolder(folderId);
|
||||||
callback(files);
|
callback(files);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error getting folder:", e);
|
||||||
|
io.emit("error", `Error: get folder. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// todo: send diffs + debounce for efficiency
|
// todo: send diffs + debounce for efficiency
|
||||||
socket.on("saveFile", async (fileId: string, body: string) => {
|
socket.on("saveFile", async (fileId: string, body: string) => {
|
||||||
try {
|
if (!fileId) return; // handles saving when no file is open
|
||||||
await saveFileRL.consume(data.userId, 1);
|
|
||||||
|
|
||||||
|
try {
|
||||||
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"rateLimit",
|
"error",
|
||||||
"Rate limited: file size too large. Please reduce the file size."
|
"Error: file size too large. Please reduce the file size."
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await saveFileRL.consume(data.userId, 1);
|
||||||
|
await saveFile(fileId, body);
|
||||||
|
} catch (e) {
|
||||||
|
io.emit("error", "Rate limited: file saving. Please slow down.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
file.data = body;
|
file.data = body;
|
||||||
|
|
||||||
fs.writeFile(path.join(dirName, file.id), body, function (err) {
|
await containers[data.sandboxId].files.write(
|
||||||
if (err) throw err;
|
path.posix.join(dirName, file.id),
|
||||||
});
|
body
|
||||||
await saveFile(fileId, body);
|
);
|
||||||
} catch (e) {
|
fixPermissions();
|
||||||
io.emit("rateLimit", "Rate limited: file saving. Please slow down.");
|
} catch (e: any) {
|
||||||
|
console.error("Error saving file:", e);
|
||||||
|
io.emit("error", `Error: file saving. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("moveFile", async (fileId: string, folderId: string, callback) => {
|
socket.on(
|
||||||
|
"moveFile",
|
||||||
|
async (fileId: string, folderId: string, callback) => {
|
||||||
|
try {
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const parts = fileId.split("/");
|
const parts = fileId.split("/");
|
||||||
const newFileId = folderId + "/" + parts.pop();
|
const newFileId = folderId + "/" + parts.pop();
|
||||||
|
|
||||||
fs.rename(
|
await moveFile(
|
||||||
path.join(dirName, fileId),
|
containers[data.sandboxId].files,
|
||||||
path.join(dirName, newFileId),
|
path.posix.join(dirName, fileId),
|
||||||
function (err) {
|
path.posix.join(dirName, newFileId)
|
||||||
if (err) throw err;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
fixPermissions();
|
||||||
|
|
||||||
file.id = newFileId;
|
file.id = newFileId;
|
||||||
|
|
||||||
await renameFile(fileId, newFileId, file.data);
|
await renameFile(fileId, newFileId, file.data);
|
||||||
const newFiles = await getSandboxFiles(data.sandboxId);
|
const newFiles = await getSandboxFiles(data.sandboxId);
|
||||||
|
|
||||||
callback(newFiles.files);
|
callback(newFiles.files);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error moving file:", e);
|
||||||
|
io.emit("error", `Error: file moving. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface CallbackResponse {
|
||||||
|
success: boolean;
|
||||||
|
apps?: string[];
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on(
|
||||||
|
"list",
|
||||||
|
async (callback: (response: CallbackResponse) => void) => {
|
||||||
|
console.log("Retrieving apps list...");
|
||||||
|
try {
|
||||||
|
if (!client) throw Error("Failed to retrieve apps list: No Dokku client")
|
||||||
|
callback({
|
||||||
|
success: true,
|
||||||
|
apps: await client.listApps()
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to retrieve apps list",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on(
|
||||||
|
"deploy",
|
||||||
|
async (callback: (response: CallbackResponse) => void) => {
|
||||||
|
try {
|
||||||
|
// Push the project files to the Dokku server
|
||||||
|
console.log("Deploying project ${data.sandboxId}...");
|
||||||
|
if (!git) throw Error("Failed to retrieve apps list: No git client")
|
||||||
|
// Remove the /project/[id]/ component of each file path:
|
||||||
|
const fixedFilePaths = sandboxFiles.fileData.map((file) => {
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
id: file.id.split("/").slice(2).join("/"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Push all files to Dokku.
|
||||||
|
await git.pushFiles(fixedFilePaths, data.sandboxId);
|
||||||
|
callback({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to deploy project: " + error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
socket.on("createFile", async (name: string, callback) => {
|
socket.on("createFile", async (name: string, callback) => {
|
||||||
try {
|
try {
|
||||||
@ -235,19 +344,27 @@ io.on("connection", async (socket) => {
|
|||||||
// limit is 200mb
|
// limit is 200mb
|
||||||
if (size > 200 * 1024 * 1024) {
|
if (size > 200 * 1024 * 1024) {
|
||||||
io.emit(
|
io.emit(
|
||||||
"rateLimit",
|
"error",
|
||||||
"Rate limited: project size exceeded. Please delete some files."
|
"Rate limited: project size exceeded. Please delete some files."
|
||||||
);
|
);
|
||||||
callback({ success: false });
|
callback({ success: false });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await createFileRL.consume(data.userId, 1);
|
await createFileRL.consume(data.userId, 1);
|
||||||
|
} catch (e) {
|
||||||
|
io.emit("error", "Rate limited: file creation. Please slow down.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const id = `projects/${data.sandboxId}/${name}`;
|
const id = `projects/${data.sandboxId}/${name}`;
|
||||||
|
|
||||||
fs.writeFile(path.join(dirName, id), "", function (err) {
|
await containers[data.sandboxId].files.write(
|
||||||
if (err) throw err;
|
path.posix.join(dirName, id),
|
||||||
});
|
""
|
||||||
|
);
|
||||||
|
fixPermissions();
|
||||||
|
|
||||||
sandboxFiles.files.push({
|
sandboxFiles.files.push({
|
||||||
id,
|
id,
|
||||||
@ -263,30 +380,42 @@ io.on("connection", async (socket) => {
|
|||||||
await createFile(id);
|
await createFile(id);
|
||||||
|
|
||||||
callback({ success: true });
|
callback({ success: true });
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
io.emit("rateLimit", "Rate limited: file creation. Please slow down.");
|
console.error("Error creating file:", e);
|
||||||
|
io.emit("error", `Error: file creation. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("createFolder", async (name: string, callback) => {
|
socket.on("createFolder", async (name: string, callback) => {
|
||||||
|
try {
|
||||||
try {
|
try {
|
||||||
await createFolderRL.consume(data.userId, 1);
|
await createFolderRL.consume(data.userId, 1);
|
||||||
|
} catch (e) {
|
||||||
|
io.emit("error", "Rate limited: folder creation. Please slow down.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const id = `projects/${data.sandboxId}/${name}`;
|
const id = `projects/${data.sandboxId}/${name}`;
|
||||||
|
|
||||||
fs.mkdir(path.join(dirName, id), { recursive: true }, function (err) {
|
await containers[data.sandboxId].files.makeDir(
|
||||||
if (err) throw err;
|
path.posix.join(dirName, id)
|
||||||
});
|
);
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
io.emit("rateLimit", "Rate limited: folder creation. Please slow down.");
|
console.error("Error creating folder:", e);
|
||||||
|
io.emit("error", `Error: folder creation. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("renameFile", async (fileId: string, newName: string) => {
|
socket.on("renameFile", async (fileId: string, newName: string) => {
|
||||||
|
try {
|
||||||
try {
|
try {
|
||||||
await renameFileRL.consume(data.userId, 1);
|
await renameFileRL.consume(data.userId, 1);
|
||||||
|
} catch (e) {
|
||||||
|
io.emit("error", "Rate limited: file renaming. Please slow down.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@ -296,29 +425,33 @@ io.on("connection", async (socket) => {
|
|||||||
const newFileId =
|
const newFileId =
|
||||||
parts.slice(0, parts.length - 1).join("/") + "/" + newName;
|
parts.slice(0, parts.length - 1).join("/") + "/" + newName;
|
||||||
|
|
||||||
fs.rename(
|
await moveFile(
|
||||||
path.join(dirName, fileId),
|
containers[data.sandboxId].files,
|
||||||
path.join(dirName, newFileId),
|
path.posix.join(dirName, fileId),
|
||||||
function (err) {
|
path.posix.join(dirName, newFileId)
|
||||||
if (err) throw err;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
fixPermissions();
|
||||||
await renameFile(fileId, newFileId, file.data);
|
await renameFile(fileId, newFileId, file.data);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
io.emit("rateLimit", "Rate limited: file renaming. Please slow down.");
|
console.error("Error renaming folder:", e);
|
||||||
return;
|
io.emit("error", `Error: folder renaming. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("deleteFile", async (fileId: string, callback) => {
|
socket.on("deleteFile", async (fileId: string, callback) => {
|
||||||
|
try {
|
||||||
try {
|
try {
|
||||||
await deleteFileRL.consume(data.userId, 1);
|
await deleteFileRL.consume(data.userId, 1);
|
||||||
|
} catch (e) {
|
||||||
|
io.emit("error", "Rate limited: file deletion. Please slow down.");
|
||||||
|
}
|
||||||
|
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
fs.unlink(path.join(dirName, fileId), function (err) {
|
await containers[data.sandboxId].files.remove(
|
||||||
if (err) throw err;
|
path.posix.join(dirName, fileId)
|
||||||
});
|
);
|
||||||
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
||||||
(f) => f.id !== fileId
|
(f) => f.id !== fileId
|
||||||
);
|
);
|
||||||
@ -327,8 +460,9 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
const newFiles = await getSandboxFiles(data.sandboxId);
|
const newFiles = await getSandboxFiles(data.sandboxId);
|
||||||
callback(newFiles.files);
|
callback(newFiles.files);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
io.emit("rateLimit", "Rate limited: file deletion. Please slow down.");
|
console.error("Error deleting file:", e);
|
||||||
|
io.emit("error", `Error: file deletion. ${e.message ?? e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -337,13 +471,14 @@ io.on("connection", async (socket) => {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
socket.on("deleteFolder", async (folderId: string, callback) => {
|
socket.on("deleteFolder", async (folderId: string, callback) => {
|
||||||
|
try {
|
||||||
const files = await getFolder(folderId);
|
const files = await getFolder(folderId);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
fs.unlink(path.join(dirName, file), function (err) {
|
await containers[data.sandboxId].files.remove(
|
||||||
if (err) throw err;
|
path.posix.join(dirName, file)
|
||||||
});
|
);
|
||||||
|
|
||||||
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
||||||
(f) => f.id !== file
|
(f) => f.id !== file
|
||||||
@ -356,82 +491,111 @@ io.on("connection", async (socket) => {
|
|||||||
const newFiles = await getSandboxFiles(data.sandboxId);
|
const newFiles = await getSandboxFiles(data.sandboxId);
|
||||||
|
|
||||||
callback(newFiles.files);
|
callback(newFiles.files);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error deleting folder:", e);
|
||||||
|
io.emit("error", `Error: folder deletion. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function toBackslashNotation(input: string) {
|
|
||||||
return input
|
|
||||||
.replace(/\\/g, "\\\\") // Escape backslashes
|
|
||||||
.replace(/\n/g, "\\n") // Escape newlines
|
|
||||||
.replace(/\r/g, "\\r") // Escape carriage returns
|
|
||||||
.replace(/\t/g, "\\t") // Escape tabs
|
|
||||||
.replace(/"/g, '\\"') // Escape double quotes
|
|
||||||
.replace(/'/g, "\\'") // Escape single quotes
|
|
||||||
.replace(/\f/g, "\\f") // Escape form feeds
|
|
||||||
.replace(/\b/g, "\\b") // Escape backspaces
|
|
||||||
.replace(/\v/g, "\\v") // Escape vertical tabs
|
|
||||||
.replace(/\0/g, "\\0") // Escape null characters
|
|
||||||
.replace(/\a/g, "\\a") // Escape alert (bell)
|
|
||||||
.replace(/\e/g, "\\e"); // Escape escape
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on("createTerminal", async (id: string, callback) => {
|
socket.on("createTerminal", async (id: string, callback) => {
|
||||||
|
try {
|
||||||
if (terminals[id] || Object.keys(terminals).length >= 4) {
|
if (terminals[id] || Object.keys(terminals).length >= 4) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onData = (data: ProcessMessage) => {
|
|
||||||
console.log("process", toBackslashNotation(data.toString()));
|
|
||||||
io.emit("terminalResponse", {
|
|
||||||
id,
|
|
||||||
data: data.toString() + "\r\n",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
console.log("Creating terminal", id);
|
try {
|
||||||
terminals[id] = await containers[data.sandboxId].process.start({
|
terminals[id] = new Terminal(containers[data.sandboxId])
|
||||||
cmd: 'TERM=xterm script -c "screen" /dev/null', // xterm vt100
|
await terminals[id].init({
|
||||||
onStdout: onData,
|
onData: (responseString: string) => {
|
||||||
onStderr: onData,
|
io.emit("terminalResponse", { id, data: responseString });
|
||||||
onExit: (code) => console.log("exit :(", code),
|
|
||||||
|
function extractPortNumber(inputString: string) {
|
||||||
|
// Remove ANSI escape codes
|
||||||
|
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, '');
|
||||||
|
|
||||||
|
// Regular expression to match port number
|
||||||
|
const regex = /http:\/\/localhost:(\d+)/;
|
||||||
|
// If a match is found, return the port number
|
||||||
|
const match = cleanedString.match(regex);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
const port = parseInt(extractPortNumber(responseString) ?? "");
|
||||||
|
if (port) {
|
||||||
|
io.emit(
|
||||||
|
"previewURL",
|
||||||
|
"https://" + containers[data.sandboxId].getHost(port)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cols: 80,
|
||||||
|
rows: 20,
|
||||||
|
//onExit: () => console.log("Terminal exited", id),
|
||||||
});
|
});
|
||||||
await terminals[id].sendStdin("clear\r\n");
|
|
||||||
await terminals[id].sendStdin("export PS1='user> '\r\n");
|
const defaultDirectory = path.posix.join(dirName, "projects", data.sandboxId);
|
||||||
await terminals[id].sendStdin("clear\r\n");
|
const defaultCommands = [
|
||||||
|
`cd "${defaultDirectory}"`,
|
||||||
|
"export PS1='user> '",
|
||||||
|
"clear"
|
||||||
|
]
|
||||||
|
for (const command of defaultCommands) await terminals[id].sendData(command + "\r");
|
||||||
|
|
||||||
console.log("Created terminal", id);
|
console.log("Created terminal", id);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Error creating terminal ${id}:`, e);
|
||||||
|
io.emit("error", `Error: terminal creation. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Error creating terminal ${id}:`, e);
|
||||||
|
io.emit("error", `Error: terminal creation. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => {
|
socket.on(
|
||||||
/*Object.values(terminals).forEach((t) => {
|
"resizeTerminal",
|
||||||
t.terminal.resize(dimensions.cols, dimensions.rows);
|
(dimensions: { cols: number; rows: number }) => {
|
||||||
});*/
|
try {
|
||||||
|
Object.values(terminals).forEach((t) => {
|
||||||
|
t.resize(dimensions);
|
||||||
});
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error resizing terminal:", e);
|
||||||
|
io.emit("error", `Error: terminal resizing. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
socket.on("terminalData", (id: string, data: string) => {
|
socket.on("terminalData", async (id: string, data: string) => {
|
||||||
|
try {
|
||||||
if (!terminals[id]) {
|
if (!terminals[id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await terminals[id].sendData(data);
|
||||||
console.log(`Writing ${toBackslashNotation(data)} to ${id}`);
|
} catch (e: any) {
|
||||||
terminals[id].sendStdin(data);
|
console.error("Error writing to terminal:", e);
|
||||||
} catch (e) {
|
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`);
|
||||||
console.log("Error writing to terminal", e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("closeTerminal", async (id: string, callback) => {
|
socket.on("closeTerminal", async (id: string, callback) => {
|
||||||
|
try {
|
||||||
if (!terminals[id]) {
|
if (!terminals[id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await terminals[id].kill();
|
await terminals[id].close();
|
||||||
delete terminals[id];
|
delete terminals[id];
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error closing terminal:", e);
|
||||||
|
io.emit("error", `Error: closing terminal. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on(
|
socket.on(
|
||||||
@ -443,6 +607,7 @@ io.on("connection", async (socket) => {
|
|||||||
instructions: string,
|
instructions: string,
|
||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
|
try {
|
||||||
const fetchPromise = fetch(
|
const fetchPromise = fetch(
|
||||||
`${process.env.DATABASE_WORKER_URL}/api/sandbox/generate`,
|
`${process.env.DATABASE_WORKER_URL}/api/sandbox/generate`,
|
||||||
{
|
{
|
||||||
@ -459,7 +624,7 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
// Generate code from cloudflare workers AI
|
// Generate code from cloudflare workers AI
|
||||||
const generateCodePromise = fetch(
|
const generateCodePromise = fetch(
|
||||||
`${process.env.AI_WORKER_URL}/api?fileName=${fileName}&code=${code}&line=${line}&instructions=${instructions}`,
|
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(fileName)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(line)}&instructions=${encodeURIComponent(instructions)}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -476,23 +641,36 @@ io.on("connection", async (socket) => {
|
|||||||
const json = await generateCodeResponse.json();
|
const json = await generateCodeResponse.json();
|
||||||
|
|
||||||
callback({ response: json.response, success: true });
|
callback({ response: json.response, success: true });
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error generating code:", e);
|
||||||
|
io.emit("error", `Error: code generation. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
|
try {
|
||||||
if (data.isOwner) {
|
if (data.isOwner) {
|
||||||
Object.entries(terminals).forEach((t) => {
|
connections[data.sandboxId]--;
|
||||||
const terminal = t[1];
|
}
|
||||||
terminal.kill();
|
|
||||||
delete terminals[t[0]];
|
if (data.isOwner && connections[data.sandboxId] <= 0) {
|
||||||
});
|
await Promise.all(
|
||||||
|
Object.entries(terminals).map(async ([key, terminal]) => {
|
||||||
|
await terminal.close();
|
||||||
|
delete terminals[key];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
|
try {
|
||||||
if (containers[data.sandboxId]) {
|
if (containers[data.sandboxId]) {
|
||||||
console.log("Closing container", data.sandboxId);
|
await containers[data.sandboxId].kill();
|
||||||
await containers[data.sandboxId].close();
|
|
||||||
delete containers[data.sandboxId];
|
delete containers[data.sandboxId];
|
||||||
console.log("Closed");
|
console.log("Closed container", data.sandboxId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error closing container ", data.sandboxId, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -518,7 +696,15 @@ io.on("connection", async (socket) => {
|
|||||||
// } else {
|
// } else {
|
||||||
// console.log("number of sockets", sockets.length);
|
// console.log("number of sockets", sockets.length);
|
||||||
// }
|
// }
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Error disconnecting:", e);
|
||||||
|
io.emit("error", `Error: disconnecting. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error connecting:", e);
|
||||||
|
io.emit("error", `Error: connection. ${e.message ?? e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
httpServer.listen(port, () => {
|
httpServer.listen(port, () => {
|
||||||
|
@ -1,177 +1,23 @@
|
|||||||
import * as dotenv from "dotenv";
|
export class LockManager {
|
||||||
import {
|
private locks: { [key: string]: Promise<any> };
|
||||||
R2FileBody,
|
|
||||||
R2Files,
|
|
||||||
Sandbox,
|
|
||||||
TFile,
|
|
||||||
TFileData,
|
|
||||||
TFolder,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
dotenv.config();
|
constructor() {
|
||||||
|
this.locks = {};
|
||||||
export const getSandboxFiles = async (id: string) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.STORAGE_WORKER_URL}/api?sandboxId=${id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const data: R2Files = await res.json();
|
|
||||||
|
|
||||||
const paths = data.objects.map((obj) => obj.key);
|
|
||||||
const processedFiles = await processFiles(paths, id);
|
|
||||||
return processedFiles;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFolder = async (folderId: string) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.STORAGE_WORKER_URL}/api?folderId=${folderId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const data: R2Files = await res.json();
|
|
||||||
|
|
||||||
return data.objects.map((obj) => obj.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const processFiles = async (paths: string[], id: string) => {
|
|
||||||
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] };
|
|
||||||
const fileData: TFileData[] = [];
|
|
||||||
|
|
||||||
paths.forEach((path) => {
|
|
||||||
const allParts = path.split("/");
|
|
||||||
if (allParts[1] !== id) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = allParts.slice(2);
|
async acquireLock<T>(key: string, task: () => Promise<T>): Promise<T> {
|
||||||
let current: TFolder = root;
|
if (!this.locks[key]) {
|
||||||
|
this.locks[key] = new Promise<T>(async (resolve, reject) => {
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
const part = parts[i];
|
|
||||||
const isFile = i === parts.length - 1 && part.includes(".");
|
|
||||||
const existing = current.children.find((child) => child.name === part);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
if (!isFile) {
|
|
||||||
current = existing as TFolder;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isFile) {
|
|
||||||
const file: TFile = { id: path, type: "file", name: part };
|
|
||||||
current.children.push(file);
|
|
||||||
fileData.push({ id: path, data: "" });
|
|
||||||
} else {
|
|
||||||
const folder: TFolder = {
|
|
||||||
// id: path, // todo: wrong id. for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css
|
|
||||||
id: `projects/${id}/${parts.slice(0, i + 1).join("/")}`,
|
|
||||||
type: "folder",
|
|
||||||
name: part,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
current.children.push(folder);
|
|
||||||
current = folder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
fileData.map(async (file) => {
|
|
||||||
const data = await fetchFileContent(file.id);
|
|
||||||
file.data = data;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
files: root.children,
|
|
||||||
fileData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchFileContent = async (fileId: string): Promise<string> => {
|
|
||||||
try {
|
try {
|
||||||
const fileRes = await fetch(
|
const result = await task();
|
||||||
`${process.env.STORAGE_WORKER_URL}/api?fileId=${fileId}`,
|
resolve(result);
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return await fileRes.text();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("ERROR fetching file:", error);
|
reject(error);
|
||||||
return "";
|
} finally {
|
||||||
|
delete this.locks[key];
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const createFile = async (fileId: string) => {
|
|
||||||
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ fileId }),
|
|
||||||
});
|
});
|
||||||
return res.ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renameFile = async (
|
|
||||||
fileId: string,
|
|
||||||
newFileId: string,
|
|
||||||
data: string
|
|
||||||
) => {
|
|
||||||
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ fileId, newFileId, data }),
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveFile = async (fileId: string, data: string) => {
|
|
||||||
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/save`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ fileId, data }),
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteFile = async (fileId: string) => {
|
|
||||||
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ fileId }),
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProjectSize = async (id: string) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.STORAGE_WORKER_URL}/api/size?sandboxId=${id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
return await this.locks[key];
|
||||||
return (await res.json()).size;
|
}
|
||||||
};
|
}
|
32
backend/storage/package-lock.json
generated
32
backend/storage/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "storage",
|
"name": "storage",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"p-limit": "^6.1.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.23.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -894,6 +895,21 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/runner/node_modules/p-limit": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"yocto-queue": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
|
||||||
@ -1766,12 +1782,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "5.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz",
|
||||||
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
|
"integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^1.0.0"
|
"yocto-queue": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@ -2970,10 +2985,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
||||||
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
|
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20"
|
"node": ">=12.20"
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"wrangler": "^3.0.0"
|
"wrangler": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"p-limit": "^6.1.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.23.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import startercode from "./startercode"
|
import pLimit from 'p-limit';
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
R2: R2Bucket
|
R2: R2Bucket
|
||||||
|
Templates: R2Bucket
|
||||||
KEY: string
|
KEY: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,19 +138,26 @@ export default {
|
|||||||
} else if (path === "/api/init" && method === "POST") {
|
} else if (path === "/api/init" && method === "POST") {
|
||||||
const initSchema = z.object({
|
const initSchema = z.object({
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
type: z.enum(["react", "node"]),
|
type: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { sandboxId, type } = initSchema.parse(body)
|
const { sandboxId, type } = initSchema.parse(body)
|
||||||
|
|
||||||
console.log(startercode[type])
|
console.log(`Copying template: ${type}`);
|
||||||
|
|
||||||
await Promise.all(
|
// List all objects under the directory
|
||||||
startercode[type].map(async (file) => {
|
const { objects } = await env.Templates.list({ prefix: type });
|
||||||
await env.R2.put(`projects/${sandboxId}/${file.name}`, file.body)
|
|
||||||
|
// Copy each object to the new directory with a 5 concurrency limit
|
||||||
|
const limit = pLimit(5);
|
||||||
|
await Promise.all(objects.map(({ key }) =>
|
||||||
|
limit(async () => {
|
||||||
|
const destinationKey = key.replace(type, `projects/${sandboxId}`);
|
||||||
|
const fileBody = await env.Templates.get(key).then(res => res?.body ?? "");
|
||||||
|
await env.R2.put(destinationKey, fileBody);
|
||||||
})
|
})
|
||||||
)
|
));
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,151 +0,0 @@
|
|||||||
const startercode = {
|
|
||||||
node: [
|
|
||||||
{ name: "index.js", body: `console.log("Hello World!")` },
|
|
||||||
{
|
|
||||||
name: "package.json",
|
|
||||||
body: `{
|
|
||||||
"name": "nodejs",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "^18.0.6"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
react: [
|
|
||||||
{
|
|
||||||
name: "package.json",
|
|
||||||
body: `{
|
|
||||||
"name": "react",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.2.66",
|
|
||||||
"@types/react-dom": "^18.2.22",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-plugin-react": "^7.34.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
|
||||||
"vite": "^5.2.0"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "vite.config.js",
|
|
||||||
body: `import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
port: 5173,
|
|
||||||
host: "0.0.0.0",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index.html",
|
|
||||||
body: `<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>React Starter Code</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "src/App.css",
|
|
||||||
body: `div {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #000;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #777;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "src/App.jsx",
|
|
||||||
body: `import './App.css'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>React Starter Code</h1>
|
|
||||||
<p>
|
|
||||||
Edit App.jsx to get started.
|
|
||||||
</p>
|
|
||||||
<button onClick={() => setCount(count => count + 1)}>
|
|
||||||
Clicked {count} times
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "src/main.jsx",
|
|
||||||
body: `import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App.jsx'
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default startercode
|
|
@ -3,7 +3,7 @@ CLERK_SECRET_KEY=
|
|||||||
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
||||||
LIVEBLOCKS_SECRET_KEY=
|
LIVEBLOCKS_SECRET_KEY=
|
||||||
|
|
||||||
NEXT_PUBLIC_SERVER_PORT=4000
|
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
|
||||||
# Set WORKER_URLs after deploying the workers.
|
# Set WORKER_URLs after deploying the workers.
|
||||||
|
@ -6,6 +6,7 @@ import { notFound, redirect } from "next/navigation"
|
|||||||
import Loading from "@/components/editor/loading"
|
import Loading from "@/components/editor/loading"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
import { TerminalProvider } from "@/context/TerminalContext"
|
||||||
|
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
|
|
||||||
@ -63,14 +64,6 @@ const CodeEditor = dynamic(() => import("@/components/editor"), {
|
|||||||
loading: () => <Loading />,
|
loading: () => <Loading />,
|
||||||
})
|
})
|
||||||
|
|
||||||
function getReactDefinitionFile() {
|
|
||||||
const reactDefinitionFile = fs.readFileSync(
|
|
||||||
"node_modules/@types/react/index.d.ts",
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
return reactDefinitionFile
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function CodePage({ params }: { params: { id: string } }) {
|
export default async function CodePage({ params }: { params: { id: string } }) {
|
||||||
const user = await currentUser()
|
const user = await currentUser()
|
||||||
const sandboxId = params.id
|
const sandboxId = params.id
|
||||||
@ -94,20 +87,21 @@ export default async function CodePage({ params }: { params: { id: string } }) {
|
|||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
const reactDefinitionFile = getReactDefinitionFile()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||||
<Room id={sandboxId}>
|
<Room id={sandboxId}>
|
||||||
|
<TerminalProvider>
|
||||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
||||||
<div className="w-screen flex grow">
|
<div className="w-screen flex grow">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
userData={userData}
|
userData={userData}
|
||||||
sandboxData={sandboxData}
|
sandboxData={sandboxData}
|
||||||
reactDefinitionFile={reactDefinitionFile}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</TerminalProvider>
|
||||||
</Room>
|
</Room>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
|
|||||||
import { ClerkProvider } from "@clerk/nextjs"
|
import { ClerkProvider } from "@clerk/nextjs"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
import { Analytics } from "@vercel/analytics/react"
|
import { Analytics } from "@vercel/analytics/react"
|
||||||
|
import { PreviewProvider } from "@/context/PreviewContext";
|
||||||
|
import { SocketProvider } from '@/context/SocketContext'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Sandbox",
|
title: "Sandbox",
|
||||||
@ -13,7 +15,7 @@ export const metadata: Metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
@ -27,7 +29,11 @@ export default function RootLayout({
|
|||||||
forcedTheme="dark"
|
forcedTheme="dark"
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
|
<SocketProvider>
|
||||||
|
<PreviewProvider>
|
||||||
{children}
|
{children}
|
||||||
|
</PreviewProvider>
|
||||||
|
</SocketProvider>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<Toaster position="bottom-left" richColors />
|
<Toaster position="bottom-left" richColors />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
13
frontend/app/providers.js
Normal file
13
frontend/app/providers.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use client"
|
||||||
|
import posthog from "posthog-js"
|
||||||
|
import { PostHogProvider } from "posthog-js/react"
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||||
|
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PHProvider({ children }) {
|
||||||
|
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
||||||
|
}
|
@ -49,11 +49,9 @@ export default function Dashboard({
|
|||||||
const q = searchParams.get("q")
|
const q = searchParams.get("q")
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { // update the dashboard to show a new project
|
||||||
if (!sandboxes) {
|
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}, [])
|
||||||
}, [sandboxes])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -37,43 +37,41 @@ import { useRouter } from "next/navigation"
|
|||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
|
|
||||||
type TOptions = "react" | "node" | "python" | "more"
|
|
||||||
|
|
||||||
const data: {
|
const data: {
|
||||||
id: TOptions
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
description: string
|
description: string
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
id: "react",
|
id: "reactjs",
|
||||||
name: "React",
|
name: "React",
|
||||||
icon: "/project-icons/react.svg",
|
icon: "/project-icons/react.svg",
|
||||||
description: "A JavaScript library for building user interfaces",
|
description: "A JavaScript library for building user interfaces",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "node",
|
id: "vanillajs",
|
||||||
name: "Node",
|
name: "HTML/JS",
|
||||||
|
icon: "/project-icons/more.svg",
|
||||||
|
description: "More coming soon, feel free to contribute on GitHub",
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nextjs",
|
||||||
|
name: "NextJS",
|
||||||
icon: "/project-icons/node.svg",
|
icon: "/project-icons/node.svg",
|
||||||
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "python",
|
id: "streamlit",
|
||||||
name: "Python",
|
name: "Streamlit",
|
||||||
icon: "/project-icons/python.svg",
|
icon: "/project-icons/python.svg",
|
||||||
description: "A high-level, general-purpose language, coming soon",
|
description: "A JavaScript runtime built on the V8 JavaScript engine",
|
||||||
disabled: true,
|
disabled: false,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "more",
|
|
||||||
name: "More Languages",
|
|
||||||
icon: "/project-icons/more.svg",
|
|
||||||
description: "More coming soon, feel free to contribute on GitHub",
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@ -95,7 +93,7 @@ export default function NewProjectModal({
|
|||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState<TOptions>("react")
|
const [selected, setSelected] = useState("reactjs")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import { toast } from "sonner";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CanvasRevealEffect } from "./projectCard/revealEffect";
|
import { CanvasRevealEffect } from "./projectCard/revealEffect";
|
||||||
|
|
||||||
const colors = {
|
const colors: { [key: string]: number[][] } = {
|
||||||
react: [
|
react: [
|
||||||
[71, 207, 237],
|
[71, 207, 237],
|
||||||
[30, 126, 148],
|
[30, 126, 148],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client"
|
||||||
@ -59,7 +59,7 @@ export default function GenerateInput({
|
|||||||
}: {
|
}: {
|
||||||
regenerate?: boolean
|
regenerate?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
if (user.generations >= 10) {
|
if (user.generations >= 1000) {
|
||||||
toast.error("You reached the maximum # of generations.")
|
toast.error("You reached the maximum # of generations.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -84,6 +84,13 @@ export default function GenerateInput({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const handleGenerateForm = useCallback(
|
||||||
|
(e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleGenerate({ regenerate: false })
|
||||||
|
},
|
||||||
|
[input, currentPrompt]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (code) {
|
if (code) {
|
||||||
@ -93,9 +100,23 @@ export default function GenerateInput({
|
|||||||
}
|
}
|
||||||
}, [code])
|
}, [code])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//listen to when Esc key is pressed and close the modal
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full pr-4 space-y-2">
|
<div className="w-full pr-4 space-y-2">
|
||||||
<div className="flex items-center font-sans space-x-2">
|
<form
|
||||||
|
onSubmit={handleGenerateForm}
|
||||||
|
className="flex items-center font-sans space-x-2"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{
|
style={{
|
||||||
@ -109,8 +130,8 @@ export default function GenerateInput({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type="submit"
|
||||||
disabled={loading.generate || loading.regenerate || input === ""}
|
disabled={loading.generate || loading.regenerate || input === ""}
|
||||||
onClick={() => handleGenerate({})}
|
|
||||||
>
|
>
|
||||||
{loading.generate ? (
|
{loading.generate ? (
|
||||||
<>
|
<>
|
||||||
@ -126,13 +147,14 @@ export default function GenerateInput({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="smIcon"
|
size="smIcon"
|
||||||
className="bg-transparent shrink-0 border-muted-foreground"
|
className="bg-transparent shrink-0 border-muted-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</form>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2">
|
<div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2">
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import monaco from "monaco-editor"
|
import * as monaco from "monaco-editor"
|
||||||
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
||||||
import { io } from "socket.io-client"
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useClerk } from "@clerk/nextjs"
|
import { useClerk } from "@clerk/nextjs"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
|
||||||
import * as Y from "yjs"
|
import * as Y from "yjs"
|
||||||
import LiveblocksProvider from "@liveblocks/yjs"
|
import LiveblocksProvider from "@liveblocks/yjs"
|
||||||
@ -18,12 +18,12 @@ import {
|
|||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable"
|
} from "@/components/ui/resizable"
|
||||||
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
|
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
|
||||||
import Tab from "../ui/tab"
|
import Tab from "../ui/tab"
|
||||||
import Sidebar from "./sidebar"
|
import Sidebar from "./sidebar"
|
||||||
import GenerateInput from "./generate"
|
import GenerateInput from "./generate"
|
||||||
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
|
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
|
||||||
import { addNew, processFileType, validateName } from "@/lib/utils"
|
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
|
||||||
import { Cursors } from "./live/cursors"
|
import { Cursors } from "./live/cursors"
|
||||||
import { Terminal } from "@xterm/xterm"
|
import { Terminal } from "@xterm/xterm"
|
||||||
import DisableAccessModal from "./live/disableModal"
|
import DisableAccessModal from "./live/disableModal"
|
||||||
@ -31,23 +31,33 @@ import Loading from "./loading"
|
|||||||
import PreviewWindow from "./preview"
|
import PreviewWindow from "./preview"
|
||||||
import Terminals from "./terminals"
|
import Terminals from "./terminals"
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
|
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
|
||||||
|
import { useSocket } from "@/context/SocketContext"
|
||||||
|
import { Button } from "../ui/button"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
reactDefinitionFile,
|
|
||||||
}: {
|
}: {
|
||||||
userData: User
|
userData: User
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox
|
||||||
reactDefinitionFile: string
|
|
||||||
}) {
|
}) {
|
||||||
const socket = io(
|
//SocketContext functions and effects
|
||||||
`http://localhost:${process.env.NEXT_PUBLIC_SERVER_PORT}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
|
const { socket, setUserAndSandboxId } = useSocket()
|
||||||
{
|
|
||||||
timeout: 2000,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||||
|
if (userData.id && sandboxData.id) {
|
||||||
|
// Check if the socket is not initialized or not connected
|
||||||
|
if (!socket || (socket && !socket.connected)) {
|
||||||
|
// Initialize socket connection
|
||||||
|
setUserAndSandboxId(userData.id, sandboxData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
|
||||||
|
|
||||||
|
//Preview Button state
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
const [disableAccess, setDisableAccess] = useState({
|
const [disableAccess, setDisableAccess] = useState({
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
@ -60,6 +70,8 @@ export default function CodeEditor({
|
|||||||
const [activeFileId, setActiveFileId] = useState<string>("")
|
const [activeFileId, setActiveFileId] = useState<string>("")
|
||||||
const [activeFileContent, setActiveFileContent] = useState("")
|
const [activeFileContent, setActiveFileContent] = useState("")
|
||||||
const [deletingFolderId, setDeletingFolderId] = useState("")
|
const [deletingFolderId, setDeletingFolderId] = useState("")
|
||||||
|
// Added this state to track the most recent content for each file
|
||||||
|
const [fileContents, setFileContents] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||||
@ -68,7 +80,6 @@ export default function CodeEditor({
|
|||||||
useState<monaco.editor.IStandaloneCodeEditor>()
|
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||||
|
|
||||||
// AI Copilot state
|
// AI Copilot state
|
||||||
const [ai, setAi] = useState(false)
|
|
||||||
const [generate, setGenerate] = useState<{
|
const [generate, setGenerate] = useState<{
|
||||||
show: boolean
|
show: boolean
|
||||||
id: string
|
id: string
|
||||||
@ -81,7 +92,8 @@ export default function CodeEditor({
|
|||||||
options: monaco.editor.IModelDeltaDecoration[]
|
options: monaco.editor.IModelDeltaDecoration[]
|
||||||
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
||||||
}>({ options: [], instance: undefined })
|
}>({ options: [], instance: undefined })
|
||||||
|
const [isSelected, setIsSelected] = useState(false)
|
||||||
|
const [showSuggestion, setShowSuggestion] = useState(false)
|
||||||
// Terminal state
|
// Terminal state
|
||||||
const [terminals, setTerminals] = useState<
|
const [terminals, setTerminals] = useState<
|
||||||
{
|
{
|
||||||
@ -90,6 +102,16 @@ export default function CodeEditor({
|
|||||||
}[]
|
}[]
|
||||||
>([])
|
>([])
|
||||||
|
|
||||||
|
// Preview state
|
||||||
|
const [previewURL, setPreviewURL] = useState<string>("")
|
||||||
|
|
||||||
|
const loadPreviewURL = (url: string) => {
|
||||||
|
// This will cause a reload if previewURL changed.
|
||||||
|
setPreviewURL(url)
|
||||||
|
// If the URL didn't change, still reload the preview.
|
||||||
|
previewWindowRef.current?.refreshIframe()
|
||||||
|
}
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id
|
const isOwner = sandboxData.userId === userData.id
|
||||||
const clerk = useClerk()
|
const clerk = useClerk()
|
||||||
|
|
||||||
@ -98,14 +120,31 @@ export default function CodeEditor({
|
|||||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||||
const userInfo = useSelf((me) => me.info)
|
const userInfo = useSelf((me) => me.info)
|
||||||
|
|
||||||
|
// Liveblocks providers map to prevent reinitializing providers
|
||||||
|
type ProviderData = {
|
||||||
|
provider: LiveblocksProvider<never, never, never, never>
|
||||||
|
yDoc: Y.Doc
|
||||||
|
yText: Y.Text
|
||||||
|
binding?: MonacoBinding
|
||||||
|
onSync: (isSynced: boolean) => void
|
||||||
|
}
|
||||||
|
const providersMap = useRef(new Map<string, ProviderData>())
|
||||||
|
|
||||||
// Refs for libraries / features
|
// Refs for libraries / features
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const monacoRef = useRef<typeof monaco | null>(null)
|
const monacoRef = useRef<typeof monaco | null>(null)
|
||||||
const generateRef = useRef<HTMLDivElement>(null)
|
const generateRef = useRef<HTMLDivElement>(null)
|
||||||
|
const suggestionRef = useRef<HTMLDivElement>(null)
|
||||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||||
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||||
|
|
||||||
|
const debouncedSetIsSelected = useRef(
|
||||||
|
debounce((value: boolean) => {
|
||||||
|
setIsSelected(value)
|
||||||
|
}, 800) //
|
||||||
|
).current
|
||||||
// Pre-mount editor keybindings
|
// Pre-mount editor keybindings
|
||||||
const handleEditorWillMount: BeforeMount = (monaco) => {
|
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||||
monaco.editor.addKeybindingRules([
|
monaco.editor.addKeybindingRules([
|
||||||
@ -122,6 +161,13 @@ export default function CodeEditor({
|
|||||||
monacoRef.current = monaco
|
monacoRef.current = monaco
|
||||||
|
|
||||||
editor.onDidChangeCursorPosition((e) => {
|
editor.onDidChangeCursorPosition((e) => {
|
||||||
|
setIsSelected(false)
|
||||||
|
const selection = editor.getSelection()
|
||||||
|
if (selection !== null) {
|
||||||
|
const hasSelection = !selection.isEmpty()
|
||||||
|
debouncedSetIsSelected(hasSelection)
|
||||||
|
setShowSuggestion(hasSelection)
|
||||||
|
}
|
||||||
const { column, lineNumber } = e.position
|
const { column, lineNumber } = e.position
|
||||||
if (lineNumber === cursorLine) return
|
if (lineNumber === cursorLine) return
|
||||||
setCursorLine(lineNumber)
|
setCursorLine(lineNumber)
|
||||||
@ -175,21 +221,52 @@ export default function CodeEditor({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleAiEdit = React.useCallback(() => {
|
||||||
// Generate widget effect
|
if (!editorRef) return
|
||||||
useEffect(() => {
|
const selection = editorRef.getSelection()
|
||||||
if (!ai) {
|
if (!selection) return
|
||||||
|
const pos = selection.getPosition()
|
||||||
|
const start = selection.getStartPosition()
|
||||||
|
const end = selection.getEndPosition()
|
||||||
|
let pref: monaco.editor.ContentWidgetPositionPreference
|
||||||
|
let id = ""
|
||||||
|
const isMultiline = start.lineNumber !== end.lineNumber
|
||||||
|
if (isMultiline) {
|
||||||
|
if (pos.lineNumber <= start.lineNumber) {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
} else {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
}
|
||||||
|
editorRef.changeViewZones(function (changeAccessor) {
|
||||||
|
if (!generateRef.current) return
|
||||||
|
if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber: start.lineNumber - 1,
|
||||||
|
heightInLines: 2,
|
||||||
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: false,
|
show: true,
|
||||||
|
pref: [pref],
|
||||||
|
id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
}, [editorRef])
|
||||||
}
|
|
||||||
|
// Generate widget effect
|
||||||
|
useEffect(() => {
|
||||||
if (generate.show) {
|
if (generate.show) {
|
||||||
|
setShowSuggestion(false)
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
if (!generateRef.current) return
|
if (!generateRef.current) return
|
||||||
|
if (!generate.id) {
|
||||||
const id = changeAccessor.addZone({
|
const id = changeAccessor.addZone({
|
||||||
afterLineNumber: cursorLine,
|
afterLineNumber: cursorLine,
|
||||||
heightInLines: 3,
|
heightInLines: 3,
|
||||||
@ -198,6 +275,10 @@ export default function CodeEditor({
|
|||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id, line: cursorLine }
|
return { ...prev, id, line: cursorLine }
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
setGenerate((prev) => {
|
||||||
|
return { ...prev, line: cursorLine }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!generateWidgetRef.current) return
|
if (!generateWidgetRef.current) return
|
||||||
@ -258,21 +339,64 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
}, [generate.show])
|
}, [generate.show])
|
||||||
|
|
||||||
|
// Suggestion widget effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (!suggestionRef.current || !editorRef) return
|
||||||
|
const widgetElement = suggestionRef.current
|
||||||
|
const suggestionWidget: monaco.editor.IContentWidget = {
|
||||||
|
getDomNode: () => {
|
||||||
|
return widgetElement
|
||||||
|
},
|
||||||
|
getId: () => {
|
||||||
|
return "suggestion.widget"
|
||||||
|
},
|
||||||
|
getPosition: () => {
|
||||||
|
const selection = editorRef?.getSelection()
|
||||||
|
const column = Math.max(3, selection?.positionColumn ?? 1)
|
||||||
|
let lineNumber = selection?.positionLineNumber ?? 1
|
||||||
|
let pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
if (lineNumber <= 3) {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
preference: [pref],
|
||||||
|
position: {
|
||||||
|
lineNumber,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
editorRef?.addContentWidget(suggestionWidget)
|
||||||
|
editorRef?.applyFontInfo(suggestionRef.current)
|
||||||
|
} else {
|
||||||
|
editorRef?.removeContentWidget(suggestionWidget)
|
||||||
|
}
|
||||||
|
}, [isSelected])
|
||||||
|
|
||||||
// Decorations effect for generate widget tips
|
// Decorations effect for generate widget tips
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (decorations.options.length === 0) {
|
if (decorations.options.length === 0) {
|
||||||
decorations.instance?.clear()
|
decorations.instance?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ai) return
|
|
||||||
|
|
||||||
const model = editorRef?.getModel()
|
const model = editorRef?.getModel()
|
||||||
const line = model?.getLineContent(cursorLine)
|
// added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file
|
||||||
|
if (model) {
|
||||||
if (line === undefined || line.trim() !== "") {
|
const totalLines = model.getLineCount();
|
||||||
decorations.instance?.clear()
|
// Check if the cursorLine is a valid number, If cursorLine is out of bounds, we fall back to 1 (the first line) as a default safe value.
|
||||||
|
const lineNumber = cursorLine > 0 && cursorLine <= totalLines ? cursorLine : 1; // fallback to a valid line number
|
||||||
|
// If for some reason the content doesn't exist, we use an empty string as a fallback.
|
||||||
|
const line = model.getLineContent(lineNumber) ?? "";
|
||||||
|
// Check if the line is not empty or only whitespace (i.e., `.trim()` removes spaces).
|
||||||
|
// If the line has content, we clear any decorations using the instance of the `decorations` object.
|
||||||
|
// Decorations refer to editor highlights, underlines, or markers, so this clears those if conditions are met.
|
||||||
|
if (line.trim() !== "") {
|
||||||
|
decorations.instance?.clear();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (decorations.instance) {
|
if (decorations.instance) {
|
||||||
decorations.instance.set(decorations.options)
|
decorations.instance.set(decorations.options)
|
||||||
@ -290,18 +414,33 @@ export default function CodeEditor({
|
|||||||
}, [decorations.options])
|
}, [decorations.options])
|
||||||
|
|
||||||
// Save file keybinding logic effect
|
// Save file keybinding logic effect
|
||||||
useEffect(() => {
|
// Function to save the file content after a debounce period
|
||||||
const down = (e: KeyboardEvent) => {
|
const debouncedSaveData = useCallback(
|
||||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
debounce((activeFileId: string | undefined) => {
|
||||||
e.preventDefault()
|
if (activeFileId) {
|
||||||
|
// Get the current content of the file
|
||||||
|
const content = fileContents[activeFileId];
|
||||||
|
|
||||||
|
// Mark the file as saved in the tabs
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
console.log(`Saving file...${activeFileId}`);
|
||||||
|
console.log(`Saving file...${content}`);
|
||||||
|
socket?.emit("saveFile", activeFileId, content);
|
||||||
|
}
|
||||||
|
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
||||||
|
[socket, fileContents]
|
||||||
|
);
|
||||||
|
|
||||||
socket.emit("saveFile", activeFileId, editorRef?.getValue())
|
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
debouncedSaveData(activeFileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down)
|
||||||
@ -309,7 +448,7 @@ export default function CodeEditor({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [tabs, activeFileId])
|
}, [activeFileId, tabs, debouncedSaveData])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -318,10 +457,15 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
if (!editorRef || !tab || !model) return
|
if (!editorRef || !tab || !model) return
|
||||||
|
|
||||||
|
let providerData: ProviderData
|
||||||
|
|
||||||
|
// When a file is opened for the first time, create a new provider and store in providersMap.
|
||||||
|
if (!providersMap.current.has(tab.id)) {
|
||||||
const yDoc = new Y.Doc()
|
const yDoc = new Y.Doc()
|
||||||
const yText = yDoc.getText(tab.id)
|
const yText = yDoc.getText(tab.id)
|
||||||
const yProvider: any = new LiveblocksProvider(room, yDoc)
|
const yProvider = new LiveblocksProvider(room, yDoc)
|
||||||
|
|
||||||
|
// Inserts the file content into the editor once when the tab is changed.
|
||||||
const onSync = (isSynced: boolean) => {
|
const onSync = (isSynced: boolean) => {
|
||||||
if (isSynced) {
|
if (isSynced) {
|
||||||
const text = yText.toString()
|
const text = yText.toString()
|
||||||
@ -339,31 +483,57 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
yProvider.on("sync", onSync)
|
yProvider.on("sync", onSync)
|
||||||
|
|
||||||
setProvider(yProvider)
|
// Save the provider to the map.
|
||||||
|
providerData = { provider: yProvider, yDoc, yText, onSync }
|
||||||
|
providersMap.current.set(tab.id, providerData)
|
||||||
|
} else {
|
||||||
|
// When a tab is opened that has been open before, reuse the existing provider.
|
||||||
|
providerData = providersMap.current.get(tab.id)!
|
||||||
|
}
|
||||||
|
|
||||||
const binding = new MonacoBinding(
|
const binding = new MonacoBinding(
|
||||||
yText,
|
providerData.yText,
|
||||||
model,
|
model,
|
||||||
new Set([editorRef]),
|
new Set([editorRef]),
|
||||||
yProvider.awareness as Awareness
|
providerData.provider.awareness as unknown as Awareness
|
||||||
)
|
)
|
||||||
|
|
||||||
|
providerData.binding = binding
|
||||||
|
setProvider(providerData.provider)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
yDoc.destroy()
|
// Cleanup logic
|
||||||
yProvider.destroy()
|
if (binding) {
|
||||||
binding.destroy()
|
binding.destroy()
|
||||||
yProvider.off("sync", onSync)
|
|
||||||
}
|
}
|
||||||
}, [editorRef, room, activeFileContent])
|
if (providerData.binding) {
|
||||||
|
providerData.binding = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [room, activeFileContent])
|
||||||
|
|
||||||
|
// Added this effect to clean up when the component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Clean up all providers when the component unmounts
|
||||||
|
providersMap.current.forEach((data) => {
|
||||||
|
if (data.binding) {
|
||||||
|
data.binding.destroy()
|
||||||
|
}
|
||||||
|
data.provider.disconnect()
|
||||||
|
data.yDoc.destroy()
|
||||||
|
})
|
||||||
|
providersMap.current.clear()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Connection/disconnection effect
|
// Connection/disconnection effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect()
|
socket?.connect()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.disconnect()
|
socket?.disconnect()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [socket])
|
||||||
|
|
||||||
// Socket event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -377,7 +547,7 @@ export default function CodeEditor({
|
|||||||
setFiles(files)
|
setFiles(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRateLimit = (message: string) => {
|
const onError = (message: string) => {
|
||||||
toast.error(message)
|
toast.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,53 +566,88 @@ export default function CodeEditor({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("connect", onConnect)
|
socket?.on("connect", onConnect)
|
||||||
socket.on("disconnect", onDisconnect)
|
socket?.on("disconnect", onDisconnect)
|
||||||
socket.on("loaded", onLoadedEvent)
|
socket?.on("loaded", onLoadedEvent)
|
||||||
socket.on("rateLimit", onRateLimit)
|
socket?.on("error", onError)
|
||||||
socket.on("terminalResponse", onTerminalResponse)
|
socket?.on("terminalResponse", onTerminalResponse)
|
||||||
socket.on("disableAccess", onDisableAccess)
|
socket?.on("disableAccess", onDisableAccess)
|
||||||
|
socket?.on("previewURL", loadPreviewURL)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("connect", onConnect)
|
socket?.off("connect", onConnect)
|
||||||
socket.off("disconnect", onDisconnect)
|
socket?.off("disconnect", onDisconnect)
|
||||||
socket.off("loaded", onLoadedEvent)
|
socket?.off("loaded", onLoadedEvent)
|
||||||
socket.off("rateLimit", onRateLimit)
|
socket?.off("error", onError)
|
||||||
socket.off("terminalResponse", onTerminalResponse)
|
socket?.off("terminalResponse", onTerminalResponse)
|
||||||
socket.off("disableAccess", onDisableAccess)
|
socket?.off("disableAccess", onDisableAccess)
|
||||||
|
socket?.off("previewURL", loadPreviewURL)
|
||||||
}
|
}
|
||||||
// }, []);
|
}, [
|
||||||
}, [terminals])
|
socket,
|
||||||
|
terminals,
|
||||||
|
setTerminals,
|
||||||
|
setFiles,
|
||||||
|
toast,
|
||||||
|
setDisableAccess,
|
||||||
|
isOwner,
|
||||||
|
loadPreviewURL,
|
||||||
|
])
|
||||||
|
|
||||||
// Helper functions for tabs:
|
// Helper functions for tabs:
|
||||||
|
|
||||||
// Select file and load content
|
// Select file and load content
|
||||||
|
|
||||||
|
// Initialize debounced function once
|
||||||
|
const fileCache = useRef(new Map())
|
||||||
|
|
||||||
|
// Debounced function to get file content
|
||||||
|
const debouncedGetFile = (tabId: any, callback: any) => {
|
||||||
|
socket?.emit("getFile", tabId, callback)
|
||||||
|
} // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
if (tab.id === activeFileId) return
|
if (tab.id === activeFileId) return;
|
||||||
|
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => ({ ...prev, show: false }));
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
show: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const exists = tabs.find((t) => t.id === tab.id)
|
|
||||||
|
|
||||||
|
// Check if the tab already exists in the list of open tabs
|
||||||
|
const exists = tabs.find((t) => t.id === tab.id);
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
setActiveFileId(exists.id)
|
// If the tab exists, make it the active tab
|
||||||
return prev
|
setActiveFileId(exists.id);
|
||||||
|
return prev;
|
||||||
}
|
}
|
||||||
return [...prev, tab]
|
// If the tab doesn't exist, add it to the list of tabs and make it active
|
||||||
})
|
return [...prev, tab];
|
||||||
|
});
|
||||||
|
|
||||||
socket.emit("getFile", tab.id, (response: string) => {
|
// If the file's content is already cached, set it as the active content
|
||||||
setActiveFileContent(response)
|
if (fileContents[tab.id]) {
|
||||||
})
|
setActiveFileContent(fileContents[tab.id]);
|
||||||
setEditorLanguage(processFileType(tab.name))
|
} else {
|
||||||
setActiveFileId(tab.id)
|
// Otherwise, fetch the content of the file and cache it
|
||||||
|
debouncedGetFile(tab.id, (response: string) => {
|
||||||
|
setFileContents(prev => ({ ...prev, [tab.id]: response }));
|
||||||
|
setActiveFileContent(response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the editor language based on the file type
|
||||||
|
setEditorLanguage(processFileType(tab.name));
|
||||||
|
// Set the active file ID to the new tab
|
||||||
|
setActiveFileId(tab.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Added this effect to update fileContents when the editor content changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeFileId) {
|
||||||
|
// Cache the current active file content using the file ID as the key
|
||||||
|
setFileContents(prev => ({ ...prev, [activeFileId]: activeFileContent }));
|
||||||
|
}
|
||||||
|
}, [activeFileContent, activeFileId]);
|
||||||
|
|
||||||
// Close tab and remove from tabs
|
// Close tab and remove from tabs
|
||||||
const closeTab = (id: string) => {
|
const closeTab = (id: string) => {
|
||||||
const numTabs = tabs.length
|
const numTabs = tabs.length
|
||||||
@ -515,7 +720,7 @@ export default function CodeEditor({
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("renameFile", id, newName)
|
socket?.emit("renameFile", id, newName)
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
|
||||||
)
|
)
|
||||||
@ -524,7 +729,7 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteFile = (file: TFile) => {
|
const handleDeleteFile = (file: TFile) => {
|
||||||
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response)
|
||||||
})
|
})
|
||||||
closeTab(file.id)
|
closeTab(file.id)
|
||||||
@ -534,11 +739,11 @@ export default function CodeEditor({
|
|||||||
setDeletingFolderId(folder.id)
|
setDeletingFolderId(folder.id)
|
||||||
console.log("deleting folder", folder.id)
|
console.log("deleting folder", folder.id)
|
||||||
|
|
||||||
socket.emit("getFolder", folder.id, (response: string[]) =>
|
socket?.emit("getFolder", folder.id, (response: string[]) =>
|
||||||
closeTabs(response)
|
closeTabs(response)
|
||||||
)
|
)
|
||||||
|
|
||||||
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response)
|
setFiles(response)
|
||||||
setDeletingFolderId("")
|
setDeletingFolderId("")
|
||||||
})
|
})
|
||||||
@ -560,31 +765,76 @@ export default function CodeEditor({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Copilot DOM elements */}
|
{/* Copilot DOM elements */}
|
||||||
|
<PreviewProvider>
|
||||||
<div ref={generateRef} />
|
<div ref={generateRef} />
|
||||||
|
<div ref={suggestionRef} className="absolute">
|
||||||
|
<AnimatePresence>
|
||||||
|
{isSelected && showSuggestion && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ ease: "easeOut", duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button size="xs" type="submit" onClick={handleAiEdit}>
|
||||||
|
<Sparkles className="h-3 w-3 mr-1" />
|
||||||
|
Edit Code
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
<div className="z-50 p-1" ref={generateWidgetRef}>
|
<div className="z-50 p-1" ref={generateWidgetRef}>
|
||||||
{generate.show && ai ? (
|
{generate.show ? (
|
||||||
<GenerateInput
|
<GenerateInput
|
||||||
user={userData}
|
user={userData}
|
||||||
socket={socket}
|
socket={socket!}
|
||||||
width={generate.width - 90}
|
width={generate.width - 90}
|
||||||
data={{
|
data={{
|
||||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||||
code: editorRef?.getValue() ?? "",
|
code:
|
||||||
|
(isSelected && editorRef?.getSelection()
|
||||||
|
? editorRef
|
||||||
|
?.getModel()
|
||||||
|
?.getValueInRange(editorRef?.getSelection()!)
|
||||||
|
: editorRef?.getValue()) ?? "",
|
||||||
line: generate.line,
|
line: generate.line,
|
||||||
}}
|
}}
|
||||||
editor={{
|
editor={{
|
||||||
language: editorLanguage,
|
language: editorLanguage,
|
||||||
}}
|
}}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
|
const line = generate.line
|
||||||
|
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id)
|
changeAccessor.removeZone(generate.id)
|
||||||
|
|
||||||
if (!generateRef.current) return
|
if (!generateRef.current) return
|
||||||
const id = changeAccessor.addZone({
|
let id = ""
|
||||||
afterLineNumber: cursorLine,
|
if (isSelected) {
|
||||||
heightInLines: 12,
|
const selection = editorRef?.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
const isAbove =
|
||||||
|
generate.pref?.[0] ===
|
||||||
|
monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
const afterLineNumber = isAbove ? line - 1 : line
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber,
|
||||||
|
heightInLines: isAbove ? 11 : 12,
|
||||||
domNode: generateRef.current,
|
domNode: generateRef.current,
|
||||||
})
|
})
|
||||||
|
const contentWidget = generate.widget
|
||||||
|
if (contentWidget) {
|
||||||
|
editorRef?.layoutContentWidget(contentWidget)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber: cursorLine,
|
||||||
|
heightInLines: 12,
|
||||||
|
|
||||||
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
}
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id }
|
return { ...prev, id }
|
||||||
})
|
})
|
||||||
@ -598,12 +848,14 @@ export default function CodeEditor({
|
|||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const file = editorRef?.getValue()
|
const selection = editorRef?.getSelection()
|
||||||
|
const range =
|
||||||
const lines = file?.split("\n") || []
|
isSelected && selection
|
||||||
lines.splice(line - 1, 0, code)
|
? selection
|
||||||
const updatedFile = lines.join("\n")
|
: new monaco.Range(line, 1, line, 1)
|
||||||
editorRef?.setValue(updatedFile)
|
editorRef?.executeEdits("ai-generation", [
|
||||||
|
{ range, text: code, forceMoveMarkers: true },
|
||||||
|
])
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
@ -625,13 +877,10 @@ export default function CodeEditor({
|
|||||||
handleRename={handleRename}
|
handleRename={handleRename}
|
||||||
handleDeleteFile={handleDeleteFile}
|
handleDeleteFile={handleDeleteFile}
|
||||||
handleDeleteFolder={handleDeleteFolder}
|
handleDeleteFolder={handleDeleteFolder}
|
||||||
socket={socket}
|
socket={socket!}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||||
deletingFolderId={deletingFolderId}
|
deletingFolderId={deletingFolderId}
|
||||||
// AI Copilot Toggle
|
|
||||||
ai={ai}
|
|
||||||
setAi={setAi}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||||
@ -683,15 +932,10 @@ export default function CodeEditor({
|
|||||||
beforeMount={handleEditorWillMount}
|
beforeMount={handleEditorWillMount}
|
||||||
onMount={handleEditorMount}
|
onMount={handleEditorMount}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (value === activeFileContent) {
|
// If the new content is different from the cached content, update it
|
||||||
setTabs((prev) =>
|
if (value !== fileContents[activeFileId]) {
|
||||||
prev.map((tab) =>
|
setActiveFileContent(value ?? ""); // Update the active file content
|
||||||
tab.id === activeFileId
|
// Mark the file as unsaved by setting 'saved' to false
|
||||||
? { ...tab, saved: true }
|
|
||||||
: tab
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId
|
tab.id === activeFileId
|
||||||
@ -699,6 +943,15 @@ export default function CodeEditor({
|
|||||||
: tab
|
: tab
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// If the content matches the cached content, mark the file as saved
|
||||||
|
setTabs((prev) =>
|
||||||
|
prev.map((tab) =>
|
||||||
|
tab.id === activeFileId
|
||||||
|
? { ...tab, saved: true }
|
||||||
|
: tab
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
@ -730,7 +983,7 @@ export default function CodeEditor({
|
|||||||
<ResizablePanel defaultSize={40}>
|
<ResizablePanel defaultSize={40}>
|
||||||
<ResizablePanelGroup direction="vertical">
|
<ResizablePanelGroup direction="vertical">
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
ref={previewPanelRef}
|
ref={usePreview().previewPanelRef}
|
||||||
defaultSize={4}
|
defaultSize={4}
|
||||||
collapsedSize={4}
|
collapsedSize={4}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
@ -740,11 +993,13 @@ export default function CodeEditor({
|
|||||||
onExpand={() => setIsPreviewCollapsed(false)}
|
onExpand={() => setIsPreviewCollapsed(false)}
|
||||||
>
|
>
|
||||||
<PreviewWindow
|
<PreviewWindow
|
||||||
collapsed={isPreviewCollapsed}
|
|
||||||
open={() => {
|
open={() => {
|
||||||
previewPanelRef.current?.expand()
|
usePreview().previewPanelRef.current?.expand()
|
||||||
setIsPreviewCollapsed(false)
|
setIsPreviewCollapsed(false)
|
||||||
}}
|
}}
|
||||||
|
collapsed={isPreviewCollapsed}
|
||||||
|
src={previewURL}
|
||||||
|
ref={previewWindowRef}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
@ -754,11 +1009,7 @@ export default function CodeEditor({
|
|||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
>
|
>
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<Terminals
|
<Terminals />
|
||||||
terminals={terminals}
|
|
||||||
setTerminals={setTerminals}
|
|
||||||
socket={socket}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||||
@ -769,6 +1020,7 @@ export default function CodeEditor({
|
|||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
</PreviewProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
78
frontend/components/editor/navbar/deploy.tsx
Normal file
78
frontend/components/editor/navbar/deploy.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTerminal } from "@/context/TerminalContext";
|
||||||
|
import { Play, Pause, Globe, Globe2 } from "lucide-react";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Sandbox, User } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function DeployButtonModal({
|
||||||
|
userData,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
userData: User;
|
||||||
|
data: Sandbox;
|
||||||
|
}) {
|
||||||
|
const { deploy } = useTerminal();
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
const handleDeploy = () => {
|
||||||
|
if (isDeploying) {
|
||||||
|
console.log("Stopping deployment...");
|
||||||
|
setIsDeploying(false);
|
||||||
|
} else {
|
||||||
|
console.log("Starting deployment...");
|
||||||
|
setIsDeploying(true);
|
||||||
|
deploy(() => {
|
||||||
|
setIsDeploying(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Globe className="w-4 h-4 mr-2" />
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg" style={{ backgroundColor: 'rgb(10,10,10)', color: 'white' }}>
|
||||||
|
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<DeploymentOption
|
||||||
|
icon={<Globe className="text-gray-500 w-5 h-5" />}
|
||||||
|
domain={`${data.id}.gitwit.app`}
|
||||||
|
timestamp="Deployed 1h ago"
|
||||||
|
user={userData.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" onClick={handleDeploy}>
|
||||||
|
{isDeploying ? "Deploying..." : "Update"}
|
||||||
|
</Button>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.ReactNode; domain: string; timestamp: string; user: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
|
||||||
|
<div className="flex items-start gap-2 relative">
|
||||||
|
<div className="flex-shrink-0">{icon}</div>
|
||||||
|
<a
|
||||||
|
href={`https://${domain}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-semibold text-gray-300 hover:underline"
|
||||||
|
>
|
||||||
|
{domain}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400 mt-0 ml-7">{timestamp} • {user}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -11,6 +11,8 @@ import { useState } from "react";
|
|||||||
import EditSandboxModal from "./edit";
|
import EditSandboxModal from "./edit";
|
||||||
import ShareSandboxModal from "./share";
|
import ShareSandboxModal from "./share";
|
||||||
import { Avatars } from "../live/avatars";
|
import { Avatars } from "../live/avatars";
|
||||||
|
import RunButtonModal from "./run";
|
||||||
|
import DeployButtonModal from "./deploy";
|
||||||
|
|
||||||
export default function Navbar({
|
export default function Navbar({
|
||||||
userData,
|
userData,
|
||||||
@ -19,15 +21,13 @@ export default function Navbar({
|
|||||||
}: {
|
}: {
|
||||||
userData: User;
|
userData: User;
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox;
|
||||||
shared: {
|
shared: { id: string; name: string }[];
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
}) {
|
}) {
|
||||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||||
const [isShareOpen, setIsShareOpen] = useState(false);
|
const [isShareOpen, setIsShareOpen] = useState(false);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id;
|
const isOwner = sandboxData.userId === userData.id;;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -62,14 +62,25 @@ export default function Navbar({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RunButtonModal
|
||||||
|
isRunning={isRunning}
|
||||||
|
setIsRunning={setIsRunning}
|
||||||
|
sandboxData={sandboxData}
|
||||||
|
/>
|
||||||
<div className="flex items-center h-full space-x-4">
|
<div className="flex items-center h-full space-x-4">
|
||||||
<Avatars />
|
<Avatars />
|
||||||
|
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
|
<>
|
||||||
|
<DeployButtonModal
|
||||||
|
data={sandboxData}
|
||||||
|
userData={userData}
|
||||||
|
/>
|
||||||
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
|
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
|
||||||
<Users className="w-4 h-4 mr-2" />
|
<Users className="w-4 h-4 mr-2" />
|
||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<UserButton userData={userData} />
|
<UserButton userData={userData} />
|
||||||
</div>
|
</div>
|
||||||
|
73
frontend/components/editor/navbar/run.tsx
Normal file
73
frontend/components/editor/navbar/run.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Play, StopCircle } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTerminal } from "@/context/TerminalContext";
|
||||||
|
import { usePreview } from "@/context/PreviewContext";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Sandbox } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function RunButtonModal({
|
||||||
|
isRunning,
|
||||||
|
setIsRunning,
|
||||||
|
sandboxData,
|
||||||
|
}: {
|
||||||
|
isRunning: boolean;
|
||||||
|
setIsRunning: (running: boolean) => void;
|
||||||
|
sandboxData: Sandbox;
|
||||||
|
}) {
|
||||||
|
const { createNewTerminal, closeTerminal, terminals } = useTerminal();
|
||||||
|
const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
|
||||||
|
// Ref to keep track of the last created terminal's ID
|
||||||
|
const lastCreatedTerminalRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Effect to update the lastCreatedTerminalRef when a new terminal is added
|
||||||
|
useEffect(() => {
|
||||||
|
if (terminals.length > 0 && !isRunning) {
|
||||||
|
const latestTerminal = terminals[terminals.length - 1];
|
||||||
|
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) {
|
||||||
|
lastCreatedTerminalRef.current = latestTerminal.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [terminals, isRunning]);
|
||||||
|
|
||||||
|
const handleRun = async () => {
|
||||||
|
if (isRunning && lastCreatedTerminalRef.current)
|
||||||
|
{
|
||||||
|
await closeTerminal(lastCreatedTerminalRef.current);
|
||||||
|
lastCreatedTerminalRef.current = null;
|
||||||
|
setIsPreviewCollapsed(true);
|
||||||
|
previewPanelRef.current?.collapse();
|
||||||
|
}
|
||||||
|
else if (!isRunning && terminals.length < 4)
|
||||||
|
{
|
||||||
|
const command = sandboxData.type === "streamlit"
|
||||||
|
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
|
||||||
|
: "yarn install && yarn dev";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a new terminal with the appropriate command
|
||||||
|
await createNewTerminal(command);
|
||||||
|
setIsPreviewCollapsed(false);
|
||||||
|
previewPanelRef.current?.expand();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to create new terminal.");
|
||||||
|
console.error("Error creating new terminal:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!isRunning) {
|
||||||
|
toast.error("You've reached the maximum number of terminals.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRunning(!isRunning);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="outline" onClick={handleRun}>
|
||||||
|
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
|
||||||
|
{isRunning ? 'Stop' : 'Run'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,64 +1,65 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Globe,
|
|
||||||
Link,
|
Link,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
UnfoldVertical,
|
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useRef, useState } from "react"
|
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function PreviewWindow({
|
export default forwardRef(function PreviewWindow({
|
||||||
collapsed,
|
collapsed,
|
||||||
open,
|
open,
|
||||||
|
src
|
||||||
}: {
|
}: {
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
open: () => void
|
open: () => void
|
||||||
}) {
|
src: string
|
||||||
const ref = useRef<HTMLIFrameElement>(null)
|
},
|
||||||
|
ref: React.Ref<{
|
||||||
|
refreshIframe: () => void
|
||||||
|
}>) {
|
||||||
|
const frameRef = useRef<HTMLIFrameElement>(null)
|
||||||
const [iframeKey, setIframeKey] = useState(0)
|
const [iframeKey, setIframeKey] = useState(0)
|
||||||
|
const refreshIframe = () => {
|
||||||
|
setIframeKey(prev => prev + 1)
|
||||||
|
}
|
||||||
|
// Refresh the preview when the URL changes.
|
||||||
|
useEffect(refreshIframe, [src])
|
||||||
|
// Expose refreshIframe method to the parent.
|
||||||
|
useImperativeHandle(ref, () => ({ refreshIframe }))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${collapsed ? "h-full" : "h-10"
|
||||||
collapsed ? "h-full" : "h-10"
|
|
||||||
} select-none w-full flex gap-2`}
|
} select-none w-full flex gap-2`}
|
||||||
>
|
>
|
||||||
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
|
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
|
||||||
<div className="text-xs">Preview</div>
|
<div className="text-xs">Preview</div>
|
||||||
<div className="flex space-x-1 translate-x-1">
|
<div className="flex space-x-1 translate-x-1">
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<PreviewButton onClick={open}>
|
<PreviewButton disabled onClick={() => { }}>
|
||||||
<UnfoldVertical className="w-4 h-4" />
|
<TerminalSquare className="w-4 h-4" />
|
||||||
</PreviewButton>
|
</PreviewButton>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Todo, make this open inspector */}
|
{/* Removed the unfoldvertical button since we have the same thing via the run button.
|
||||||
{/* <PreviewButton disabled onClick={() => {}}>
|
|
||||||
<TerminalSquare className="w-4 h-4" />
|
<PreviewButton onClick={open}>
|
||||||
|
<UnfoldVertical className="w-4 h-4" />
|
||||||
</PreviewButton> */}
|
</PreviewButton> */}
|
||||||
|
|
||||||
<PreviewButton
|
<PreviewButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`http://localhost:5173`)
|
navigator.clipboard.writeText(src)
|
||||||
toast.info("Copied preview link to clipboard")
|
toast.info("Copied preview link to clipboard")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link className="w-4 h-4" />
|
<Link className="w-4 h-4" />
|
||||||
</PreviewButton>
|
</PreviewButton>
|
||||||
<PreviewButton
|
<PreviewButton onClick={refreshIframe}>
|
||||||
onClick={() => {
|
|
||||||
// if (ref.current) {
|
|
||||||
// ref.current.contentWindow?.location.reload();
|
|
||||||
// }
|
|
||||||
setIframeKey((prev) => prev + 1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RotateCw className="w-3 h-3" />
|
<RotateCw className="w-3 h-3" />
|
||||||
</PreviewButton>
|
</PreviewButton>
|
||||||
</>
|
</>
|
||||||
@ -70,16 +71,16 @@ export default function PreviewWindow({
|
|||||||
<div className="w-full grow rounded-md overflow-hidden bg-foreground">
|
<div className="w-full grow rounded-md overflow-hidden bg-foreground">
|
||||||
<iframe
|
<iframe
|
||||||
key={iframeKey}
|
key={iframeKey}
|
||||||
ref={ref}
|
ref={frameRef}
|
||||||
width={"100%"}
|
width={"100%"}
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
src={`http://localhost:5173`}
|
src={src}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
function PreviewButton({
|
function PreviewButton({
|
||||||
children,
|
children,
|
||||||
@ -92,8 +93,7 @@ function PreviewButton({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${disabled ? "pointer-events-none opacity-50" : ""
|
||||||
disabled ? "pointer-events-none opacity-50" : ""
|
|
||||||
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
|
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
@ -32,8 +32,6 @@ export default function Sidebar({
|
|||||||
socket,
|
socket,
|
||||||
setFiles,
|
setFiles,
|
||||||
addNew,
|
addNew,
|
||||||
ai,
|
|
||||||
setAi,
|
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
}: {
|
}: {
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox;
|
||||||
@ -50,8 +48,6 @@ export default function Sidebar({
|
|||||||
socket: Socket;
|
socket: Socket;
|
||||||
setFiles: (files: (TFile | TFolder)[]) => void;
|
setFiles: (files: (TFile | TFolder)[]) => void;
|
||||||
addNew: (name: string, type: "file" | "folder") => void;
|
addNew: (name: string, type: "file" | "folder") => void;
|
||||||
ai: boolean;
|
|
||||||
setAi: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
deletingFolderId: string;
|
deletingFolderId: string;
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null); // drop target
|
const ref = useRef(null); // drop target
|
||||||
@ -186,20 +182,6 @@ export default function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Sparkles
|
|
||||||
className={`h-4 w-4 mr-2 ${
|
|
||||||
ai ? "text-indigo-500" : "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
Copilot{" "}
|
|
||||||
<span className="font-mono text-muted-foreground inline-block ml-1.5 text-xs leading-none border border-b-2 border-muted-foreground py-1 px-1.5 rounded-md">
|
|
||||||
⌘G
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Switch checked={ai} onCheckedChange={setAi} />
|
|
||||||
</div>
|
|
||||||
{/* <Button className="w-full">
|
{/* <Button className="w-full">
|
||||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
||||||
</Button> */}
|
</Button> */}
|
||||||
|
@ -2,35 +2,45 @@
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Tab from "@/components/ui/tab";
|
import Tab from "@/components/ui/tab";
|
||||||
import { closeTerminal, createTerminal } from "@/lib/terminal";
|
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
|
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
|
||||||
import { Socket } from "socket.io-client";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import EditorTerminal from "./terminal";
|
import EditorTerminal from "./terminal";
|
||||||
import { useState } from "react";
|
import { useTerminal } from "@/context/TerminalContext";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSocket } from "@/context/SocketContext"
|
||||||
|
|
||||||
export default function Terminals({
|
export default function Terminals() {
|
||||||
|
|
||||||
|
const { socket } = useSocket();
|
||||||
|
|
||||||
|
const {
|
||||||
terminals,
|
terminals,
|
||||||
setTerminals,
|
setTerminals,
|
||||||
socket,
|
createNewTerminal,
|
||||||
}: {
|
closeTerminal,
|
||||||
terminals: { id: string; terminal: Terminal | null }[];
|
activeTerminalId,
|
||||||
setTerminals: React.Dispatch<
|
setActiveTerminalId,
|
||||||
React.SetStateAction<
|
creatingTerminal,
|
||||||
{
|
} = useTerminal();
|
||||||
id: string;
|
|
||||||
terminal: Terminal | null;
|
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
socket: Socket;
|
|
||||||
}) {
|
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState("");
|
|
||||||
const [creatingTerminal, setCreatingTerminal] = useState(false);
|
|
||||||
const [closingTerminal, setClosingTerminal] = useState("");
|
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
||||||
|
|
||||||
|
// Effect to set the active terminal when a new one is created
|
||||||
|
useEffect(() => {
|
||||||
|
if (terminals.length > 0 && !activeTerminalId) {
|
||||||
|
setActiveTerminalId(terminals[terminals.length - 1].id);
|
||||||
|
}
|
||||||
|
}, [terminals, activeTerminalId, setActiveTerminalId]);
|
||||||
|
|
||||||
|
const handleCreateTerminal = () => {
|
||||||
|
if (terminals.length >= 4) {
|
||||||
|
toast.error("You reached the maximum # of terminals.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createNewTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
|
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
|
||||||
@ -39,18 +49,7 @@ export default function Terminals({
|
|||||||
key={term.id}
|
key={term.id}
|
||||||
creating={creatingTerminal}
|
creating={creatingTerminal}
|
||||||
onClick={() => setActiveTerminalId(term.id)}
|
onClick={() => setActiveTerminalId(term.id)}
|
||||||
onClose={() =>
|
onClose={() => closeTerminal(term.id)}
|
||||||
closeTerminal({
|
|
||||||
term,
|
|
||||||
terminals,
|
|
||||||
setTerminals,
|
|
||||||
setActiveTerminalId,
|
|
||||||
setClosingTerminal,
|
|
||||||
socket,
|
|
||||||
activeTerminalId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
closing={closingTerminal === term.id}
|
|
||||||
selected={activeTerminalId === term.id}
|
selected={activeTerminalId === term.id}
|
||||||
>
|
>
|
||||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||||
@ -59,18 +58,7 @@ export default function Terminals({
|
|||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
disabled={creatingTerminal}
|
disabled={creatingTerminal}
|
||||||
onClick={() => {
|
onClick={handleCreateTerminal}
|
||||||
if (terminals.length >= 4) {
|
|
||||||
toast.error("You reached the maximum # of terminals.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createTerminal({
|
|
||||||
setTerminals,
|
|
||||||
setActiveTerminalId,
|
|
||||||
setCreatingTerminal,
|
|
||||||
socket,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="smIcon"
|
size="smIcon"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
|
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
|
||||||
|
@ -55,7 +55,6 @@ export default function EditorTerminal({
|
|||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
|
||||||
const disposableOnData = term.onData((data) => {
|
const disposableOnData = term.onData((data) => {
|
||||||
console.log("terminalData", id, data);
|
|
||||||
socket.emit("terminalData", id, data);
|
socket.emit("terminalData", id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,6 +73,20 @@ export default function EditorTerminal({
|
|||||||
};
|
};
|
||||||
}, [term, terminalRef.current]);
|
}, [term, terminalRef.current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!term) return;
|
||||||
|
const handleTerminalResponse = (response: { id: string; data: string }) => {
|
||||||
|
if (response.id === id) {
|
||||||
|
term.write(response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.on("terminalResponse", handleTerminalResponse);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("terminalResponse", handleTerminalResponse);
|
||||||
|
};
|
||||||
|
}, [term, id, socket]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
@ -22,6 +22,7 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2",
|
||||||
|
xs: "h-6 px-2.5 py-1.5 rounded-sm text-[0.7rem]",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-9 w-9",
|
icon: "h-9 w-9",
|
||||||
|
33
frontend/components/ui/popover.tsx
Normal file
33
frontend/components/ui/popover.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
@ -42,13 +42,13 @@ export default function UserButton({ userData }: { userData: User }) {
|
|||||||
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
|
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
|
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
|
||||||
AI Usage: {userData.generations}/10
|
AI Usage: {userData.generations}/1000
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
|
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-indigo-500 rounded-full"
|
className="h-full bg-indigo-500 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
width: `${(userData.generations * 100) / 10}%`,
|
width: `${(userData.generations * 100) / 1000}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
34
frontend/context/PreviewContext.tsx
Normal file
34
frontend/context/PreviewContext.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useRef } from 'react';
|
||||||
|
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||||
|
|
||||||
|
interface PreviewContextType {
|
||||||
|
isPreviewCollapsed: boolean;
|
||||||
|
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
previewURL: string;
|
||||||
|
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
previewPanelRef: React.RefObject<ImperativePanelHandle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PreviewContext = createContext<PreviewContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
|
||||||
|
const [previewURL, setPreviewURL] = useState<string>("");
|
||||||
|
const previewPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
|
||||||
|
{children}
|
||||||
|
</PreviewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePreview = () => {
|
||||||
|
const context = useContext(PreviewContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('usePreview must be used within a PreviewProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
63
frontend/context/SocketContext.tsx
Normal file
63
frontend/context/SocketContext.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
interface SocketContextType {
|
||||||
|
socket: Socket | null;
|
||||||
|
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SocketContext = createContext<SocketContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
|
const [sandboxId, setSandboxId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId && sandboxId) {
|
||||||
|
console.log("Initializing socket connection...");
|
||||||
|
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
|
||||||
|
console.log("Socket instance:", newSocket);
|
||||||
|
setSocket(newSocket);
|
||||||
|
|
||||||
|
newSocket.on('connect', () => {
|
||||||
|
console.log("Socket connected:", newSocket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('disconnect', () => {
|
||||||
|
console.log("Socket disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("Disconnecting socket...");
|
||||||
|
newSocket.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [userId, sandboxId]);
|
||||||
|
|
||||||
|
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
|
||||||
|
setUserId(newUserId);
|
||||||
|
setSandboxId(newSandboxId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
socket,
|
||||||
|
setUserAndSandboxId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={ value }>
|
||||||
|
{children}
|
||||||
|
</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSocket = (): SocketContextType => {
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSocket must be used within a SocketProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
95
frontend/context/TerminalContext.tsx
Normal file
95
frontend/context/TerminalContext.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
|
||||||
|
import { useSocket } from '@/context/SocketContext';
|
||||||
|
|
||||||
|
interface TerminalContextType {
|
||||||
|
terminals: { id: string; terminal: Terminal | null }[];
|
||||||
|
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
|
||||||
|
activeTerminalId: string;
|
||||||
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
creatingTerminal: boolean;
|
||||||
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
createNewTerminal: (command?: string) => Promise<void>;
|
||||||
|
closeTerminal: (id: string) => void;
|
||||||
|
deploy: (callback: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { socket } = useSocket();
|
||||||
|
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
|
||||||
|
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
|
||||||
|
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const createNewTerminal = async (command?: string): Promise<void> => {
|
||||||
|
if (!socket) return;
|
||||||
|
setCreatingTerminal(true);
|
||||||
|
try {
|
||||||
|
createTerminalHelper({
|
||||||
|
setTerminals,
|
||||||
|
setActiveTerminalId,
|
||||||
|
setCreatingTerminal,
|
||||||
|
command,
|
||||||
|
socket,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating terminal:", error);
|
||||||
|
} finally {
|
||||||
|
setCreatingTerminal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTerminal = (id: string) => {
|
||||||
|
if (!socket) return;
|
||||||
|
const terminalToClose = terminals.find(term => term.id === id);
|
||||||
|
if (terminalToClose) {
|
||||||
|
closeTerminalHelper({
|
||||||
|
term: terminalToClose,
|
||||||
|
terminals,
|
||||||
|
setTerminals,
|
||||||
|
setActiveTerminalId,
|
||||||
|
setClosingTerminal: () => {},
|
||||||
|
socket,
|
||||||
|
activeTerminalId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deploy = (callback: () => void) => {
|
||||||
|
if (!socket) console.error("Couldn't deploy: No socket");
|
||||||
|
console.log("Deploying...")
|
||||||
|
socket?.emit("deploy", () => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
terminals,
|
||||||
|
setTerminals,
|
||||||
|
activeTerminalId,
|
||||||
|
setActiveTerminalId,
|
||||||
|
creatingTerminal,
|
||||||
|
setCreatingTerminal,
|
||||||
|
createNewTerminal,
|
||||||
|
closeTerminal,
|
||||||
|
deploy
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TerminalContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</TerminalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTerminal = (): TerminalContextType => {
|
||||||
|
const context = useContext(TerminalContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTerminal must be used within a TerminalProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
295
frontend/lib/file-extension-to-language.json
Normal file
295
frontend/lib/file-extension-to-language.json
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"_coffee": "coffeescript",
|
||||||
|
"_js": "javascript",
|
||||||
|
"adp": "tcl",
|
||||||
|
"al": "perl",
|
||||||
|
"ant": "xml",
|
||||||
|
"aw": "php",
|
||||||
|
"axml": "xml",
|
||||||
|
"bash": "shell",
|
||||||
|
"bats": "shell",
|
||||||
|
"bones": "javascript",
|
||||||
|
"boot": "clojure",
|
||||||
|
"builder": "ruby",
|
||||||
|
"bzl": "python",
|
||||||
|
"c": "c",
|
||||||
|
"c++": "cpp",
|
||||||
|
"cake": "coffeescript",
|
||||||
|
"cats": "c",
|
||||||
|
"cc": "cpp",
|
||||||
|
"ccxml": "xml",
|
||||||
|
"cfg": "ini",
|
||||||
|
"cgi": "shell",
|
||||||
|
"cjsx": "coffeescript",
|
||||||
|
"cl2": "clojure",
|
||||||
|
"clixml": "xml",
|
||||||
|
"clj": "clojure",
|
||||||
|
"cljc": "clojure",
|
||||||
|
"cljs.hl": "clojure",
|
||||||
|
"cljs": "clojure",
|
||||||
|
"cljscm": "clojure",
|
||||||
|
"cljx": "clojure",
|
||||||
|
"coffee": "coffeescript",
|
||||||
|
"command": "shell",
|
||||||
|
"cp": "cpp",
|
||||||
|
"cpp": "cpp",
|
||||||
|
"cproject": "xml",
|
||||||
|
"cql": "sql",
|
||||||
|
"csl": "xml",
|
||||||
|
"cson": "coffeescript",
|
||||||
|
"csproj": "xml",
|
||||||
|
"ct": "xml",
|
||||||
|
"ctp": "php",
|
||||||
|
"cxx": "cpp",
|
||||||
|
"ddl": "sql",
|
||||||
|
"dfm": "pascal",
|
||||||
|
"dita": "xml",
|
||||||
|
"ditamap": "xml",
|
||||||
|
"ditaval": "xml",
|
||||||
|
"dll.config": "xml",
|
||||||
|
"dotsettings": "xml",
|
||||||
|
"dpr": "pascal",
|
||||||
|
"ecl": "ecl",
|
||||||
|
"eclxml": "ecl",
|
||||||
|
"es": "javascript",
|
||||||
|
"es6": "javascript",
|
||||||
|
"ex": "elixir",
|
||||||
|
"exs": "elixir",
|
||||||
|
"fcgi": "shell",
|
||||||
|
"filters": "xml",
|
||||||
|
"frag": "javascript",
|
||||||
|
"fsproj": "xml",
|
||||||
|
"fxml": "xml",
|
||||||
|
"gemspec": "ruby",
|
||||||
|
"geojson": "json",
|
||||||
|
"glade": "xml",
|
||||||
|
"gml": "xml",
|
||||||
|
"god": "ruby",
|
||||||
|
"grxml": "xml",
|
||||||
|
"gs": "javascript",
|
||||||
|
"gyp": "python",
|
||||||
|
"h": "cpp",
|
||||||
|
"h++": "cpp",
|
||||||
|
"handlebars": "handlebars",
|
||||||
|
"hbs": "handlebars",
|
||||||
|
"hcl": "hcl",
|
||||||
|
"hh": "cpp",
|
||||||
|
"hic": "clojure",
|
||||||
|
"hpp": "cpp",
|
||||||
|
"htm": "html",
|
||||||
|
"html.hl": "html",
|
||||||
|
"html": "html",
|
||||||
|
"hxx": "cpp",
|
||||||
|
"iced": "coffeescript",
|
||||||
|
"idc": "c",
|
||||||
|
"iml": "xml",
|
||||||
|
"inc": "sql",
|
||||||
|
"ini": "ini",
|
||||||
|
"inl": "cpp",
|
||||||
|
"ipp": "cpp",
|
||||||
|
"irbrc": "ruby",
|
||||||
|
"ivy": "xml",
|
||||||
|
"j2": "python",
|
||||||
|
"jake": "javascript",
|
||||||
|
"jbuilder": "ruby",
|
||||||
|
"jelly": "xml",
|
||||||
|
"jinja": "python",
|
||||||
|
"jinja2": "python",
|
||||||
|
"js": "javascript",
|
||||||
|
"jsb": "javascript",
|
||||||
|
"jscad": "javascript",
|
||||||
|
"jsfl": "javascript",
|
||||||
|
"jsm": "javascript",
|
||||||
|
"json": "json",
|
||||||
|
"jsproj": "xml",
|
||||||
|
"jss": "javascript",
|
||||||
|
"kml": "xml",
|
||||||
|
"ksh": "shell",
|
||||||
|
"kt": "kotlin",
|
||||||
|
"ktm": "kotlin",
|
||||||
|
"kts": "kotlin",
|
||||||
|
"launch": "xml",
|
||||||
|
"lmi": "python",
|
||||||
|
"lock": "json",
|
||||||
|
"lpr": "pascal",
|
||||||
|
"lua": "lua",
|
||||||
|
"markdown": "markdown",
|
||||||
|
"md": "markdown",
|
||||||
|
"mdpolicy": "xml",
|
||||||
|
"mkd": "markdown",
|
||||||
|
"mkdn": "markdown",
|
||||||
|
"mkdown": "markdown",
|
||||||
|
"mm": "xml",
|
||||||
|
"mod": "xml",
|
||||||
|
"mspec": "ruby",
|
||||||
|
"mustache": "python",
|
||||||
|
"mxml": "xml",
|
||||||
|
"njs": "javascript",
|
||||||
|
"nproj": "xml",
|
||||||
|
"nse": "lua",
|
||||||
|
"nuspec": "xml",
|
||||||
|
"odd": "xml",
|
||||||
|
"osm": "xml",
|
||||||
|
"pac": "javascript",
|
||||||
|
"pas": "pascal",
|
||||||
|
"pd_lua": "lua",
|
||||||
|
"perl": "perl",
|
||||||
|
"ph": "perl",
|
||||||
|
"php": "php",
|
||||||
|
"php3": "php",
|
||||||
|
"php4": "php",
|
||||||
|
"php5": "php",
|
||||||
|
"phps": "php",
|
||||||
|
"phpt": "php",
|
||||||
|
"pl": "perl",
|
||||||
|
"plist": "xml",
|
||||||
|
"pluginspec": "xml",
|
||||||
|
"plx": "perl",
|
||||||
|
"pm": "perl",
|
||||||
|
"pod": "perl",
|
||||||
|
"podspec": "ruby",
|
||||||
|
"pp": "pascal",
|
||||||
|
"prc": "sql",
|
||||||
|
"prefs": "ini",
|
||||||
|
"pro": "ini",
|
||||||
|
"properties": "ini",
|
||||||
|
"props": "xml",
|
||||||
|
"ps1": "powershell",
|
||||||
|
"ps1xml": "xml",
|
||||||
|
"psc1": "xml",
|
||||||
|
"psd1": "powershell",
|
||||||
|
"psgi": "perl",
|
||||||
|
"psm1": "powershell",
|
||||||
|
"pt": "xml",
|
||||||
|
"py": "python",
|
||||||
|
"pyde": "python",
|
||||||
|
"pyp": "python",
|
||||||
|
"pyt": "python",
|
||||||
|
"pyw": "python",
|
||||||
|
"r": "r",
|
||||||
|
"rabl": "ruby",
|
||||||
|
"rake": "ruby",
|
||||||
|
"rb": "ruby",
|
||||||
|
"rbuild": "ruby",
|
||||||
|
"rbw": "ruby",
|
||||||
|
"rbx": "ruby",
|
||||||
|
"rbxs": "lua",
|
||||||
|
"rd": "r",
|
||||||
|
"rdf": "xml",
|
||||||
|
"reek": "yaml",
|
||||||
|
"rest.txt": "restructuredtext",
|
||||||
|
"rest": "restructuredtext",
|
||||||
|
"ron": "markdown",
|
||||||
|
"rpy": "python",
|
||||||
|
"rq": "sparql",
|
||||||
|
"rs.in": "rust",
|
||||||
|
"rs": "rust",
|
||||||
|
"rss": "xml",
|
||||||
|
"rst.txt": "restructuredtext",
|
||||||
|
"rst": "restructuredtext",
|
||||||
|
"rsx": "r",
|
||||||
|
"ru": "ruby",
|
||||||
|
"ruby": "ruby",
|
||||||
|
"rviz": "yaml",
|
||||||
|
"sbt": "scala",
|
||||||
|
"sc": "scala",
|
||||||
|
"scala": "scala",
|
||||||
|
"scm": "scheme",
|
||||||
|
"scxml": "xml",
|
||||||
|
"sh.in": "shell",
|
||||||
|
"sh": "shell",
|
||||||
|
"sjs": "javascript",
|
||||||
|
"sld": "scheme",
|
||||||
|
"sls": "scheme",
|
||||||
|
"sparql": "sparql",
|
||||||
|
"sps": "scheme",
|
||||||
|
"sql": "sql",
|
||||||
|
"srdf": "xml",
|
||||||
|
"ss": "scheme",
|
||||||
|
"ssjs": "javascript",
|
||||||
|
"st": "html",
|
||||||
|
"storyboard": "xml",
|
||||||
|
"sttheme": "xml",
|
||||||
|
"sublime_metrics": "javascript",
|
||||||
|
"sublime_session": "javascript",
|
||||||
|
"sublime-build": "javascript",
|
||||||
|
"sublime-commands": "javascript",
|
||||||
|
"sublime-completions": "javascript",
|
||||||
|
"sublime-keymap": "javascript",
|
||||||
|
"sublime-macro": "javascript",
|
||||||
|
"sublime-menu": "javascript",
|
||||||
|
"sublime-mousemap": "javascript",
|
||||||
|
"sublime-project": "javascript",
|
||||||
|
"sublime-settings": "javascript",
|
||||||
|
"sublime-snippet": "xml",
|
||||||
|
"sublime-syntax": "yaml",
|
||||||
|
"sublime-theme": "javascript",
|
||||||
|
"sublime-workspace": "javascript",
|
||||||
|
"sv": "systemverilog",
|
||||||
|
"svh": "systemverilog",
|
||||||
|
"syntax": "yaml",
|
||||||
|
"t": "perl",
|
||||||
|
"tab": "sql",
|
||||||
|
"tac": "python",
|
||||||
|
"targets": "xml",
|
||||||
|
"tcc": "cpp",
|
||||||
|
"tcl": "tcl",
|
||||||
|
"tf": "hcl",
|
||||||
|
"thor": "ruby",
|
||||||
|
"tm": "tcl",
|
||||||
|
"tmcommand": "xml",
|
||||||
|
"tml": "xml",
|
||||||
|
"tmlanguage": "xml",
|
||||||
|
"tmpreferences": "xml",
|
||||||
|
"tmsnippet": "xml",
|
||||||
|
"tmtheme": "xml",
|
||||||
|
"tmux": "shell",
|
||||||
|
"tool": "shell",
|
||||||
|
"topojson": "json",
|
||||||
|
"tpp": "cpp",
|
||||||
|
"ts": "typescript",
|
||||||
|
"tsx": "typescript",
|
||||||
|
"udf": "sql",
|
||||||
|
"ui": "xml",
|
||||||
|
"urdf": "xml",
|
||||||
|
"ux": "xml",
|
||||||
|
"v": "verilog",
|
||||||
|
"vbproj": "xml",
|
||||||
|
"vcxproj": "xml",
|
||||||
|
"veo": "verilog",
|
||||||
|
"vh": "systemverilog",
|
||||||
|
"viw": "sql",
|
||||||
|
"vssettings": "xml",
|
||||||
|
"vxml": "xml",
|
||||||
|
"w": "c",
|
||||||
|
"watchr": "ruby",
|
||||||
|
"wlua": "lua",
|
||||||
|
"wsdl": "xml",
|
||||||
|
"wsf": "xml",
|
||||||
|
"wsgi": "python",
|
||||||
|
"wxi": "xml",
|
||||||
|
"wxl": "xml",
|
||||||
|
"wxs": "xml",
|
||||||
|
"x3d": "xml",
|
||||||
|
"xacro": "xml",
|
||||||
|
"xaml": "xml",
|
||||||
|
"xht": "html",
|
||||||
|
"xhtml": "html",
|
||||||
|
"xib": "xml",
|
||||||
|
"xlf": "xml",
|
||||||
|
"xliff": "xml",
|
||||||
|
"xmi": "xml",
|
||||||
|
"xml.dist": "xml",
|
||||||
|
"xml": "xml",
|
||||||
|
"xproj": "xml",
|
||||||
|
"xpy": "python",
|
||||||
|
"xsd": "xml",
|
||||||
|
"xsjs": "javascript",
|
||||||
|
"xsjslib": "javascript",
|
||||||
|
"xul": "xml",
|
||||||
|
"yaml-tmlanguage": "yaml",
|
||||||
|
"yaml": "yaml",
|
||||||
|
"yml": "yaml",
|
||||||
|
"zcml": "xml",
|
||||||
|
"zsh": "shell"
|
||||||
|
}
|
@ -8,6 +8,7 @@ export const createTerminal = ({
|
|||||||
setTerminals,
|
setTerminals,
|
||||||
setActiveTerminalId,
|
setActiveTerminalId,
|
||||||
setCreatingTerminal,
|
setCreatingTerminal,
|
||||||
|
command,
|
||||||
socket,
|
socket,
|
||||||
}: {
|
}: {
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{
|
setTerminals: React.Dispatch<React.SetStateAction<{
|
||||||
@ -16,6 +17,7 @@ export const createTerminal = ({
|
|||||||
}[]>>;
|
}[]>>;
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
command?: string;
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
||||||
}) => {
|
}) => {
|
||||||
@ -29,6 +31,7 @@ export const createTerminal = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
socket.emit("createTerminal", id, () => {
|
socket.emit("createTerminal", id, () => {
|
||||||
setCreatingTerminal(false);
|
setCreatingTerminal(false);
|
||||||
|
if (command) socket.emit("terminalData", id, command + "\n");
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export type User = {
|
|||||||
export type Sandbox = {
|
export type Sandbox = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: "react" | "node";
|
type: string;
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private";
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -2,18 +2,19 @@ import { type ClassValue, clsx } from "clsx"
|
|||||||
// import { toast } from "sonner"
|
// import { toast } from "sonner"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { Sandbox, TFile, TFolder } from "./types"
|
import { Sandbox, TFile, TFolder } from "./types"
|
||||||
|
import fileExtToLang from "./file-extension-to-language.json"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processFileType(file: string) {
|
export function processFileType(file: string) {
|
||||||
const ending = file.split(".").pop()
|
const extension = file.split(".").pop()
|
||||||
|
const fileExtToLangMap = fileExtToLang as Record<string, string>
|
||||||
|
if (extension && fileExtToLangMap[extension]) {
|
||||||
|
return fileExtToLangMap[extension]
|
||||||
|
}
|
||||||
|
|
||||||
if (ending === "ts" || ending === "tsx") return "typescript"
|
|
||||||
if (ending === "js" || ending === "jsx") return "javascript"
|
|
||||||
|
|
||||||
if (ending) return ending
|
|
||||||
return "plaintext"
|
return "plaintext"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,3 +62,16 @@ export function addNew(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debounce<T extends (...args: any[]) => void>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): T {
|
||||||
|
let timeout: NodeJS.Timeout | null = null
|
||||||
|
return function (...args: Parameters<T>) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => func(...args), wait)
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
444
frontend/package-lock.json
generated
444
frontend/package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"monaco-themes": "^0.4.4",
|
"monaco-themes": "^0.4.4",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"posthog-js": "^1.147.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.51.3",
|
"react-hook-form": "^7.51.3",
|
||||||
@ -1118,6 +1120,419 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.7"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0",
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.4",
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.0",
|
||||||
|
"use-sidecar": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
|
||||||
@ -3269,6 +3684,30 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/posthog-js": {
|
||||||
|
"version": "1.147.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.147.0.tgz",
|
||||||
|
"integrity": "sha512-cALKIcix1W9xMsfUUI/QUFhtBeabDGsQCb3lpZEFd7WCPNLOcGmpeomm4xMX7MO2OnfK6Ov91/HNyfK1wncjSA==",
|
||||||
|
"dependencies": {
|
||||||
|
"fflate": "^0.4.8",
|
||||||
|
"preact": "^10.19.3",
|
||||||
|
"web-vitals": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/posthog-js/node_modules/fflate": {
|
||||||
|
"version": "0.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||||
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
||||||
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
|
||||||
|
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pvtsutils": {
|
"node_modules/pvtsutils": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
|
||||||
@ -4083,6 +4522,11 @@
|
|||||||
"@types/jasmine": "^3.6.3"
|
"@types/jasmine": "^3.6.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-vitals": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-U6bAxeudnhDqcXNl50JC4hLlqox9DZnngxfISZm3DMZnonW35xtJOVUc091L+DOY+6hVZVpKXoiCP0RiT6339Q=="
|
||||||
|
},
|
||||||
"node_modules/webcrypto-core": {
|
"node_modules/webcrypto-core": {
|
||||||
"version": "1.7.9",
|
"version": "1.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.9.tgz",
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"monaco-themes": "^0.4.4",
|
"monaco-themes": "^0.4.4",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"posthog-js": "^1.147.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.51.3",
|
"react-hook-form": "^7.51.3",
|
||||||
|
630
package-lock.json
generated
Normal file
630
package-lock.json
generated
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
{
|
||||||
|
"name": "sandbox",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-popover": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz",
|
||||||
|
"integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz",
|
||||||
|
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.7"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0",
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||||
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
|
||||||
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/invariant": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.4",
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.0",
|
||||||
|
"use-sidecar": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||||
|
},
|
||||||
|
"node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-popover": "^1.1.1"
|
||||||
|
}
|
||||||
|
}
|
47
tests/index.ts
Normal file
47
tests/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Import necessary modules
|
||||||
|
import { io, Socket } from "socket.io-client";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
interface CallbackResponse {
|
||||||
|
success: boolean;
|
||||||
|
apps?: string[];
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let socketRef: Socket = io(
|
||||||
|
`http://localhost:4000?userId=user_2hFB6KcK6bb3Gx9241UXsxFq4kO&sandboxId=v30a2c48xal03tzio7mapt19`,
|
||||||
|
{
|
||||||
|
timeout: 2000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
socketRef.on("connect", async () => {
|
||||||
|
console.log("Connected to the server");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
socketRef.emit("list", (response: CallbackResponse) => {
|
||||||
|
if (response.success) {
|
||||||
|
console.log("List of apps:", response.apps);
|
||||||
|
} else {
|
||||||
|
console.log("Error:", response.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.emit("deploy", (response: CallbackResponse) => {
|
||||||
|
if (response.success) {
|
||||||
|
console.log("It worked!");
|
||||||
|
} else {
|
||||||
|
console.log("Error:", response.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.on("disconnect", () => {
|
||||||
|
console.log("Disconnected from the server");
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.on("connect_error", (error: Error) => {
|
||||||
|
console.error("Connection error:", error);
|
||||||
|
});
|
310
tests/package-lock.json
generated
Normal file
310
tests/package-lock.json
generated
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
{
|
||||||
|
"name": "socket-io-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "socket-io-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.14.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
|
||||||
|
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||||
|
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||||
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||||
|
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||||
|
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
||||||
|
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
tests/package.json
Normal file
21
tests/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "socket-io-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A test script for socket.io-client using ES6 modules and TypeScript",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "npm run build && node dist/index.js"
|
||||||
|
},
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"socket.io-client": "^4.7.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
}
|
||||||
|
}
|
13
tests/tsconfig.json
Normal file
13
tests/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["index.ts"]
|
||||||
|
}
|
Reference in New Issue
Block a user