formatting

This commit is contained in:
Ishaan Dey 2024-05-13 22:04:00 -07:00
parent fb1b95a157
commit 5b72f84951
4 changed files with 237 additions and 231 deletions

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"build": "npm i typescript && npx tsc", "build": "npm i && npx tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"dev": "nodemon src/index.ts" "dev": "nodemon src/index.ts"
}, },
@ -16,10 +16,10 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"yaml": "^2.4.2", "yaml": "^2.4.2",
"zod": "^3.23.6", "zod": "^3.23.6"
"typescript": "^5.4.5"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.4.5",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^20.12.8", "@types/node": "^20.12.8",

View File

@ -1,159 +1,148 @@
import express, { Express, Request, Response } from "express" import express, { Express, Request, Response } from "express";
import dotenv from "dotenv" import dotenv from "dotenv";
import fs from "fs" import fs from "fs";
import yaml from "yaml" import yaml from "yaml";
import path from "path" import path from "path";
import cors from "cors" import cors from "cors";
import { import {
KubeConfig, KubeConfig,
AppsV1Api, AppsV1Api,
CoreV1Api, CoreV1Api,
NetworkingV1Api, NetworkingV1Api,
} from "@kubernetes/client-node" } from "@kubernetes/client-node";
import { z } from "zod" import { z } from "zod";
const app = express()
const port = process.env.PORT || 4001
app.use(express.json())
dotenv.config()
const app = express();
const port = process.env.PORT || 4001;
app.use(express.json());
dotenv.config();
// const corsOptions = { // const corsOptions = {
// origin: ['http://localhost:3000', 'https://s.ishaand.com', 'http://localhost:4000', /\.ws\.ishaand\.com$/], // origin: ['http://localhost:3000', 'https://s.ishaand.com', 'http://localhost:4000', /\.ws\.ishaand\.com$/],
// } // }
// app.use(cors(corsOptions)) // app.use(cors(corsOptions))
app.use(cors()) app.use(cors());
const kubeconfig = new KubeConfig() const kubeconfig = new KubeConfig();
if (process.env.NODE_ENV !== "deployment") { kubeconfig.loadFromOptions({
kubeconfig.loadFromOptions({ clusters: [
clusters: [ {
{ name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
name: 'gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster', server: process.env.GKE_CLUSTER_SERVER!,
server: process.env.GKE_CLUSTER_SERVER!, caData: process.env.GKE_CLUSTER_CA_DATA,
caData: process.env.GKE_CLUSTER_CA_DATA, },
} ],
], users: [
users: [ {
{ name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
name: 'gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster', exec: {
exec: { apiVersion: "client.authentication.k8s.io/v1beta1",
apiVersion: 'client.authentication.k8s.io/v1beta1', command: "gke-gcloud-auth-plugin",
command: 'gke-gcloud-auth-plugin', installHint:
installHint: 'Install gke-gcloud-auth-plugin for use with kubectl by following https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin', "Install gke-gcloud-auth-plugin for use with kubectl by following https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin",
interactiveMode: 'IfAvailable', interactiveMode: "IfAvailable",
provideClusterInfo: true provideClusterInfo: true,
} },
} },
], ],
contexts: [ contexts: [
{ {
name: 'gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster', name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
cluster: 'gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster', cluster: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
user: 'gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster' user: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
} },
], ],
currentContext: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster" currentContext: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
}); });
}
kubeconfig.loadFromDefault()
const appsV1Api = kubeconfig.makeApiClient(AppsV1Api) const appsV1Api = kubeconfig.makeApiClient(AppsV1Api);
const coreV1Api = kubeconfig.makeApiClient(CoreV1Api) const coreV1Api = kubeconfig.makeApiClient(CoreV1Api);
const networkingV1Api = kubeconfig.makeApiClient(NetworkingV1Api) const networkingV1Api = kubeconfig.makeApiClient(NetworkingV1Api);
const readAndParseKubeYaml = ( const readAndParseKubeYaml = (
filePath: string, filePath: string,
sandboxId: string sandboxId: string
): Array<any> => { ): Array<any> => {
const fileContent = fs.readFileSync(filePath, "utf8") const fileContent = fs.readFileSync(filePath, "utf8");
const docs = yaml.parseAllDocuments(fileContent).map((doc) => { const docs = yaml.parseAllDocuments(fileContent).map((doc) => {
let docString = doc.toString() let docString = doc.toString();
const regex = new RegExp(`<SANDBOX>`, "g") const regex = new RegExp(`<SANDBOX>`, "g");
docString = docString.replace(regex, sandboxId) docString = docString.replace(regex, sandboxId);
if (!process.env.CF_API_TOKEN) { if (!process.env.CF_API_TOKEN) {
throw new Error("CF_API_TOKEN is not defined") throw new Error("CF_API_TOKEN is not defined");
} }
const regexEnv1 = new RegExp(`<CF_API_TOKEN>`, "g") const regexEnv1 = new RegExp(`<CF_API_TOKEN>`, "g");
docString = docString.replace(regexEnv1, process.env.CF_API_TOKEN) docString = docString.replace(regexEnv1, process.env.CF_API_TOKEN);
if (!process.env.CF_USER_ID) { if (!process.env.CF_USER_ID) {
throw new Error("CF_USER_ID is not defined") throw new Error("CF_USER_ID is not defined");
} }
const regexEnv2 = new RegExp(`<CF_USER_ID>`, "g") const regexEnv2 = new RegExp(`<CF_USER_ID>`, "g");
docString = docString.replace(regexEnv2, process.env.CF_USER_ID) docString = docString.replace(regexEnv2, process.env.CF_USER_ID);
return yaml.parse(docString) return yaml.parse(docString);
}) });
return docs return docs;
} };
const dataSchema = z.object({ const dataSchema = z.object({
userId: z.string(), userId: z.string(),
sandboxId: z.string(), sandboxId: z.string(),
}) });
const namespace = "ingress-nginx" const namespace = "ingress-nginx";
app.get("/", async (req, res) => {
res.status(200).send({ message: "Orchestrator" });
});
app.post("/test", async (req, res) => { app.post("/test", async (req, res) => {
const pods = await coreV1Api.listNamespacedPod(namespace) const pods = await coreV1Api.listNamespacedPod(namespace);
res.status(200).send({ pods: pods.body.items.map(item => item?.metadata?.generateName), message: "Orchestrator is up and running." }) res.status(200).send({
// res.status(200).send({ message: "Orchestrator is up and running." }) pods: pods.body.items.map((item) => item?.metadata?.generateName),
}) message: "Orchestrator is up and running.",
});
});
app.post("/start", async (req, res) => { app.post("/start", async (req, res) => {
const { sandboxId } = dataSchema.parse(req.body) const { sandboxId } = dataSchema.parse(req.body);
// async function main() {
// const client = new container.v1.ClusterManagerClient();
// async function quickstart() {
// const zone = 'us-central1-a';
// const projectId = await client.getProjectId();
// const request = {
// projectId: projectId,
// zone: zone,
// };
// const [response] = await client.listClusters(request);
// console.log('Clusters: ', response);
// }
// quickstart();
// }
// main().catch(console.error);
try { try {
console.log("Creating resources for sandbox", sandboxId);
console.log("Creating resources for sandbox", sandboxId)
const kubeManifests = readAndParseKubeYaml( const kubeManifests = readAndParseKubeYaml(
path.join(__dirname, "../service.yaml"), path.join(__dirname, "../service.yaml"),
sandboxId sandboxId
) );
async function resourceExists(api: any, getMethod: string, name: string) { async function resourceExists(api: any, getMethod: string, name: string) {
try { try {
await api[getMethod](name, namespace) await api[getMethod](name, namespace);
return true return true;
} catch (e: any) { } catch (e: any) {
if (e.response && e.response.statusCode === 404) { if (e.response && e.response.statusCode === 404) {
console.log("Resource does not exist.", e.response.body.message, e.response.body.details) console.log(
return false "Resource does not exist.",
e.response.body.message,
e.response.body.details
);
return false;
} }
throw e throw e;
} }
} }
const createResource = async (api: any, method: string, manifest: any) => { const createResource = async (api: any, method: string, manifest: any) => {
const { kind, metadata: { name } } = manifest; const {
if (!(await resourceExists(api, 'readNamespaced' + kind, name))) { kind,
await api['createNamespaced' + kind](namespace, manifest); metadata: { name },
} = manifest;
if (!(await resourceExists(api, "readNamespaced" + kind, name))) {
await api["createNamespaced" + kind](namespace, manifest);
console.log(`Created ${kind.toLowerCase()}`, name); console.log(`Created ${kind.toLowerCase()}`, name);
} else { } else {
console.log(`${kind} ${name} already exists.`); console.log(`${kind} ${name} already exists.`);
@ -161,74 +150,77 @@ app.post("/start", async (req, res) => {
}; };
const promises = kubeManifests.map(async (manifest) => { const promises = kubeManifests.map(async (manifest) => {
const { kind, metadata: { name } } = manifest const {
kind,
metadata: { name },
} = manifest;
console.log("Kind:", kind) console.log("Kind:", kind);
switch (manifest.kind) { switch (manifest.kind) {
case 'Deployment': case "Deployment":
return createResource(appsV1Api, 'Deployment', manifest); return createResource(appsV1Api, "Deployment", manifest);
case 'Service': case "Service":
return createResource(coreV1Api, 'Service', manifest); return createResource(coreV1Api, "Service", manifest);
case 'Ingress': case "Ingress":
return createResource(networkingV1Api, 'Ingress', manifest); return createResource(networkingV1Api, "Ingress", manifest);
default: default:
console.error("Unsupported kind:", manifest.kind); console.error("Unsupported kind:", manifest.kind);
return Promise.reject("Unsupported kind: " + manifest.kind); return Promise.reject("Unsupported kind: " + manifest.kind);
} }
}) });
await Promise.all(promises) await Promise.all(promises);
console.log("All done!") console.log("All done!");
res.status(200).send({ message: "Resources created." }) res.status(200).send({ message: "Resources created." });
} catch (error: any) { } catch (error: any) {
const body = error.response.body const body = error.response.body;
console.log("Failed to create resources", error) console.log("Failed to create resources", error);
if (body.code === 409) { if (body.code === 409) {
return res.status(200).send({ message: "Resource already exists." }) return res.status(200).send({ message: "Resource already exists." });
} }
res.status(500).send({ message: "Failed to create resources." }) res.status(500).send({ message: "Failed to create resources." });
} }
}) });
app.post("/stop", async (req, res) => { app.post("/stop", async (req, res) => {
const { sandboxId } = dataSchema.parse(req.body) const { sandboxId } = dataSchema.parse(req.body);
console.log("Deleting resources for sandbox", sandboxId) console.log("Deleting resources for sandbox", sandboxId);
try { try {
const kubeManifests = readAndParseKubeYaml( const kubeManifests = readAndParseKubeYaml(
path.join(__dirname, "../service.yaml"), path.join(__dirname, "../service.yaml"),
sandboxId sandboxId
) );
const promises = kubeManifests.map(async (manifest) => { const promises = kubeManifests.map(async (manifest) => {
if (manifest.kind === "Deployment") if (manifest.kind === "Deployment")
await appsV1Api.deleteNamespacedDeployment( await appsV1Api.deleteNamespacedDeployment(
manifest.metadata?.name || "", manifest.metadata?.name || "",
namespace namespace
) );
else if (manifest.kind === "Service") else if (manifest.kind === "Service")
await coreV1Api.deleteNamespacedService( await coreV1Api.deleteNamespacedService(
manifest.metadata?.name || "", manifest.metadata?.name || "",
namespace namespace
) );
else if (manifest.kind === "Ingress") else if (manifest.kind === "Ingress")
await networkingV1Api.deleteNamespacedIngress( await networkingV1Api.deleteNamespacedIngress(
manifest.metadata?.name || "", manifest.metadata?.name || "",
namespace namespace
) );
}) });
await Promise.all(promises) await Promise.all(promises);
res.status(200).send({ message: "Resources deleted." }) res.status(200).send({ message: "Resources deleted." });
} catch (error) { } catch (error) {
console.log("Failed to delete resources", error) console.log("Failed to delete resources", error);
res.status(500).send({ message: "Failed to delete resources." }) res.status(500).send({ message: "Failed to delete resources." });
} }
}) });
app.listen(port, () => { app.listen(port, () => {
console.log(`Listening on port: ${port}`) console.log(`Listening on port: ${port}`);
}) });

View File

@ -1,4 +1,4 @@
import e from "cors" import e from "cors";
import { import {
R2FileBody, R2FileBody,
R2Files, R2Files,
@ -7,55 +7,55 @@ import {
TFileData, TFileData,
TFolder, TFolder,
User, User,
} from "./types" } from "./types";
export const getSandboxFiles = async (id: string) => { export const getSandboxFiles = async (id: string) => {
const res = await fetch( const res = await fetch(
`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` `https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`
) );
const data: R2Files = await res.json() const data: R2Files = await res.json();
const paths = data.objects.map((obj) => obj.key) const paths = data.objects.map((obj) => obj.key);
const processedFiles = await processFiles(paths, id) const processedFiles = await processFiles(paths, id);
return processedFiles return processedFiles;
} };
export const getFolder = async (folderId: string) => { export const getFolder = async (folderId: string) => {
const res = await fetch( const res = await fetch(
`https://storage.ishaan1013.workers.dev/api?folderId=${folderId}` `https://storage.ishaan1013.workers.dev/api?folderId=${folderId}`
) );
const data: R2Files = await res.json() const data: R2Files = await res.json();
return data.objects.map((obj) => obj.key) return data.objects.map((obj) => obj.key);
} };
const processFiles = async (paths: string[], id: string) => { const processFiles = async (paths: string[], id: string) => {
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } const root: TFolder = { id: "/", type: "folder", name: "/", children: [] };
const fileData: TFileData[] = [] const fileData: TFileData[] = [];
paths.forEach((path) => { paths.forEach((path) => {
const allParts = path.split("/") const allParts = path.split("/");
if (allParts[1] !== id) { if (allParts[1] !== id) {
return return;
} }
const parts = allParts.slice(2) const parts = allParts.slice(2);
let current: TFolder = root let current: TFolder = root;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const part = parts[i] const part = parts[i];
const isFile = i === parts.length - 1 && part.includes(".") const isFile = i === parts.length - 1 && part.includes(".");
const existing = current.children.find((child) => child.name === part) const existing = current.children.find((child) => child.name === part);
if (existing) { if (existing) {
if (!isFile) { if (!isFile) {
current = existing as TFolder current = existing as TFolder;
} }
} else { } else {
if (isFile) { if (isFile) {
const file: TFile = { id: path, type: "file", name: part } const file: TFile = { id: path, type: "file", name: part };
current.children.push(file) current.children.push(file);
fileData.push({ id: path, data: "" }) fileData.push({ id: path, data: "" });
} else { } else {
const folder: TFolder = { const folder: TFolder = {
// id: path, // todo: wrong id. for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css // id: path, // todo: wrong id. for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css
@ -63,38 +63,38 @@ const processFiles = async (paths: string[], id: string) => {
type: "folder", type: "folder",
name: part, name: part,
children: [], children: [],
} };
current.children.push(folder) current.children.push(folder);
current = folder current = folder;
} }
} }
} }
}) });
await Promise.all( await Promise.all(
fileData.map(async (file) => { fileData.map(async (file) => {
const data = await fetchFileContent(file.id) const data = await fetchFileContent(file.id);
file.data = data file.data = data;
}) })
) );
return { return {
files: root.children, files: root.children,
fileData, fileData,
} };
} };
const fetchFileContent = async (fileId: string): Promise<string> => { const fetchFileContent = async (fileId: string): Promise<string> => {
try { try {
const fileRes = await fetch( const fileRes = await fetch(
`https://storage.ishaan1013.workers.dev/api?fileId=${fileId}` `https://storage.ishaan1013.workers.dev/api?fileId=${fileId}`
) );
return await fileRes.text() return await fileRes.text();
} catch (error) { } catch (error) {
console.error("ERROR fetching file:", error) console.error("ERROR fetching file:", error);
return "" return "";
} }
} };
export const createFile = async (fileId: string) => { export const createFile = async (fileId: string) => {
const res = await fetch(`https://storage.ishaan1013.workers.dev/api`, { const res = await fetch(`https://storage.ishaan1013.workers.dev/api`, {
@ -103,9 +103,9 @@ export const createFile = async (fileId: string) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ fileId }), body: JSON.stringify({ fileId }),
}) });
return res.ok return res.ok;
} };
export const renameFile = async ( export const renameFile = async (
fileId: string, fileId: string,
@ -118,9 +118,9 @@ export const renameFile = async (
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ fileId, newFileId, data }), body: JSON.stringify({ fileId, newFileId, data }),
}) });
return res.ok return res.ok;
} };
export const saveFile = async (fileId: string, data: string) => { export const saveFile = async (fileId: string, data: string) => {
const res = await fetch(`https://storage.ishaan1013.workers.dev/api/save`, { const res = await fetch(`https://storage.ishaan1013.workers.dev/api/save`, {
@ -129,9 +129,9 @@ export const saveFile = async (fileId: string, data: string) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ fileId, data }), body: JSON.stringify({ fileId, data }),
}) });
return res.ok return res.ok;
} };
export const deleteFile = async (fileId: string) => { export const deleteFile = async (fileId: string) => {
const res = await fetch(`https://storage.ishaan1013.workers.dev/api`, { const res = await fetch(`https://storage.ishaan1013.workers.dev/api`, {
@ -140,16 +140,16 @@ export const deleteFile = async (fileId: string) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ fileId }), body: JSON.stringify({ fileId }),
}) });
return res.ok return res.ok;
} };
export const getProjectSize = async (id: string) => { export const getProjectSize = async (id: string) => {
const res = await fetch( const res = await fetch(
`https://storage.ishaan1013.workers.dev/api/size?sandboxId=${id}` `https://storage.ishaan1013.workers.dev/api/size?sandboxId=${id}`
) );
return (await res.json()).size return (await res.json()).size;
} };
export const generateCode = async ({ export const generateCode = async ({
fileName, fileName,
@ -157,10 +157,10 @@ export const generateCode = async ({
line, line,
instructions, instructions,
}: { }: {
fileName: string fileName: string;
code: string code: string;
line: number line: number;
instructions: string instructions: string;
}) => { }) => {
return await fetch( return await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`, `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`,
@ -194,21 +194,21 @@ ${code}`,
], ],
}), }),
} }
) );
} };
export const stopServer = async (sandboxId: string, userId: string) => { export const stopServer = async (sandboxId: string, userId: string) => {
const res = await fetch("https://sylvan-epoch-422219-f9.uc.r.appspot.com/stop", { const res = await fetch("http://localhost:4001/stop", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
sandboxId, sandboxId,
userId userId,
}), }),
}) });
const data = await res.json() const data = await res.json();
return data return data;
} };

View File

@ -1,21 +1,20 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx";
// import { toast } from "sonner" // import { toast } from "sonner"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
import { Sandbox, TFile, TFolder } from "./types" import { Sandbox, TFile, TFolder } from "./types";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }
export function processFileType(file: string) { export function processFileType(file: string) {
const ending = file.split(".").pop() const ending = file.split(".").pop();
if (ending === "ts" || ending === "tsx") return "typescript" if (ending === "ts" || ending === "tsx") return "typescript";
if (ending === "js" || ending === "jsx") return "javascript" if (ending === "js" || ending === "jsx") return "javascript";
if (ending) return ending if (ending) return ending;
return "plaintext" return "plaintext";
} }
export function validateName( export function validateName(
@ -24,7 +23,7 @@ export function validateName(
type: "file" | "folder" type: "file" | "folder"
) { ) {
if (newName === oldName || newName.length === 0) { if (newName === oldName || newName.length === 0) {
return { status: false, message: "" } return { status: false, message: "" };
} }
if ( if (
newName.includes("/") || newName.includes("/") ||
@ -33,12 +32,17 @@ export function validateName(
(type === "file" && !newName.includes(".")) || (type === "file" && !newName.includes(".")) ||
(type === "folder" && newName.includes(".")) (type === "folder" && newName.includes("."))
) { ) {
return { status: false, message: "Invalid file name." } return { status: false, message: "Invalid file name." };
} }
return { status: true, message: "" } return { status: true, message: "" };
} }
export function addNew(name: string, type: "file" | "folder", setFiles: React.Dispatch<React.SetStateAction<(TFolder | TFile)[]>>, sandboxData: Sandbox) { export function addNew(
name: string,
type: "file" | "folder",
setFiles: React.Dispatch<React.SetStateAction<(TFolder | TFile)[]>>,
sandboxData: Sandbox
) {
if (type === "file") { if (type === "file") {
setFiles((prev) => [ setFiles((prev) => [
...prev, ...prev,
@ -46,34 +50,44 @@ export function addNew(name: string, type: "file" | "folder", setFiles: React.Di
]); ]);
} else { } else {
console.log("adding folder"); console.log("adding folder");
setFiles(prev => [...prev, { id: `projects/${sandboxData.id}/${name}`, name, type: "folder", children: [] }]) setFiles((prev) => [
...prev,
{
id: `projects/${sandboxData.id}/${name}`,
name,
type: "folder",
children: [],
},
]);
} }
} }
export async function startServer(sandboxId: string, userId: string, callback: (success: boolean) => void) { export async function startServer(
sandboxId: string,
userId: string,
callback: (success: boolean) => void
) {
try { try {
const res = await fetch("https://sylvan-epoch-422219-f9.uc.r.appspot.com/start", { const res = await fetch("http://localhost:4001/start", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
sandboxId, sandboxId,
userId userId,
}), }),
}) });
if (res.status !== 200) { if (res.status !== 200) {
console.error("Failed to start server", res) console.error("Failed to start server", res);
callback(false) callback(false);
} }
callback(true) callback(true);
} catch (error) { } catch (error) {
console.error("Failed to start server", error) console.error("Failed to start server", error);
callback(false) callback(false);
} }
} }