Compare commits

..

4 Commits

Author SHA1 Message Date
44f803ffaf feat: add run button 2024-07-15 15:33:26 -04:00
d9ce147e09 fix: editor on windows 2024-07-08 16:48:06 -04:00
398139ec36 fix: store rooms in map 2024-07-06 18:18:13 -04:00
bd6284df8f docs: add information about E2B 2024-06-19 21:57:40 -04:00
137 changed files with 3109 additions and 10087 deletions

View File

@ -1,6 +0,0 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false,
"insertFinalNewline": true
}

View File

@ -1,8 +0,0 @@
{
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}

View File

@ -1,7 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Ishaan Dey Copyright (c) 2024 Ishaan Dey
Copyright (c) 2024 GitWit, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

282
README.md
View File

@ -1,213 +1,103 @@
# GitWit Sandbox 📦🪄 # Sandbox 📦🪄
![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72) <img width="1799" alt="Screenshot 2024-05-31 at 8 33 56AM" src="https://github.com/ishaan1013/sandbox/assets/69771365/3f73d7c0-f82a-4997-b01e-eaa043e95113">
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration and AI chat. Sandbox is an open-source cloud-based code editing environment with custom AI code autocompletion and real-time collaboration.
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/). Check out the [Twitter thread](https://x.com/ishaandey_/status/1796338262002573526) with the demo video!
Check out this [guide](https://dev.to/jamesmurdza/how-to-setup-ishaan1013sandbox-locally-503p) made by [@jamesmurdza](https://x.com/jamesmurdza) on setting it up locally!
## Running Locally ## Running Locally
Notes: ### Frontend
- Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files. Install dependencies
- Right now we are loading project templates from a custom Cloudflare bucket which isn't covered in this guide, but that be updated/fixed very soon.
### 0. Requirements
The application uses NodeJS for the backend, NextJS for the frontend and Cloudflare workers for additional backend tasks.
Needed accounts to set up:
- [Clerk](https://clerk.com/): Used for user authentication.
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
- [E2B](https://e2b.dev/): Used for the terminals and live preview.
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
### 1. Initial setup
No surprise in the first step:
```bash ```bash
git clone https://github.com/jamesmurdza/sandbox cd frontend
cd sandbox npm install
``` ```
Run `npm install` in: Add the required environment variables in `.env` (example file provided in `.env.example`). You will need to make an account on [Clerk](https://clerk.com/) and [Liveblocks](https://liveblocks.io/) to get API keys.
``` Then, run in development mode
/frontend
/backend/database
/backend/storage
/backend/server
/backend/ai
```
### 2. Adding Clerk
Setup the Clerk account.
Get the API keys from Clerk.
Update `/frontend/.env`:
```
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY='🔑'
CLERK_SECRET_KEY='🔑'
```
### 3. Deploying the storage bucket
Go to Cloudflare.
Create and name an R2 storage bucket in the control panel.
Copy the account ID of one domain.
Update `/backend/storage/src/wrangler.toml`:
```
account_id = '🔑'
bucket_name = '🔑'
key = 'SUPERDUPERSECRET'
```
In the `/backend/storage/src` directory:
```
npx wrangler deploy
```
### 4. Deploying the database
Create a database:
```
npx wrangler d1 create sandbox-database
```
Use the output for the next setp.
Update `/backend/database/src/wrangler.toml`:
```
database_name = '🔑'
database_id = '🔑'
KEY = 'SUPERDUPERSECRET'
STORAGE_WORKER_URL = 'https://storage.🍎.workers.dev'
```
In the `/backend/database/src` directory:
```
npx wrangler deploy
```
### 5. Applying the database schema
Delete the `/backend/database/drizzle/meta` directory.
In the `/backend/database/` directory:
```
npm run generate
npx wrangler d1 execute sandbox-database --remote --file=./drizzle/0000_🍏_🍐.sql
```
### 6. Configuring the server
Update `/backend/server/.env`:
```
DATABASE_WORKER_URL='https://database.🍎.workers.dev'
STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
WORKERS_KEY='SUPERDUPERSECRET'
```
### 7. Adding Liveblocks
Setup the Liveblocks account.
Update `/frontend/.env`:
```
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY='🔑'
LIVEBLOCKS_SECRET_KEY='🔑'
```
### 8. Adding E2B
Setup the E2B account.
Update `/backend/server/.env`:
```
E2B_API_KEY='🔑'
```
### 9. Adding AI code generation
In the `/backend/ai` directory:
```
npx wrangler deploy
```
Update `/backend/server/.env`:
```
AI_WORKER_URL='https://ai.🍎.workers.dev'
```
### 10. Configuring the frontend
Update `/frontend/.env`:
```
NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
```
### 11. Running the IDE
Run `npm run dev` simultaneously in:
```
/frontend
/backend/server
```
## Setting up Deployments
The steps above do not include steps to setup [Dokku](https://github.com/dokku/dokku), which is required for deployments.
**Note:** This is completely optional to set up if you just want to run GitWit Sandbox.
Setting up deployments first requires a separate domain (such as gitwit.app, which we use).
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
```bash ```bash
DOKKU_HOST= npm run dev
DOKKU_USERNAME=
DOKKU_KEY=
``` ```
## Creating Custom Templates ### Backend
We're working on a process whereby anyone can contribute a custom template that others can use in the Sandbox environment. The process includes: 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.
- Creating a [custom E2B Sandbox](https://e2b.dev/docs/sandbox-template) including the template files and dependencies You will need to make an account on [E2B](https://e2b.dev/) to get an API key.
- Creating a file to specify the run command (e.g. "npm run dev")
- Testing the template with Dokku for deployment
Please reach out to us [on Discord](https://discord.gitwit.dev/) if you're interested in contributing. #### Socket.io server
Install dependencies
```bash
cd backend/server
npm install
```
Add the required environment variables in `.env` (example file provided in `.env.example`)
Project files will be stored in the `projects/<project-id>` directory. The middleware contains basic authorization logic for connecting to the server.
Run in development mode
```bash
npm run dev
```
This directory is dockerized, so feel free to deploy a container on any platform of your choice! I chose not to deploy this project for public access due to costs & safety, but deploying your own for personal use should be no problem.
#### Cloudflare Workers (Database, Storage, AI)
Directories:
- `/backend/database`: D1 database
- `/backend/storage`: R2 storage
- `/backend/ai`: Workers AI
Install dependencies
```bash
cd backend/database
npm install
cd ../storage
npm install
cd ../ai
npm install
```
Read the [documentation](https://developers.cloudflare.com/workers/) to learn more about workers.
For each directory, add the required environment variables in `wrangler.toml` (example file provided in `wrangler.example.toml`). For the AI worker, you can define any value you want for the `CF_AI_KEY` -- set this in other `.env` files to authorize access.
Run in development mode
```bash
npm run dev
```
Deploy to Cloudflare with [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
```bash
npx wrangler deploy
```
---
## Contributing ## Contributing
Thanks for your interest in contributing! Review this section before submitting your first pull request. If you need any help, feel free contact us [on Discord](https://discord.gitwit.dev/). Thanks for your interest in contributing! Review this section before submitting your first pull request. If you need any help, feel free to reach out to [@ishaandey\_](https://x.com/ishaandey_).
Please prioritize existing issues, but feel free to contribute new issues if you have ideas for a feature or bug that you think would be useful.
### Structure ### Structure
@ -226,13 +116,13 @@ backend/
└── ai └── ai
``` ```
| Path | Description | | Path | Description |
| ------------------ | ------------------------------------------------------------ | | ------------------ | -------------------------------------------------------------------------- |
| `frontend` | The Next.js application for the frontend. | | `frontend` | The Next.js application for the frontend. |
| `backend/server` | The Express websocket server. | | `backend/server` | The Express websocket server. |
| `backend/database` | API for interfacing with the D1 database (SQLite). | | `backend/database` | API for interfacing with the D1 database (SQLite). |
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. | | `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
| `backend/ai` | API for making requests to Workers AI . | | `backend/ai` | API for making requests to Workers AI . |
### Development ### Development
@ -261,15 +151,11 @@ It should be in the form `category(scope or module): message` in your commit mes
- `feat / feature`: all changes that introduce completely new code or new - `feat / feature`: all changes that introduce completely new code or new
features features
- `fix`: changes that fix a bug (ideally you will additionally reference an - `fix`: changes that fix a bug (ideally you will additionally reference an
issue if present) issue if present)
- `refactor`: any code related change that is not a fix nor a feature - `refactor`: any code related change that is not a fix nor a feature
- `docs`: changing existing or creating new documentation (i.e. README, docs for - `docs`: changing existing or creating new documentation (i.e. README, docs for
usage of a lib or cli usage) usage of a lib or cli usage)
- `chore`: all changes to the repository that do not fit into any of the above - `chore`: all changes to the repository that do not fit into any of the above
categories categories

5
backend/ai/.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false
}

View File

@ -7,9 +7,6 @@
"": { "": {
"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",
@ -18,28 +15,6 @@
"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",
@ -886,19 +861,11 @@
"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",
@ -977,17 +944,6 @@
"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",
@ -1009,17 +965,6 @@
"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",
@ -1063,11 +1008,6 @@
"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",
@ -1201,17 +1141,6 @@
"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",
@ -1285,14 +1214,6 @@
"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",
@ -1366,14 +1287,6 @@
"@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",
@ -1421,36 +1334,6 @@
"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",
@ -1569,14 +1452,6 @@
"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",
@ -1735,25 +1610,6 @@
"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",
@ -1819,7 +1675,8 @@
"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",
@ -1848,43 +1705,6 @@
"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",
@ -2402,11 +2222,6 @@
"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",
@ -2477,7 +2292,8 @@
"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",
@ -3011,28 +2827,6 @@
} }
} }
}, },
"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",

View File

@ -1,22 +1,19 @@
{ {
"name": "ai", "name": "ai",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "dev": "wrangler dev",
"start": "wrangler dev", "start": "wrangler dev",
"test": "vitest", "test": "vitest",
"cf-typegen": "wrangler types" "cf-typegen": "wrangler types"
}, },
"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",
"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"
}
}

View File

@ -1,128 +1,43 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js"
export interface Env { export interface Env {
ANTHROPIC_API_KEY: string AI: any
} }
export default { export default {
async fetch(request: Request, env: Env): Promise<Response> { async fetch(request, env): Promise<Response> {
// Handle CORS preflight requests if (request.method !== "GET") {
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
})
}
if (request.method !== "GET" && request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 }) return new Response("Method Not Allowed", { status: 405 })
} }
let body const url = new URL(request.url)
let isEditCodeWidget = false const fileName = url.searchParams.get("fileName")
if (request.method === "POST") { const instructions = url.searchParams.get("instructions")
body = (await request.json()) as { const line = url.searchParams.get("line")
messages: unknown const code = url.searchParams.get("code")
context: unknown
activeFileContent: string
}
} else {
const url = new URL(request.url)
const fileName = url.searchParams.get("fileName") || ""
const code = url.searchParams.get("code") || ""
const line = url.searchParams.get("line") || ""
const instructions = url.searchParams.get("instructions") || ""
body = { const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [{ role: "human", content: instructions }], messages: [
context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`, {
activeFileContent: code, role: "system",
} content:
isEditCodeWidget = true "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.",
}
const messages = body.messages
const context = body.context
const activeFileContent = body.activeFileContent
if (!Array.isArray(messages) || messages.length === 0) {
return new Response("Invalid or empty messages", { status: 400 })
}
let systemMessage
if (isEditCodeWidget) {
systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code.
Context:
${context}
Active File Content:
${activeFileContent}
Instructions: ${messages[0].content}
Respond only with the modified code that can directly replace the existing code.`
} else {
systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
\`\`\`python
print("Hello, World!")
\`\`\`
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
${context ? `Context:\n${context}\n` : ""}
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
}
const anthropicMessages = messages.map((msg) => ({
role: msg.role === "human" ? "user" : "assistant",
content: msg.content,
})) as MessageParam[]
try {
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })
const stream = await anthropic.messages.create({
model: "claude-3-5-sonnet-20240620",
max_tokens: 1024,
system: systemMessage,
messages: anthropicMessages,
stream: true,
})
const encoder = new TextEncoder()
const streamResponse = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
const bytes = encoder.encode(chunk.delta.text)
controller.enqueue(bytes)
}
}
controller.close()
}, },
}) {
role: "user",
return new Response(streamResponse, { content: `The file is called ${fileName}.`,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-cache",
Connection: "keep-alive",
}, },
}) {
} catch (error) { role: "user",
console.error("Error:", error) content: `Here are my instructions on what to generate: ${instructions}.`,
return new Response("Internal Server Error", { status: 500 }) },
} {
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 new Response(JSON.stringify(response))
}, },
} } satisfies ExportedHandler<Env>

View File

@ -1,30 +1,25 @@
// test/index.spec.ts // test/index.spec.ts
import { import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
createExecutionContext, import { describe, it, expect } from 'vitest';
env, import worker from '../src/index';
SELF,
waitOnExecutionContext,
} from "cloudflare:test"
import { describe, expect, it } from "vitest"
import worker from "../src/index"
// For now, you'll need to do something like this to get a correctly-typed // For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`. // `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties> const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
describe("Hello World worker", () => { describe('Hello World worker', () => {
it("responds with Hello World! (unit style)", async () => { it('responds with Hello World! (unit style)', async () => {
const request = new IncomingRequest("http://example.com") const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`. // Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext() const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx) const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx) await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
it("responds with Hello World! (integration style)", async () => { it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch("https://example.com") const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
}) });

View File

@ -1,11 +1,11 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": [ "types": [
"@cloudflare/workers-types/experimental", "@cloudflare/workers-types/experimental",
"@cloudflare/vitest-pool-workers" "@cloudflare/vitest-pool-workers"
] ]
}, },
"include": ["./**/*.ts", "../src/env.d.ts"], "include": ["./**/*.ts", "../src/env.d.ts"],
"exclude": [] "exclude": []
} }

View File

@ -12,9 +12,7 @@
/* Language and Environment */ /* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [ "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */, "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */

View File

@ -1,11 +1,11 @@
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ export default defineWorkersConfig({
test: { test: {
poolOptions: { poolOptions: {
workers: { workers: {
wrangler: { configPath: "./wrangler.toml" }, wrangler: { configPath: "./wrangler.toml" },
}, },
}, },
}, },
}) });

View File

@ -1,3 +1,4 @@
// Generated by Wrangler // Generated by Wrangler
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
interface Env {} interface Env {
}

View File

@ -5,6 +5,3 @@ compatibility_flags = ["nodejs_compat"]
[ai] [ai]
binding = "AI" binding = "AI"
[vars]
ANTHROPIC_API_KEY = ""

View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false
}

View File

@ -1,4 +1,4 @@
import type { Config } from "drizzle-kit" import type { Config } from "drizzle-kit";
export default process.env.LOCAL_DB_PATH export default process.env.LOCAL_DB_PATH
? ({ ? ({
@ -16,4 +16,4 @@ export default process.env.LOCAL_DB_PATH
wranglerConfigPath: "wrangler.toml", wranglerConfigPath: "wrangler.toml",
dbName: "d1-sandbox", dbName: "d1-sandbox",
}, },
} satisfies Config) } satisfies Config);

View File

@ -1,32 +1,32 @@
{ {
"name": "database", "name": "database",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "dev": "wrangler dev",
"start": "wrangler dev", "start": "wrangler dev",
"test": "vitest", "test": "vitest",
"generate": "drizzle-kit generate:sqlite --schema=src/schema.ts", "generate": "drizzle-kit generate:sqlite --schema=src/schema.ts",
"up": "drizzle-kit up:sqlite --schema=src/schema.ts", "up": "drizzle-kit up:sqlite --schema=src/schema.ts",
"db:studio": "cross-env LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio" "db:studio": "cross-env LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/vitest-pool-workers": "^0.1.0",
"@cloudflare/workers-types": "^4.20240405.0", "@cloudflare/workers-types": "^4.20240405.0",
"@types/itty-router-extras": "^0.4.3", "@types/itty-router-extras": "^0.4.3",
"drizzle-kit": "^0.20.17", "drizzle-kit": "^0.20.17",
"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": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"better-sqlite3": "^9.5.0", "better-sqlite3": "^9.5.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
"itty-router": "^5.0.16", "itty-router": "^5.0.16",
"itty-router-extras": "^0.4.6", "itty-router-extras": "^0.4.6",
"zod": "^3.22.4" "zod": "^3.22.4"
} }
} }

View File

@ -1,11 +1,11 @@
// import type { DrizzleD1Database } from "drizzle-orm/d1"; // import type { DrizzleD1Database } from "drizzle-orm/d1";
import { drizzle } from "drizzle-orm/d1" import { drizzle } from "drizzle-orm/d1"
import { json } from "itty-router-extras" import { json } from "itty-router-extras"
import { z } from "zod" import { ZodError, z } from "zod"
import { and, eq, sql } from "drizzle-orm" import { user, sandbox, usersToSandboxes } from "./schema"
import * as schema from "./schema" import * as schema from "./schema"
import { sandbox, user, usersToSandboxes } from "./schema" import { and, eq, sql } from "drizzle-orm"
export interface Env { export interface Env {
DB: D1Database DB: D1Database
@ -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.string(), type: z.enum(["react", "node"]),
name: z.string(), name: z.string(),
userId: z.string(), userId: z.string(),
visibility: z.enum(["public", "private"]), visibility: z.enum(["public", "private"]),

View File

@ -1,6 +1,6 @@
import { createId } from "@paralleldrive/cuid2" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { relations } from "drizzle-orm" import { createId } from "@paralleldrive/cuid2";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { relations, sql } from "drizzle-orm";
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id") id: text("id")
@ -11,14 +11,14 @@ export const user = sqliteTable("user", {
email: text("email").notNull(), email: text("email").notNull(),
image: text("image"), image: text("image"),
generations: integer("generations").default(0), generations: integer("generations").default(0),
}) });
export type User = typeof user.$inferSelect export type User = typeof user.$inferSelect;
export const userRelations = relations(user, ({ many }) => ({ export const userRelations = relations(user, ({ many }) => ({
sandbox: many(sandbox), sandbox: many(sandbox),
usersToSandboxes: many(usersToSandboxes), usersToSandboxes: many(usersToSandboxes),
})) }));
export const sandbox = sqliteTable("sandbox", { export const sandbox = sqliteTable("sandbox", {
id: text("id") id: text("id")
@ -26,15 +26,15 @@ export const sandbox = sqliteTable("sandbox", {
.primaryKey() .primaryKey()
.unique(), .unique(),
name: text("name").notNull(), name: text("name").notNull(),
type: text("type").notNull(), type: text("type", { enum: ["react", "node"] }).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")
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
}) });
export type Sandbox = typeof sandbox.$inferSelect export type Sandbox = typeof sandbox.$inferSelect;
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({ export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
author: one(user, { author: one(user, {
@ -42,7 +42,7 @@ export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
references: [user.id], references: [user.id],
}), }),
usersToSandboxes: many(usersToSandboxes), usersToSandboxes: many(usersToSandboxes),
})) }));
export const usersToSandboxes = sqliteTable("users_to_sandboxes", { export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
userId: text("userId") userId: text("userId")
@ -52,18 +52,15 @@ export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
.notNull() .notNull()
.references(() => sandbox.id), .references(() => sandbox.id),
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }), sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
}) });
export const usersToSandboxesRelations = relations( export const usersToSandboxesRelations = relations(usersToSandboxes, ({ one }) => ({
usersToSandboxes, group: one(sandbox, {
({ one }) => ({ fields: [usersToSandboxes.sandboxId],
group: one(sandbox, { references: [sandbox.id],
fields: [usersToSandboxes.sandboxId], }),
references: [sandbox.id], user: one(user, {
}), fields: [usersToSandboxes.userId],
user: one(user, { references: [user.id],
fields: [usersToSandboxes.userId], }),
references: [user.id], }));
}),
})
)

View File

@ -1,30 +1,25 @@
// test/index.spec.ts // test/index.spec.ts
import { import { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test";
createExecutionContext, import { describe, it, expect } from "vitest";
env, import worker from "../src/index";
SELF,
waitOnExecutionContext,
} from "cloudflare:test"
import { describe, expect, it } from "vitest"
import worker from "../src/index"
// For now, you'll need to do something like this to get a correctly-typed // For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`. // `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties> const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
describe("Hello World worker", () => { describe("Hello World worker", () => {
it("responds with Hello World! (unit style)", async () => { it("responds with Hello World! (unit style)", async () => {
const request = new IncomingRequest("http://example.com") const request = new IncomingRequest("http://example.com");
// Create an empty context to pass to `worker.fetch()`. // Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext() const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx) const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx) await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
it("responds with Hello World! (integration style)", async () => { it("responds with Hello World! (integration style)", async () => {
const response = await SELF.fetch("https://example.com") const response = await SELF.fetch("https://example.com");
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
}) });

View File

@ -1,11 +1,11 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": [ "types": [
"@cloudflare/workers-types/experimental", "@cloudflare/workers-types/experimental",
"@cloudflare/vitest-pool-workers" "@cloudflare/vitest-pool-workers"
] ]
}, },
"include": ["./**/*.ts", "../src/env.d.ts"], "include": ["./**/*.ts", "../src/env.d.ts"],
"exclude": [] "exclude": []
} }

View File

@ -12,9 +12,7 @@
/* Language and Environment */ /* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [ "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */, "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */

View File

@ -1,11 +1,11 @@
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ export default defineWorkersConfig({
test: { test: {
poolOptions: { poolOptions: {
workers: { workers: {
wrangler: { configPath: "./wrangler.toml" }, wrangler: { configPath: "./wrangler.toml" },
}, },
}, },
}, },
}) });

View File

@ -1,13 +1,8 @@
# 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= E2B_API_KEY=
DOKKU_HOST=
DOKKU_USERNAME=
DOKKU_KEY=

View File

@ -1,7 +1,5 @@
{ {
"watch": [ "watch": ["src"],
"src"
],
"ext": "ts", "ext": "ts",
"exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\"" "exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\""
} }

View File

@ -12,28 +12,25 @@
"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", "e2b": "^0.16.1",
"express": "^4.19.2", "express": "^4.19.2",
"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"
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.25.0", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -41,28 +38,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag=="
},
"node_modules/@connectrpc/connect": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz",
"integrity": "sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==",
"peerDependencies": {
"@bufbuild/protobuf": "^1.4.2"
}
},
"node_modules/@connectrpc/connect-web": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.4.0.tgz",
"integrity": "sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==",
"peerDependencies": {
"@bufbuild/protobuf": "^1.4.2",
"@connectrpc/connect": "1.4.0"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -85,9 +60,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true "dev": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
@ -100,44 +75,10 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@kwsites/file-exists": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
"dependencies": {
"debug": "^4.1.1"
}
},
"node_modules/@kwsites/file-exists/node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@kwsites/file-exists/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/@kwsites/promise-deferred": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
},
"node_modules/@socket.io/component-emitter": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.2", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.1.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" "integrity": "sha512-dzJtaDAAoXx4GCOJpbB2eG/Qj8VDpdwkLsWGzGm+0L7E8/434RyMbAHmk9ubXWVAb9nXmc44jUf8GKqVDiKezg=="
}, },
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.11",
@ -208,9 +149,9 @@
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "4.19.5", "version": "4.19.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz",
"integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@ -232,9 +173,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.15", "version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -272,23 +213,11 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/@types/ssh2": { "node_modules/abbrev": {
"version": "1.15.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true, "dev": true
"dependencies": {
"@types/node": "^18.11.18"
}
},
"node_modules/@types/ssh2/node_modules/@types/node": {
"version": "18.19.44",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz",
"integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
@ -303,9 +232,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.12.1", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -315,13 +244,10 @@
} }
}, },
"node_modules/acorn-walk": { "node_modules/acorn-walk": {
"version": "8.3.3", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"dev": true, "dev": true,
"dependencies": {
"acorn": "^8.11.0"
},
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@ -372,14 +298,6 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"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",
@ -394,14 +312,6 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -448,12 +358,12 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.0.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -465,7 +375,6 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -473,15 +382,6 @@
"node": ">=6.14.2" "node": ">=6.14.2"
} }
}, },
"node_modules/buildcheck": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -523,6 +423,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chalk/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/chalk/node_modules/supports-color": { "node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -587,11 +495,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/compare-versions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -624,6 +527,28 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1" "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
} }
}, },
"node_modules/concurrently/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -668,20 +593,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -765,19 +676,56 @@
} }
}, },
"node_modules/e2b": { "node_modules/e2b": {
"version": "0.16.2-beta.47", "version": "0.16.1",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz", "resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.1.tgz",
"integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==", "integrity": "sha512-2L1R/REEB+EezD4Q4MmcXXNATjvCYov2lv/69+PY6V95+wl1PZblIMTYAe7USxX6P6sqANxNs+kXqZr6RvXkSw==",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0", "isomorphic-ws": "^5.0.0",
"@connectrpc/connect": "^1.4.0", "normalize-path": "^3.0.0",
"@connectrpc/connect-web": "^1.4.0", "openapi-typescript-fetch": "^1.1.3",
"compare-versions": "^6.1.0", "path-browserify": "^1.0.1",
"openapi-fetch": "^0.9.7", "platform": "^1.3.6",
"platform": "^1.3.6" "ws": "^8.15.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
}
},
"node_modules/e2b/node_modules/utf-8-validate": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz",
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/e2b/node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"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/ee-first": { "node_modules/ee-first": {
@ -799,9 +747,9 @@
} }
}, },
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.5.5", "version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
"dependencies": { "dependencies": {
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
@ -812,16 +760,16 @@
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
"ws": "~8.17.1" "ws": "~8.11.0"
}, },
"engines": { "engines": {
"node": ">=10.2.0" "node": ">=10.2.0"
} }
}, },
"node_modules/engine.io-parser": { "node_modules/engine.io-parser": {
"version": "5.2.3", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
} }
@ -835,9 +783,9 @@
} }
}, },
"node_modules/engine.io/node_modules/debug": { "node_modules/engine.io/node_modules/debug": {
"version": "4.3.6", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -855,26 +803,6 @@
"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=="
}, },
"node_modules/engine.io/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/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@ -957,9 +885,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
@ -1073,11 +1001,12 @@
} }
}, },
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=4"
} }
}, },
"node_modules/has-property-descriptors": { "node_modules/has-property-descriptors": {
@ -1219,11 +1148,31 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/make-error": { "node_modules/make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -1298,12 +1247,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/nan": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
"optional": true
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1317,7 +1260,6 @@
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true, "optional": true,
"peer": true,
"bin": { "bin": {
"node-gyp-build": "bin.js", "node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js", "node-gyp-build-optional": "optional.js",
@ -1325,9 +1267,9 @@
} }
}, },
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.4", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz",
"integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chokidar": "^3.5.2", "chokidar": "^3.5.2",
@ -1353,9 +1295,9 @@
} }
}, },
"node_modules/nodemon/node_modules/debug": { "node_modules/nodemon/node_modules/debug": {
"version": "4.3.6", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
@ -1369,38 +1311,31 @@
} }
} }
}, },
"node_modules/nodemon/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/nodemon/node_modules/ms": { "node_modules/nodemon/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 "dev": true
}, },
"node_modules/nodemon/node_modules/supports-color": { "node_modules/nopt": {
"version": "5.5.0", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"has-flag": "^3.0.0" "abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
}, },
"engines": { "engines": {
"node": ">=4" "node": "*"
} }
}, },
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1414,12 +1349,9 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.2", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -1435,19 +1367,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/openapi-fetch": { "node_modules/openapi-typescript-fetch": {
"version": "0.9.8", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz", "resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz",
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==", "integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==",
"dependencies": { "engines": {
"openapi-typescript-helpers": "^0.0.8" "node": ">= 12.0.0",
"npm": ">= 7.0.0"
} }
}, },
"node_modules/openapi-typescript-helpers": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz",
"integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1456,6 +1384,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -1595,10 +1528,13 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true, "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -1694,41 +1630,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-git": {
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz",
"integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==",
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
"debug": "^4.3.5"
},
"funding": {
"type": "github",
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/simple-git/node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/simple-git/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/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@ -1759,18 +1660,18 @@
} }
}, },
"node_modules/socket.io-adapter": { "node_modules/socket.io-adapter": {
"version": "2.5.5", "version": "2.5.4",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==",
"dependencies": { "dependencies": {
"debug": "~4.3.4", "debug": "~4.3.4",
"ws": "~8.17.1" "ws": "~8.11.0"
} }
}, },
"node_modules/socket.io-adapter/node_modules/debug": { "node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.6", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -1788,26 +1689,6 @@
"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=="
}, },
"node_modules/socket.io-adapter/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/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
@ -1821,9 +1702,9 @@
} }
}, },
"node_modules/socket.io-parser/node_modules/debug": { "node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.6", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -1842,9 +1723,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/socket.io/node_modules/debug": { "node_modules/socket.io/node_modules/debug": {
"version": "4.3.6", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -1867,23 +1748,6 @@
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
}, },
"node_modules/ssh2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.9",
"nan": "^2.18.0"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -1917,17 +1781,15 @@
} }
}, },
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "8.1.1", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": { "dependencies": {
"has-flag": "^4.0.0" "has-flag": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=4"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
@ -1951,10 +1813,13 @@
} }
}, },
"node_modules/touch": { "node_modules/touch": {
"version": "3.1.1", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"dev": true, "dev": true,
"dependencies": {
"nopt": "~1.0.10"
},
"bin": { "bin": {
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
@ -2011,14 +1876,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.3", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
}, },
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
@ -2033,9 +1893,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.4", "version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -2065,9 +1925,9 @@
} }
}, },
"node_modules/utf-8-validate": { "node_modules/utf-8-validate": {
"version": "6.0.4", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true, "peer": true,
@ -2116,6 +1976,26 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"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/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -2124,6 +2004,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@ -2159,9 +2045,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.23.8", "version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -14,21 +14,18 @@
"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", "e2b": "^0.16.1",
"express": "^4.19.2", "express": "^4.19.2",
"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"
} }
} }

View File

@ -1,90 +0,0 @@
// AIWorker class for handling AI-related operations
export class AIWorker {
private aiWorkerUrl: string
private cfAiKey: string
private databaseWorkerUrl: string
private workersKey: string
// Constructor to initialize AIWorker with necessary URLs and keys
constructor(
aiWorkerUrl: string,
cfAiKey: string,
databaseWorkerUrl: string,
workersKey: string
) {
this.aiWorkerUrl = aiWorkerUrl
this.cfAiKey = cfAiKey
this.databaseWorkerUrl = databaseWorkerUrl
this.workersKey = workersKey
}
// Method to generate code based on user input
async generateCode(
userId: string,
fileName: string,
code: string,
line: number,
instructions: string
): Promise<{ response: string; success: boolean }> {
try {
const fetchPromise = fetch(
`${process.env.DATABASE_WORKER_URL}/api/sandbox/generate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.WORKERS_KEY}`,
},
body: JSON.stringify({
userId: userId,
}),
}
)
// Generate code from cloudflare workers AI
const generateCodePromise = fetch(
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(
fileName
)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(
line
)}&instructions=${encodeURIComponent(instructions)}`,
{
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.CF_AI_KEY}`,
},
}
)
const [fetchResponse, generateCodeResponse] = await Promise.all([
fetchPromise,
generateCodePromise,
])
if (!generateCodeResponse.ok) {
throw new Error(`HTTP error! status: ${generateCodeResponse.status}`)
}
const reader = generateCodeResponse.body?.getReader()
const decoder = new TextDecoder()
let result = ""
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
result += decoder.decode(value, { stream: true })
}
}
// The result should now contain only the modified code
return { response: result.trim(), success: true }
} catch (e: any) {
console.error("Error generating code:", e)
return {
response: "Error generating code. Please try again.",
success: false,
}
}
}
}

View File

@ -1,58 +0,0 @@
import { Socket } from "socket.io"
class Counter {
private count: number = 0
increment() {
this.count++
}
decrement() {
this.count = Math.max(0, this.count - 1)
}
getValue(): number {
return this.count
}
}
// Owner Connection Management
export class ConnectionManager {
// Counts how many times the owner is connected to a sandbox
private ownerConnections: Record<string, Counter> = {}
// Stores all sockets connected to a given sandbox
private sockets: Record<string, Set<Socket>> = {}
// Checks if the owner of a sandbox is connected
ownerIsConnected(sandboxId: string): boolean {
return this.ownerConnections[sandboxId]?.getValue() > 0
}
// Adds a connection for a sandbox
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
this.sockets[sandboxId] ??= new Set()
this.sockets[sandboxId].add(socket)
// If the connection is for the owner, increments the owner connection counter
if (isOwner) {
this.ownerConnections[sandboxId] ??= new Counter()
this.ownerConnections[sandboxId].increment()
}
}
// Removes a connection for a sandbox
removeConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
this.sockets[sandboxId]?.delete(socket)
// If the connection being removed is for the owner, decrements the owner connection counter
if (isOwner) {
this.ownerConnections[sandboxId]?.decrement()
}
}
// Returns the set of sockets connected to a given sandbox
connectionsForSandbox(sandboxId: string): Set<Socket> {
return this.sockets[sandboxId] ?? new Set();
}
}

View File

@ -1,40 +0,0 @@
import { SSHConfig, SSHSocketClient } from "./SSHSocketClient"
// Interface for the response structure from Dokku commands
export interface DokkuResponse {
ok: boolean
output: string
}
// DokkuClient class extends SSHSocketClient to interact with Dokku via SSH
export class DokkuClient extends SSHSocketClient {
constructor(config: SSHConfig) {
// Initialize with Dokku daemon socket path
super(config, "/var/run/dokku-daemon/dokku-daemon.sock")
}
// Send a command to Dokku and parse the response
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")
}
// Parse the JSON response from Dokku
return JSON.parse(response)
} catch (error: any) {
throw new Error(`Failed to send command: ${error.message}`)
}
}
// List all deployed Dokku apps
async listApps(): Promise<string[]> {
const response = await this.sendCommand("apps:list")
// Split the output by newline and remove the header
return response.output.split("\n").slice(1)
}
}
export { SSHConfig }

View File

@ -1,506 +0,0 @@
import { FilesystemEvent, Sandbox, WatchHandle } from "e2b"
import path from "path"
import RemoteFileStorage from "./RemoteFileStorage"
import { MAX_BODY_SIZE } from "./ratelimit"
import { TFile, TFileData, TFolder } from "./types"
// Convert list of paths to the hierchical file structure used by the editor
function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }
paths.forEach((path) => {
const parts = path.split("/")
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: `/${parts.join("/")}`, type: "file", name: part }
current.children.push(file)
} else {
const folder: TFolder = {
id: `/${parts.slice(0, i + 1).join("/")}`,
type: "folder",
name: part,
children: [],
}
current.children.push(folder)
current = folder
}
}
}
})
return root.children
}
// FileManager class to handle file operations in a sandbox
export class FileManager {
private sandboxId: string
private sandbox: Sandbox
public files: (TFolder | TFile)[]
public fileData: TFileData[]
private fileWatchers: WatchHandle[] = []
private dirName = "/home/user/project"
private refreshFileList: ((files: (TFolder | TFile)[]) => void) | null
// Constructor to initialize the FileManager
constructor(
sandboxId: string,
sandbox: Sandbox,
refreshFileList: ((files: (TFolder | TFile)[]) => void) | null
) {
this.sandboxId = sandboxId
this.sandbox = sandbox
this.files = []
this.fileData = []
this.refreshFileList = refreshFileList
}
// Fetch file data from list of paths
private async generateFileData(paths: string[]): Promise<TFileData[]> {
const fileData: TFileData[] = []
for (const path of paths) {
const parts = path.split("/")
const isFile = parts.length > 0 && parts[parts.length - 1].length > 0
if (isFile) {
const fileId = `/${parts.join("/")}`
const data = await RemoteFileStorage.fetchFileContent(`projects/${this.sandboxId}${fileId}`)
fileData.push({ id: fileId, data })
}
}
return fileData
}
// Convert local file path to remote path
private getRemoteFileId(localId: string): string {
return `projects/${this.sandboxId}${localId}`
}
// Convert remote file path to local file path
private getLocalFileId(remoteId: string): string | undefined {
const allParts = remoteId.split("/")
if (allParts[1] !== this.sandboxId) return undefined;
return allParts.slice(2).join("/")
}
// Convert remote file paths to local file paths
private getLocalFileIds(remoteIds: string[]): string[] {
return remoteIds
.map(this.getLocalFileId.bind(this))
.filter((id) => id !== undefined);
}
// Download files from remote storage
private async updateFileData(): Promise<TFileData[]> {
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
const localPaths = this.getLocalFileIds(remotePaths)
this.fileData = await this.generateFileData(localPaths)
return this.fileData
}
// Update file structure
private async updateFileStructure(): Promise<(TFolder | TFile)[]> {
const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId)
const localPaths = this.getLocalFileIds(remotePaths)
this.files = generateFileStructure(localPaths)
return this.files
}
// Initialize the FileManager
async initialize() {
// Download files from remote file storage
await this.updateFileStructure()
await this.updateFileData()
// Copy all files from the project to the container
const promises = this.fileData.map(async (file) => {
try {
const filePath = path.join(this.dirName, file.id)
const parentDirectory = path.dirname(filePath)
if (!this.sandbox.files.exists(parentDirectory)) {
await this.sandbox.files.makeDir(parentDirectory)
}
await this.sandbox.files.write(filePath, file.data)
} catch (e: any) {
console.log("Failed to create file: " + e)
}
})
await Promise.all(promises)
// Make the logged in user the owner of all project files
this.fixPermissions()
await this.watchDirectory(this.dirName)
await this.watchSubdirectories(this.dirName)
}
// Check if the given path is a directory
private async isDirectory(directoryPath: string): Promise<boolean> {
try {
const result = await this.sandbox.commands.run(
`[ -d "${directoryPath}" ] && echo "true" || echo "false"`
)
return result.stdout.trim() === "true"
} catch (e: any) {
console.log("Failed to check if directory: " + e)
return false
}
}
// Change the owner of the project directory to user
private async fixPermissions() {
try {
await this.sandbox.commands.run(
`sudo chown -R user "${this.dirName}"`
)
} catch (e: any) {
console.log("Failed to fix permissions: " + e)
}
}
// Watch a directory for changes
async watchDirectory(directory: string): Promise<WatchHandle | undefined> {
try {
const handle = await this.sandbox.files.watch(
directory,
async (event: FilesystemEvent) => {
try {
function removeDirName(path: string, dirName: string) {
return path.startsWith(dirName)
? path.slice(dirName.length)
: path
}
// This is the absolute file path in the container
const containerFilePath = path.posix.join(directory, event.name)
// This is the file path relative to the project directory
const sandboxFilePath = removeDirName(containerFilePath, this.dirName)
// This is the directory being watched relative to the project directory
const sandboxDirectory = removeDirName(directory, this.dirName)
// Helper function to find a folder by id
function findFolderById(
files: (TFolder | TFile)[],
folderId: string
) {
return files.find(
(file: TFolder | TFile) =>
file.type === "folder" && file.id === folderId
)
}
// Handle file/directory creation event
if (event.type === "create") {
const folder = findFolderById(
this.files,
sandboxDirectory
) as TFolder
const isDir = await this.isDirectory(containerFilePath)
const newItem = isDir
? ({
id: sandboxFilePath,
name: event.name,
type: "folder",
children: [],
} as TFolder)
: ({
id: sandboxFilePath,
name: event.name,
type: "file",
} as TFile)
if (folder) {
// If the folder exists, add the new item (file/folder) as a child
folder.children.push(newItem)
} else {
// If folder doesn't exist, add the new item to the root
this.files.push(newItem)
}
if (!isDir) {
const fileData = await this.sandbox.files.read(
containerFilePath
)
const fileContents =
typeof fileData === "string" ? fileData : ""
this.fileData.push({
id: sandboxFilePath,
data: fileContents,
})
}
console.log(`Create ${sandboxFilePath}`)
}
// Handle file/directory removal or rename event
else if (event.type === "remove" || event.type == "rename") {
const folder = findFolderById(
this.files,
sandboxDirectory
) as TFolder
const isDir = await this.isDirectory(containerFilePath)
const isFileMatch = (file: TFolder | TFile | TFileData) =>
file.id === sandboxFilePath ||
file.id.startsWith(containerFilePath + "/")
if (folder) {
// Remove item from its parent folder
folder.children = folder.children.filter(
(file: TFolder | TFile) => !isFileMatch(file)
)
} else {
// Remove from the root if it's not inside a folder
this.files = this.files.filter(
(file: TFolder | TFile) => !isFileMatch(file)
)
}
// Also remove any corresponding file data
this.fileData = this.fileData.filter(
(file: TFileData) => !isFileMatch(file)
)
console.log(`Removed: ${sandboxFilePath}`)
}
// Handle file write event
else if (event.type === "write") {
const folder = findFolderById(
this.files,
sandboxDirectory
) as TFolder
const fileToWrite = this.fileData.find(
(file) => file.id === sandboxFilePath
)
if (fileToWrite) {
fileToWrite.data = await this.sandbox.files.read(
containerFilePath
)
console.log(`Write to ${sandboxFilePath}`)
} else {
// If the file is part of a folder structure, locate it and update its data
const fileInFolder = folder?.children.find(
(file) => file.id === sandboxFilePath
)
if (fileInFolder) {
const fileData = await this.sandbox.files.read(
containerFilePath
)
const fileContents =
typeof fileData === "string" ? fileData : ""
this.fileData.push({
id: sandboxFilePath,
data: fileContents,
})
console.log(`Write to ${sandboxFilePath}`)
}
}
}
// Tell the client to reload the file list
if (event.type !== "chmod") {
this.refreshFileList?.(this.files)
}
} catch (error) {
console.error(
`Error handling ${event.type} event for ${event.name}:`,
error
)
}
},
{ timeout: 0 }
)
this.fileWatchers.push(handle)
return handle
} catch (error) {
console.error(`Error watching filesystem:`, error)
}
}
// Watch subdirectories recursively
async watchSubdirectories(directory: string) {
const dirContent = await this.sandbox.files.list(directory)
await Promise.all(
dirContent.map(async (item) => {
if (item.type === "dir") {
console.log("Watching " + item.path)
await this.watchDirectory(item.path)
}
})
)
}
// Get file content
async getFile(fileId: string): Promise<string | undefined> {
const file = this.fileData.find((f) => f.id === fileId)
return file?.data
}
// Get folder content
async getFolder(folderId: string): Promise<string[]> {
const remotePaths = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId))
return this.getLocalFileIds(remotePaths)
}
// Save file content
async saveFile(fileId: string, body: string): Promise<void> {
if (!fileId) return // handles saving when no file is open
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
throw new Error("File size too large. Please reduce the file size.")
}
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
const file = this.fileData.find((f) => f.id === fileId)
if (!file) return
file.data = body
await this.sandbox.files.write(path.posix.join(this.dirName, file.id), body)
this.fixPermissions()
}
// Move a file to a different folder
async moveFile(
fileId: string,
folderId: string
): Promise<(TFolder | TFile)[]> {
const fileData = this.fileData.find((f) => f.id === fileId)
const file = this.files.find((f) => f.id === fileId)
if (!fileData || !file) return this.files
const parts = fileId.split("/")
const newFileId = folderId + "/" + parts.pop()
await this.moveFileInContainer(fileId, newFileId)
await this.fixPermissions()
fileData.id = newFileId
file.id = newFileId
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data)
return this.updateFileStructure()
}
// Move a file within the container
private async moveFileInContainer(oldPath: string, newPath: string) {
try {
const fileContents = await this.sandbox.files.read(
path.posix.join(this.dirName, oldPath)
)
await this.sandbox.files.write(
path.posix.join(this.dirName, newPath),
fileContents
)
await this.sandbox.files.remove(path.posix.join(this.dirName, oldPath))
} catch (e) {
console.error(`Error moving file from ${oldPath} to ${newPath}:`, e)
}
}
// Create a new file
async createFile(name: string): Promise<boolean> {
const size: number = await RemoteFileStorage.getProjectSize(this.sandboxId)
if (size > 200 * 1024 * 1024) {
throw new Error("Project size exceeded. Please delete some files.")
}
const id = `/${name}`
await this.sandbox.files.write(path.posix.join(this.dirName, id), "")
await this.fixPermissions()
this.files.push({
id,
name,
type: "file",
})
this.fileData.push({
id,
data: "",
})
await RemoteFileStorage.createFile(this.getRemoteFileId(id))
return true
}
// Create a new folder
async createFolder(name: string): Promise<void> {
const id = `/${name}`
await this.sandbox.files.makeDir(path.posix.join(this.dirName, id))
}
// Rename a file
async renameFile(fileId: string, newName: string): Promise<void> {
const fileData = this.fileData.find((f) => f.id === fileId)
const file = this.files.find((f) => f.id === fileId)
if (!fileData || !file) return
const parts = fileId.split("/")
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName
await this.moveFileInContainer(fileId, newFileId)
await this.fixPermissions()
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data)
fileData.id = newFileId
file.id = newFileId
}
// Delete a file
async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> {
const file = this.fileData.find((f) => f.id === fileId)
if (!file) return this.files
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
this.fileData = this.fileData.filter(
(f) => f.id !== fileId
)
await RemoteFileStorage.deleteFile(this.getRemoteFileId(fileId))
return this.updateFileStructure()
}
// Delete a folder
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
const files = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId))
await Promise.all(
files.map(async (file) => {
await this.sandbox.files.remove(path.posix.join(this.dirName, file))
this.fileData = this.fileData.filter(
(f) => f.id !== file
)
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
})
)
return this.updateFileStructure()
}
// Close all file watchers
async closeWatchers() {
await Promise.all(
this.fileWatchers.map(async (handle: WatchHandle) => {
await handle.close()
})
)
}
}

View File

@ -1,117 +0,0 @@
import * as dotenv from "dotenv"
import { R2Files } from "./types"
dotenv.config()
export const RemoteFileStorage = {
getSandboxPaths: 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()
return data.objects.map((obj) => obj.key)
},
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)
},
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 ""
}
},
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
},
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
},
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
},
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
},
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
}
}
export default RemoteFileStorage

View File

@ -1,98 +0,0 @@
import { Client } from "ssh2"
// Interface defining the configuration for SSH connection
export interface SSHConfig {
host: string
port?: number
username: string
privateKey: Buffer
}
// Class to handle SSH connections and communicate with a Unix socket
export class SSHSocketClient {
private conn: Client
private config: SSHConfig
private socketPath: string
private isConnected: boolean = false
// Constructor initializes the SSH client and sets up configuration
constructor(config: SSHConfig, socketPath: string) {
this.conn = new Client()
this.config = { ...config, port: 22 } // Default port to 22 if not provided
this.socketPath = socketPath
this.setupTerminationHandlers()
}
// Set up handlers for graceful termination
private setupTerminationHandlers() {
process.on("SIGINT", this.closeConnection.bind(this))
process.on("SIGTERM", this.closeConnection.bind(this))
}
// Method to close the SSH connection
private closeConnection() {
console.log("Closing SSH connection...")
this.conn.end()
this.isConnected = false
process.exit(0)
}
// Method to establish the SSH connection
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)
})
}
// Method to send data through the SSH connection to the Unix socket
sendData(data: string): Promise<string> {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error("SSH connection is not established"))
return
}
// Use netcat to send data to the Unix socket
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()))
})
}
)
})
}
}

View File

@ -1,243 +0,0 @@
import { Sandbox as E2BSandbox } from "e2b"
import { Socket } from "socket.io"
import { AIWorker } from "./AIWorker"
import { CONTAINER_TIMEOUT } from "./constants"
import { DokkuClient } from "./DokkuClient"
import { FileManager } from "./FileManager"
import {
createFileRL,
createFolderRL,
deleteFileRL,
renameFileRL,
saveFileRL,
} from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient"
import { TerminalManager } from "./TerminalManager"
import { TFile, TFolder } from "./types"
import { LockManager } from "./utils"
const lockManager = new LockManager()
// Define a type for SocketHandler functions
type SocketHandler<T = Record<string, any>> = (args: T) => any;
// Extract port number from a string
function extractPortNumber(inputString: string): number | null {
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
const regex = /http:\/\/localhost:(\d+)/
const match = cleanedString.match(regex)
return match ? parseInt(match[1]) : null
}
type ServerContext = {
aiWorker: AIWorker;
dokkuClient: DokkuClient | null;
gitClient: SecureGitClient | null;
};
export class Sandbox {
// Sandbox properties:
sandboxId: string;
fileManager: FileManager | null;
terminalManager: TerminalManager | null;
container: E2BSandbox | null;
// Server context:
dokkuClient: DokkuClient | null;
gitClient: SecureGitClient | null;
aiWorker: AIWorker;
constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
// Sandbox properties:
this.sandboxId = sandboxId;
this.fileManager = null;
this.terminalManager = null;
this.container = null;
// Server context:
this.aiWorker = aiWorker;
this.dokkuClient = dokkuClient;
this.gitClient = gitClient;
}
// Initializes the container for the sandbox environment
async initialize(
fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined
) {
// Acquire a lock to ensure exclusive access to the sandbox environment
await lockManager.acquireLock(this.sandboxId, async () => {
// Check if a container already exists and is running
if (this.container && await this.container.isRunning()) {
console.log(`Found existing container ${this.sandboxId}`)
} else {
console.log("Creating container", this.sandboxId)
// Create a new container with a specified timeout
this.container = await E2BSandbox.create({
timeoutMs: CONTAINER_TIMEOUT,
})
}
})
// Ensure a container was successfully created
if (!this.container) throw new Error("Failed to create container")
// Initialize the terminal manager if it hasn't been set up yet
if (!this.terminalManager) {
this.terminalManager = new TerminalManager(this.container)
console.log(`Terminal manager set up for ${this.sandboxId}`)
}
// Initialize the file manager if it hasn't been set up yet
if (!this.fileManager) {
this.fileManager = new FileManager(
this.sandboxId,
this.container,
fileWatchCallback ?? null
)
// Initialize the file manager and emit the initial files
await this.fileManager.initialize()
}
}
// Called when the client disconnects from the Sandbox
async disconnect() {
// Close all terminals managed by the terminal manager
await this.terminalManager?.closeAllTerminals()
// This way the terminal manager will be set up again if we reconnect
this.terminalManager = null;
// Close all file watchers managed by the file manager
await this.fileManager?.closeWatchers()
// This way the file manager will be set up again if we reconnect
this.fileManager = null;
}
handlers(connection: { userId: string, isOwner: boolean, socket: Socket }) {
// Handle heartbeat from a socket connection
const handleHeartbeat: SocketHandler = (_: any) => {
// Only keep the sandbox alive if the owner is still connected
if (connection.isOwner) {
this.container?.setTimeout(CONTAINER_TIMEOUT)
}
}
// Handle getting a file
const handleGetFile: SocketHandler = ({ fileId }: any) => {
return this.fileManager?.getFile(fileId)
}
// Handle getting a folder
const handleGetFolder: SocketHandler = ({ folderId }: any) => {
return this.fileManager?.getFolder(folderId)
}
// Handle saving a file
const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => {
await saveFileRL.consume(connection.userId, 1);
return this.fileManager?.saveFile(fileId, body)
}
// Handle moving a file
const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => {
return this.fileManager?.moveFile(fileId, folderId)
}
// Handle listing apps
const handleListApps: SocketHandler = async (_: any) => {
if (!this.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client")
return { success: true, apps: await this.dokkuClient.listApps() }
}
// Handle deploying code
const handleDeploy: SocketHandler = async (_: any) => {
if (!this.gitClient) throw Error("No git client")
if (!this.fileManager) throw Error("No file manager")
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId)
return { success: true }
}
// Handle creating a file
const handleCreateFile: SocketHandler = async ({ name }: any) => {
await createFileRL.consume(connection.userId, 1);
return { "success": await this.fileManager?.createFile(name) }
}
// Handle creating a folder
const handleCreateFolder: SocketHandler = async ({ name }: any) => {
await createFolderRL.consume(connection.userId, 1);
return { "success": await this.fileManager?.createFolder(name) }
}
// Handle renaming a file
const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => {
await renameFileRL.consume(connection.userId, 1)
return this.fileManager?.renameFile(fileId, newName)
}
// Handle deleting a file
const handleDeleteFile: SocketHandler = async ({ fileId }: any) => {
await deleteFileRL.consume(connection.userId, 1)
return this.fileManager?.deleteFile(fileId)
}
// Handle deleting a folder
const handleDeleteFolder: SocketHandler = ({ folderId }: any) => {
return this.fileManager?.deleteFolder(folderId)
}
// Handle creating a terminal session
const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
await lockManager.acquireLock(this.sandboxId, async () => {
await this.terminalManager?.createTerminal(id, (responseString: string) => {
connection.socket.emit("terminalResponse", { id, data: responseString })
const port = extractPortNumber(responseString)
if (port) {
connection.socket.emit(
"previewURL",
"https://" + this.container?.getHost(port)
)
}
})
})
}
// Handle resizing a terminal
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => {
this.terminalManager?.resizeTerminal(dimensions)
}
// Handle sending data to a terminal
const handleTerminalData: SocketHandler = ({ id, data }: any) => {
return this.terminalManager?.sendTerminalData(id, data)
}
// Handle closing a terminal
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
return this.terminalManager?.closeTerminal(id)
}
// Handle generating code
const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => {
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
}
return {
"heartbeat": handleHeartbeat,
"getFile": handleGetFile,
"getFolder": handleGetFolder,
"saveFile": handleSaveFile,
"moveFile": handleMoveFile,
"list": handleListApps,
"deploy": handleDeploy,
"createFile": handleCreateFile,
"createFolder": handleCreateFolder,
"renameFile": handleRenameFile,
"deleteFile": handleDeleteFile,
"deleteFolder": handleDeleteFolder,
"createTerminal": handleCreateTerminal,
"resizeTerminal": handleResizeTerminal,
"terminalData": handleTerminalData,
"closeTerminal": handleCloseTerminal,
"generateCode": handleGenerateCode,
};
}
}

View File

@ -1,84 +0,0 @@
import fs from "fs"
import os from "os"
import path from "path"
import simpleGit, { SimpleGit } from "simple-git"
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
}
}
}

View File

@ -1,70 +0,0 @@
import { ProcessHandle, Sandbox } 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();

View File

@ -1,74 +0,0 @@
import { Sandbox } from "e2b"
import { Terminal } from "./Terminal"
export class TerminalManager {
private sandbox: Sandbox
private terminals: Record<string, Terminal> = {}
constructor(sandbox: Sandbox) {
this.sandbox = sandbox
}
async createTerminal(
id: string,
onData: (responseString: string) => void
): Promise<void> {
if (this.terminals[id]) {
return
}
this.terminals[id] = new Terminal(this.sandbox)
await this.terminals[id].init({
onData,
cols: 80,
rows: 20,
})
const defaultDirectory = "/home/user/project"
const defaultCommands = [
`cd "${defaultDirectory}"`,
"export PS1='user> '",
"clear",
]
for (const command of defaultCommands) {
await this.terminals[id].sendData(command + "\r")
}
console.log("Created terminal", id)
}
async resizeTerminal(dimensions: {
cols: number
rows: number
}): Promise<void> {
Object.values(this.terminals).forEach((t) => {
t.resize(dimensions)
})
}
async sendTerminalData(id: string, data: string): Promise<void> {
if (!this.terminals[id]) {
return
}
await this.terminals[id].sendData(data)
}
async closeTerminal(id: string): Promise<void> {
if (!this.terminals[id]) {
return
}
await this.terminals[id].close()
delete this.terminals[id]
}
async closeAllTerminals(): Promise<void> {
await Promise.all(
Object.entries(this.terminals).map(async ([key, terminal]) => {
await terminal.close()
delete this.terminals[key]
})
)
}
}

View File

@ -1,2 +0,0 @@
// The amount of time in ms that a container will stay alive without a hearbeat.
export const CONTAINER_TIMEOUT = 120_000

View 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.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 {
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;
};

View File

@ -1,182 +1,601 @@
import cors from "cors" import os from "os";
import dotenv from "dotenv" import path from "path";
import express, { Express } from "express" import cors from "cors";
import fs from "fs" import express, { Express } from "express";
import { createServer } from "http" import dotenv from "dotenv";
import { Server, Socket } from "socket.io" import { createServer } from "http";
import { AIWorker } from "./AIWorker" import { Server } from "socket.io";
import { ConnectionManager } from "./ConnectionManager" import { z } from "zod";
import { DokkuClient } from "./DokkuClient" import { User } from "./types";
import { Sandbox } from "./SandboxManager" import {
import { SecureGitClient } from "./SecureGitClient" createFile,
import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware deleteFile,
import { TFile, TFolder } from "./types" getFolder,
getProjectSize,
getSandboxFiles,
renameFile,
saveFile,
} from "./fileoperations";
import { LockManager } from "./utils";
import { Sandbox, Terminal, FilesystemManager } from "e2b";
import {
MAX_BODY_SIZE,
createFileRL,
createFolderRL,
deleteFileRL,
renameFileRL,
saveFileRL,
} from "./ratelimit";
// Log errors and send a notification to the client dotenv.config();
export const handleErrors = (message: string, error: any, socket: Socket) => {
console.error(message, error);
socket.emit("error", `${message} ${error.message ?? error}`);
};
// Handle uncaught exceptions const app: Express = express();
process.on("uncaughtException", (error) => { const port = process.env.PORT || 4000;
console.error("Uncaught Exception:", error) app.use(cors());
// Do not exit the process const httpServer = createServer(app);
})
// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason)
// Do not exit the process
})
// Initialize containers and managers
const connections = new ConnectionManager()
const sandboxes: Record<string, Sandbox> = {}
// Load environment variables
dotenv.config()
// Initialize Express app and create HTTP server
const app: Express = express()
const port = process.env.PORT || 4000
app.use(cors())
const httpServer = createServer(app)
const io = new Server(httpServer, { const io = new Server(httpServer, {
cors: { cors: {
origin: "*", // Allow connections from any origin origin: "*",
}, },
}) });
// Middleware for socket authentication let inactivityTimeout: NodeJS.Timeout | null = null;
io.use(socketAuth) // Use the new socketAuth middleware let isOwnerConnected = false;
// Check for required environment variables const containers: Record<string, Sandbox> = {};
if (!process.env.DOKKU_HOST) const connections: Record<string, number> = {};
console.warn("Environment variable DOKKU_HOST is not defined") const terminals: Record<string, Terminal> = {};
if (!process.env.DOKKU_USERNAME)
console.warn("Environment variable DOKKU_USERNAME is not defined")
if (!process.env.DOKKU_KEY)
console.warn("Environment variable DOKKU_KEY is not defined")
// Initialize Dokku client const dirName = "/home/user";
const dokkuClient =
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
dokkuClient?.connect()
// Initialize Git client used to deploy Dokku apps const moveFile = async (
const gitClient = filesystem: FilesystemManager,
process.env.DOKKU_HOST && process.env.DOKKU_KEY filePath: string,
? new SecureGitClient( newFilePath: string
`dokku@${process.env.DOKKU_HOST}`, ) => {
process.env.DOKKU_KEY const fileContents = await filesystem.readBytes(filePath);
) await filesystem.writeBytes(newFilePath, fileContents);
: null await filesystem.remove(filePath);
};
// Add this near the top of the file, after other initializations io.use(async (socket, next) => {
const aiWorker = new AIWorker( const handshakeSchema = z.object({
process.env.AI_WORKER_URL!, userId: z.string(),
process.env.CF_AI_KEY!, sandboxId: z.string(),
process.env.DATABASE_WORKER_URL!, EIO: z.string(),
process.env.WORKERS_KEY! transport: z.string(),
) });
const q = socket.handshake.query;
const parseQuery = handshakeSchema.safeParse(q);
if (!parseQuery.success) {
next(new Error("Invalid request."));
return;
}
const { sandboxId, userId } = parseQuery.data;
const dbUser = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
);
const dbUserJSON = (await dbUser.json()) as User;
if (!dbUserJSON) {
next(new Error("DB error."));
return;
}
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId);
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
(uts) => uts.sandboxId === sandboxId
);
if (!sandbox && !sharedSandboxes) {
next(new Error("Invalid credentials."));
return;
}
socket.data = {
userId,
sandboxId: sandboxId,
isOwner: sandbox !== undefined,
};
next();
});
const lockManager = new LockManager();
// Handle a client connecting to the server
io.on("connection", async (socket) => { io.on("connection", async (socket) => {
try { try {
// This data comes is added by our authentication middleware if (inactivityTimeout) clearTimeout(inactivityTimeout);
const data = socket.data as { const data = socket.data as {
userId: string userId: string;
sandboxId: string sandboxId: string;
isOwner: boolean isOwner: boolean;
};
if (data.isOwner) {
isOwnerConnected = true;
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1;
} else {
if (!isOwnerConnected) {
socket.emit("disableAccess", "The sandbox owner is not connected.");
return;
}
} }
// Register the connection await lockManager.acquireLock(data.sandboxId, async () => {
connections.addConnectionForSandbox(socket, data.sandboxId, data.isOwner) try {
if (!containers[data.sandboxId]) {
// Disable access unless the sandbox owner is connected containers[data.sandboxId] = await Sandbox.create();
if (!data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { console.log("Created container ", data.sandboxId);
socket.emit("disableAccess", "The sandbox owner is not connected.") io.emit(
return "previewURL",
} "https://" + containers[data.sandboxId].getHostname(5173)
);
try {
// Create or retrieve the sandbox manager for the given sandbox ID
const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId,
{
aiWorker, dokkuClient, gitClient,
} }
) } catch (e: any) {
sandboxes[data.sandboxId] = sandboxManager console.error(`Error creating container ${data.sandboxId}:`, e);
io.emit("error", `Error: container creation. ${e.message ?? e}`);
}
});
// This callback recieves an update when the file list changes, and notifies all relevant connections. // Change the owner of the project directory to user
const sendFileNotifications = (files: (TFolder | TFile)[]) => { const fixPermissions = async () => {
connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => { await containers[data.sandboxId].process.startAndWait(
socket.emit("loaded", files); `sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
}); );
}; };
// Initialize the sandbox container const sandboxFiles = await getSandboxFiles(data.sandboxId);
// The file manager and terminal managers will be set up if they have been closed sandboxFiles.fileData.forEach(async (file) => {
await sandboxManager.initialize(sendFileNotifications) const filePath = path.join(dirName, file.id);
socket.emit("loaded", sandboxManager.fileManager?.files) await containers[data.sandboxId].filesystem.makeDir(
path.dirname(filePath)
);
await containers[data.sandboxId].filesystem.write(filePath, file.data);
});
fixPermissions();
// Register event handlers for the sandbox socket.emit("loaded", sandboxFiles.files);
// For each event handler, listen on the socket for that event
// Pass connection-specific information to the handlers
Object.entries(sandboxManager.handlers({
userId: data.userId,
isOwner: data.isOwner,
socket
})).forEach(([event, handler]) => {
socket.on(event, async (options: any, callback?: (response: any) => void) => {
try {
const result = await handler(options)
callback?.(result);
} catch (e: any) {
handleErrors(`Error processing event "${event}":`, e, socket);
}
});
});
// Handle disconnection event socket.on("getFile", (fileId: string, callback) => {
socket.on("disconnect", async () => { console.log(fileId);
try {
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return;
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) => {
try {
const files = await getFolder(folderId);
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
socket.on("saveFile", async (fileId: string, body: string) => {
try {
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
socket.emit(
"error",
"Error: file size too large. Please reduce the file size."
);
return;
}
try { try {
// Deregister the connection await saveFileRL.consume(data.userId, 1);
connections.removeConnectionForSandbox(socket, data.sandboxId, data.isOwner) await saveFile(fileId, body);
} catch (e) {
// If the owner has disconnected from all sockets, close open terminals and file watchers.o io.emit("error", "Rate limited: file saving. Please slow down.");
// The sandbox itself will timeout after the heartbeat stops. return;
if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) {
await sandboxManager.disconnect()
socket.broadcast.emit(
"disableAccess",
"The sandbox owner has disconnected."
)
}
} catch (e: any) {
handleErrors("Error disconnecting:", e, socket);
} }
})
} catch (e: any) { const file = sandboxFiles.fileData.find((f) => f.id === fileId);
handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket); if (!file) return;
} file.data = body;
await containers[data.sandboxId].filesystem.write(
path.join(dirName, file.id),
body
);
fixPermissions();
} 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) => {
try {
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return;
const parts = fileId.split("/");
const newFileId = folderId + "/" + parts.pop();
await moveFile(
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
);
fixPermissions();
file.id = newFileId;
await renameFile(fileId, newFileId, file.data);
const newFiles = await getSandboxFiles(data.sandboxId);
callback(newFiles.files);
} catch (e: any) {
console.error("Error moving file:", e);
io.emit("error", `Error: file moving. ${e.message ?? e}`);
}
}
);
socket.on("createFile", async (name: string, callback) => {
try {
const size: number = await getProjectSize(data.sandboxId);
// limit is 200mb
if (size > 200 * 1024 * 1024) {
io.emit(
"error",
"Rate limited: project size exceeded. Please delete some files."
);
callback({ success: false });
return;
}
try {
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}`;
await containers[data.sandboxId].filesystem.write(
path.join(dirName, id),
""
);
fixPermissions();
sandboxFiles.files.push({
id,
name,
type: "file",
});
sandboxFiles.fileData.push({
id,
data: "",
});
await createFile(id);
callback({ success: true });
} catch (e: any) {
console.error("Error creating file:", e);
io.emit("error", `Error: file creation. ${e.message ?? e}`);
}
});
socket.on("createFolder", async (name: string, callback) => {
try {
try {
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}`;
await containers[data.sandboxId].filesystem.makeDir(
path.join(dirName, id)
);
callback();
} catch (e: any) {
console.error("Error creating folder:", e);
io.emit("error", `Error: folder creation. ${e.message ?? e}`);
}
});
socket.on("renameFile", async (fileId: string, newName: string) => {
try {
try {
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);
if (!file) return;
file.id = newName;
const parts = fileId.split("/");
const newFileId =
parts.slice(0, parts.length - 1).join("/") + "/" + newName;
await moveFile(
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
);
fixPermissions();
await renameFile(fileId, newFileId, file.data);
} catch (e: any) {
console.error("Error renaming folder:", e);
io.emit("error", `Error: folder renaming. ${e.message ?? e}`);
}
});
socket.on("deleteFile", async (fileId: string, callback) => {
try {
try {
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);
if (!file) return;
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, fileId)
);
sandboxFiles.fileData = sandboxFiles.fileData.filter(
(f) => f.id !== fileId
);
await deleteFile(fileId);
const newFiles = await getSandboxFiles(data.sandboxId);
callback(newFiles.files);
} catch (e: any) {
console.error("Error deleting file:", e);
io.emit("error", `Error: file deletion. ${e.message ?? e}`);
}
});
// todo
// socket.on("renameFolder", async (folderId: string, newName: string) => {
// });
socket.on("deleteFolder", async (folderId: string, callback) => {
try {
const files = await getFolder(folderId);
await Promise.all(
files.map(async (file) => {
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, file)
);
sandboxFiles.fileData = sandboxFiles.fileData.filter(
(f) => f.id !== file
);
await deleteFile(file);
})
);
const newFiles = await getSandboxFiles(data.sandboxId);
callback(newFiles.files);
} catch (e: any) {
console.error("Error deleting folder:", e);
io.emit("error", `Error: folder deletion. ${e.message ?? e}`);
}
});
socket.on("createTerminal", async (id: string, callback) => {
try {
if (terminals[id] || Object.keys(terminals).length >= 4) {
return;
}
await lockManager.acquireLock(data.sandboxId, async () => {
try {
terminals[id] = await containers[data.sandboxId].terminal.start({
onData: (data: string) => {
io.emit("terminalResponse", { id, data });
},
size: { cols: 80, rows: 20 },
onExit: () => console.log("Terminal exited", id),
});
await terminals[id].sendData(
`cd "${path.join(dirName, "projects", data.sandboxId)}"\r`
);
await terminals[id].sendData("export PS1='user> '\rclear\r");
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();
} 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 }) => {
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) => {
try {
if (!terminals[id]) {
return;
}
terminals[id].sendData(data);
} catch (e: any) {
console.error("Error writing to terminal:", e);
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`);
}
});
socket.on("closeTerminal", async (id: string, callback) => {
try {
if (!terminals[id]) {
return;
}
await terminals[id].kill();
delete terminals[id];
callback();
} catch (e: any) {
console.error("Error closing terminal:", e);
io.emit("error", `Error: closing terminal. ${e.message ?? e}`);
}
});
socket.on(
"generateCode",
async (
fileName: string,
code: string,
line: number,
instructions: string,
callback
) => {
try {
const fetchPromise = fetch(
`${process.env.DATABASE_WORKER_URL}/api/sandbox/generate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.WORKERS_KEY}`,
},
body: JSON.stringify({
userId: data.userId,
}),
}
);
// Generate code from cloudflare workers AI
const generateCodePromise = fetch(
`${process.env.AI_WORKER_URL}/api?fileName=${fileName}&code=${code}&line=${line}&instructions=${instructions}`,
{
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.CF_AI_KEY}`,
},
}
);
const [fetchResponse, generateCodeResponse] = await Promise.all([
fetchPromise,
generateCodePromise,
]);
const json = await generateCodeResponse.json();
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 () => {
try {
if (data.isOwner) {
connections[data.sandboxId]--;
}
if (data.isOwner && connections[data.sandboxId] <= 0) {
await Promise.all(
Object.entries(terminals).map(async ([key, terminal]) => {
await terminal.kill();
delete terminals[key];
})
);
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (containers[data.sandboxId]) {
await containers[data.sandboxId].close();
delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId);
}
} catch (error) {
console.error("Error closing container ", data.sandboxId, error);
}
});
socket.broadcast.emit(
"disableAccess",
"The sandbox owner has disconnected."
);
}
// const sockets = await io.fetchSockets();
// if (inactivityTimeout) {
// clearTimeout(inactivityTimeout);
// }
// if (sockets.length === 0) {
// console.log("STARTING TIMER");
// inactivityTimeout = setTimeout(() => {
// io.fetchSockets().then(async (sockets) => {
// if (sockets.length === 0) {
// console.log("Server stopped", res);
// }
// });
// }, 20000);
// } else {
// 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) { } catch (e: any) {
handleErrors("Error connecting:", e, socket); console.error("Error connecting:", e);
io.emit("error", `Error: connection. ${e.message ?? e}`);
} }
}) });
// Start the server
httpServer.listen(port, () => { httpServer.listen(port, () => {
console.log(`Server running on port ${port}`) console.log(`Server running on port ${port}`);
}) });

View File

@ -30,4 +30,4 @@ export const deleteFileRL = new RateLimiterMemory({
export const deleteFolderRL = new RateLimiterMemory({ export const deleteFolderRL = new RateLimiterMemory({
points: 1, points: 1,
duration: 2, duration: 2,
}) })

View File

@ -1,63 +0,0 @@
import { Socket } from "socket.io"
import { z } from "zod"
import { User } from "./types"
// Middleware for socket authentication
export const socketAuth = async (socket: Socket, next: Function) => {
// Define the schema for handshake query validation
const handshakeSchema = z.object({
userId: z.string(),
sandboxId: z.string(),
EIO: z.string(),
transport: z.string(),
})
const q = socket.handshake.query
const parseQuery = handshakeSchema.safeParse(q)
// Check if the query is valid according to the schema
if (!parseQuery.success) {
next(new Error("Invalid request."))
return
}
const { sandboxId, userId } = parseQuery.data
// Fetch user data from the database
const dbUser = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
)
const dbUserJSON = (await dbUser.json()) as User
// Check if user data was retrieved successfully
if (!dbUserJSON) {
next(new Error("DB error."))
return
}
// Check if the user owns the sandbox or has shared access
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
(uts) => uts.sandboxId === sandboxId
)
// If user doesn't own or have shared access to the sandbox, deny access
if (!sandbox && !sharedSandboxes) {
next(new Error("Invalid credentials."))
return
}
// Set socket data with user information
socket.data = {
userId,
sandboxId: sandboxId,
isOwner: sandbox !== undefined,
}
// Allow the connection
next()
}

View File

@ -1,75 +1,70 @@
// DB Types // DB Types
export type User = { export type User = {
id: string id: string;
name: string name: string;
email: string email: string;
generations: number generations: number;
sandbox: Sandbox[] sandbox: Sandbox[];
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[];
} };
export type Sandbox = { export type Sandbox = {
id: string id: string;
name: string name: string;
type: "react" | "node" type: "react" | "node";
visibility: "public" | "private" visibility: "public" | "private";
createdAt: Date createdAt: Date;
userId: string userId: string;
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[];
} };
export type UsersToSandboxes = { export type UsersToSandboxes = {
userId: string userId: string;
sandboxId: string sandboxId: string;
sharedOn: Date sharedOn: Date;
} };
export type TFolder = { export type TFolder = {
id: string id: string;
type: "folder" type: "folder";
name: string name: string;
children: (TFile | TFolder)[] children: (TFile | TFolder)[];
} };
export type TFile = { export type TFile = {
id: string id: string;
type: "file" type: "file";
name: string name: string;
} };
export type TFileData = { export type TFileData = {
id: string id: string;
data: string data: string;
} };
export type R2Files = { export type R2Files = {
objects: R2FileData[] objects: R2FileData[];
truncated: boolean truncated: boolean;
delimitedPrefixes: any[] delimitedPrefixes: any[];
} };
export type R2FileData = { export type R2FileData = {
storageClass: string storageClass: string;
uploaded: string uploaded: string;
checksums: any checksums: any;
httpEtag: string httpEtag: string;
etag: string etag: string;
size: number size: number;
version: string version: string;
key: string key: string;
} };
export type R2FileBody = R2FileData & { export type R2FileBody = R2FileData & {
body: ReadableStream body: ReadableStream;
bodyUsed: boolean bodyUsed: boolean;
arrayBuffer: Promise<ArrayBuffer> arrayBuffer: Promise<ArrayBuffer>;
text: Promise<string> text: Promise<string>;
json: Promise<any> json: Promise<any>;
blob: Promise<Blob> blob: Promise<Blob>;
} };
export interface DokkuResponse {
success: boolean
apps?: string[]
message?: string
}

View File

@ -1,23 +1,23 @@
export class LockManager { export class LockManager {
private locks: { [key: string]: Promise<any> } private locks: { [key: string]: Promise<any> };
constructor() { constructor() {
this.locks = {} this.locks = {};
} }
async acquireLock<T>(key: string, task: () => Promise<T>): Promise<T> { async acquireLock<T>(key: string, task: () => Promise<T>): Promise<T> {
if (!this.locks[key]) { if (!this.locks[key]) {
this.locks[key] = new Promise<T>(async (resolve, reject) => { this.locks[key] = new Promise<T>(async (resolve, reject) => {
try { try {
const result = await task() const result = await task();
resolve(result) resolve(result);
} catch (error) { } catch (error) {
reject(error) reject(error);
} finally { } finally {
delete this.locks[key] delete this.locks[key];
} }
}) });
} }
return await this.locks[key] return await this.locks[key];
} }
} }

View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false
}

View File

@ -8,7 +8,6 @@
"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": {
@ -895,21 +894,6 @@
"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",
@ -1782,11 +1766,12 @@
} }
}, },
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "6.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
"integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
"dev": true,
"dependencies": { "dependencies": {
"yocto-queue": "^1.1.1" "yocto-queue": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -2985,9 +2970,10 @@
"dev": true "dev": true
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "1.1.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": { "engines": {
"node": ">=12.20" "node": ">=12.20"
}, },

View File

@ -1,23 +1,22 @@
{ {
"name": "storage", "name": "storage",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev --remote", "dev": "wrangler dev --remote",
"start": "wrangler dev", "start": "wrangler dev",
"test": "vitest", "test": "vitest",
"cf-typegen": "wrangler types" "cf-typegen": "wrangler types"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/vitest-pool-workers": "^0.1.0",
"@cloudflare/workers-types": "^4.20240419.0", "@cloudflare/workers-types": "^4.20240419.0",
"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": { "dependencies": {
"p-limit": "^6.1.0", "zod": "^3.23.4"
"zod": "^3.23.4" }
} }
}

View File

@ -1,9 +1,8 @@
import pLimit from "p-limit"
import { z } from "zod" import { z } from "zod"
import startercode from "./startercode"
export interface Env { export interface Env {
R2: R2Bucket R2: R2Bucket
Templates: R2Bucket
KEY: string KEY: string
} }
@ -138,29 +137,18 @@ 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.string(), type: z.enum(["react", "node"]),
}) })
const body = await request.json() const body = await request.json()
const { sandboxId, type } = initSchema.parse(body) const { sandboxId, type } = initSchema.parse(body)
console.log(`Copying template: ${type}`) console.log(startercode[type])
// List all objects under the directory
const { objects } = await env.Templates.list({ prefix: type })
// Copy each object to the new directory with a 5 concurrency limit
const limit = pLimit(5)
await Promise.all( await Promise.all(
objects.map(({ key }) => startercode[type].map(async (file) => {
limit(async () => { await env.R2.put(`projects/${sandboxId}/${file.name}`, file.body)
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

View File

@ -0,0 +1,151 @@
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

View File

@ -1,30 +1,25 @@
// test/index.spec.ts // test/index.spec.ts
import { import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
createExecutionContext, import { describe, it, expect } from 'vitest';
env, import worker from '../src/index';
SELF,
waitOnExecutionContext,
} from "cloudflare:test"
import { describe, expect, it } from "vitest"
import worker from "../src/index"
// For now, you'll need to do something like this to get a correctly-typed // For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`. // `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties> const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
describe("Hello World worker", () => { describe('Hello World worker', () => {
it("responds with Hello World! (unit style)", async () => { it('responds with Hello World! (unit style)', async () => {
const request = new IncomingRequest("http://example.com") const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`. // Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext() const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx) const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx) await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
it("responds with Hello World! (integration style)", async () => { it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch("https://example.com") const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
}) });
}) });

View File

@ -1,11 +1,11 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": [ "types": [
"@cloudflare/workers-types/experimental", "@cloudflare/workers-types/experimental",
"@cloudflare/vitest-pool-workers" "@cloudflare/vitest-pool-workers"
] ]
}, },
"include": ["./**/*.ts", "../src/env.d.ts"], "include": ["./**/*.ts", "../src/env.d.ts"],
"exclude": [] "exclude": []
} }

View File

@ -12,9 +12,7 @@
/* Language and Environment */ /* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [ "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */, "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */

View File

@ -1,11 +1,11 @@
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ export default defineWorkersConfig({
test: { test: {
poolOptions: { poolOptions: {
workers: { workers: {
wrangler: { configPath: "./wrangler.toml" }, wrangler: { configPath: "./wrangler.toml" },
}, },
}, },
}, },
}) });

View File

@ -1,3 +1,4 @@
// Generated by Wrangler // Generated by Wrangler
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen`
interface Env {} interface Env {
}

View File

@ -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_URL=http://localhost:4000 NEXT_PUBLIC_SERVER_PORT=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.

5
frontend/.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false
}

View File

@ -1,11 +1,11 @@
import { Room } from "@/components/editor/live/room"
import Loading from "@/components/editor/loading"
import Navbar from "@/components/editor/navbar" import Navbar from "@/components/editor/navbar"
import { TerminalProvider } from "@/context/TerminalContext" import { Room } from "@/components/editor/live/room"
import { Sandbox, User, UsersToSandboxes } from "@/lib/types" import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import dynamic from "next/dynamic"
import { notFound, redirect } from "next/navigation" import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic"
import fs from "fs"
export const revalidate = 0 export const revalidate = 0
@ -63,6 +63,14 @@ 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
@ -86,22 +94,20 @@ 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}> <Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<TerminalProvider> <div className="w-screen flex grow">
<Navbar <CodeEditor
userData={userData} userData={userData}
sandboxData={sandboxData} sandboxData={sandboxData}
shared={shared} reactDefinitionFile={reactDefinitionFile}
/> />
<div className="w-screen flex grow"> </div>
<CodeEditor userData={userData} sandboxData={sandboxData} /> </Room>
</div> </div>
</TerminalProvider>
</Room>
</div>
</>
) )
} }

View File

@ -1,8 +1,8 @@
import { UserButton, currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
import Dashboard from "@/components/dashboard" import Dashboard from "@/components/dashboard"
import Navbar from "@/components/dashboard/navbar" import Navbar from "@/components/dashboard/navbar"
import { User } from "@/lib/types" import { Sandbox, User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
export default async function DashboardPage() { export default async function DashboardPage() {
const user = await currentUser() const user = await currentUser()

View File

@ -99,29 +99,6 @@
); /* violet 900 -> bg */ ); /* violet 900 -> bg */
} }
.light .gradient-button-bg {
background: radial-gradient(
circle at top,
#262626 0%,
#f5f5f5 50%
); /* Dark gray -> Light gray */
}
.light .gradient-button {
background: radial-gradient(
circle at bottom,
hsl(0, 10%, 25%) -10%,
#9d9d9d 50%
); /* Light gray -> Almost white */
}
.light .gradient-button-bg > div:hover {
background: radial-gradient(
circle at bottom,
hsl(0, 10%, 25%) -10%,
#9d9d9d 80%
); /* Light gray -> Almost white */
}
.inline-decoration::before { .inline-decoration::before {
content: "Generate"; content: "Generate";
color: #525252; color: #525252;

View File

@ -1,13 +1,12 @@
import { Toaster } from "@/components/ui/sonner"
import { ThemeProvider } from "@/components/ui/theme-provider"
import { PreviewProvider } from "@/context/PreviewContext"
import { SocketProvider } from '@/context/SocketContext'
import { ClerkProvider } from "@clerk/nextjs"
import { Analytics } from "@vercel/analytics/react"
import { GeistMono } from "geist/font/mono"
import { GeistSans } from "geist/font/sans"
import type { Metadata } from "next" import type { Metadata } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import "./globals.css" import "./globals.css"
import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext';
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -25,12 +24,13 @@ export default function RootLayout({
<body> <body>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="dark"
forcedTheme="dark"
disableTransitionOnChange disableTransitionOnChange
> >
<SocketProvider> <TerminalProvider>
<PreviewProvider>{children}</PreviewProvider> {children}
</SocketProvider> </TerminalProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
</ThemeProvider> </ThemeProvider>

View File

@ -1,13 +1,13 @@
import Landing from "@/components/landing" import { currentUser } from "@clerk/nextjs";
import { currentUser } from "@clerk/nextjs" import { redirect } from "next/navigation";
import { redirect } from "next/navigation" import Landing from "@/components/landing";
export default async function Home() { export default async function Home() {
const user = await currentUser() const user = await currentUser();
if (user) { if (user) {
redirect("/dashboard") redirect("/dashboard");
} }
return <Landing /> return <Landing />;
} }

View File

@ -1,13 +0,0 @@
"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>
}

View File

@ -3,9 +3,16 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import Image from "next/image"
import { useState } from "react"
import { Button } from "../ui/button"
import { ChevronRight } from "lucide-react"
export default function AboutModal({ export default function AboutModal({
open, open,

View File

@ -1,16 +1,24 @@
"use client" "use client"
import { Button } from "@/components/ui/button"
import CustomButton from "@/components/ui/customButton" import CustomButton from "@/components/ui/customButton"
import { Sandbox } from "@/lib/types" import { Button } from "@/components/ui/button"
import { Code2, FolderDot, HelpCircle, Plus, Users } from "lucide-react" import {
import { useRouter, useSearchParams } from "next/navigation" Code2,
FolderDot,
HelpCircle,
Plus,
Settings,
Users,
} from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { toast } from "sonner" import { Sandbox } from "@/lib/types"
import AboutModal from "./about"
import NewProjectModal from "./newProject"
import DashboardProjects from "./projects" import DashboardProjects from "./projects"
import DashboardSharedWithMe from "./shared" import DashboardSharedWithMe from "./shared"
import NewProjectModal from "./newProject"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import AboutModal from "./about"
import { toast } from "sonner"
type TScreen = "projects" | "shared" | "settings" | "search" type TScreen = "projects" | "shared" | "settings" | "search"
@ -42,9 +50,10 @@ export default function Dashboard({
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 (
<> <>

View File

@ -1,10 +1,9 @@
import Logo from "@/assets/logo.svg"
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
import { User } from "@/lib/types"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import UserButton from "../../ui/userButton" import Logo from "@/assets/logo.svg"
import DashboardNavbarSearch from "./search" import DashboardNavbarSearch from "./search"
import UserButton from "../../ui/userButton"
import { User } from "@/lib/types"
export default function DashboardNavbar({ userData }: { userData: User }) { export default function DashboardNavbar({ userData }: { userData: User }) {
return ( return (
@ -20,7 +19,6 @@ export default function DashboardNavbar({ userData }: { userData: User }) {
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<DashboardNavbarSearch /> <DashboardNavbarSearch />
<ThemeSwitcher />
<UserButton userData={userData} /> <UserButton userData={userData} />
</div> </div>
</div> </div>

View File

@ -1,12 +1,13 @@
"use client" "use client";
import { Search } from "lucide-react" import { Input } from "../../ui/input";
import { useRouter } from "next/navigation" import { Search } from "lucide-react";
import { Input } from "../../ui/input" import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export default function DashboardNavbarSearch() { export default function DashboardNavbarSearch() {
// const [search, setSearch] = useState(""); // const [search, setSearch] = useState("");
const router = useRouter() const router = useRouter();
// useEffect(() => { // useEffect(() => {
// const delayDebounceFn = setTimeout(() => { // const delayDebounceFn = setTimeout(() => {
@ -28,14 +29,14 @@ export default function DashboardNavbarSearch() {
// onChange={(e) => setSearch(e.target.value)} // onChange={(e) => setSearch(e.target.value)}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") { if (e.target.value === "") {
router.push(`/dashboard`) router.push(`/dashboard`);
return return;
} }
router.push(`/dashboard?q=${e.target.value}`) router.push(`/dashboard?q=${e.target.value}`);
}} }}
placeholder="Search projects..." placeholder="Search projects..."
className="pl-8" className="pl-8"
/> />
</div> </div>
) );
} }

View File

@ -3,14 +3,16 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { zodResolver } from "@hookform/resolvers/zod"
import Image from "next/image" import Image from "next/image"
import { useCallback, useEffect, useMemo, useState } from "react" import { useState } from "react"
import { set, z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod"
import { import {
Form, Form,
@ -29,17 +31,51 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { createSandbox } from "@/lib/actions"
import { projectTemplates } from "@/lib/data"
import { useUser } from "@clerk/nextjs" import { useUser } from "@clerk/nextjs"
import { ChevronLeft, ChevronRight, Loader2, Search } from "lucide-react" import { createSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Loader2 } from "lucide-react"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { cn } from "@/lib/utils" type TOptions = "react" | "node" | "python" | "more"
import type { EmblaCarouselType } from "embla-carousel"
import useEmblaCarousel from "embla-carousel-react" const data: {
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures" id: TOptions
name: string
icon: string
description: string
disabled: boolean
}[] = [
{
id: "react",
name: "React",
icon: "/project-icons/react.svg",
description: "A JavaScript library for building user interfaces",
disabled: false,
},
{
id: "node",
name: "Node",
icon: "/project-icons/node.svg",
description: "A JavaScript runtime built on the V8 JavaScript engine",
disabled: false,
},
{
id: "python",
name: "Python",
icon: "/project-icons/python.svg",
description: "A high-level, general-purpose language, coming soon",
disabled: true,
},
{
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({
name: z name: z
.string() .string()
@ -59,20 +95,11 @@ export default function NewProjectModal({
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {
const router = useRouter() const [selected, setSelected] = useState<TOptions>("react")
const user = useUser()
const [selected, setSelected] = useState("reactjs")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }, [ const router = useRouter()
WheelGesturesPlugin(),
]) const user = useUser()
const {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
} = usePrevNextButtons(emblaApi)
const [search, setSearch] = useState("")
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -82,26 +109,6 @@ export default function NewProjectModal({
}, },
}) })
const handleTemplateClick = useCallback(
({ id, index }: { id: string; index: number }) => {
setSelected(id)
emblaApi?.scrollTo(index)
},
[emblaApi]
)
const filteredTemplates = useMemo(
() =>
projectTemplates.filter(
(item) =>
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.description.toLowerCase().includes(search.toLowerCase())
),
[search, projectTemplates]
)
const emptyTemplates = useMemo(
() => filteredTemplates.length === 0,
[filteredTemplates]
)
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
if (!user.isSignedIn) return if (!user.isSignedIn) return
@ -111,6 +118,7 @@ export default function NewProjectModal({
const id = await createSandbox(sandboxData) const id = await createSandbox(sandboxData)
router.push(`/code/${id}`) router.push(`/code/${id}`)
} }
return ( return (
<Dialog <Dialog
open={open} open={open}
@ -118,93 +126,29 @@ export default function NewProjectModal({
if (!loading) setOpen(open) if (!loading) setOpen(open)
}} }}
> >
<DialogContent className="max-h-[95vh] overflow-y-auto"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create A Sandbox</DialogTitle> <DialogTitle>Create A Sandbox</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-2 max-w-full overflow-hidden"> <div className="grid grid-cols-2 w-full gap-2 mt-2">
<div className="flex items-center justify-end"> {data.map((item) => (
<SearchInput <button
{...{ disabled={item.disabled || loading}
value: search, key={item.id}
onValueChange: setSearch, onClick={() => setSelected(item.id)}
}} className={`${
/> selected === item.id ? "border-foreground" : "border-border"
</div> } rounded-md border bg-card text-card-foreground shadow text-left p-4 flex flex-col transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed`}
<div className="overflow-hidden relative" ref={emblaRef}>
<div
className={cn(
"grid grid-flow-col gap-x-2 min-h-[97px]",
emptyTemplates ? "auto-cols-[100%]" : "auto-cols-[200px]"
)}
> >
{filteredTemplates.map((item, i) => ( <div className="space-x-2 flex items-center justify-start w-full">
<button <Image alt="" src={item.icon} width={20} height={20} />
disabled={item.disabled || loading} <div className="font-medium">{item.name}</div>
key={item.id} </div>
onClick={handleTemplateClick.bind(null, { <div className="mt-2 text-muted-foreground text-sm">
id: item.id, {item.description}
index: i, </div>
})} </button>
className={cn( ))}
selected === item.id
? "shadow-foreground"
: "shadow-border",
"shadow-[0_0_0_1px_inset] rounded-md border bg-card text-card-foreground text-left p-4 flex flex-col transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
)}
>
<div className="space-x-2 flex items-center justify-start w-full">
<Image alt="" src={item.icon} width={20} height={20} />
<div className="font-medium">{item.name}</div>
</div>
<div className="mt-2 text-muted-foreground text-xs line-clamp-2">
{item.description}
</div>
</button>
))}
{emptyTemplates && (
<div className="flex flex-col gap-2 items-center text-center justify-center text-muted-foreground text-sm">
<p>No templates found</p>
<Button size="xs" asChild>
<a
href="https://github.com/jamesmurdza/sandbox"
target="_blank"
>
Contribute
</a>
</Button>
</div>
)}
</div>
<div
className={cn(
"absolute transition-all opacity-100 duration-400 bg-gradient-to-r from-background via-background to-transparent w-14 pl-1 left-0 top-0 -translate-x-1 bottom-0 h-full flex items-center",
prevBtnDisabled && "opacity-0 pointer-events-none"
)}
>
<Button
size="smIcon"
className="rounded-full"
onClick={onPrevButtonClick}
>
<ChevronLeft className="size-5" />
</Button>
</div>
<div
className={cn(
"absolute transition-all opacity-100 duration-400 bg-gradient-to-l from-background via-background to-transparent w-14 pl-1 right-0 top-0 translate-x-1 bottom-0 h-full flex items-center",
nextBtnDisabled && "opacity-0 pointer-events-none"
)}
>
<Button
size="smIcon"
className="rounded-full"
onClick={onNextButtonClick}
>
<ChevronRight className="size-5" />
</Button>
</div>
</div>
</div> </div>
<Form {...form}> <Form {...form}>
@ -272,68 +216,3 @@ export default function NewProjectModal({
</Dialog> </Dialog>
) )
} }
function SearchInput({
value,
onValueChange,
}: {
value?: string
onValueChange?: (value: string) => void
}) {
const onSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault()
console.log("searching")
}, [])
return (
<form {...{ onSubmit }} className="w-40 h-8 ">
<label
htmlFor="template-search"
className="flex gap-2 rounded-sm transition-colors bg-gray-100 dark:bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:hsl(var(--muted-foreground),50%)] h-full items-center px-2"
>
<Search className="size-4 text-[--s-color] transition-colors" />
<input
id="template-search"
type="text"
name="search"
placeholder="Search templates"
value={value}
onChange={(e) => onValueChange?.(e.target.value)}
className="bg-transparent placeholder:text-muted-foreground w-full focus:outline-none text-xs"
/>
</label>
</form>
)
}
const usePrevNextButtons = (emblaApi: EmblaCarouselType | undefined) => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollPrev()
}, [emblaApi])
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollNext()
}, [emblaApi])
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canScrollPrev())
setNextBtnDisabled(!emblaApi.canScrollNext())
}, [])
useEffect(() => {
if (!emblaApi) return
onSelect(emblaApi)
emblaApi.on("reInit", onSelect).on("select", onSelect)
}, [emblaApi, onSelect])
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
}
}

View File

@ -1,30 +1,30 @@
"use client" "use client";
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types";
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react" import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu";
export default function ProjectCardDropdown({ export default function ProjectCardDropdown({
sandbox, sandbox,
onVisibilityChange, onVisibilityChange,
onDelete, onDelete,
}: { }: {
sandbox: Sandbox sandbox: Sandbox;
onVisibilityChange: (sandbox: Sandbox) => void onVisibilityChange: (sandbox: Sandbox) => void;
onDelete: (sandbox: Sandbox) => void onDelete: (sandbox: Sandbox) => void;
}) { }) {
return ( return (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger <DropdownMenuTrigger
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault();
e.stopPropagation() e.stopPropagation();
}} }}
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground" className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
> >
@ -33,8 +33,8 @@ export default function ProjectCardDropdown({
<DropdownMenuContent className="w-40"> <DropdownMenuContent className="w-40">
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
onVisibilityChange(sandbox) onVisibilityChange(sandbox);
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
@ -52,8 +52,8 @@ export default function ProjectCardDropdown({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
onDelete(sandbox) onDelete(sandbox);
}} }}
className="!text-destructive cursor-pointer" className="!text-destructive cursor-pointer"
> >
@ -62,5 +62,5 @@ export default function ProjectCardDropdown({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }

View File

@ -1,14 +1,13 @@
"use client" "use client"
import { Card } from "@/components/ui/card"
import { projectTemplates } from "@/lib/data"
import { Sandbox } from "@/lib/types"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "framer-motion"
import { Clock, Globe, Lock } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import ProjectCardDropdown from "./dropdown" import ProjectCardDropdown from "./dropdown"
import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation"
export default function ProjectCard({ export default function ProjectCard({
children, children,
@ -44,9 +43,7 @@ export default function ProjectCard({
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`) setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
} }
}, [sandbox]) }, [sandbox])
const projectIcon =
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
"/project-icons/node.svg"
return ( return (
<Card <Card
tabIndex={0} tabIndex={0}
@ -68,7 +65,16 @@ export default function ProjectCard({
</AnimatePresence> </AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10"> <div className="space-x-2 flex items-center justify-start w-full z-10">
<Image alt="" src={projectIcon} width={20} height={20} /> <Image
alt=""
src={
sandbox.type === "react"
? "/project-icons/react.svg"
: "/project-icons/node.svg"
}
width={20}
height={20}
/>
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden"> <div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{sandbox.name} {sandbox.name}
</div> </div>

View File

@ -1,8 +1,8 @@
"use client" "use client";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Canvas, useFrame, useThree } from "@react-three/fiber" import { Canvas, useFrame, useThree } from "@react-three/fiber";
import React, { useMemo, useRef } from "react" import React, { useMemo, useRef } from "react";
import * as THREE from "three" import * as THREE from "three";
export const CanvasRevealEffect = ({ export const CanvasRevealEffect = ({
animationSpeed = 0.4, animationSpeed = 0.4,
@ -12,12 +12,12 @@ export const CanvasRevealEffect = ({
dotSize, dotSize,
showGradient = true, showGradient = true,
}: { }: {
animationSpeed?: number animationSpeed?: number;
opacities?: number[] opacities?: number[];
colors?: number[][] colors?: number[][];
containerClassName?: string containerClassName?: string;
dotSize?: number dotSize?: number;
showGradient?: boolean showGradient?: boolean;
}) => { }) => {
return ( return (
<div className={cn("h-full relative bg-white w-full", containerClassName)}> <div className={cn("h-full relative bg-white w-full", containerClassName)}>
@ -41,16 +41,16 @@ export const CanvasRevealEffect = ({
<div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" /> <div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
)} )}
</div> </div>
) );
} };
interface DotMatrixProps { interface DotMatrixProps {
colors?: number[][] colors?: number[][];
opacities?: number[] opacities?: number[];
totalSize?: number totalSize?: number;
dotSize?: number dotSize?: number;
shader?: string shader?: string;
center?: ("x" | "y")[] center?: ("x" | "y")[];
} }
const DotMatrix: React.FC<DotMatrixProps> = ({ const DotMatrix: React.FC<DotMatrixProps> = ({
@ -69,7 +69,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[0], colors[0],
colors[0], colors[0],
colors[0], colors[0],
] ];
if (colors.length === 2) { if (colors.length === 2) {
colorsArray = [ colorsArray = [
colors[0], colors[0],
@ -78,7 +78,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1], colors[1],
colors[1], colors[1],
colors[1], colors[1],
] ];
} else if (colors.length === 3) { } else if (colors.length === 3) {
colorsArray = [ colorsArray = [
colors[0], colors[0],
@ -87,7 +87,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1], colors[1],
colors[2], colors[2],
colors[2], colors[2],
] ];
} }
return { return {
@ -111,8 +111,8 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
value: dotSize, value: dotSize,
type: "uniform1f", type: "uniform1f",
}, },
} };
}, [colors, opacities, totalSize, dotSize]) }, [colors, opacities, totalSize, dotSize]);
return ( return (
<Shader <Shader
@ -168,87 +168,87 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
uniforms={uniforms} uniforms={uniforms}
maxFps={60} maxFps={60}
/> />
) );
} };
type Uniforms = { type Uniforms = {
[key: string]: { [key: string]: {
value: number[] | number[][] | number value: number[] | number[][] | number;
type: string type: string;
} };
} };
const ShaderMaterial = ({ const ShaderMaterial = ({
source, source,
uniforms, uniforms,
maxFps = 60, maxFps = 60,
}: { }: {
source: string source: string;
hovered?: boolean hovered?: boolean;
maxFps?: number maxFps?: number;
uniforms: Uniforms uniforms: Uniforms;
}) => { }) => {
const { size } = useThree() const { size } = useThree();
const ref = useRef<THREE.Mesh>() const ref = useRef<THREE.Mesh>();
let lastFrameTime = 0 let lastFrameTime = 0;
useFrame(({ clock }) => { useFrame(({ clock }) => {
if (!ref.current) return if (!ref.current) return;
const timestamp = clock.getElapsedTime() const timestamp = clock.getElapsedTime();
if (timestamp - lastFrameTime < 1 / maxFps) { if (timestamp - lastFrameTime < 1 / maxFps) {
return return;
} }
lastFrameTime = timestamp lastFrameTime = timestamp;
const material: any = ref.current.material const material: any = ref.current.material;
const timeLocation = material.uniforms.u_time const timeLocation = material.uniforms.u_time;
timeLocation.value = timestamp timeLocation.value = timestamp;
}) });
const getUniforms = () => { const getUniforms = () => {
const preparedUniforms: any = {} const preparedUniforms: any = {};
for (const uniformName in uniforms) { for (const uniformName in uniforms) {
const uniform: any = uniforms[uniformName] const uniform: any = uniforms[uniformName];
switch (uniform.type) { switch (uniform.type) {
case "uniform1f": case "uniform1f":
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" } preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
break break;
case "uniform3f": case "uniform3f":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: new THREE.Vector3().fromArray(uniform.value), value: new THREE.Vector3().fromArray(uniform.value),
type: "3f", type: "3f",
} };
break break;
case "uniform1fv": case "uniform1fv":
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" } preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
break break;
case "uniform3fv": case "uniform3fv":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: uniform.value.map((v: number[]) => value: uniform.value.map((v: number[]) =>
new THREE.Vector3().fromArray(v) new THREE.Vector3().fromArray(v)
), ),
type: "3fv", type: "3fv",
} };
break break;
case "uniform2f": case "uniform2f":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value), value: new THREE.Vector2().fromArray(uniform.value),
type: "2f", type: "2f",
} };
break break;
default: default:
console.error(`Invalid uniform type for '${uniformName}'.`) console.error(`Invalid uniform type for '${uniformName}'.`);
break break;
} }
} }
preparedUniforms["u_time"] = { value: 0, type: "1f" } preparedUniforms["u_time"] = { value: 0, type: "1f" };
preparedUniforms["u_resolution"] = { preparedUniforms["u_resolution"] = {
value: new THREE.Vector2(size.width * 2, size.height * 2), value: new THREE.Vector2(size.width * 2, size.height * 2),
} // Initialize u_resolution }; // Initialize u_resolution
return preparedUniforms return preparedUniforms;
} };
// Shader material // Shader material
const material = useMemo(() => { const material = useMemo(() => {
@ -272,33 +272,33 @@ const ShaderMaterial = ({
blending: THREE.CustomBlending, blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor, blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor, blendDst: THREE.OneFactor,
}) });
return materialObject return materialObject;
}, [size.width, size.height, source]) }, [size.width, size.height, source]);
return ( return (
<mesh ref={ref as any}> <mesh ref={ref as any}>
<planeGeometry args={[2, 2]} /> <planeGeometry args={[2, 2]} />
<primitive object={material} attach="material" /> <primitive object={material} attach="material" />
</mesh> </mesh>
) );
} };
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => { const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
return ( return (
<Canvas className="absolute inset-0 h-full w-full"> <Canvas className="absolute inset-0 h-full w-full">
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} /> <ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
</Canvas> </Canvas>
) );
} };
interface ShaderProps { interface ShaderProps {
source: string source: string;
uniforms: { uniforms: {
[key: string]: { [key: string]: {
value: number[] | number[][] | number value: number[] | number[][] | number;
type: string type: string;
} };
} };
maxFps?: number maxFps?: number;
} }

View File

@ -1,14 +1,18 @@
"use client" "use client";
import { deleteSandbox, updateSandbox } from "@/lib/actions" import { Sandbox } from "@/lib/types";
import { Sandbox } from "@/lib/types" import ProjectCard from "./projectCard";
import Link from "next/link" import Image from "next/image";
import { useEffect, useState } from "react" import ProjectCardDropdown from "./projectCard/dropdown";
import { toast } from "sonner" import { Clock, Globe, Lock } from "lucide-react";
import ProjectCard from "./projectCard" import Link from "next/link";
import { CanvasRevealEffect } from "./projectCard/revealEffect" import { Card } from "../ui/card";
import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { toast } from "sonner";
import { useEffect, useState } from "react";
import { CanvasRevealEffect } from "./projectCard/revealEffect";
const colors: { [key: string]: number[][] } = { const colors = {
react: [ react: [
[71, 207, 237], [71, 207, 237],
[30, 126, 148], [30, 126, 148],
@ -17,37 +21,38 @@ const colors: { [key: string]: number[][] } = {
[86, 184, 72], [86, 184, 72],
[59, 112, 52], [59, 112, 52],
], ],
} };
export default function DashboardProjects({ export default function DashboardProjects({
sandboxes, sandboxes,
q, q,
}: { }: {
sandboxes: Sandbox[] sandboxes: Sandbox[];
q: string | null q: string | null;
}) { }) {
const [deletingId, setDeletingId] = useState<string>("") const [deletingId, setDeletingId] = useState<string>("");
const onDelete = async (sandbox: Sandbox) => { const onDelete = async (sandbox: Sandbox) => {
setDeletingId(sandbox.id) setDeletingId(sandbox.id);
toast(`Project ${sandbox.name} deleted.`) toast(`Project ${sandbox.name} deleted.`);
await deleteSandbox(sandbox.id) await deleteSandbox(sandbox.id);
} };
useEffect(() => { useEffect(() => {
if (deletingId) { if (deletingId) {
setDeletingId("") setDeletingId("");
} }
}, [sandboxes]) }, [sandboxes]);
const onVisibilityChange = async (sandbox: Sandbox) => { const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = sandbox.visibility === "public" ? "private" : "public" const newVisibility =
toast(`Project ${sandbox.name} is now ${newVisibility}.`) sandbox.visibility === "public" ? "private" : "public";
toast(`Project ${sandbox.name} is now ${newVisibility}.`);
await updateSandbox({ await updateSandbox({
id: sandbox.id, id: sandbox.id,
visibility: newVisibility, visibility: newVisibility,
}) });
} };
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
@ -60,7 +65,7 @@ export default function DashboardProjects({
{sandboxes.map((sandbox) => { {sandboxes.map((sandbox) => {
if (q && q.length > 0) { if (q && q.length > 0) {
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) { if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
return null return null;
} }
} }
return ( return (
@ -88,7 +93,7 @@ export default function DashboardProjects({
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" /> <div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard> </ProjectCard>
</Link> </Link>
) );
})} })}
</div> </div>
) : ( ) : (
@ -98,5 +103,5 @@ export default function DashboardProjects({
)} )}
</div> </div>
</div> </div>
) );
} }

View File

@ -1,27 +1,29 @@
import { Sandbox } from "@/lib/types";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table";
import { ChevronRight } from "lucide-react" import Image from "next/image";
import Image from "next/image" import Button from "../ui/customButton";
import Link from "next/link" import { ChevronRight } from "lucide-react";
import Avatar from "../ui/avatar" import Avatar from "../ui/avatar";
import Button from "../ui/customButton" import Link from "next/link";
export default function DashboardSharedWithMe({ export default function DashboardSharedWithMe({
shared, shared,
}: { }: {
shared: { shared: {
id: string id: string;
name: string name: string;
type: "react" | "node" type: "react" | "node";
author: string author: string;
sharedOn: Date sharedOn: Date;
}[] }[];
}) { }) {
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
@ -84,5 +86,5 @@ export default function DashboardSharedWithMe({
</div> </div>
)} )}
</div> </div>
) );
} }

View File

@ -1,51 +0,0 @@
import { Send, StopCircle } from "lucide-react"
import { Button } from "../../ui/button"
interface ChatInputProps {
input: string
setInput: (input: string) => void
isGenerating: boolean
handleSend: () => void
handleStopGeneration: () => void
}
export default function ChatInput({
input,
setInput,
isGenerating,
handleSend,
handleStopGeneration,
}: ChatInputProps) {
return (
<div className="flex space-x-2 min-w-0">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
placeholder="Type your message..."
disabled={isGenerating}
/>
{isGenerating ? (
<Button
onClick={handleStopGeneration}
variant="destructive"
size="icon"
className="h-10 w-10"
>
<StopCircle className="w-4 h-4" />
</Button>
) : (
<Button
onClick={handleSend}
disabled={isGenerating}
size="icon"
className="h-10 w-10"
>
<Send className="w-4 h-4" />
</Button>
)}
</div>
)
}

View File

@ -1,226 +0,0 @@
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
import React, { useState } from "react"
import ReactMarkdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
import remarkGfm from "remark-gfm"
import { Button } from "../../ui/button"
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
interface MessageProps {
message: {
role: "user" | "assistant"
content: string
context?: string
}
setContext: (context: string | null) => void
setIsContextExpanded: (isExpanded: boolean) => void
}
export default function ChatMessage({
message,
setContext,
setIsContextExpanded,
}: MessageProps) {
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
number | null
>(null)
const [copiedText, setCopiedText] = useState<string | null>(null)
const renderCopyButton = (text: any) => (
<Button
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
{copiedText === stringifyContent(text) ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
)
const askAboutCode = (code: any) => {
const contextString = stringifyContent(code)
setContext(`Regarding this code:\n${contextString}`)
setIsContextExpanded(false)
}
const renderMarkdownElement = (props: any) => {
const { node, children } = props
const content = stringifyContent(children)
return (
<div className="relative group">
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(content)}
<Button
onClick={() => askAboutCode(content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
{React.createElement(
node.tagName,
{
...props,
className: `${
props.className || ""
} hover:bg-transparent rounded p-1 transition-colors`,
},
children
)}
</div>
)
}
return (
<div className="text-left relative">
<div
className={`relative p-2 rounded-lg ${
message.role === "user"
? "bg-[#262626] text-white"
: "bg-transparent text-white"
} max-w-full`}
>
{message.role === "user" && (
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(message.content)}
<Button
onClick={() => askAboutCode(message.content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
)}
{message.context && (
<div className="mb-2 bg-input rounded-lg">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
}
>
<span className="text-sm text-gray-300">Context</span>
{expandedMessageIndex === 0 ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</div>
{expandedMessageIndex === 0 && (
<div className="relative">
<div className="absolute top-0 right-0 flex p-1">
{renderCopyButton(
message.context.replace(/^Regarding this code:\n/, "")
)}
</div>
{(() => {
const code = message.context.replace(
/^Regarding this code:\n/,
""
)
const match = /language-(\w+)/.exec(code)
const language = match ? match[1] : "typescript"
return (
<div className="pt-6">
<textarea
value={code}
onChange={(e) => {
const updatedContext = `Regarding this code:\n${e.target.value}`
setContext(updatedContext)
}}
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
rows={code.split("\n").length}
style={{
resize: "vertical",
minHeight: "100px",
maxHeight: "400px",
}}
/>
</div>
)
})()}
</div>
)}
</div>
)}
{message.role === "assistant" ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "")
return match ? (
<div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
{match[1]}
</div>
<div className="absolute top-0 right-0 flex">
{renderCopyButton(children)}
<Button
onClick={() => askAboutCode(children)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
<div className="pt-6">
<SyntaxHighlighter
style={vscDarkPlus as any}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
padding: "0.5rem",
fontSize: "0.875rem",
}}
>
{stringifyContent(children)}
</SyntaxHighlighter>
</div>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
p: renderMarkdownElement,
h1: renderMarkdownElement,
h2: renderMarkdownElement,
h3: renderMarkdownElement,
h4: renderMarkdownElement,
h5: renderMarkdownElement,
h6: renderMarkdownElement,
ul: (props) => (
<ul className="list-disc pl-6 mb-4 space-y-2">
{props.children}
</ul>
),
ol: (props) => (
<ol className="list-decimal pl-6 mb-4 space-y-2">
{props.children}
</ol>
),
}}
>
{message.content}
</ReactMarkdown>
) : (
<div className="whitespace-pre-wrap group">{message.content}</div>
)}
</div>
</div>
)
}

View File

@ -1,60 +0,0 @@
import { ChevronDown, ChevronUp, X } from "lucide-react"
interface ContextDisplayProps {
context: string | null
isContextExpanded: boolean
setIsContextExpanded: (isExpanded: boolean) => void
setContext: (context: string | null) => void
}
export default function ContextDisplay({
context,
isContextExpanded,
setIsContextExpanded,
setContext,
}: ContextDisplayProps) {
if (!context) return null
return (
<div className="mb-2 bg-input p-2 rounded-lg">
<div className="flex justify-between items-center">
<div
className="flex-grow cursor-pointer"
onClick={() => setIsContextExpanded(!isContextExpanded)}
>
<span className="text-sm text-gray-300">Context</span>
</div>
<div className="flex items-center">
{isContextExpanded ? (
<ChevronUp
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(false)}
/>
) : (
<ChevronDown
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(true)}
/>
)}
<X
size={16}
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
onClick={() => setContext(null)}
/>
</div>
</div>
{isContextExpanded && (
<textarea
value={context.replace(/^Regarding this code:\n/, "")}
onChange={(e) =>
setContext(`Regarding this code:\n${e.target.value}`)
}
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
rows={5}
/>
)}
</div>
)
}

View File

@ -1,110 +0,0 @@
import { X } from "lucide-react"
import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots"
import ChatInput from "./ChatInput"
import ChatMessage from "./ChatMessage"
import ContextDisplay from "./ContextDisplay"
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
interface Message {
role: "user" | "assistant"
content: string
context?: string
}
export default function AIChat({
activeFileContent,
activeFileName,
onClose,
}: {
activeFileContent: string
activeFileName: string
onClose: () => void
}) {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState("")
const [isGenerating, setIsGenerating] = useState(false)
const chatContainerRef = useRef<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const [context, setContext] = useState<string | null>(null)
const [isContextExpanded, setIsContextExpanded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
scrollToBottom()
}, [messages])
const scrollToBottom = () => {
if (chatContainerRef.current) {
setTimeout(() => {
chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: "smooth",
})
}, 100)
}
}
return (
<div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b">
<span className="text-muted-foreground/50 font-medium">CHAT</span>
<div className="flex items-center h-full">
<span className="text-muted-foreground/50 font-medium">
{activeFileName}
</span>
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
<button
onClick={onClose}
className="text-muted-foreground/50 hover:text-muted-foreground focus:outline-none"
aria-label="Close AI Chat"
>
<X size={18} />
</button>
</div>
</div>
<div
ref={chatContainerRef}
className="flex-grow overflow-y-auto p-4 space-y-4"
>
{messages.map((message, messageIndex) => (
<ChatMessage
key={messageIndex}
message={message}
setContext={setContext}
setIsContextExpanded={setIsContextExpanded}
/>
))}
{isLoading && <LoadingDots />}
</div>
<div className="p-4 border-t mb-14">
<ContextDisplay
context={context}
isContextExpanded={isContextExpanded}
setIsContextExpanded={setIsContextExpanded}
setContext={setContext}
/>
<ChatInput
input={input}
setInput={setInput}
isGenerating={isGenerating}
handleSend={() =>
handleSend(
input,
context,
messages,
setMessages,
setInput,
setIsContextExpanded,
setIsGenerating,
setIsLoading,
abortControllerRef,
activeFileContent
)
}
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
/>
</div>
</div>
)
}

View File

@ -1,180 +0,0 @@
import React from "react"
export const stringifyContent = (
content: any,
seen = new WeakSet()
): string => {
if (typeof content === "string") {
return content
}
if (content === null) {
return "null"
}
if (content === undefined) {
return "undefined"
}
if (typeof content === "number" || typeof content === "boolean") {
return content.toString()
}
if (typeof content === "function") {
return content.toString()
}
if (typeof content === "symbol") {
return content.toString()
}
if (typeof content === "bigint") {
return content.toString() + "n"
}
if (React.isValidElement(content)) {
return React.Children.toArray(
(content as React.ReactElement).props.children
)
.map((child) => stringifyContent(child, seen))
.join("")
}
if (Array.isArray(content)) {
return (
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
)
}
if (typeof content === "object") {
if (seen.has(content)) {
return "[Circular]"
}
seen.add(content)
try {
const pairs = Object.entries(content).map(
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
)
return "{" + pairs.join(", ") + "}"
} catch (error) {
return Object.prototype.toString.call(content)
}
}
return String(content)
}
export const copyToClipboard = (
text: string,
setCopiedText: (text: string | null) => void
) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000)
})
}
export const handleSend = async (
input: string,
context: string | null,
messages: any[],
setMessages: React.Dispatch<React.SetStateAction<any[]>>,
setInput: React.Dispatch<React.SetStateAction<string>>,
setIsContextExpanded: React.Dispatch<React.SetStateAction<boolean>>,
setIsGenerating: React.Dispatch<React.SetStateAction<boolean>>,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string
) => {
if (input.trim() === "" && !context) return
const newMessage = {
role: "user" as const,
content: input,
context: context || undefined,
}
const updatedMessages = [...messages, newMessage]
setMessages(updatedMessages)
setInput("")
setIsContextExpanded(false)
setIsGenerating(true)
setIsLoading(true)
abortControllerRef.current = new AbortController()
try {
const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === "user" ? "human" : "assistant",
content: msg.content,
}))
const response = await fetch(
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: anthropicMessages,
context: context || undefined,
activeFileContent: activeFileContent,
}),
signal: abortControllerRef.current.signal,
}
)
if (!response.ok) {
throw new Error("Failed to get AI response")
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
const assistantMessage = { role: "assistant" as const, content: "" }
setMessages([...updatedMessages, assistantMessage])
setIsLoading(false)
let buffer = ""
const updateInterval = 100
let lastUpdateTime = Date.now()
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const currentTime = Date.now()
if (currentTime - lastUpdateTime > updateInterval) {
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
lastUpdateTime = currentTime
}
}
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
}
} catch (error: any) {
if (error.name === "AbortError") {
console.log("Generation aborted")
} else {
console.error("Error fetching AI response:", error)
const errorMessage = {
role: "assistant" as const,
content: "Sorry, I encountered an error. Please try again.",
}
setMessages((prev) => [...prev, errorMessage])
}
} finally {
setIsGenerating(false)
setIsLoading(false)
abortControllerRef.current = null
}
}
export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null>
) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}

View File

@ -1,13 +1,13 @@
"use client" "use client"
import { User } from "@/lib/types" import { useEffect, useRef, useState } from "react"
import { Editor } from "@monaco-editor/react"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import { toast } from "sonner"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client"
import { Editor } from "@monaco-editor/react"
import { User } from "@/lib/types"
import { toast } from "sonner"
import { usePathname, useRouter } from "next/navigation"
// import monaco from "monaco-editor" // import monaco from "monaco-editor"
export default function GenerateInput({ export default function GenerateInput({
@ -59,7 +59,7 @@ export default function GenerateInput({
}: { }: {
regenerate?: boolean regenerate?: boolean
}) => { }) => {
if (user.generations >= 1000) { if (user.generations >= 10) {
toast.error("You reached the maximum # of generations.") toast.error("You reached the maximum # of generations.")
return return
} }
@ -68,12 +68,10 @@ export default function GenerateInput({
setCurrentPrompt(input) setCurrentPrompt(input)
socket.emit( socket.emit(
"generateCode", "generateCode",
{ data.fileName,
fileName: data.fileName, data.code,
code: data.code, data.line,
line: data.line, regenerate ? currentPrompt : input,
instructions: regenerate ? currentPrompt : input
},
(res: { response: string; success: boolean }) => { (res: { response: string; success: boolean }) => {
console.log("Generated code", res.response, res.success) console.log("Generated code", res.response, res.success)
// if (!res.success) { // if (!res.success) {
@ -86,13 +84,6 @@ export default function GenerateInput({
} }
) )
} }
const handleGenerateForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleGenerate({ regenerate: false })
},
[input, currentPrompt]
)
useEffect(() => { useEffect(() => {
if (code) { if (code) {
@ -102,23 +93,9 @@ 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">
<form <div className="flex items-center font-sans space-x-2">
onSubmit={handleGenerateForm}
className="flex items-center font-sans space-x-2"
>
<input <input
ref={inputRef} ref={inputRef}
style={{ style={{
@ -132,8 +109,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 ? (
<> <>
@ -149,14 +126,13 @@ 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>
</form> </div>
{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">

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
"use client" "use client";
import { useOthers } from "@/liveblocks.config" import { useOthers } from "@/liveblocks.config";
const classNames = { const classNames = {
red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium", red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium",
@ -14,10 +14,10 @@ const classNames = {
purple: purple:
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium", "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium", pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
} };
export function Avatars() { export function Avatars() {
const users = useOthers() const users = useOthers();
return ( return (
<> <>
@ -30,12 +30,12 @@ export function Avatars() {
.slice(0, 2) .slice(0, 2)
.map((letter) => letter[0].toUpperCase())} .map((letter) => letter[0].toUpperCase())}
</div> </div>
) );
})} })}
</div> </div>
{users.length > 0 ? ( {users.length > 0 ? (
<div className="h-full w-[1px] bg-border mx-2" /> <div className="h-full w-[1px] bg-border mx-2" />
) : null} ) : null}
</> </>
) );
} }

View File

@ -1,10 +1,11 @@
import { colors } from "@/lib/colors" import { useEffect, useMemo, useState } from "react"
import { import {
AwarenessList, AwarenessList,
TypedLiveblocksProvider, TypedLiveblocksProvider,
UserAwareness, UserAwareness,
useSelf,
} from "@/liveblocks.config" } from "@/liveblocks.config"
import { useEffect, useMemo, useState } from "react" import { colors } from "@/lib/colors"
export function Cursors({ export function Cursors({
yProvider, yProvider,

View File

@ -1,35 +1,43 @@
"use client" "use client";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" DialogTrigger,
} from "@/components/ui/dialog";
import { Loader2 } from "lucide-react" import {
import { useRouter } from "next/navigation" ChevronRight,
import { useEffect } from "react" FileStack,
Globe,
Loader2,
TextCursor,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function DisableAccessModal({ export default function DisableAccessModal({
open, open,
setOpen, setOpen,
message, message,
}: { }: {
open: boolean open: boolean;
setOpen: (open: boolean) => void setOpen: (open: boolean) => void;
message: string message: string;
}) { }) {
const router = useRouter() const router = useRouter();
useEffect(() => { useEffect(() => {
if (open) { if (open) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
router.push("/dashboard") router.push("/dashboard");
}, 5000) }, 5000);
return () => clearTimeout(timeout) return () => clearTimeout(timeout);
} }
}, []) }, []);
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
@ -46,5 +54,5 @@ export default function DisableAccessModal({
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }

View File

@ -1,13 +1,14 @@
"use client" "use client";
import { RoomProvider } from "@/liveblocks.config" import { RoomProvider } from "@/liveblocks.config";
import { ClientSideSuspense } from "@liveblocks/react";
export function Room({ export function Room({
id, id,
children, children,
}: { }: {
id: string id: string;
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<RoomProvider <RoomProvider
@ -20,5 +21,5 @@ export function Room({
{children} {children}
{/* </ClientSideSuspense> */} {/* </ClientSideSuspense> */}
</RoomProvider> </RoomProvider>
) );
} }

View File

@ -1,6 +1,9 @@
"use client" "use client"
import Image from "next/image"
import Logo from "@/assets/logo.svg" import Logo from "@/assets/logo.svg"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,9 +11,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import Image from "next/image"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
export default function Loading({ export default function Loading({
@ -84,10 +84,8 @@ export default function Loading({
</div> </div>
</div> </div>
<div className="w-full mt-1 flex flex-col"> <div className="w-full mt-1 flex flex-col">
<div className="w-full flex flex-col justify-center"> <div className="w-full flex justify-center">
{new Array(6).fill(0).map((_, i) => ( <Loader2 className="w-4 h-4 animate-spin" />
<Skeleton key={i} className="h-[1.625rem] mb-0.5 rounded-sm" />
))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,101 +0,0 @@
"use client"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { useTerminal } from "@/context/TerminalContext"
import { Sandbox, User } from "@/lib/types"
import { Globe } from "lucide-react"
import { useState } from "react"
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>
)
}

View File

@ -1,57 +1,60 @@
"use client" "use client";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" DialogTrigger,
import { zodResolver } from "@hookform/resolvers/zod" } from "@/components/ui/dialog";
import { useForm } from "react-hook-form" import { z } from "zod";
import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button"
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select";
import { deleteSandbox, updateSandbox } from "@/lib/actions" import { Loader2 } from "lucide-react";
import { Sandbox } from "@/lib/types" import { useState } from "react";
import { Loader2 } from "lucide-react" import { Sandbox } from "@/lib/types";
import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button";
import { useState } from "react" import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { toast } from "sonner" import { useRouter } from "next/navigation";
import { toast } from "sonner";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1).max(16), name: z.string().min(1).max(16),
visibility: z.enum(["public", "private"]), visibility: z.enum(["public", "private"]),
}) });
export default function EditSandboxModal({ export default function EditSandboxModal({
open, open,
setOpen, setOpen,
data, data,
}: { }: {
open: boolean open: boolean;
setOpen: (open: boolean) => void setOpen: (open: boolean) => void;
data: Sandbox data: Sandbox;
}) { }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const [loadingDelete, setLoadingDelete] = useState(false) const [loadingDelete, setLoadingDelete] = useState(false);
const router = useRouter() const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -59,22 +62,22 @@ export default function EditSandboxModal({
name: data.name, name: data.name,
visibility: data.visibility, visibility: data.visibility,
}, },
}) });
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true) setLoading(true);
await updateSandbox({ id: data.id, ...values }) await updateSandbox({ id: data.id, ...values });
toast.success("Sandbox updated successfully") toast.success("Sandbox updated successfully");
setLoading(false) setLoading(false);
} }
async function onDelete() { async function onDelete() {
setLoadingDelete(true) setLoadingDelete(true);
await deleteSandbox(data.id) await deleteSandbox(data.id);
router.push("/dashboard") router.push("/dashboard");
} }
return ( return (
@ -150,5 +153,5 @@ export default function EditSandboxModal({
</Button> </Button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }

View File

@ -1,34 +1,40 @@
"use client" "use client";
import Logo from "@/assets/logo.svg" import Image from "next/image";
import { Button } from "@/components/ui/button" import Logo from "@/assets/logo.svg";
import { ThemeSwitcher } from "@/components/ui/theme-switcher" import { Pencil, Users, Play, StopCircle } from "lucide-react";
import UserButton from "@/components/ui/userButton" import Link from "next/link";
import { Sandbox, User } from "@/lib/types" import { Sandbox, User } from "@/lib/types";
import { Pencil, Users } from "lucide-react" import UserButton from "@/components/ui/userButton";
import Image from "next/image" import { Button } from "@/components/ui/button";
import Link from "next/link" import { useState } from "react";
import { useState } from "react" import EditSandboxModal from "./edit";
import { Avatars } from "../live/avatars" import ShareSandboxModal from "./share";
import DeployButtonModal from "./deploy" import { Avatars } from "../live/avatars";
import EditSandboxModal from "./edit" import RunButtonModal from "./run";
import RunButtonModal from "./run" import { Terminal } from "@xterm/xterm";
import ShareSandboxModal from "./share" import { Socket } from "socket.io-client";
import Terminals from "../terminals";
export default function Navbar({ export default function Navbar({
userData, userData,
sandboxData, sandboxData,
shared, shared,
socket,
}: { }: {
userData: User userData: User;
sandboxData: Sandbox sandboxData: Sandbox;
shared: { id: string; name: string }[] shared: { id: string; name: string }[];
socket: Socket;
}) { }) {
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 [isRunning, setIsRunning] = useState(false);
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState("");
const [creatingTerminal, setCreatingTerminal] = useState(false);
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id;;
return ( return (
<> <>
@ -66,24 +72,19 @@ export default function Navbar({
<RunButtonModal <RunButtonModal
isRunning={isRunning} isRunning={isRunning}
setIsRunning={setIsRunning} 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 ? (
<> <Button variant="outline" onClick={() => setIsShareOpen(true)}>
<DeployButtonModal data={sandboxData} userData={userData} /> <Users className="w-4 h-4 mr-2" />
<Button variant="outline" onClick={() => setIsShareOpen(true)}> Share
<Users className="w-4 h-4 mr-2" /> </Button>
Share
</Button>
</>
) : null} ) : null}
<ThemeSwitcher />
<UserButton userData={userData} /> <UserButton userData={userData} />
</div> </div>
</div> </div>
</> </>
) );
} }

View File

@ -1,78 +1,60 @@
"use client" "use client";
import { Button } from "@/components/ui/button" import { Play, StopCircle } from "lucide-react";
import { usePreview } from "@/context/PreviewContext" import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext" import { useTerminal } from "@/context/TerminalContext";
import { Sandbox } from "@/lib/types" import { closeTerminal } from "@/lib/terminal";
import { Play, StopCircle } from "lucide-react"
import { useEffect, useRef } from "react"
import { toast } from "sonner"
export default function RunButtonModal({ export default function RunButtonModal({
isRunning, isRunning,
setIsRunning, setIsRunning,
sandboxData,
}: { }: {
isRunning: boolean isRunning: boolean;
setIsRunning: (running: boolean) => void setIsRunning: (running: boolean) => void;
sandboxData: Sandbox
}) { }) {
const { createNewTerminal, closeTerminal, terminals } = useTerminal() const { createNewTerminal, terminals, setTerminals, socket, setActiveTerminalId } = 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 const handleRun = () => {
useEffect(() => { if (isRunning) {
if (terminals.length > 0 && !isRunning) { console.log('Stopping sandbox...');
const latestTerminal = terminals[terminals.length - 1] console.log('Closing Terminal');
if ( console.log('Closing Preview Window');
latestTerminal &&
latestTerminal.id !== lastCreatedTerminalRef.current // Close all terminals if needed
) { terminals.forEach(term => {
lastCreatedTerminalRef.current = latestTerminal.id if (term.terminal) {
// Assuming you have a closeTerminal function similar to createTerminal
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal: () => { },
socket: socket!,
activeTerminalId: term.id,
});
}
});
} else {
console.log('Running sandbox...');
console.log('Opening Terminal');
console.log('Opening Preview Window');
if (terminals.length < 4) {
createNewTerminal();
} else {
console.error('Maximum number of terminals reached.');
} }
} }
}, [terminals, isRunning]) setIsRunning(!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 ( return (
<Button variant="outline" onClick={handleRun}> <>
{isRunning ? ( <Button variant="outline" onClick={handleRun}>
<StopCircle className="w-4 h-4 mr-2" /> {isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
) : ( {isRunning ? 'Stop' : 'Run'}
<Play className="w-4 h-4 mr-2" /> </Button>
)} </>
{isRunning ? "Stop" : "Run"} );
</Button>
)
} }

View File

@ -6,11 +6,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { import {
Form, Form,
FormControl, FormControl,
@ -19,13 +18,14 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { shareSandbox } from "@/lib/actions" import { Link, Loader2, UserPlus, X } from "lucide-react"
import { Sandbox } from "@/lib/types"
import { DialogDescription } from "@radix-ui/react-dialog"
import { Link, Loader2, UserPlus } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button"
import { shareSandbox } from "@/lib/actions"
import { toast } from "sonner" import { toast } from "sonner"
import SharedUser from "./sharedUser" import SharedUser from "./sharedUser"
import { DialogDescription } from "@radix-ui/react-dialog"
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(), email: z.string().email(),

View File

@ -1,72 +1,87 @@
"use client" "use client"
import { Link, RotateCw, UnfoldVertical } from "lucide-react"
import { import {
forwardRef, ChevronLeft,
useEffect, ChevronRight,
useImperativeHandle, Globe,
useRef, Link,
useState, RotateCw,
} from "react" TerminalSquare,
UnfoldVertical,
} from "lucide-react"
import { useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
export default forwardRef(function PreviewWindow( export default function PreviewWindow({
{ collapsed,
collapsed, open,
open, src
src, }: {
}: { collapsed: boolean
collapsed: boolean open: () => void
open: () => void src: string
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 className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between"> <div
<div className="text-xs">Preview</div> className={`${
<div className="flex space-x-1 translate-x-1"> collapsed ? "h-full" : "h-10"
{collapsed ? ( } select-none w-full flex gap-2`}
<PreviewButton onClick={open}> >
<UnfoldVertical className="w-4 h-4" /> <div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
</PreviewButton> <div className="text-xs">Preview</div>
) : ( <div className="flex space-x-1 translate-x-1">
<> {collapsed ? (
<PreviewButton onClick={open}> <PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" /> <UnfoldVertical className="w-4 h-4" />
</PreviewButton> </PreviewButton>
) : (
<>
{/* Todo, make this open inspector */}
{/* <PreviewButton disabled onClick={() => {}}>
<TerminalSquare className="w-4 h-4" />
</PreviewButton> */}
<PreviewButton <PreviewButton
onClick={() => { onClick={() => {
navigator.clipboard.writeText(src) 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 onClick={refreshIframe}> <PreviewButton
<RotateCw className="w-3 h-3" /> onClick={() => {
</PreviewButton> // if (ref.current) {
</> // ref.current.contentWindow?.location.reload();
)} // }
setIframeKey((prev) => prev + 1)
}}
>
<RotateCw className="w-3 h-3" />
</PreviewButton>
</>
)}
</div>
</div> </div>
</div> </div>
{collapsed ? null : (
<div className="w-full grow rounded-md overflow-hidden bg-foreground">
<iframe
key={iframeKey}
ref={ref}
width={"100%"}
height={"100%"}
src={src}
/>
</div>
)}
</> </>
) )
}) }
function PreviewButton({ function PreviewButton({
children, children,

View File

@ -1,18 +1,18 @@
"use client" "use client";
import Image from "next/image";
import { getIconForFile } from "vscode-icons-js";
import { TFile, TTab } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu";
import { TFile, TTab } from "@/lib/types" import { Loader2, Pencil, Trash2 } from "lucide-react";
import { Loader2, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFile } from "vscode-icons-js"
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter" import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
export default function SidebarFile({ export default function SidebarFile({
data, data,
@ -22,36 +22,36 @@ export default function SidebarFile({
movingId, movingId,
deletingFolderId, deletingFolderId,
}: { }: {
data: TFile data: TFile;
selectFile: (file: TTab) => void selectFile: (file: TTab) => void;
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean ) => boolean;
handleDeleteFile: (file: TFile) => void handleDeleteFile: (file: TFile) => void;
movingId: string movingId: string;
deletingFolderId: string deletingFolderId: string;
}) { }) {
const isMoving = movingId === data.id const isMoving = movingId === data.id;
const isDeleting = const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId) deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
const ref = useRef(null) // for draggable const ref = useRef(null); // for draggable
const [dragging, setDragging] = useState(false) const [dragging, setDragging] = useState(false);
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null);
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`) const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false);
const [pendingDelete, setPendingDelete] = useState(isDeleting) const [pendingDelete, setPendingDelete] = useState(isDeleting);
useEffect(() => { useEffect(() => {
setPendingDelete(isDeleting) setPendingDelete(isDeleting);
}, [isDeleting]) }, [isDeleting]);
useEffect(() => { useEffect(() => {
const el = ref.current const el = ref.current;
if (el) if (el)
return draggable({ return draggable({
@ -59,14 +59,14 @@ export default function SidebarFile({
onDragStart: () => setDragging(true), onDragStart: () => setDragging(true),
onDrop: () => setDragging(false), onDrop: () => setDragging(false),
getInitialData: () => ({ id: data.id }), getInitialData: () => ({ id: data.id }),
}) });
}, []) }, []);
useEffect(() => { useEffect(() => {
if (editing) { if (editing) {
setTimeout(() => inputRef.current?.focus(), 0) setTimeout(() => inputRef.current?.focus(), 0);
} }
}, [editing, inputRef.current]) }, [editing, inputRef.current]);
const renameFile = () => { const renameFile = () => {
const renamed = handleRename( const renamed = handleRename(
@ -74,12 +74,12 @@ export default function SidebarFile({
inputRef.current?.value ?? data.name, inputRef.current?.value ?? data.name,
data.name, data.name,
"file" "file"
) );
if (!renamed && inputRef.current) { if (!renamed && inputRef.current) {
inputRef.current.value = data.name inputRef.current.value = data.name;
} }
setEditing(false) setEditing(false);
} };
return ( return (
<ContextMenu> <ContextMenu>
@ -88,11 +88,11 @@ export default function SidebarFile({
disabled={pendingDelete || dragging || isMoving} disabled={pendingDelete || dragging || isMoving}
onClick={() => { onClick={() => {
if (!editing && !pendingDelete && !isMoving) if (!editing && !pendingDelete && !isMoving)
selectFile({ ...data, saved: true }) selectFile({ ...data, saved: true });
}}
onDoubleClick={() => {
setEditing(true)
}} }}
// onDoubleClick={() => {
// setEditing(true)
// }}
className={`${ className={`${
dragging ? "opacity-50 hover:!bg-background" : "" dragging ? "opacity-50 hover:!bg-background" : ""
} data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring`} } data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring`}
@ -119,8 +119,8 @@ export default function SidebarFile({
) : ( ) : (
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault();
renameFile() renameFile();
}} }}
> >
<input <input
@ -138,8 +138,8 @@ export default function SidebarFile({
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
console.log("rename") console.log("rename");
setEditing(true) setEditing(true);
}} }}
> >
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
@ -148,9 +148,9 @@ export default function SidebarFile({
<ContextMenuItem <ContextMenuItem
disabled={pendingDelete} disabled={pendingDelete}
onClick={() => { onClick={() => {
console.log("delete") console.log("delete");
setPendingDelete(true) setPendingDelete(true);
handleDeleteFile(data) handleDeleteFile(data);
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
@ -158,5 +158,5 @@ export default function SidebarFile({
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
) );
} }

View File

@ -1,20 +1,18 @@
"use client" "use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js";
import { TFile, TFolder, TTab } from "@/lib/types";
import SidebarFile from "./file";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu";
import { TFile, TFolder, TTab } from "@/lib/types" import { Loader2, Pencil, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils" import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { AnimatePresence, motion } from "framer-motion"
import { ChevronRight, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import SidebarFile from "./file"
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out // Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
@ -27,27 +25,27 @@ export default function SidebarFolder({
movingId, movingId,
deletingFolderId, deletingFolderId,
}: { }: {
data: TFolder data: TFolder;
selectFile: (file: TTab) => void selectFile: (file: TTab) => void;
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean ) => boolean;
handleDeleteFile: (file: TFile) => void handleDeleteFile: (file: TFile) => void;
handleDeleteFolder: (folder: TFolder) => void handleDeleteFolder: (folder: TFolder) => void;
movingId: string movingId: string;
deletingFolderId: string deletingFolderId: string;
}) { }) {
const ref = useRef(null) // drop target const ref = useRef(null); // drop target
const [isDraggedOver, setIsDraggedOver] = useState(false) const [isDraggedOver, setIsDraggedOver] = useState(false);
const isDeleting = const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId) deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
useEffect(() => { useEffect(() => {
const el = ref.current const el = ref.current;
if (el) if (el)
return dropTargetForElements({ return dropTargetForElements({
@ -69,17 +67,17 @@ export default function SidebarFolder({
// no dropping while awaiting move // no dropping while awaiting move
canDrop: () => { canDrop: () => {
return !movingId return !movingId;
}, },
}) });
}, []) }, []);
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false);
const folder = isOpen const folder = isOpen
? getIconForOpenFolder(data.name) ? getIconForOpenFolder(data.name)
: getIconForFolder(data.name) : getIconForFolder(data.name);
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null);
// const [editing, setEditing] = useState(false); // const [editing, setEditing] = useState(false);
// useEffect(() => { // useEffect(() => {
@ -98,12 +96,6 @@ export default function SidebarFolder({
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm"
} w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`} } w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`}
> >
<ChevronRight
className={cn(
"min-w-3 min-h-3 mr-1 ml-auto transition-all duration-300",
isOpen ? "transform rotate-90" : ""
)}
/>
<Image <Image
src={`/icons/${folder}`} src={`/icons/${folder}`}
alt="Folder icon" alt="Folder icon"
@ -157,65 +149,48 @@ export default function SidebarFolder({
<ContextMenuItem <ContextMenuItem
disabled={isDeleting} disabled={isDeleting}
onClick={() => { onClick={() => {
handleDeleteFolder(data) handleDeleteFolder(data);
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete Delete
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
<AnimatePresence> {isOpen ? (
{isOpen ? ( <div
<motion.div className={`flex w-full items-stretch ${
className="overflow-y-hidden" isDraggedOver ? "rounded-b-sm bg-secondary/50" : ""
initial={{ }`}
height: 0, >
opacity: 0, <div className="w-[1px] bg-border mx-2 h-full"></div>
}} <div className="flex flex-col grow">
animate={{ {data.children.map((child) =>
height: "auto", child.type === "file" ? (
opacity: 1, <SidebarFile
}} key={child.id}
exit={{ data={child}
height: 0, selectFile={selectFile}
opacity: 0, handleRename={handleRename}
}} handleDeleteFile={handleDeleteFile}
> movingId={movingId}
<div deletingFolderId={deletingFolderId}
className={cn( />
isDraggedOver ? "rounded-b-sm bg-secondary/50" : "" ) : (
)} <SidebarFolder
> key={child.id}
<div className="flex flex-col grow ml-2 pl-2 border-l border-border"> data={child}
{data.children.map((child) => selectFile={selectFile}
child.type === "file" ? ( handleRename={handleRename}
<SidebarFile handleDeleteFile={handleDeleteFile}
key={child.id} handleDeleteFolder={handleDeleteFolder}
data={child} movingId={movingId}
selectFile={selectFile} deletingFolderId={deletingFolderId}
handleRename={handleRename} />
handleDeleteFile={handleDeleteFile} )
movingId={movingId} )}
deletingFolderId={deletingFolderId} </div>
/> </div>
) : ( ) : null}
<SidebarFolder
key={child.id}
data={child}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
movingId={movingId}
deletingFolderId={deletingFolderId}
/>
)
)}
</div>
</div>
</motion.div>
) : null}
</AnimatePresence>
</ContextMenu> </ContextMenu>
) );
} }

View File

@ -1,20 +1,26 @@
"use client" "use client";
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types" import {
import { FilePlus, FolderPlus, MessageSquareMore, Sparkles } from "lucide-react" FilePlus,
import { useEffect, useMemo, useRef, useState } from "react" FolderPlus,
import { Socket } from "socket.io-client" Loader2,
import SidebarFile from "./file" MonitorPlay,
import SidebarFolder from "./folder" Search,
import New from "./new" Sparkles,
} from "lucide-react";
import SidebarFile from "./file";
import SidebarFolder from "./folder";
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import New from "./new";
import { Socket } from "socket.io-client";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { sortFileExplorer } from "@/lib/utils"
import { import {
dropTargetForElements, dropTargetForElements,
monitorForElements, monitorForElements,
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter" } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import Button from "@/components/ui/customButton";
export default function Sidebar({ export default function Sidebar({
sandboxData, sandboxData,
@ -26,84 +32,86 @@ export default function Sidebar({
socket, socket,
setFiles, setFiles,
addNew, addNew,
ai,
setAi,
deletingFolderId, deletingFolderId,
}: { }: {
sandboxData: Sandbox sandboxData: Sandbox;
files: (TFile | TFolder)[] files: (TFile | TFolder)[];
selectFile: (tab: TTab) => void selectFile: (tab: TTab) => void;
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean ) => boolean;
handleDeleteFile: (file: TFile) => void handleDeleteFile: (file: TFile) => void;
handleDeleteFolder: (folder: TFolder) => void handleDeleteFolder: (folder: TFolder) => void;
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;
deletingFolderId: string ai: boolean;
setAi: React.Dispatch<React.SetStateAction<boolean>>;
deletingFolderId: string;
}) { }) {
const ref = useRef(null) // drop target const ref = useRef(null); // drop target
const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(
null
);
const [movingId, setMovingId] = useState("");
const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null)
const [movingId, setMovingId] = useState("")
const sortedFiles = useMemo(() => {
return sortFileExplorer(files)
}, [files])
useEffect(() => { useEffect(() => {
const el = ref.current const el = ref.current;
if (el) { if (el) {
return dropTargetForElements({ return dropTargetForElements({
element: el, element: el,
getData: () => ({ id: `projects/${sandboxData.id}` }), getData: () => ({ id: `projects/${sandboxData.id}` }),
canDrop: ({ source }) => { canDrop: ({ source }) => {
const file = files.find((child) => child.id === source.data.id) const file = files.find((child) => child.id === source.data.id);
return !file return !file;
}, },
}) });
} }
}, [files]) }, [files]);
useEffect(() => { useEffect(() => {
return monitorForElements({ return monitorForElements({
onDrop({ source, location }) { onDrop({ source, location }) {
const destination = location.current.dropTargets[0] const destination = location.current.dropTargets[0];
if (!destination) { if (!destination) {
return return;
} }
const fileId = source.data.id as string const fileId = source.data.id as string;
const folderId = destination.data.id as string const folderId = destination.data.id as string;
const fileFolder = fileId.split("/").slice(0, -1).join("/") const fileFolder = fileId.split("/").slice(0, -1).join("/");
if (fileFolder === folderId) { if (fileFolder === folderId) {
return return;
} }
console.log("move file", fileId, "to folder", folderId) console.log("move file", fileId, "to folder", folderId);
setMovingId(fileId) setMovingId(fileId);
socket.emit( socket.emit(
"moveFile", "moveFile",
{ fileId,
fileId, folderId,
folderId
},
(response: (TFolder | TFile)[]) => { (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response);
setMovingId("") setMovingId("");
} }
) );
}, },
}) });
}, []) }, []);
return ( return (
<div className="h-full w-56 select-none flex flex-col text-sm"> <div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
<div className="flex-grow overflow-auto p-2 pb-[84px]"> <div className="w-full flex flex-col items-start">
<div className="flex w-full items-center justify-between h-8 mb-1"> <div className="flex w-full items-center justify-between h-8 mb-1 ">
<div className="text-muted-foreground">Explorer</div> <div className="text-muted-foreground">Explorer</div>
<div className="flex space-x-1"> <div className="flex space-x-1">
<button <button
@ -133,15 +141,13 @@ export default function Sidebar({
isDraggedOver ? "bg-secondary/50" : "" isDraggedOver ? "bg-secondary/50" : ""
} rounded-sm w-full mt-1 flex flex-col`} } rounded-sm w-full mt-1 flex flex-col`}
> */} > */}
{sortedFiles.length === 0 ? ( {files.length === 0 ? (
<div className="w-full flex flex-col justify-center"> <div className="w-full flex justify-center">
{new Array(6).fill(0).map((_, i) => ( <Loader2 className="w-4 h-4 animate-spin" />
<Skeleton key={i} className="h-[1.625rem] mb-0.5 rounded-sm" />
))}
</div> </div>
) : ( ) : (
<> <>
{sortedFiles.map((child) => {files.map((child) =>
child.type === "file" ? ( child.type === "file" ? (
<SidebarFile <SidebarFile
key={child.id} key={child.id}
@ -170,7 +176,7 @@ export default function Sidebar({
socket={socket} socket={socket}
type={creatingNew} type={creatingNew}
stopEditing={() => { stopEditing={() => {
setCreatingNew(null) setCreatingNew(null);
}} }}
addNew={addNew} addNew={addNew}
/> />
@ -179,38 +185,25 @@ export default function Sidebar({
)} )}
</div> </div>
</div> </div>
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background"> <div className="w-full space-y-4">
<Button <div className="flex items-center justify-between w-full">
variant="ghost" <div className="flex items-center">
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" <Sparkles
disabled className={`h-4 w-4 mr-2 ${
aria-disabled="true" ai ? "text-indigo-500" : "text-muted-foreground"
style={{ opacity: 1 }} }`}
> />
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" /> Copilot{" "}
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">
<div className="ml-auto"> G
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground"> </span>
<span className="text-xs"></span>G
</kbd>
</div> </div>
</Button> <Switch checked={ai} onCheckedChange={setAi} />
<Button </div>
variant="ghost" {/* <Button className="w-full">
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" <MonitorPlay className="w-4 h-4 mr-2" /> Run
disabled </Button> */}
aria-disabled="true"
style={{ opacity: 1 }}
>
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Chat
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>L
</kbd>
</div>
</Button>
</div> </div>
</div> </div>
) );
} }

View File

@ -1,9 +1,9 @@
"use client" "use client";
import { validateName } from "@/lib/utils" import { validateName } from "@/lib/utils";
import Image from "next/image" import Image from "next/image";
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react";
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client";
export default function New({ export default function New({
socket, socket,
@ -11,42 +11,42 @@ export default function New({
stopEditing, stopEditing,
addNew, addNew,
}: { }: {
socket: Socket socket: Socket;
type: "file" | "folder" type: "file" | "folder";
stopEditing: () => void stopEditing: () => void;
addNew: (name: string, type: "file" | "folder") => void addNew: (name: string, type: "file" | "folder") => void;
}) { }) {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null);
function createNew() { function createNew() {
const name = inputRef.current?.value const name = inputRef.current?.value;
if (name) { if (name) {
const valid = validateName(name, "", type) const valid = validateName(name, "", type);
if (valid.status) { if (valid.status) {
if (type === "file") { if (type === "file") {
socket.emit( socket.emit(
"createFile", "createFile",
{ name }, name,
({ success }: { success: boolean }) => { ({ success }: { success: boolean }) => {
if (success) { if (success) {
addNew(name, type) addNew(name, type);
} }
} }
) );
} else { } else {
socket.emit("createFolder", { name }, () => { socket.emit("createFolder", name, () => {
addNew(name, type) addNew(name, type);
}) });
} }
} }
} }
stopEditing() stopEditing();
} }
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus();
}, []) }, []);
return ( return (
<div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"> <div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
@ -63,8 +63,8 @@ export default function New({
/> />
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault();
createNew() createNew();
}} }}
> >
<input <input
@ -74,5 +74,5 @@ export default function New({
/> />
</form> </form>
</div> </div>
) );
} }

View File

@ -1,44 +1,21 @@
"use client" "use client";
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 { useSocket } from "@/context/SocketContext" import { closeTerminal } from "@/lib/terminal";
import { useTerminal } from "@/context/TerminalContext" 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 { toast } from "sonner";
import { useEffect } from "react" import EditorTerminal from "./terminal";
import { toast } from "sonner" import { useState } from "react";
import EditorTerminal from "./terminal" import { useTerminal } from "@/context/TerminalContext";
export default function Terminals() { export default function Terminals() {
const { socket } = useSocket() const { terminals, setTerminals, socket, createNewTerminal } = useTerminal();
const [activeTerminalId, setActiveTerminalId] = useState("");
const { const [creatingTerminal, setCreatingTerminal] = useState(false);
terminals, const [closingTerminal, setClosingTerminal] = useState("");
setTerminals, const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
createNewTerminal,
closeTerminal,
activeTerminalId,
setActiveTerminalId,
creatingTerminal,
} = useTerminal()
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 (
<> <>
@ -48,7 +25,18 @@ export default function Terminals() {
key={term.id} key={term.id}
creating={creatingTerminal} creating={creatingTerminal}
onClick={() => setActiveTerminalId(term.id)} onClick={() => setActiveTerminalId(term.id)}
onClose={() => closeTerminal(term.id)} onClose={() =>
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket: 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" />
@ -57,7 +45,13 @@ export default function Terminals() {
))} ))}
<Button <Button
disabled={creatingTerminal} disabled={creatingTerminal}
onClick={handleCreateTerminal} onClick={() => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createNewTerminal();
}}
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`}
@ -84,7 +78,7 @@ export default function Terminals() {
? { ...term, terminal: t } ? { ...term, terminal: t }
: term : term
) )
) );
}} }}
visible={activeTerminalId === term.id} visible={activeTerminalId === term.id}
/> />
@ -97,5 +91,5 @@ export default function Terminals() {
</div> </div>
)} )}
</> </>
) );
} }

View File

@ -1,14 +1,13 @@
"use client" "use client";
import { FitAddon } from "@xterm/addon-fit" import { Terminal } from "@xterm/xterm";
import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit";
import "./xterm.css" import "./xterm.css";
import { useEffect, useRef, useState } from "react";
import { Socket } from "socket.io-client";
import { Loader2 } from "lucide-react";
import { debounce } from "@/lib/utils"
import { Loader2 } from "lucide-react"
import { useTheme } from "next-themes"
import { ElementRef, useEffect, useRef } from "react"
import { Socket } from "socket.io-client"
export default function EditorTerminal({ export default function EditorTerminal({
socket, socket,
id, id,
@ -16,112 +15,69 @@ export default function EditorTerminal({
setTerm, setTerm,
visible, visible,
}: { }: {
socket: Socket socket: Socket;
id: string id: string;
term: Terminal | null term: Terminal | null;
setTerm: (term: Terminal) => void setTerm: (term: Terminal) => void;
visible: boolean visible: boolean;
}) { }) {
const { theme } = useTheme() const terminalRef = useRef(null);
const terminalContainerRef = useRef<ElementRef<"div">>(null)
const fitAddonRef = useRef<FitAddon | null>(null)
useEffect(() => { useEffect(() => {
if (!terminalContainerRef.current) return if (!terminalRef.current) return;
// console.log("new terminal", id, term ? "reusing" : "creating"); // console.log("new terminal", id, term ? "reusing" : "creating");
const terminal = new Terminal({ const terminal = new Terminal({
cursorBlink: true, cursorBlink: true,
theme: theme === "light" ? lightTheme : darkTheme, theme: {
background: "#262626",
},
fontFamily: "var(--font-geist-mono)", fontFamily: "var(--font-geist-mono)",
fontSize: 14, fontSize: 14,
lineHeight: 1.5, lineHeight: 1.5,
letterSpacing: 0, letterSpacing: 0,
}) });
setTerm(terminal) setTerm(terminal);
const dispose = () => {
terminal.dispose() return () => {
} if (terminal) terminal.dispose();
return dispose };
}, []) }, []);
useEffect(() => { useEffect(() => {
if (term) { if (!term) return;
term.options.theme = theme === "light" ? lightTheme : darkTheme
}
}, [theme])
useEffect(() => { if (!terminalRef.current) return;
if (!term) return const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
if (!terminalContainerRef.current) return term.open(terminalRef.current);
if (!fitAddonRef.current) { fitAddon.fit();
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
term.open(terminalContainerRef.current)
fitAddon.fit()
fitAddonRef.current = fitAddon
}
const disposableOnData = term.onData((data) => { const disposableOnData = term.onData((data) => {
socket.emit("terminalData", { id, data }) console.log("terminalData", id, data);
}) socket.emit("terminalData", id, data);
});
const disposableOnResize = term.onResize((dimensions) => { const disposableOnResize = term.onResize((dimensions) => {
fitAddonRef.current?.fit() // const terminal_size = {
socket.emit("terminalResize", { dimensions }) // width: dimensions.cols,
}) // height: dimensions.rows,
const resizeObserver = new ResizeObserver( // };
debounce((entries) => { fitAddon.fit();
if (!fitAddonRef.current || !terminalContainerRef.current) return socket.emit("terminalResize", dimensions);
});
const entry = entries[0]
if (!entry) return
const { width, height } = entry.contentRect
// Only call fit if the size has actually changed
if (
width !== terminalContainerRef.current.offsetWidth ||
height !== terminalContainerRef.current.offsetHeight
) {
try {
fitAddonRef.current.fit()
} catch (err) {
console.error("Error during fit:", err)
}
}
}, 50) // Debounce for 50ms
)
// start observing for resize
resizeObserver.observe(terminalContainerRef.current)
return () => {
disposableOnData.dispose()
disposableOnResize.dispose()
resizeObserver.disconnect()
}
}, [term, terminalContainerRef.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 () => { return () => {
socket.off("terminalResponse", handleTerminalResponse) disposableOnData.dispose();
} disposableOnResize.dispose();
}, [term, id, socket]) };
}, [term, terminalRef.current]);
return ( return (
<> <>
<div <div
ref={terminalContainerRef} ref={terminalRef}
style={{ display: visible ? "block" : "none" }} style={{ display: visible ? "block" : "none" }}
className="w-full h-full text-left" className="w-full h-full text-left"
> >
@ -133,58 +89,5 @@ export default function EditorTerminal({
) : null} ) : null}
</div> </div>
</> </>
) );
}
const lightTheme = {
foreground: "#2e3436",
background: "#ffffff",
black: "#2e3436",
brightBlack: "#555753",
red: "#cc0000",
brightRed: "#ef2929",
green: "#4e9a06",
brightGreen: "#8ae234",
yellow: "#c4a000",
brightYellow: "#fce94f",
blue: "#3465a4",
brightBlue: "#729fcf",
magenta: "#75507b",
brightMagenta: "#ad7fa8",
cyan: "#06989a",
brightCyan: "#34e2e2",
white: "#d3d7cf",
brightWhite: "#eeeeec",
cursor: "#2e3436",
cursorAccent: "#ffffff",
selectionBackground: "#3465a4",
selectionForeground: "#ffffff",
selectionInactiveBackground: "#264973",
}
// Dark Theme
const darkTheme = {
foreground: "#f8f8f2",
background: "#0a0a0a",
black: "#21222c",
brightBlack: "#6272a4",
red: "#ff5555",
brightRed: "#ff6e6e",
green: "#50fa7b",
brightGreen: "#69ff94",
yellow: "#f1fa8c",
brightYellow: "#ffffa5",
blue: "#bd93f9",
brightBlue: "#d6acff",
magenta: "#ff79c6",
brightMagenta: "#ff92df",
cyan: "#8be9fd",
brightCyan: "#a4ffff",
white: "#f8f8f2",
brightWhite: "#ffffff",
cursor: "#f8f8f2",
cursorAccent: "#0a0a0a",
selectionBackground: "#264973",
selectionForeground: "#ffffff",
selectionInactiveBackground: "#1a3151",
} }

View File

@ -35,7 +35,7 @@
* Default styles for xterm.js * Default styles for xterm.js
*/ */
.xterm { .xterm {
cursor: text; cursor: text;
position: relative; position: relative;
user-select: none; user-select: none;
@ -80,7 +80,7 @@
.xterm .composition-view { .xterm .composition-view {
/* TODO: Composition position got messed up somewhere */ /* TODO: Composition position got messed up somewhere */
background: transparent; background: transparent;
color: #fff; color: #FFF;
display: none; display: none;
position: absolute; position: absolute;
white-space: nowrap; white-space: nowrap;
@ -154,12 +154,12 @@
} }
.xterm .xterm-accessibility-tree:not(.debug) *::selection { .xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent; color: transparent;
} }
.xterm .xterm-accessibility-tree { .xterm .xterm-accessibility-tree {
user-select: text; user-select: text;
white-space: pre; white-space: pre;
} }
.xterm .live-region { .xterm .live-region {
@ -176,55 +176,33 @@
opacity: 1 !important; opacity: 1 !important;
} }
.xterm-underline-1 { .xterm-underline-1 { text-decoration: underline; }
text-decoration: underline; .xterm-underline-2 { text-decoration: double underline; }
} .xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-2 { .xterm-underline-4 { text-decoration: dotted underline; }
text-decoration: double underline; .xterm-underline-5 { text-decoration: dashed underline; }
}
.xterm-underline-3 {
text-decoration: wavy underline;
}
.xterm-underline-4 {
text-decoration: dotted underline;
}
.xterm-underline-5 {
text-decoration: dashed underline;
}
.xterm-overline { .xterm-overline {
text-decoration: overline; text-decoration: overline;
} }
.xterm-overline.xterm-underline-1 { .xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
text-decoration: overline underline; .xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
} .xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-2 { .xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
text-decoration: overline double underline; .xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
}
.xterm-overline.xterm-underline-3 {
text-decoration: overline wavy underline;
}
.xterm-overline.xterm-underline-4 {
text-decoration: overline dotted underline;
}
.xterm-overline.xterm-underline-5 {
text-decoration: overline dashed underline;
}
.xterm-strikethrough { .xterm-strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
.xterm-screen .xterm-decoration-container .xterm-decoration { .xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6; z-index: 6;
position: absolute; position: absolute;
} }
.xterm-screen .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
.xterm-decoration-container z-index: 7;
.xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
} }
.xterm-decoration-overview-ruler { .xterm-decoration-overview-ruler {
@ -238,4 +216,4 @@
.xterm-decoration-top { .xterm-decoration-top {
z-index: 2; z-index: 2;
position: relative; position: relative;
} }

View File

@ -1,10 +1,9 @@
import Logo from "@/assets/logo.svg"
import CustomButton from "@/components/ui/customButton"
import { ChevronRight } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Logo from "@/assets/logo.svg"
import XLogo from "@/assets/x.svg"
import Button from "@/components/ui/customButton"
import { ChevronRight } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { Button } from "../ui/button"
import { ThemeSwitcher } from "../ui/theme-switcher"
export default function Landing() { export default function Landing() {
return ( return (
@ -21,37 +20,21 @@ export default function Landing() {
/> />
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Button variant="outline" size="icon" asChild> <a href="https://www.x.com/ishaandey_" target="_blank">
<a href="https://www.x.com/ishaandey_" target="_blank"> <Image src={XLogo} alt="X Logo" width={18} height={18} />
<svg </a>
width="1200"
height="1227"
viewBox="0 0 1200 1227"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-[1.125rem] text-muted-foreground"
>
<path
d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"
fill="currentColor"
/>
</svg>
</a>
</Button>
<ThemeSwitcher />
</div> </div>
</div> </div>
<h1 className="text-2xl font-medium text-center mt-16"> <h1 className="text-2xl font-medium text-center mt-16">
A Collaborative + AI-Powered Code Environment A Collaborative + AI-Powered Code Environment
</h1> </h1>
<p className="text-muted-foreground mt-4 text-center "> <div className="text-muted-foreground mt-4 text-center ">
Sandbox is an open-source cloud-based code editing environment with Sandbox is an open-source cloud-based code editing environment with
custom AI code autocompletion and real-time collaboration. custom AI code autocompletion and real-time collaboration.
</p> </div>
<div className="mt-8 flex space-x-4"> <div className="mt-8 flex space-x-4">
<Link href="/sign-up"> <Link href="/sign-up">
<CustomButton>Go To App</CustomButton> <Button>Go To App</Button>
</Link> </Link>
<a <a
href="https://github.com/ishaan1013/sandbox" href="https://github.com/ishaan1013/sandbox"

View File

@ -1,9 +1,9 @@
"use client" "use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types" import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider> return <NextThemesProvider {...props}>{children}</NextThemesProvider>
} }

View File

@ -1,43 +0,0 @@
import React from "react"
const LoadingDots: React.FC = () => {
return (
<span className="loading-dots">
<span className="dot">.</span>
<span className="dot">.</span>
<span className="dot">.</span>
<style jsx>{`
.loading-dots {
display: inline-block;
font-size: 24px;
}
.dot {
opacity: 0;
animation: showHideDot 1.5s ease-in-out infinite;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.5s;
}
.dot:nth-child(3) {
animation-delay: 1s;
}
@keyframes showHideDot {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`}</style>
</span>
)
}
export default LoadingDots

Some files were not shown because too many files have changed in this diff Show More