2024-04-29 00:50:25 -04:00
import fs from "fs"
2024-04-29 02:19:27 -04:00
import os from "os"
2024-04-29 00:50:25 -04:00
import path from "path"
2024-04-30 00:24:32 -04:00
import express , { Express } from "express"
2024-04-18 16:40:08 -04:00
import dotenv from "dotenv"
import { createServer } from "http"
import { Server } from "socket.io"
import { z } from "zod"
2024-04-21 22:55:49 -04:00
import { User } from "./types"
2024-04-30 01:56:43 -04:00
import {
createFile ,
deleteFile ,
getSandboxFiles ,
renameFile ,
saveFile ,
} from "./utils"
2024-04-29 21:36:33 -04:00
import { IDisposable , IPty , spawn } from "node-pty"
2024-05-05 12:55:34 -07:00
import {
2024-05-05 12:58:45 -07:00
MAX_BODY_SIZE ,
2024-05-05 12:55:34 -07:00
createFileRL ,
deleteFileRL ,
renameFileRL ,
saveFileRL ,
} from "./ratelimit"
2024-04-18 16:40:08 -04:00
dotenv . config ( )
const app : Express = express ( )
const port = process . env . PORT || 4000
// app.use(cors())
const httpServer = createServer ( app )
const io = new Server ( httpServer , {
cors : {
origin : "*" ,
} ,
} )
2024-04-29 21:36:33 -04:00
const terminals : {
[ id : string ] : { terminal : IPty ; onData : IDisposable ; onExit : IDisposable }
} = { }
2024-04-28 20:06:47 -04:00
2024-04-29 00:50:25 -04:00
const dirName = path . join ( __dirname , ".." )
2024-04-21 22:55:49 -04:00
const handshakeSchema = z . object ( {
userId : z.string ( ) ,
sandboxId : z.string ( ) ,
EIO : z.string ( ) ,
transport : z.string ( ) ,
} )
2024-04-18 16:40:08 -04:00
io . use ( async ( socket , next ) = > {
const q = socket . handshake . query
2024-04-21 22:55:49 -04:00
const parseQuery = handshakeSchema . safeParse ( q )
if ( ! parseQuery . success ) {
console . log ( "Invalid request." )
2024-04-18 16:40:08 -04:00
next ( new Error ( "Invalid request." ) )
2024-04-21 22:55:49 -04:00
return
}
2024-04-26 21:57:30 -04:00
const { sandboxId , userId } = parseQuery . data
2024-04-26 02:10:37 -04:00
const dbUser = await fetch ( ` http://localhost:8787/api/user?id= ${ userId } ` )
2024-04-21 22:55:49 -04:00
const dbUserJSON = ( await dbUser . json ( ) ) as User
if ( ! dbUserJSON ) {
console . log ( "DB error." )
next ( new Error ( "DB error." ) )
return
2024-04-18 16:40:08 -04:00
}
2024-04-26 02:10:37 -04:00
const sandbox = dbUserJSON . sandbox . find ( ( s ) = > s . id === sandboxId )
2024-05-03 14:58:56 -07:00
const sharedSandboxes = dbUserJSON . usersToSandboxes . find (
( uts ) = > uts . sandboxId === sandboxId
)
2024-04-18 16:40:08 -04:00
2024-05-03 14:58:56 -07:00
if ( ! sandbox && ! sharedSandboxes ) {
2024-04-21 22:55:49 -04:00
console . log ( "Invalid credentials." )
2024-04-18 16:40:08 -04:00
next ( new Error ( "Invalid credentials." ) )
2024-04-21 22:55:49 -04:00
return
}
2024-04-26 02:10:37 -04:00
socket . data = {
id : sandboxId ,
userId ,
2024-04-18 16:40:08 -04:00
}
next ( )
} )
io . on ( "connection" , async ( socket ) = > {
2024-04-21 22:55:49 -04:00
const data = socket . data as {
userId : string
2024-04-26 02:10:37 -04:00
id : string
2024-04-21 22:55:49 -04:00
}
2024-04-26 02:10:37 -04:00
const sandboxFiles = await getSandboxFiles ( data . id )
2024-04-29 00:50:25 -04:00
sandboxFiles . fileData . forEach ( ( file ) = > {
const filePath = path . join ( dirName , file . id )
fs . mkdirSync ( path . dirname ( filePath ) , { recursive : true } )
fs . writeFile ( filePath , file . data , function ( err ) {
if ( err ) throw err
} )
} )
2024-04-18 16:40:08 -04:00
2024-04-26 21:57:30 -04:00
socket . emit ( "loaded" , sandboxFiles . files )
2024-04-27 19:12:25 -04:00
2024-04-27 00:20:17 -04:00
socket . on ( "getFile" , ( fileId : string , callback ) = > {
const file = sandboxFiles . fileData . find ( ( f ) = > f . id === fileId )
if ( ! file ) return
callback ( file . data )
} )
2024-04-27 19:12:25 -04:00
// todo: send diffs + debounce for efficiency
socket . on ( "saveFile" , async ( fileId : string , body : string ) = > {
2024-05-05 12:55:34 -07:00
try {
await saveFileRL . consume ( data . userId , 1 )
2024-04-27 19:12:25 -04:00
2024-05-05 12:58:45 -07:00
if ( Buffer . byteLength ( body , "utf-8" ) > MAX_BODY_SIZE ) {
socket . emit (
"rateLimit" ,
"Rate limited: file size too large. Please reduce the file size."
)
return
}
2024-05-05 12:55:34 -07:00
const file = sandboxFiles . fileData . find ( ( f ) = > f . id === fileId )
if ( ! file ) return
file . data = body
fs . writeFile ( path . join ( dirName , file . id ) , body , function ( err ) {
if ( err ) throw err
} )
await saveFile ( fileId , body )
} catch ( e ) {
socket . emit ( "rateLimit" , "Rate limited: file saving. Please slow down." )
}
2024-04-27 16:41:25 -04:00
} )
2024-04-27 19:12:25 -04:00
2024-04-29 00:50:25 -04:00
socket . on ( "createFile" , async ( name : string ) = > {
2024-05-05 12:55:34 -07:00
try {
await createFileRL . consume ( data . userId , 1 )
2024-04-29 00:50:25 -04:00
2024-05-05 12:55:34 -07:00
const id = ` projects/ ${ data . id } / ${ name } `
2024-04-29 00:50:25 -04:00
2024-05-05 12:55:34 -07:00
fs . writeFile ( path . join ( dirName , id ) , "" , function ( err ) {
if ( err ) throw err
} )
2024-04-29 00:50:25 -04:00
2024-05-05 12:55:34 -07:00
sandboxFiles . files . push ( {
id ,
name ,
type : "file" ,
} )
sandboxFiles . fileData . push ( {
id ,
data : "" ,
} )
2024-04-29 00:50:25 -04:00
2024-05-05 12:55:34 -07:00
await createFile ( id )
} catch ( e ) {
socket . emit ( "rateLimit" , "Rate limited: file creation. Please slow down." )
}
2024-04-29 00:50:25 -04:00
} )
2024-04-27 14:23:09 -04:00
socket . on ( "renameFile" , async ( fileId : string , newName : string ) = > {
2024-05-05 12:55:34 -07:00
try {
await renameFileRL . consume ( data . userId , 1 )
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
fs . rename (
path . join ( dirName , fileId ) ,
path . join ( dirName , newFileId ) ,
function ( err ) {
if ( err ) throw err
}
)
await renameFile ( fileId , newFileId , file . data )
} catch ( e ) {
socket . emit ( "rateLimit" , "Rate limited: file renaming. Please slow down." )
return
}
2024-04-27 14:23:09 -04:00
} )
2024-04-28 20:06:47 -04:00
2024-04-30 01:56:43 -04:00
socket . on ( "deleteFile" , async ( fileId : string , callback ) = > {
2024-05-05 12:55:34 -07:00
try {
await deleteFileRL . consume ( data . userId , 1 )
const file = sandboxFiles . fileData . find ( ( f ) = > f . id === fileId )
if ( ! file ) return
2024-04-30 01:56:43 -04:00
2024-05-05 12:55:34 -07:00
fs . unlink ( path . join ( dirName , fileId ) , function ( err ) {
if ( err ) throw err
} )
sandboxFiles . fileData = sandboxFiles . fileData . filter (
( f ) = > f . id !== fileId
)
2024-04-30 01:56:43 -04:00
2024-05-05 12:55:34 -07:00
await deleteFile ( fileId )
2024-04-30 01:56:43 -04:00
2024-05-05 12:55:34 -07:00
const newFiles = await getSandboxFiles ( data . id )
callback ( newFiles . files )
} catch ( e ) {
socket . emit ( "rateLimit" , "Rate limited: file deletion. Please slow down." )
}
2024-04-30 01:56:43 -04:00
} )
2024-04-28 20:06:47 -04:00
socket . on ( "createTerminal" , ( { id } : { id : string } ) = > {
2024-05-05 12:55:34 -07:00
if ( terminals [ id ] ) {
console . log ( "Terminal already exists." )
return
}
if ( Object . keys ( terminals ) . length >= 4 ) {
console . log ( "Too many terminals." )
return
}
2024-04-29 02:19:27 -04:00
const pty = spawn ( os . platform ( ) === "win32" ? "cmd.exe" : "bash" , [ ] , {
name : "xterm" ,
cols : 100 ,
cwd : path.join ( dirName , "projects" , data . id ) ,
} )
2024-04-29 21:36:33 -04:00
const onData = pty . onData ( ( data ) = > {
2024-04-29 02:19:27 -04:00
socket . emit ( "terminalResponse" , {
// data: Buffer.from(data, "utf-8").toString("base64"),
data ,
} )
} )
2024-04-29 21:36:33 -04:00
const onExit = pty . onExit ( ( code ) = > console . log ( "exit :(" , code ) )
2024-04-29 02:19:27 -04:00
2024-04-30 22:48:36 -04:00
pty . write ( "clear\r" )
2024-04-29 21:36:33 -04:00
terminals [ id ] = {
terminal : pty ,
onData ,
onExit ,
}
2024-04-28 20:06:47 -04:00
} )
2024-04-29 02:19:27 -04:00
socket . on ( "terminalData" , ( id : string , data : string ) = > {
2024-04-28 20:06:47 -04:00
if ( ! terminals [ id ] ) {
console . log ( "terminals" , terminals )
return
}
2024-04-29 02:19:27 -04:00
try {
2024-04-29 21:36:33 -04:00
terminals [ id ] . terminal . write ( data )
2024-04-29 02:19:27 -04:00
} catch ( e ) {
console . log ( "Error writing to terminal" , e )
}
2024-04-28 20:06:47 -04:00
} )
2024-05-03 00:52:01 -07:00
socket . on (
"generateCode" ,
async (
fileName : string ,
code : string ,
line : number ,
instructions : string ,
callback
) = > {
console . log ( "Generating code..." )
const res = await fetch (
` https://api.cloudflare.com/client/v4/accounts/ ${ process . env . CF_USER_ID } /ai/run/@cf/meta/llama-3-8b-instruct ` ,
{
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ process . env . CF_API_TOKEN } ` ,
} ,
body : JSON.stringify ( {
messages : [
{
role : "system" ,
content :
2024-05-03 12:56:44 -07:00
"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 should also generate code that is clear and easy to read. When you generate code, you should only return the code, and nothing else. You should not include backticks in the code you generate." ,
2024-05-03 00:52:01 -07:00
} ,
{
role : "user" ,
2024-05-03 12:56:44 -07:00
content : ` The file is called ${ fileName } . ` ,
} ,
{
role : "user" ,
content : ` Here are my instructions on what to generate: ${ instructions } . ` ,
} ,
{
role : "user" ,
content : ` Suggest me code to insert at line ${ line } in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
$ { code } ` ,
2024-05-03 00:52:01 -07:00
} ,
] ,
} ) ,
}
)
const json = await res . json ( )
callback ( json )
}
)
2024-04-29 21:36:33 -04:00
socket . on ( "disconnect" , ( ) = > {
Object . entries ( terminals ) . forEach ( ( t ) = > {
const { terminal , onData , onExit } = t [ 1 ]
if ( os . platform ( ) !== "win32" ) terminal . kill ( )
onData . dispose ( )
onExit . dispose ( )
delete terminals [ t [ 0 ] ]
} )
} )
2024-04-18 16:40:08 -04:00
} )
httpServer . listen ( port , ( ) = > {
console . log ( ` Server running on port ${ port } ` )
} )