diff --git a/.env.example b/.env.example index d8ee01842..6078881cf 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,10 @@ SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key ### Visit: https://github.com/e2b-dev/infra/blob/main/README.md for a self-hosting guide INFRA_API_URL=https://api.e2b.dev +### Default domain for the E2B SDK +### Used for Sandbox Details Page +NEXT_PUBLIC_E2B_DOMAIN=e2b.dev + ### KV database configuration KV_REST_API_TOKEN= KV_REST_API_URL= diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..8b77f12ba --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +archive/ \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcb3efcf0..f31eaf29e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: SUPABASE_SERVICE_ROLE_KEY: test-service-role-key INFRA_API_URL: https://api.e2b-test.dev BILLING_API_URL: https://billing.e2b-test.dev + NEXT_PUBLIC_E2B_DOMAIN: e2b-test.dev NEXT_PUBLIC_POSTHOG_KEY: test-posthog-key NEXT_PUBLIC_SUPABASE_URL: https://test-supabase-url.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY: test-supabase-anon-key diff --git a/bun.lock b/bun.lock index 59b196972..0f6d2906a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,6 @@ "": { "name": "@e2b/dashboard", "dependencies": { - "@connectrpc/connect": "^2.0.2", "@fumadocs/mdx-remote": "^1.2.0", "@google-cloud/storage": "^7.15.2", "@hookform/resolvers": "^3.10.0", @@ -47,6 +46,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "e2b": "^1.10.0", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", @@ -61,6 +61,7 @@ "next-safe-action": "^7.10.4", "next-themes": "^0.4.4", "openapi-fetch": "^0.14.0", + "pathe": "^2.0.3", "pino": "^9.7.0", "postgres": "^3.4.5", "posthog-js": "^1.214.0", @@ -106,7 +107,6 @@ "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.1.0-rc.2", "drizzle-kit": "^0.30.3", - "e2b": "^1.7.1", "eslint": "^9.19.0", "eslint-config-next": "^15.1.6", "eslint-config-prettier": "^10.0.1", @@ -169,11 +169,11 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], + "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="], "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], - "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], @@ -223,7 +223,7 @@ "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], - "@connectrpc/connect": ["@connectrpc/connect@2.0.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-jAbVMHVtDCydGt2P20VpmLjbLtERqSV0RMSyQF3k2zhK8pzQ2QaCAcyVhufClqrOAFZUKL5BqVYtttaxvhmRgg=="], + "@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="], "@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="], @@ -309,7 +309,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], + "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -667,45 +667,45 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.1", "", { "os": "android", "cpu": "arm" }, "sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.1", "", { "os": "android", "cpu": "arm64" }, "sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.1", "", { "os": "win32", "cpu": "x64" }, "sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -847,9 +847,9 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.4", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ=="], "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], @@ -1167,7 +1167,7 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], @@ -1263,7 +1263,7 @@ "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], - "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], @@ -1499,7 +1499,7 @@ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - "e2b": ["e2b@1.9.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-MM3RhWW7YENYocTy20BvKVcn8li/FxkDrHINS7tmz00ffl1ZavQTRxCI9Sl8ofeRg+HVMlqO4W8LJ+ij9VTZPg=="], + "e2b": ["e2b@1.10.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-m0lt8hTQ84M7tUjF2Dw7oNwfMcc8EyCHJtA1vX6Sv3OO2OtjPdCky854XWY+UejDK+q3m5vuSpSgLgeE0rJ7LA=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -1557,7 +1557,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], + "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], "eslint-config-next": ["eslint-config-next@15.4.4", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-sK/lWLUVF5om18O5w76Jt3F8uzu/LP5mVa6TprCMWkjWHUmByq80iHGHcdH7k1dLiJlj+DRIWf98d5piwRsSuA=="], @@ -1679,7 +1679,7 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "framer-motion": ["framer-motion@12.23.9", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ=="], + "framer-motion": ["framer-motion@12.23.10", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ziXHr+C91FhgdSV65YA9SNbLy7uIDQ0pq7pEWlMP6Bh9UJAHFUNUvKWzE41g1B7YuvgJtUUNLgNmZyHd/YQ2gA=="], "fs-extra": ["fs-extra@4.0.3", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg=="], @@ -1687,11 +1687,11 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fumadocs-core": ["fumadocs-core@15.6.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-n+IXfJs+nQMpH2vC4g5ipfUhfZD+ML8tVUUW+Nsc5SddpVbxlYytP9PSJw3kdyfookiyZDhcpH5Jz8/G6pqXcg=="], + "fumadocs-core": ["fumadocs-core@15.6.6", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-90sUbejUDevfDHykXXudw+3xTqYjuSZU1evhJiRBiZ0Oy0xQX4p5zPO48b5dhuVp44osvOH0ZKfHsVdkor6kZQ=="], - "fumadocs-mdx": ["fumadocs-mdx@11.7.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.5" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-Cjel0WZHqKaRDxRK6yQW/bUnMMq3Sy+TL4U3S6A4Htwbc22qoPi/ZRz7kP2i43TEml/AVVpostu4XdjDRcWgbg=="], + "fumadocs-mdx": ["fumadocs-mdx@11.7.1", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.10" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-zY2s3OP0XsNhayp1ac3Qz/xSZLdfjFE3zCCt+LDlwAfRwlpP8WdwfUNsPzZSnnXYigLl0oEQNuL+SF4ZgXctfQ=="], - "fumadocs-ui": ["fumadocs-ui@15.6.5", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.5", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-YrVlHtXXW9Y+bo2lAoCj2ifpLmzRCWnMTY2QYpeHbnpIte3oJyAea3nhF+rpw3/XxE66Wjluzw/6GXOlpzcpvw=="], + "fumadocs-ui": ["fumadocs-ui@15.6.6", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.6", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-Ft/F8yrea7Z1kcI6NDFxKUwLiE4b0elvMDGfmJ/EZJHTuHfl5niXUUCCfvgkTDcZRYjPJktnMxC2msr78wWrzA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1937,7 +1937,7 @@ "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], @@ -2195,7 +2195,7 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "motion": ["motion@12.23.9", "", { "dependencies": { "framer-motion": "^12.23.9", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5PDgsbNtZ4cpfew3STYL0p06rIiy8vOveQuQBXUAa2+m1WMzjf65DXYn6eo88dM2s+XLxAQq3ZiOjcnKMACEtQ=="], + "motion": ["motion@12.23.10", "", { "dependencies": { "framer-motion": "^12.23.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-pE3WsRXbRjVQaB2vCmVLt8gYiZdX11KYIoWblc49JnG2dTn6oHobIw+bORL31ZfiYBZD8BYmqkvEZp+HLPBBVw=="], "motion-dom": ["motion-dom@12.23.9", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A=="], @@ -2375,7 +2375,7 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.1.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0" }, "optionalPeers": ["vue-tsc"] }, "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], @@ -2517,7 +2517,7 @@ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], + "rollup": ["rollup@4.46.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.1", "@rollup/rollup-android-arm64": "4.46.1", "@rollup/rollup-darwin-arm64": "4.46.1", "@rollup/rollup-darwin-x64": "4.46.1", "@rollup/rollup-freebsd-arm64": "4.46.1", "@rollup/rollup-freebsd-x64": "4.46.1", "@rollup/rollup-linux-arm-gnueabihf": "4.46.1", "@rollup/rollup-linux-arm-musleabihf": "4.46.1", "@rollup/rollup-linux-arm64-gnu": "4.46.1", "@rollup/rollup-linux-arm64-musl": "4.46.1", "@rollup/rollup-linux-loongarch64-gnu": "4.46.1", "@rollup/rollup-linux-ppc64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-musl": "4.46.1", "@rollup/rollup-linux-s390x-gnu": "4.46.1", "@rollup/rollup-linux-x64-gnu": "4.46.1", "@rollup/rollup-linux-x64-musl": "4.46.1", "@rollup/rollup-win32-arm64-msvc": "4.46.1", "@rollup/rollup-win32-ia32-msvc": "4.46.1", "@rollup/rollup-win32-x64-msvc": "4.46.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ=="], "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], @@ -2813,7 +2813,7 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], @@ -2887,8 +2887,6 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -2933,8 +2931,6 @@ "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -2963,6 +2959,8 @@ "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "@sentry/nextjs/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "@sentry/nextjs/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "@sentry/node/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2977,8 +2975,6 @@ "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.2.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA=="], - "@tailwindcss/node/jiti": ["jiti@2.5.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-NWDAhdnATItTnRhip9VTd8oXDjVcbhetRN6YzckApnXGxpGUooKMAaf0KVvlZG0+KlJMGkeLElVn4M1ReuxKUQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], @@ -2991,7 +2987,7 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -3031,12 +3027,8 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "e2b/@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="], - "e2b/openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3045,8 +3037,6 @@ "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3081,20 +3071,12 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -3243,8 +3225,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], @@ -3259,6 +3239,8 @@ "@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "@sentry/nextjs/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@sentry/node/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/webpack-plugin/unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -3267,8 +3249,6 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], - "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@types/jest/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3297,8 +3277,6 @@ "e2b/openapi-fetch/openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="], - "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "extract-zip/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "fumadocs-core/@shikijs/rehype/@shikijs/types": ["@shikijs/types@3.8.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg=="], @@ -3353,20 +3331,12 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "mermaid.cli/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "mermaid.cli/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], diff --git a/next.config.mjs b/next.config.mjs index 26623c764..d243ba06c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,10 +1,11 @@ import { withSentryConfig } from '@sentry/nextjs' -import { createMDX } from 'fumadocs-mdx/next' -const withMDX = createMDX() /** @type {import('next').NextConfig} */ const config = { + eslint: { + dirs: ['src', 'scripts'], // Only run ESLint on these directories during production builds + }, reactStrictMode: true, experimental: { reactCompiler: true, @@ -26,7 +27,7 @@ const config = { trailingSlash: false, headers: async () => [ { - source: '/:path*', + source: '/(.*)', headers: [ { // config to prevent the browser from rendering the page inside a frame or iframe and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking @@ -95,7 +96,7 @@ const config = { skipTrailingSlashRedirect: true, } -export default withSentryConfig(withMDX(config), { +export default withSentryConfig(config, { // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options diff --git a/package.json b/package.json index 93b15a6a3..411769730 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "<<<<<< Gen": "", "generate:infra": "bunx openapi-typescript ./spec/openapi.yaml -o ./src/types/infra-api.d.ts", "generate:supabase": "bunx supabase@latest gen types typescript --schema public > src/types/database.types.ts --project-id $SUPABASE_PROJECT_ID", - "generate:envd": "buf generate --template ./spec/envd/buf.gen.yaml", "<<<<<< Scripts": "", "scripts:check-app-env": "bun scripts/check-app-env.ts", "scripts:check-e2e-env": "bun scripts/check-e2e-env.ts", @@ -27,7 +26,6 @@ "<<<<<< Development": "", "shad": "bunx shadcn@canary", "prebuild": "bun scripts:check-app-env", - "postinstall": "fumadocs-mdx", "<<<<<< Testing": "", "test:run": "bun scripts:check-all-env && vitest run", "test:integration": "bun scripts:check-app-env && vitest run src/__test__/integration/", @@ -38,7 +36,6 @@ "test:development:metrics": "vitest run src/__test__/development/metrics.test.ts" }, "dependencies": { - "@connectrpc/connect": "^2.0.2", "@fumadocs/mdx-remote": "^1.2.0", "@google-cloud/storage": "^7.15.2", "@hookform/resolvers": "^3.10.0", @@ -81,6 +78,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "e2b": "^1.10.0", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", @@ -95,6 +93,7 @@ "next-safe-action": "^7.10.4", "next-themes": "^0.4.4", "openapi-fetch": "^0.14.0", + "pathe": "^2.0.3", "pino": "^9.7.0", "postgres": "^3.4.5", "posthog-js": "^1.214.0", @@ -141,7 +140,6 @@ "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.1.0-rc.2", "drizzle-kit": "^0.30.3", - "e2b": "^1.7.1", "eslint": "^9.19.0", "eslint-config-next": "^15.1.6", "eslint-config-prettier": "^10.0.1", diff --git a/source.config.ts b/source.config.ts deleted file mode 100644 index 12285dc34..000000000 --- a/source.config.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - defineConfig, - defineDocs, - frontmatterSchema, - metaSchema, -} from 'fumadocs-mdx/config' - -export const docs = defineDocs({ - dir: 'src/content/docs', - docs: { schema: frontmatterSchema }, - meta: { schema: metaSchema }, -}) - -export default defineConfig({ - lastModifiedTime: 'git', -}) - -/* export default defineConfig({ - lastModifiedTime: 'git', - mdxOptions: { - rehypeCodeOptions: { - lazy: true, - experimentalJSEngine: true, - langs: ['ts', 'js', 'html', 'tsx', 'mdx'], - inline: 'tailing-curly-colon', - themes: { - light: 'github-light', - dark: 'github-dark', - }, - transformers: [ - ...(rehypeCodeDefaultOptions.transformers ?? []), - transformerTwoslash(), - { - name: 'transformers:remove-notation-escape', - code(hast) { - for (const line of hast.children) { - if (line.type !== 'element') continue - - const lastSpan = line.children.findLast( - (v) => v.type === 'element' - ) - - const head = lastSpan?.children[0] - if (head?.type !== 'text') return - - head.value = head.value.replace(/\[\\!code/g, '[!code') - } - }, - }, - ], - }, - remarkPlugins: [ - remarkMermaid, - remarkMath, - [remarkInstall, { persist: { id: 'package-manager' } }], - [remarkDocGen, { generators: [fileGenerator()] }], - remarkTypeScriptToJavaScript, - ], - rehypePlugins: (v) => [rehypeKatex, ...v], - }, -}) */ diff --git a/spec/envd/buf.gen.yaml b/spec/envd/buf.gen.yaml deleted file mode 100644 index a3317f9b4..000000000 --- a/spec/envd/buf.gen.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# buf.gen.yaml defines a local generation template. -# For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml -version: v2 -plugins: - - local: protoc-gen-es - out: ./src/lib/clients/envd - opt: - - target=ts - - local: protoc-gen-connect-es - out: ./src/lib/clients/envd - opt: - - target=ts - -managed: - enabled: true - override: - - file_option: optimize_for - value: SPEED - -inputs: - - directory: spec/envd diff --git a/spec/envd/filesystem/filesystem.proto b/spec/envd/filesystem/filesystem.proto deleted file mode 100644 index f80c37c17..000000000 --- a/spec/envd/filesystem/filesystem.proto +++ /dev/null @@ -1,124 +0,0 @@ -syntax = "proto3"; - -package filesystem; - -service Filesystem { - rpc Stat(StatRequest) returns (StatResponse); - rpc MakeDir(MakeDirRequest) returns (MakeDirResponse); - rpc Move(MoveRequest) returns (MoveResponse); - rpc ListDir(ListDirRequest) returns (ListDirResponse); - rpc Remove(RemoveRequest) returns (RemoveResponse); - - rpc WatchDir(WatchDirRequest) returns (stream WatchDirResponse); - - // Non-streaming versions of WatchDir - rpc CreateWatcher(CreateWatcherRequest) returns (CreateWatcherResponse); - rpc GetWatcherEvents(GetWatcherEventsRequest) returns (GetWatcherEventsResponse); - rpc RemoveWatcher(RemoveWatcherRequest) returns (RemoveWatcherResponse); -} - -message MoveRequest { - string source = 1; - string destination = 2; -} - -message MoveResponse { - EntryInfo entry = 1; -} - -message MakeDirRequest { - string path = 1; -} - -message MakeDirResponse { - EntryInfo entry = 1; -} - -message RemoveRequest { - string path = 1; -} - -message RemoveResponse {} - -message StatRequest { - string path = 1; -} - -message StatResponse { - EntryInfo entry = 1; -} - -message EntryInfo { - string name = 1; - FileType type = 2; - string path = 3; -} - -enum FileType { - FILE_TYPE_UNSPECIFIED = 0; - FILE_TYPE_FILE = 1; - FILE_TYPE_DIRECTORY = 2; -} - -message ListDirRequest { - string path = 1; - uint32 depth = 2; -} - -message ListDirResponse { - repeated EntryInfo entries = 1; -} - -message WatchDirRequest { - string path = 1; - bool recursive = 2; -} - -message FilesystemEvent { - string name = 1; - EventType type = 2; -} - -message WatchDirResponse { - oneof event { - StartEvent start = 1; - FilesystemEvent filesystem = 2; - KeepAlive keepalive = 3; - } - - message StartEvent {} - - message KeepAlive {} -} - -message CreateWatcherRequest { - string path = 1; - bool recursive = 2; -} - -message CreateWatcherResponse { - string watcher_id = 1; -} - -message GetWatcherEventsRequest { - string watcher_id = 1; -} - -message GetWatcherEventsResponse { - repeated FilesystemEvent events = 1; -} - -message RemoveWatcherRequest { - string watcher_id = 1; -} - -message RemoveWatcherResponse {} - -enum EventType { - EVENT_TYPE_UNSPECIFIED = 0; - EVENT_TYPE_CREATE = 1; - EVENT_TYPE_WRITE = 2; - EVENT_TYPE_REMOVE = 3; - EVENT_TYPE_RENAME = 4; - EVENT_TYPE_CHMOD = 5; -} diff --git a/src/app/_docs/[[...slug]]/page.tsx b/src/app/_docs/[[...slug]]/page.tsx deleted file mode 100644 index 4c00c93e3..000000000 --- a/src/app/_docs/[[...slug]]/page.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { - DocsBody, - DocsDescription, - DocsPage, - DocsTitle, -} from 'fumadocs-ui/page' -import type { Metadata } from 'next' -import { notFound } from 'next/navigation' -/* import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui"; */ -/* import * as Preview from "@/components/preview"; */ -import { createMetadata, metadataImage } from '@/configs/fumadocs' -import { METADATA } from '@/configs/metadata' -import components from '@/features/docs/components' -import Footer from '@/features/docs/footer/footer' -import { source } from '@/lib/source' -import { cn } from '@/lib/utils' -import { buttonVariants } from '@/ui/primitives/button' - -/* function PreviewRenderer({ preview }: { preview: string }): ReactNode { - if (preview && preview in Preview) { - const Comp = Preview[preview as keyof typeof Preview]; - return ; - } - - return null; -} */ - -export default async function Page(props: { - params: Promise<{ slug?: string[] }> -}): Promise> { - const params = await props.params - - const page = source.getPage(params.slug) - - if (!page) notFound() - - const path = `src/content/docs/${page.file.path}` - const { body: Mdx, toc, lastModified } = page.data - - return ( - , - }} - article={{ - className: 'pb-16 xl:pt-10 max-w-3xl xl:ml-0', - }} - > - {page.data.title} - {page.data.description} - - {/* {preview ? : null} */} - - {/* {page.data.index ? : null} */} - - - ) -} - -export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }> -}): Promise { - const params = await props.params - const page = source.getPage(params.slug) - - if (!page) notFound() - - const description = page.data.description ?? METADATA.description - - return createMetadata( - metadataImage.withImage(page.slugs, { - title: page.data.title, - description, - openGraph: { - url: `/docs/${page.slugs.join('/')}`, - }, - }) - ) -} - -export function generateStaticParams(): { slug: string[] }[] { - return source.generateParams() -} diff --git a/src/app/_docs/layout.tsx b/src/app/_docs/layout.tsx deleted file mode 100644 index 212391e46..000000000 --- a/src/app/_docs/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import '@/styles/docs.css' - -import { baseOptions } from '@/app/layout.config' -import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs' -import type { ReactNode } from 'react' -/* import "fumadocs-twoslash/twoslash.css"; */ -import { Nav } from '@/features/docs/navbar/navbar' -import Sidebar from '@/features/docs/sidebar/sidebar' -import { source } from '@/lib/source' -import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' -/* import { Trigger } from "@/components/ai/search-ai"; */ - -const docsOptions: DocsLayoutProps = { - ...baseOptions, - tree: source.pageTree, - sidebar: { - component: , - }, -} - -export default function Layout({ children }: { children: ReactNode }) { - return ( -
-
- ) -} diff --git a/src/app/_docs/not-found.tsx b/src/app/_docs/not-found.tsx deleted file mode 100644 index fb3a8e9a9..000000000 --- a/src/app/_docs/not-found.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import NotFound from '@/ui/not-found' - -export default function NotFoundPage() { - return ( -
- -
- ) -} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 6f1cd3b34..a5ed3566e 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,7 +1,7 @@ import { COOKIE_KEYS } from '@/configs/keys' import { DashboardTitleProvider } from '@/features/dashboard/dashboard-title-provider' +import { ServerContextProvider } from '@/features/dashboard/server-context' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { ServerContextProvider } from '@/lib/hooks/use-server-context' import { resolveTeamIdInServerComponent, resolveTeamSlugInServerComponent, diff --git a/src/configs/fumadocs.ts b/src/configs/fumadocs.ts deleted file mode 100644 index 5d0dd8d66..000000000 --- a/src/configs/fumadocs.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { source } from '@/lib/source' -import { createMetadataImage } from 'fumadocs-core/server' -import type { Metadata } from 'next/types' - -export const metadataImage = createMetadataImage({ - source, - imageRoute: 'og', -}) - -export function createMetadata(override: Metadata): Metadata { - return { - ...override, - openGraph: { - title: override.title ?? undefined, - description: override.description ?? undefined, - url: 'https://fumadocs.vercel.app', - images: '/banner.png', - siteName: 'Fumadocs', - ...override.openGraph, - }, - twitter: { - card: 'summary_large_image', - creator: '@money_is_shark', - title: override.title ?? undefined, - description: override.description ?? undefined, - images: '/banner.png', - ...override.twitter, - }, - } -} diff --git a/src/configs/metadata.ts b/src/configs/metadata.ts index 1d42e34c2..606a3fc97 100644 --- a/src/configs/metadata.ts +++ b/src/configs/metadata.ts @@ -1,4 +1,12 @@ +import type { Metadata } from 'next/types' + export const METADATA = { title: 'E2B - Code Interpreting for AI apps', description: 'Open-source secure sandboxes for AI code execution', } + +export function createMetadata(override: Metadata): Metadata { + return { + ...override, + } +} diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx b/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx deleted file mode 100644 index 49a552fc6..000000000 --- a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx +++ /dev/null @@ -1,532 +0,0 @@ ---- -title: Overview -description: This guide will show you how to analyze data with AI using E2B Sandbox. ---- - -You can use E2B Sandbox to run AI-generated code to analyze data. Here's how the AI data analysis workflow usually looks like: -1. Your user has a dataset in CSV format or other formats. -2. You prompt the LLM to generate code (usually Python) based on the user's data. -3. The sandbox runs the AI-generated code and returns the results. -4. You display the results to the user. - ---- - -## Example: Analyze CSV file with E2B and Claude 3.5 Sonnet -This short example will show you how to use E2B Sandbox to run AI-generated code to analyze CSV data. - -### Table of Contents -1. [Install dependencies](#1-install-dependencies) -2. [Set your API keys](#2-set-your-api-keys) -3. [Download example CSV file](#3-download-example-csv-file) -4. [Initialize the sandbox and upload the dataset to the sandbox](#4-initialize-the-sandbox-and-upload-the-dataset-to-the-sandbox) -5. [Prepare the method for running AI-generated code](#5-prepare-the-method-for-running-ai-generated-code) -6. [Prepare the prompt and initialize Anthropic client](#6-prepare-the-prompt-and-initialize-anthropic-client) -7. [Connect the sandbox to the LLM with tool calling](#7-connect-the-sandbox-to-the-llm-with-tool-calling) -8. [Parse the LLM response and run the AI-generated code in the sandbox](#8-parse-the-llm-response-and-run-the-ai-generated-code-in-the-sandbox) -9. [Save the generated chart](#9-save-the-generated-chart) -10. [Run the code](#10-run-the-code) -11. [Full final code](#11-full-final-code) - -### 1. Install dependencies -Install the E2B SDK and Claude SDK to your project by running the following command in your terminal. - - -```bash tab -npm i @e2b/code-interpreter @anthropic-ai/sdk dotenv -``` - -```bash tab -pip install e2b-code-interpreter anthropic python-dotenv -``` - - -### 2. Set your API keys -1. Get your E2B API key from [E2B Dashboard](/dashboard?tab=keys). -2. Get your Claude API key from [Claude API Dashboard](https://console.anthropic.com/settings/keys). -3. Paste the keys into your `.env` file. - - -```bash tab -E2B_API_KEY=e2b_*** -ANTHROPIC_API_KEY=sk-ant-*** -``` - - -### 3. Download example CSV file -{/* We'll be using the publicly available [AirBnB NYC dataset](https://www.kaggle.com/datasets/dgomonov/new-york-city-airbnb-open-data). */} - -We'll be using the publicly available [dataset of about 10,000 movies](https://www.kaggle.com/datasets/muqarrishzaib/tmdb-10000-movies-dataset). -1. Click the "Download" button at the top of the page. -2. Select "Download as zip (2 MB)". -3. Unzip the file and you should see `dataset.csv` file. Move it to the root of your project. - - -### 4. Initialize the sandbox and upload the dataset to the sandbox -We'll upload the dataset from the third step to the sandbox and save it as `dataset.csv` file. - - -```ts tab title="index.ts" -import 'dotenv/config' -import fs from 'fs' -import { Sandbox } from '@e2b/code-interpreter' - -// Create sandbox -const sbx = await Sandbox.create() - -// Upload the dataset to the sandbox -const content = fs.readFileSync('dataset.csv') -const datasetPathInSandbox = await sbx.files.write('dataset.csv', content) -``` - -```python tab title="main.py" -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox - -# Create sandbox -sbx = Sandbox() - -# Upload the dataset to the sandbox -dataset_path_in_sandbox = "" -with open("dataset.csv", "rb") as f: - dataset_path_in_sandbox = sbx.files.write("dataset.csv", f) -``` - - -### 5. Prepare the method for running AI-generated code -Add the following code to the file. Here we're adding the method for code execution. - - -```js tab title="index.ts" -// ... code from the previous step - -async function runAIGeneratedCode(aiGeneratedCode: string) { - console.log('Running the code in the sandbox....') - const execution = await sbx.runCode(aiGeneratedCode) - console.log('Code execution finished!') - console.log(execution) -} -``` -```python tab title="main.py" -# ... code from the previous step - -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.run_code(ai_generated_code) - print('Code execution finished!') - print(execution) -``` - - -### 6. Prepare the prompt and initialize Anthropic client -The prompt we'll be using describes the dataset and the analysis we want to perform like this: - 1. Describe the columns in the CSV dataset. - 2. Ask the LLM what we want to analyze - here we want to analyze the vote average over time. We're asking for a chart as the output. - 3. Instruct the LLM to generate Python code for the data analysis. - - -```js tab title="index.ts" -import Anthropic from '@anthropic-ai/sdk' - -const prompt = ` -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at ${dataset_path_in_sandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly` - -const anthropic = new Anthropic() -console.log('Waiting for the model response...') -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], -}) -``` - -```python tab title="main.py" -from anthropic import Anthropic - -prompt = ''' -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at {datasetPathInSandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly''' - -anthropic = Anthropic() -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ] -) -``` - - -### 7. Connect the sandbox to the LLM with tool calling -We'll use Claude's ability to [use tools (function calling)](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) to run the code in the sandbox. - -The way we'll do it is by connecting the method for running AI-generated code we created in the previous step to the Claude model. - -Update the initialization of the Anthropic client to include the tool use like this: - -```js tab title="index.ts" -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], - tools: [ // $HighlightLine - { // $HighlightLine - name: 'run_python_code', // $HighlightLine - description: 'Run Python code', // $HighlightLine - input_schema: { // $HighlightLine - type: 'object', // $HighlightLine - properties: { // $HighlightLine - code: { // $HighlightLine - type: 'string', // $HighlightLine - description: 'The Python code to run', // $HighlightLine - }, // $HighlightLine - }, // $HighlightLine - required: ['code'], // $HighlightLine - }, // $HighlightLine - }, // $HighlightLine - ], // $HighlightLine -}) -``` -```python tab title="main.py" -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ], - tools=[ # $HighlightLine - { # $HighlightLine - "name": "run_python_code", # $HighlightLine - "description": "Run Python code", # $HighlightLine - "input_schema": { # $HighlightLine - "type": "object", # $HighlightLine - "properties": { # $HighlightLine - "code": { "type": "string", "description": "The Python code to run" }, # $HighlightLine - }, # $HighlightLine - "required": ["code"], # $HighlightLine - }, # $HighlightLine - }, # $HighlightLine - ], # $HighlightLine -) -``` - - -### 8. Parse the LLM response and run the AI-generated code in the sandbox -Now we'll parse the `msg` object to get the code from the LLM's response based on the tool we created in the previous step. -Once we have the code, we'll pass it to the `runAIGeneratedCode` method in JavaScript or `run_ai_generated_code` method in Python we created in the previous step to run the code in the sandbox. - - -```js tab title="index.ts" -// ... code from the previous steps - -interface CodeRunToolInput { - code: string -} - -for (const contentBlock of msg.content) { - if (contentBlock.type === 'tool_use') { - if (contentBlock.name === 'run_python_code') { - const code = (contentBlock.input as CodeRunToolInput).code - console.log('Will run following code in the sandbox', code) - // Execute the code in the sandbox - await runAIGeneratedCode(code) - } - } -} -``` -```python tab title="main.py" -for content_block in msg.content: - if content_block.type == 'tool_use': - if content_block.name == 'run_python_code': - code = content_block.input['code'] - print('Will run following code in the sandbox', code) - # Execute the code in the sandbox - run_ai_generated_code(code) -``` - - -### 9. Save the generated chart -When running code in the sandbox for data analysis, you can get different types of results. -Including stdout, stderr, charts, tables, text, runtime errors, and more. - -In this example we're specifically asking for a chart so we'll be looking for the chart in the results. - -Let's update the `runAIGeneratedCode` method in JavaScript and `run_ai_generated_code` method in Python to check for the chart in the results and save it to the file. - -```js tab title="index.ts" -async function runAIGeneratedCode(aiGeneratedCode: string) { - console.log('Running the code in the sandbox....') - const execution = await sbx.runCode(aiGeneratedCode) - console.log('Code execution finished!') - - // First let's check if the code ran successfully. - if (execution.error) { // $HighlightLine - console.error('AI-generated code had an error.') // $HighlightLine - console.log(execution.error.name) // $HighlightLine - console.log(execution.error.value) // $HighlightLine - console.log(execution.error.traceback) // $HighlightLine - process.exit(1) // $HighlightLine - } // $HighlightLine - - // Iterate over all the results and specifically check for png files that will represent the chart. - let resultIdx = 0 // $HighlightLine - for (const result of execution.results) { // $HighlightLine - if (result.png) { // $HighlightLine - // Save the png to a file - // The png is in base64 format. - fs.writeFileSync(`chart-${resultIdx}.png`, result.png, { encoding: 'base64' }) // $HighlightLine - console.log(`Chart saved to chart-${resultIdx}.png`) // $HighlightLine - resultIdx++ // $HighlightLine - } // $HighlightLine - } // $HighlightLine -} -``` -```python tab title="main.py" -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.run_code(ai_generated_code) - print('Code execution finished!') - - # First let's check if the code ran successfully. - if execution.error: # $HighlightLine - print('AI-generated code had an error.') # $HighlightLine - print(execution.error.name) # $HighlightLine - print(execution.error.value) # $HighlightLine - print(execution.error.traceback) # $HighlightLine - sys.exit(1) # $HighlightLine - - # Iterate over all the results and specifically check for png files that will represent the chart. - result_idx = 0 # $HighlightLine - for result in execution.results: # $HighlightLine - if result.png: # $HighlightLine - # Save the png to a file - # The png is in base64 format. - with open(f'chart-{result_idx}.png', 'wb') as f: # $HighlightLine - f.write(base64.b64decode(result.png)) # $HighlightLine - print(f'Chart saved to chart-{result_idx}.png') # $HighlightLine - result_idx += 1 # $HighlightLine -``` - - -### 10. Run the code -Now you can run the whole code to see the results. - - -```bash tab -npx tsx index.ts -``` - -```bash tab -python main.py -``` - - -You should see the chart in the root of your project that will look similar to this: - -![Chart visualizing voting average of our dataset over time](/graphics/docs/analyze-data-chart.png) - -### Full final code -Check the full code in JavaScript and Python below: - -```js tab title="index.ts" -import 'dotenv/config' -import fs from 'fs' -import Anthropic from '@anthropic-ai/sdk' -import { Sandbox } from '@e2b/code-interpreter' - -// Create sandbox -const sbx = await Sandbox.create() - -// Upload the dataset to the sandbox -const content = fs.readFileSync('dataset.csv') -const datasetPathInSandbox = await sbx.files.write('/home/user/dataset.csv', content) - -async function runAIGeneratedCode(aiGeneratedCode: string) { - const execution = await sbx.runCode(aiGeneratedCode) - if (execution.error) { - console.error('AI-generated code had an error.') - console.log(execution.error.name) - console.log(execution.error.value) - console.log(execution.error.traceback) - process.exit(1) - } - // Iterate over all the results and specifically check for png files that will represent the chart. - let resultIdx = 0 - for (const result of execution.results) { - if (result.png) { - // Save the png to a file - // The png is in base64 format. - fs.writeFileSync(`chart-${resultIdx}.png`, result.png, { encoding: 'base64' }) - console.log('Chart saved to chart-${resultIdx}.png') - resultIdx++ - } - } -} - -const prompt = ` -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at ${datasetPathInSandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly` - -const anthropic = new Anthropic() -console.log('Waiting for the model response...') -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], - tools: [ - { - name: 'run_python_code', - description: 'Run Python code', - input_schema: { - type: 'object', - properties: { - code: { - type: 'string', - description: 'The Python code to run', - }, - }, - required: ['code'], - }, - }, - ], -}) - -interface CodeRunToolInput { - code: string -} - -for (const contentBlock of msg.content) { - if (contentBlock.type === 'tool_use') { - if (contentBlock.name === 'run_python_code') { - const code = (contentBlock.input as CodeRunToolInput).code - console.log('Will run following code in the sandbox', code) - // Execute the code in the sandbox - await runAIGeneratedCode(code) - } - } -} -``` -```python tab title="main.py" -import sys -import base64 -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox -from anthropic import Anthropic - -# Create sandbox -sbx = Sandbox() - -# Upload the dataset to the sandbox -with open("../dataset.csv", "rb") as f: - dataset_path_in_sandbox = sbx.files.write("dataset.csv", f) - - -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.notebook.exec_cell(ai_generated_code) - print('Code execution finished!') - - # First let's check if the code ran successfully. - if execution.error: - print('AI-generated code had an error.') - print(execution.error.name) - print(execution.error.value) - print(execution.error.traceback) - sys.exit(1) - - # Iterate over all the results and specifically check for png files that will represent the chart. - result_idx = 0 - for result in execution.results: - if result.png: - # Save the png to a file - # The png is in base64 format. - with open(f'chart-{result_idx}.png', 'wb') as f: - f.write(base64.b64decode(result.png)) - print(f'Chart saved to chart-{result_idx}.png') - result_idx += 1 - -prompt = f""" -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at {dataset_path_in_sandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. -Write Python code that analyzes the dataset based on my request and produces right chart accordingly""" - -anthropic = Anthropic() -print("Waiting for model response...") -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ], - tools=[ - { - "name": "run_python_code", - "description": "Run Python code", - "input_schema": { - "type": "object", - "properties": { - "code": { "type": "string", "description": "The Python code to run" }, - }, - "required": ["code"] - } - } - ] -) - -for content_block in msg.content: - if content_block.type == "tool_use": - if content_block.name == "run_python_code": - code = content_block.input["code"] - print("Will run following code in the sandbox", code) - # Execute the code in the sandbox - run_ai_generated_code(code) - -``` - diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json b/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json deleted file mode 100644 index a97598499..000000000 --- a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Analyze data with AI", - "pages": ["index", "pre-installed-libraries"] -} diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx b/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx deleted file mode 100644 index 73418feda..000000000 --- a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Pre-installed libraries ---- - -The sandbox comes with a [set of pre-installed Python libraries](https://github.com/e2b-dev/code-interpreter/blob/main/template/requirements.txt) for data analysis -but you can [install additional packages](/docs/quickstart/install-custom-packages): -- `aiohttp` (v3.9.3) -- `beautifulsoup4` (v4.12.3) -- `bokeh` (v3.3.4) -- `gensim` (v4.3.2) -- `imageio` (v2.34.0) -- `joblib` (v1.3.2) -- `librosa` (v0.10.1) -- `matplotlib` (v3.8.3) -- `nltk` (v3.8.1) -- `numpy` (v1.26.4) -- `opencv-python` (v4.9.0.80) -- `openpyxl` (v3.1.2) -- `pandas` (v1.5.3) -- `plotly` (v5.19.0) -- `pytest` (v8.1.0) -- `python`-docx (v1.1.0) -- `pytz` (v2024.1) -- `requests` (v2.26.0) -- `scikit-image` (v0.22.0) -- `scikit-learn` (v1.4.1.post1) -- `scipy` (v1.12.0) -- `seaborn` (v0.13.2) -- `soundfile` (v0.12.1) -- `spacy` (v3.7.4) -- `textblob` (v0.18.0) -- `tornado` (v6.4) -- `urllib3` (v1.26.7) -- `xarray` (v2024.2.0) -- `xlrd` (v2.0.1) -- `sympy` (v1.12) diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx b/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx deleted file mode 100644 index 78de55f57..000000000 --- a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Overview -description: E2B Sandbox allows you to create charts and visualizations by executing Python code inside the sandbox with `runCode()` method in JavaScript and `run_code()` method in Python. ---- - -These charts and visualizations can be [static](/docs/code-interpreting/create-charts-visualizations/static-charts) or [interactive](/docs/code-interpreting/create-charts-visualizations/interactive-charts) plots. - -{/* -Learn more about different types of results that E2B Sandbox can return [(TODO: link)here](/docs/code-interpreting/results). - */} - diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx b/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx deleted file mode 100644 index c436fa005..000000000 --- a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Interactive charts -description: E2B allows you to create interactive charts with custom styling. ---- - -E2B automatically detects charts when executing Python code with `runCode()` in JavaScript or `run_code()` in Python. The Python code must include Matplotlib charts. - -When a chart is detected, E2B sends the data of the chart back to the client. You can access the chart in the `execution.results` array where each item is a `Result` object with the `chart` property. - - -Try out [AI Data Analyst](https://github.com/e2b-dev/ai-analyst/) - a Next.js app that uses E2B to create interactive charts. - - -Here's a simple example of bar chart: - -```js tab -import { Sandbox, BarChart } from '@e2b/code-interpreter' - -const code = ` -import matplotlib.pyplot as plt - -# Prepare data -authors = ['Author A', 'Author B', 'Author C', 'Author D'] -sales = [100, 200, 300, 400] - -# Create and customize the bar chart -plt.figure(figsize=(10, 6)) -plt.bar(authors, sales, label='Books Sold', color='blue') -plt.xlabel('Authors') -plt.ylabel('Number of Books Sold') -plt.title('Book Sales by Authors') - -# Display the chart -plt.tight_layout() -plt.show() -` - -const sandbox = await Sandbox.create() -const result = await sandbox.runCode(code) -const chart = result.results[0].chart as BarChart - -console.log('Type:', chart.type) -console.log('Title:', chart.title) -console.log('X Label:', chart.x_label) -console.log('Y Label:', chart.y_label) -console.log('X Unit:', chart.x_unit) -console.log('Y Unit:', chart.y_unit) -console.log('Elements:', chart.elements) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -code = """ -import matplotlib.pyplot as plt - -# Prepare data -authors = ['Author A', 'Author B', 'Author C', 'Author D'] -sales = [100, 200, 300, 400] - -# Create and customize the bar chart -plt.figure(figsize=(10, 6)) -plt.bar(authors, sales, label='Books Sold', color='blue') -plt.xlabel('Authors') -plt.ylabel('Number of Books Sold') -plt.title('Book Sales by Authors') - -# Display the chart -plt.tight_layout() -plt.show() -""" - -sandbox = Sandbox() -execution = sandbox.run_code(code) -chart = execution.results[0].chart - -print('Type:', chart.type) -print('Title:', chart.title) -print('X Label:', chart.x_label) -print('Y Label:', chart.y_label) -print('X Unit:', chart.x_unit) -print('Y Unit:', chart.y_unit) -print('Elements:') -for element in chart.elements: - print('\n Label:', element.label) - print(' Value:', element.value) - print(' Group:', element.group) -``` - - -The code above will output the following: - -```bash tab -Type: bar -Title: Book Sales by Authors -X Label: Authors -Y Label: Number of Books Sold -X Unit: null -Y Unit: null -Elements: [ - { - label: "Author A", - group: "Books Sold", - value: 100, - }, { - label: "Author B", - group: "Books Sold", - value: 200, - }, { - label: "Author C", - group: "Books Sold", - value: 300, - }, { - label: "Author D", - group: "Books Sold", - value: 400, - } -] -``` - -```bash tab -Type: ChartType.BAR -Title: Book Sales by Authors -X Label: Authors -Y Label: Number of Books Sold -X Unit: None -Y Unit: None -Elements: - - Label: Author A - Value: 100.0 - Group: Books Sold - - Label: Author B - Value: 200.0 - Group: Books Sold - - Label: Author C - Value: 300.0 - Group: Books Sold - - Label: Author D - Value: 400.0 - Group: Books Sold -``` - - -You can send this data to your frontend to create an interactive chart with your favorite charting library. - ---- - -## Supported intertactive charts -The following charts are currently supported: -- Line chart -- Bar chart -- Scatter plot -- Pie chart -- Box and whisker plot - - -{/* The following charts are currently supported: -- [Line chart](#line-chart) -- [Bar chart](#bar-chart) -- [Scatter plot](#scatter-plot) -- [Pie chart](#pie-chart) -- [Box and whisker plot](#box-and-whisker-plot) - - -## Line chart - -## Bar chart - -## Scatter plot - -## Pie chart - -## Box and whisker plot */} \ No newline at end of file diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json b/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json deleted file mode 100644 index da28260ea..000000000 --- a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Create charts & visualizations", - "pages": ["index", "static-charts", "interactive-charts"] -} diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx b/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx deleted file mode 100644 index adb55485c..000000000 --- a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Static charts ---- - -Every time you run Python code with `runCode()` in JavaScript or `run_code()` method in Python, the code is executed in a headless Jupyter server inside the sandbox. - -E2B automatically detects any plots created with Matplotlib and sends them back to the client as images encoded in the base64 format. -These images are directly accesible on the `result` items in the `execution.results` array. - -Here's how to retrieve a static chart from the executed Python code that contains a Matplotlib plot. - -```js tab -import { Sandbox } from '@e2b/code-interpreter' -import fs from 'fs' - -const codeToRun = ` -import matplotlib.pyplot as plt - -plt.plot([1, 2, 3, 4]) -plt.ylabel('some numbers') -plt.show() -` -const sandbox = await Sandbox.create() - -// Run the code inside the sandbox -const execution = await sandbox.runCode(codeToRun) - -// There's only one result in this case - the plot displayed with `plt.show()` -const firstResult = execution.results[0] - -if (firstResult.png) { - // Save the png to a file. The png is in base64 format. - fs.writeFileSync('chart.png', firstResult.png, { encoding: 'base64' }) - console.log('Chart saved as chart.png') -} -``` - -```python tab -import base64 -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() - -code_to_run = """ -import matplotlib.pyplot as plt - -plt.plot([1, 2, 3, 4]) -plt.ylabel('some numbers') -plt.show() -""" - -sandbox = Sandbox() - -# Run the code inside the sandbox -execution = sandbox.run_code(code_to_run) - -# There's only one result in this case - the plot displayed with `plt.show()` -first_result = execution.results[0] - -if first_result.png: - // Save the png to a file. The png is in base64 format. - with open('chart.png', 'wb') as f: - f.write(base64.b64decode(first_result.png)) - print('Chart saved as chart.png') -``` - - -The code in the variable `codeToRun`/`code_to_run` will produce this following plot that we're saving as `chart.png` file. -![Static chart produced by the code](/graphics/docs/static-chart.png) \ No newline at end of file diff --git a/src/content/docs/(documentation)/code-interpreting/supported-languages/meta.json b/src/content/docs/(documentation)/code-interpreting/supported-languages/meta.json deleted file mode 100644 index 3a2900ea8..000000000 --- a/src/content/docs/(documentation)/code-interpreting/supported-languages/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Supported languages", - "pages": ["index"] -} diff --git a/src/content/docs/(documentation)/index.mdx b/src/content/docs/(documentation)/index.mdx deleted file mode 100644 index 7c864d781..000000000 --- a/src/content/docs/(documentation)/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: E2B Documentation -description: Here you'll find all the guides, concepts, and SDK references for developing with E2B. ---- - - - -```bash tab -npm i @e2b/code-interpreter -``` - -```bash tab -pip install e2b-code-interpreter -``` - - - -## What is E2B? - -E2B is an [open-source](https://github.com/e2b-dev) infrastructure that allows you run to AI-generated code in secure isolated sandboxes in the cloud. -To start and control sandboxes, use our [Python SDK](https://pypi.org/project/e2b/) or [JavaScript SDK](https://www.npmjs.com/package/e2b). - -Some of the typical use cases for E2B are AI data analysis or visualization, running AI-generated code of various languages, playground for coding agents, environment for codegen evals, or running full AI-generated apps like in [Fragments](https://github.com/e2b-dev/fragments). - -### Under the hood - -The E2B Sandbox is a small isolated VM the can be started very quickly (~150ms). You can think of it as a small computer for the AI model. You can run many sandboxes at once. Typically, you run separate sandbox for each LLM, user, or AI agent session in your app. -For example, if you were building an AI data analysis chatbot, you would start the sandbox for every user session. - -## Quickstart - -## Code interpreting with AI - -## Learn the core concepts diff --git a/src/content/docs/(documentation)/meta.json b/src/content/docs/(documentation)/meta.json deleted file mode 100644 index 534af30f4..000000000 --- a/src/content/docs/(documentation)/meta.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "Documentation", - "description": "The E2B documentation", - "root": true, - "defaultOpen": true, - "pages": [ - "---Home---", - "quickstart", - "quickstart/migrating-from-v0", - "--- ---", - "---Code Interpreting---", - "code-interpreting/analyze-data-with-ai", - "code-interpreting/create-charts-visualizations" - ] -} diff --git a/src/content/docs/(documentation)/quickstart/connect-llms.mdx b/src/content/docs/(documentation)/quickstart/connect-llms.mdx deleted file mode 100644 index f5b7dbac8..000000000 --- a/src/content/docs/(documentation)/quickstart/connect-llms.mdx +++ /dev/null @@ -1,772 +0,0 @@ ---- -title: Connect LLMs to E2B -description: E2B can work with any LLM and AI framework. The easiest way to connect an LLM to E2B is to use the tool use capabilities of the LLM (sometimes known as function calling). ---- - -If the LLM doesn't support tool use, you can, for example, prompt the LLM to output code snippets and then manually extract the code snippets with [RegEx](https://en.wikipedia.org/wiki/Regular_expression). - -## Contents -- [OpenAI](#openai) -- [Anthropic](#anthropic) -- [Mistral](#mistral) -- [Groq](#groq) -- [Vercel AI SDK](#vercel-ai-sdk) -- [CrewAI](#crewai) -- [LangChain](#langchain) -- [LlamaIndex](#llamaindex) -- [Ollama](#ollama) - ---- - -## OpenAI - -### Simple - - - -```python tab -# pip install openai e2b-code-interpreter -from openai import OpenAI -from e2b_code_interpreter import Sandbox - -# Create OpenAI client -client = OpenAI() -system = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send messages to OpenAI API -response = client.chat.completions.create( - model="gpt-4o", - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": prompt} - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -if code: - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - - print(result) -``` - - - -### Function calling - - - -```python tab -# pip install openai e2b-code-interpreter -import json -from openai import OpenAI -from e2b_code_interpreter import Sandbox - -# Create OpenAI client -client = OpenAI() -model = "gpt-4o" - -# Define the messages -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "type": "function", - "function": { - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return result", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } - } -}] - -# Generate text with OpenAI -response = client.chat.completions.create( - model=model, - messages=messages, - tools=tools, -) - -# Append the response message to the messages list -response_message = response.choices[0].message -messages.append(response_message) - -# Execute the tool if it's called by the model -if response_message.tool_calls: - for tool_call in response_message.tool_calls: - if tool_call.function.name == "execute_python": - # Create a sandbox and execute the code - with Sandbox() as sandbox: - code = json.loads(tool_call.function.arguments)['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Send the result back to the model - messages.append({ - "role": "tool", - "name": "execute_python", - "content": result, - "tool_call_id": tool_call.id, - }) - -# Generate the final response -final_response = client.chat.completions.create( - model=model, - messages=messages -) - -print(final_response.choices[0].message.content) -``` - - - ---- - -## Anthropic - -### Simple - - - -```python tab -# pip install anthropic e2b-code-interpreter -from anthropic import Anthropic -from e2b_code_interpreter import Sandbox - -# Create Anthropic client -anthropic = Anthropic() -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send messages to Anthropic API -response = anthropic.messages.create( - model="claude-3-5-sonnet-20240620", - max_tokens=1024, - messages=[ - {"role": "assistant", "content": system_prompt}, - {"role": "user", "content": prompt} - ] -) - -# Extract code from response -code = response.content[0].text - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.logs.stdout - -print(result) -``` - - - -### Function calling - - - -```python tab -# pip install anthropic e2b-code-interpreter -from anthropic import Anthropic -from e2b_code_interpreter import Sandbox - -# Create Anthropic client -client = Anthropic() -model = "claude-3-5-sonnet-20240620" - -# Define the messages -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return (not print) the result", - "input_schema": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } -}] - -# Generate text with Anthropic -message = client.messages.create( - model=model, - max_tokens=1024, - messages=messages, - tools=tools -) - -# Append the response message to the messages list -messages.append({ - "role": "assistant", - "content": message.content -}) - -# Execute the tool if it's called by the model -if message.stop_reason == "tool_use": - tool_use = next(block for block in message.content if block.type == "tool_use") - tool_name = tool_use.name - tool_input = tool_use.input - - if tool_name == "execute_python": - with Sandbox() as sandbox: - code = tool_input['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Append the tool result to the messages list - messages.append({ - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tool_use.id, - "content": result, - } - ], - }) - -# Generate the final response -final_response = client.messages.create( - model=model, - max_tokens=1024, - messages=messages, - tools=tools -) - -print(final_response.content[0].text) -``` - - - ---- - -## Mistral - -### Simple - - - -```python tab -# pip install mistralai e2b-code-interpreter -import os -from mistralai import Mistral -from e2b_code_interpreter import Sandbox - -# Create Mistral client -client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send the prompt to the model -response = client.chat.complete( - model="codestral-latest", - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": prompt} - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - -### Function calling - - - -```python tab -# pip install mistralai e2b-code-interpreter -import os -import json -from mistralai import Mistral -from e2b_code_interpreter import Sandbox - -# Create Mistral client -client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) -model = "mistral-large-latest" -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "type": "function", - "function": { - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return result", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } - } -}] - -# Send the prompt to the model -response = client.chat.complete( - model=model, - messages=messages, - tools=tools -) - -# Append the response message to the messages list -response_message = response.choices[0].message -messages.append(response_message) - -# Execute the tool if it's called by the model -if response_message.tool_calls: - for tool_call in response_message.tool_calls: - if tool_call.function.name == "execute_python": - # Create a sandbox and execute the code - with Sandbox() as sandbox: - code = json.loads(tool_call.function.arguments)['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Send the result back to the model - messages.append({ - "role": "tool", - "name": "execute_python", - "content": result, - "tool_call_id": tool_call.id, - }) - -# Generate the final response -final_response = client.chat.complete( - model=model, - messages=messages, -) - -print(final_response.choices[0].message.content) -``` - - - ---- - -## Groq - - -```python tab -# pip install groq e2b-code-interpreter -import os -from groq import Groq -from e2b_code_interpreter import Sandbox - -api_key = os.environ["GROQ_API_KEY"] - -# Create Groq client -client = Groq(api_key=api_key) -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry.'" - -# Send the prompt to the model -response = client.chat.completions.create( - model="llama3-70b-8192", - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": prompt}, - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - ---- - -## Vercel AI SDK -Vercel's [AI SDK](https://sdk.vercel.ai) offers support for multiple different LLM providers through a unified JavaScript interface that's easy to use. - -### Simple - - - -```js tab -// npm install ai @ai-sdk/openai @e2b/code-interpreter -import { openai } from '@ai-sdk/openai' -import { generateText } from 'ai' -import { Sandbox } from '@e2b/code-interpreter' - -// Create OpenAI client -const model = openai('gpt-4o') -const system = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -const prompt = "Calculate how many r's are in the word 'strawberry'" - -// Generate code with OpenAI -const { text: code } = await generateText({ - model, - system, - prompt -}) - -// Run the code in E2B Sandbox -const sandbox = await Sandbox.create() -const { text, results, logs, error } = await sandbox.runCode(code) - -console.log(text) -``` - - - -### Function calling - - - -```js tab -// npm install ai @ai-sdk/openai zod @e2b/code-interpreter -import { openai } from '@ai-sdk/openai' -import { generateText } from 'ai' -import z from 'zod' -import { Sandbox } from '@e2b/code-interpreter' - -// Create OpenAI client -const model = openai('gpt-4o') - -const prompt = "Calculate how many r's are in the word 'strawberry'" - -// Generate text with OpenAI -const { text } = await generateText({ - model, - prompt, - tools: { - // Define a tool that runs code in a sandbox - execute_python: { - description: 'Execute python code in a Jupyter notebook cell and return result', - parameters: z.object({ - code: z.string().describe('The python code to execute in a single cell'), - }), - execute: async ({ code }) => { - // Create a sandbox, execute LLM-generated code, and return the result - const sandbox = await Sandbox.create() - const { text, results, logs, error } = await sandbox.runCode(code) - return results - }, - }, - }, - // This is required to feed the tool call result back to the LLM - maxSteps: 2 -}) - -console.log(text) -``` - - - ---- - -## CrewAI -[CrewAI](https://crewai.com/) is a platform for building AI agents. - - - -```python tab -# pip install crewai crewai[tools] e2b-code-interpreter -from crewai_tools import tool -from crewai import Agent, Task, Crew, LLM -from e2b_code_interpreter import Sandbox - -@tool("Python interpreter tool") -def execute_python(code: str): - """ - Execute Python code and return the results. - """ - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define the agent -python_executor = Agent( - role='Python Executor', - goal='Execute Python code and return the results', - backstory='You are an expert Python programmer capable of executing code and returning results.', - tools=[execute_python], - llm=LLM(model="gpt-4o") -) - -# Define the task -execute_task = Task( - description="Calculate how many r's are in the word 'strawberry'", - agent=python_executor, - expected_output="The number of r's in the word 'strawberry'" -) - -# Create the crew -code_execution_crew = Crew( - agents=[python_executor], - tasks=[execute_task], - verbose=True, -) - -# Run the crew -result = code_execution_crew.kickoff() -print(result) -``` - - - ---- - -## LangChain -[LangChain](https://langchain.com/) offers support multiple different LLM providers. - -### Simple - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_openai import ChatOpenAI -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.output_parsers import StrOutputParser -from e2b_code_interpreter import Sandbox - -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Create LangChain components -llm = ChatOpenAI(model="gpt-4o") -prompt_template = ChatPromptTemplate.from_messages([ - ("system", system_prompt), - ("human", "{input}") -]) - -output_parser = StrOutputParser() - -# Create the chain -chain = prompt_template | llm | output_parser - -# Run the chain -code = chain.invoke({"input": prompt}) - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - -### Agent - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.tools import tool -from langchain.agents import create_tool_calling_agent, AgentExecutor -from langchain_openai import ChatOpenAI -from e2b_code_interpreter import Sandbox - -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Define the tool -@tool -def execute_python(code: str): - """ - Execute python code in a Jupyter notebook. - """ - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define LangChain components -prompt_template = ChatPromptTemplate.from_messages([ - ("system", system_prompt), - ("human", "{input}"), - ("placeholder", "{agent_scratchpad}"), -]) - -tools = [execute_python] -llm = ChatOpenAI(model="gpt-4o", temperature=0) - -agent = create_tool_calling_agent(llm, tools, prompt_template) -agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) - -# Run the agent -agent_executor.invoke({"input": prompt}) -``` - - - -### Function calling - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_openai import ChatOpenAI -from langchain.tools import Tool -from langchain.schema import HumanMessage, AIMessage, FunctionMessage -from e2b_code_interpreter import Sandbox - -def execute_python(code: str): - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define a tool that uses the E2B Sandbox -e2b_sandbox_tool = Tool( - name="execute_python", - func=execute_python, - description="Execute python code in a Jupyter notebook cell and return result" -) - -# Initialize the language model and bind the tool -llm = ChatOpenAI(model="gpt-4o").bind_tools([e2b_sandbox_tool]) - -# Define the messages -messages = [ - HumanMessage(content="Calculate how many 'r's are in the word 'strawberry'.") -] - -# Run the model with a prompt -result = llm.invoke(messages) -messages.append(AIMessage(content=result.content)) - -# Check if the model called the tool -if result.additional_kwargs.get('tool_calls'): - tool_call = result.additional_kwargs['tool_calls'][0] - if tool_call['function']['name'] == "execute_python": - code = tool_call['function']['arguments'] - execution_result = execute_python(code) - - # Send the result back to the model - messages.append( - FunctionMessage(name="execute_python", content=execution_result) - ) - -final_result = llm.invoke(messages) -print(final_result.content) -``` - - - ---- - -## LlamaIndex -[LlamaIndex](https://www.llamaindex.ai/) offers support multiple different LLM providers. - - -```python tab -# pip install llama-index e2b-code-interpreter -from llama_index.core.tools import FunctionTool -from llama_index.llms.openai import OpenAI -from llama_index.core.agent import ReActAgent -from e2b_code_interpreter import Sandbox - -# Define the tool -def execute_python(code: str): - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -e2b_sandbox_tool = FunctionTool.from_defaults( - name="execute_python", - description="Execute python code in a Jupyter notebook cell and return result", - fn=execute_python -) - -# Initialize LLM -llm = OpenAI(model="gpt-4o") - -# Initialize ReAct agent -agent = ReActAgent.from_tools([e2b_sandbox_tool], llm=llm, verbose=True) -agent.chat("Calculate how many r's are in the word 'strawberry'") -``` - - - -## Ollama - - - -```python tab -# pip install ollama -import ollama -from e2b_code_interpreter import Sandbox - -# Send the prompt to the model -response = ollama.chat( - model="llama3.2", - messages=[{ - "role": "system", - "content": "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." - }, - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -]) - -# Extract the code from the response -code = response['message']['content'] - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.logs.stdout - -print(result) -``` - - diff --git a/src/content/docs/(documentation)/quickstart/index.mdx b/src/content/docs/(documentation)/quickstart/index.mdx deleted file mode 100644 index 3b3daed45..000000000 --- a/src/content/docs/(documentation)/quickstart/index.mdx +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Running your first Sandbox -description: This guide will show you how to start your first E2B Sandbox. ---- - -## 1. Create E2B account - -Every new E2B account get $100 in credits. You can sign up [here](e2b.dev/sign-up). - - -## 2. Set your environment variables -1. Navigate to the [E2B Dashboard](/dashboard?tab=keys). -2. Copy your API key. -3. Paste your E2B API key into your `.env` file. -```bash .env -E2B_API_KEY=e2b_*** -``` - -## 3. Install E2B SDK -Install the E2B SDK to your project by running the following command in your terminal. - - -```bash tab -npm i @e2b/code-interpreter dotenv -``` - -```bash tab -pip install e2b-code-interpreter python-dotenv -``` - - - - -## 4. Write code for starting Sandbox -We'll write the minimal code for starting Sandbox, executing Python inside it and listing all files inside the root directory. - - -```ts tab title="index.js" -import 'dotenv/config' -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() // By default the sandbox is alive for 5 minutes -const execution = await sbx.runCode('print("hello world")') // Execute Python inside the sandbox -console.log(execution.logs) - -const files = await sbx.files.list('/') -console.log(files) -``` - -```python tab title="main.py" -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() # By default the sandbox is alive for 5 minutes -execution = sbx.run_code("print('hello world')") # Execute Python inside the sandbox -print(execution.logs) - -files = sbx.files.list("/") -print(files) -``` - - - - -## 5. Start your first E2B Sandbox -Run the code with the following command: - - -```bash tab -npx tsx ./index.ts -``` - -```bash tab -python main.py -``` - - - diff --git a/src/content/docs/(documentation)/quickstart/install-custom-packages.mdx b/src/content/docs/(documentation)/quickstart/install-custom-packages.mdx deleted file mode 100644 index 857307bb1..000000000 --- a/src/content/docs/(documentation)/quickstart/install-custom-packages.mdx +++ /dev/null @@ -1,198 +0,0 @@ ---- -title: Install custom packages -description: Here you'll find two ways to install custom packages in the E2B Sandbox. ---- - -There are two ways to install custom packages in the E2B Sandbox. - -1. [Create custom sandbox with preinstalled packages](#create-a-custom-sandbox). -2. [Install packages during the sandbox runtime](#install-packages-during-the-sandbox-runtime). - ---- - -## Create a custom sandbox - -Use this option if you know beforehand what packages you need in the sandbox. - -Prerequisites: -- E2B CLI -- Docker running - - -Custom sandbox template is a Docker image that we automatically convert to a sandbox that you can then start with our SDK. - - -### 1. Install E2B CLI -Install the [E2B CLI](https://npmjs.com/package/@e2b/cli) globally on your machine with NPM. - - -```bash tab -npm i -g @e2b/cli -``` - - - -### 2. Login to E2B CLI -Before you can create a custom sandbox, you need to login to E2B CLI. - - -```bash tab -e2b auth login -``` - - - -### 2. Initialize a sandbox template - - -```bash tab -e2b template init -``` - - - -### 3. Specify the packages you need in `e2b.Dockerfile` -Edit the E2B Dockerfile to install the packages you need. - - -You need to use the `e2bdev/code-interpreter:latest` base image. - - - - -```dockerfile tab -FROM e2bdev/code-interpreter:latest - -RUN pip install cowsay -RUN npm install cowsay -``` - - - -### 4. Build the sandbox template -Run the following command to build the sandbox template. - - -```bash tab -e2b template build -c "/root/.jupyter/start-up.sh" -``` - - - -This will take a while, as it convert the Docker image to a sandbox which is a small VM. -At the end of the process you will see the sandbox ID like this: -``` -Running postprocessing. It can take up to few minutes. - -Postprocessing finished. - -✅ Building sandbox template YOUR_TEMPLATE_ID finished. -``` - -### 5. Start your custom sandbox -Now you can pass the template ID to the SDK to start your custom sandbox. - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create({ - template: 'YOUR_TEMPLATE_ID', -}) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox(template='YOUR_TEMPLATE_ID') -``` - - - ---- - -## Install packages during the sandbox runtime -Use this option if don't know beforehand what packages you need in the sandbox. You can install packages with the package manager of your choice. - - -The packages installed during the runtime are available only in the running sandbox instance. -When you start a new sandbox instance, the packages are not be available. - - -### 1. Install Python packages with PIP - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -sbx.commands.run('pip install cowsay') // This will install the cowsay package -sbx.runCode(` - import cowsay - cowsay.cow("Hello, world!") -`) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("pip install cowsay") // This will install the cowsay package -sbx.run_code(""" - import cowsay - cowsay.cow("Hello, world!") -""") -``` - - - -### 2. Install Node.js packages with NPM - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -sbx.commands.run('npm install cowsay') // This will install the cowsay package -sbx.runCode(` - const cowsay = require('cowsay') - console.log(cowsay.say({ text: 'Hello, world!' })) -`, { language: 'javascript' }) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("npm install cowsay") // This will install the cowsay package -sbx.run_code(""" - import { say } from 'cowsay' - console.log(say('Hello, world!')) -""", language="javascript") -``` - - - -### 3. Install packages with package manager of your choice -Since E2B Sandboxes are Debian based machines, you can use any package manager supported by Debian. -You just need to make sure that the package manager is already installed in the sandbox. - -For example, to install `curl` and `git`, you can use the following commands: - - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -await sbx.commands.run('apt-get update && apt-get install -y curl git') -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("apt-get update && apt-get install -y curl git") -``` - - \ No newline at end of file diff --git a/src/content/docs/(documentation)/quickstart/meta.json b/src/content/docs/(documentation)/quickstart/meta.json deleted file mode 100644 index ef625cb13..000000000 --- a/src/content/docs/(documentation)/quickstart/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Quickstart", - "description": "This guide will show you how to start your first E2B Sandbox.", - "pages": ["index", "...", "!migrating-from-v0"] -} diff --git a/src/content/docs/(documentation)/quickstart/migrating-from-v0.mdx b/src/content/docs/(documentation)/quickstart/migrating-from-v0.mdx deleted file mode 100644 index 22ce60eee..000000000 --- a/src/content/docs/(documentation)/quickstart/migrating-from-v0.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Migrating E2B SDK from v0.* to v1.0 using Grit ---- - -This guide explains how to migrate your project from **E2B SDK `v0.*`** to **E2B SDK `v1.0`** using [Grit](https://www.grit.io/) and our custom migrations. -This mostly automates the process of searching through your codebase to update code to migrate to the new SDK without errors. However, this is not a 100% accurate process, and you should expect human intervention to be required. - -## Step 1: Install the Grit CLI - -You can install **Grit CLI** using one of the following methods: - -### Option 1: Install via NPM - -```package-install -npm install --location=global @getgrit/cli -``` - -### Option 2: Install via bash script - -```bash -curl -fsSL https://docs.grit.io/install | bash -``` - -## Step 2: Prepare for migration - -Before applying the migration, format your code and commit your changes to ensure you have a stable state to revert to if necessary. - -```bash -git add . -git commit -m "Last changes made" -``` - -## Step 3: Run Grit with the Custom Pattern - -To apply the custom migration pattern, run the following command for your project. The `--interactive` flag allows you to review each change as it is made. - -### For JavaScript/TypeScript: - -```bash -grit apply github.com/e2b-dev/e2b-cookbook#e2b_v0_to_v1_js --interactive -``` - -### For Python: - -```bash -grit apply github.com/e2b-dev/e2b-cookbook#e2b_v0_to_v1_py --interactive -``` - -## Step 5: Review changes - -Once the migration is applied, review the changes for possible mistakes, including code that was broken (false positives) or outdated code that was not fixed (false negatives) by the migration. Such issues you may encounter include: - -- ⚠️ If non-E2B objects with `.close()` methods, `.id` attributes, etc. exist in files that import E2B libraries, they will be incorrectly changed. You need to reject these changes manually. -- ⚠️ The output structure of `Sandbox.list()` has changed in the new SDK. You must manually rewrite your code to account for this. -- ⚠️ The global `cwd` option no longer exists when creating a Sandbox. You must manually rewrite your code to include the `cwd` option with each command. - -You should manually review all changes made by Grit to fix these "gotchas" and others which are sure to exist. - -## Step 6: Commit changes - -Before commiting the changes made by Grit, it's recommended to re-format the generated code following the convention of your choice. After verifying the changes and reformatting code, commit them to Git. - -```bash -git add . -git commit -m "Migrate project to E2B SDK v1.0" -``` - -## Step 7: Test your application - -Now, thoroughly test your application to make sure the migration is successful and everything is functioning as expected. - ---- - -By following these steps, you can easily migrate your TypeScript or Python project from **E2B SDK `v0.*`** to **E2B SDK `v1.0`** using Grit. Don’t forget to use version control to track all changes throughout the process and to thoroughly test your application after migrating. diff --git a/src/content/docs/(documentation)/quickstart/upload-download-files.mdx b/src/content/docs/(documentation)/quickstart/upload-download-files.mdx deleted file mode 100644 index 953493c46..000000000 --- a/src/content/docs/(documentation)/quickstart/upload-download-files.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Upload & downloads files -description: E2B Sandbox allows you to upload and downloads file to and from the Sandbox. ---- - -An alternative way to get your data to the sandbox is to create a [custom sandbox template](/docs/sandbox-template). - -## Upload file - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -// Read local file -const content = fs.readFileSync('/local/file') - -const sbx = await Sandbox.create() -// Upload file to the sandbox to path '/home/user/my-file' -await sbx.files.write('/home/user/my-file', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() - -# Read local file -with open("/local/file", "rb") as file: - # Upload file to the sandbox to path '/home/user/my-file' - sbx.files.write("/home/user/my-file", file) -``` - - - -## Upload multiple files -Currently, if you want to upload multiple files, you need to upload each one of the separately. -We're working on a better solution. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -// Read local files -const fileA = fs.readFileSync('/local/file/a') -const fileB = fs.readFileSync('/local/file/b') - -const sbx = await Sandbox.create() -// Upload file A to the sandbox to path '/home/user/my-file-a' -await sbx.files.write('/home/user/my-file-a', content) -// Upload file B to the sandbox to path '/home/user/my-file-b' -await sbx.files.write('/home/user/my-file-b', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() - -# Read local file -with open("/local/file", "rb") as file: - # Upload file to the sandbox to path '/home/user/my-file' - sbx.files.write("/home/user/my-file", file) -``` - - - -## Upload directory -We currently don't support an easy way to upload a whole directory. -You need to upload each file separately. - -We're working on a better solution. - ---- - -## Download file -To download a file, you need to first get the file's content and then write it to a local file. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() -// Download file from the sandbox to path '/home/user/my-file' -const content = await sbx.files.read('/home/user/my-file') -// Write file to local path -fs.writeFileSync('/local/file', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() -# Download file from the sandbox to path '/home/user/my-file' -content = sbx.files.read('/home/user/my-file') -# Write file to local path -with open('/local/file', 'w') as file: - file.write(content) -``` - - - -## Download multiple files -To download multiple files, you need to download each one of them separately from the sandbox. - -We're working on a better solution. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() -// Download file A from the sandbox to path '/home/user/my-file' -const contentA = await sbx.files.read('/home/user/my-file-a') -// Write file A to local path -fs.writeFileSync('/local/file/a', contentA) - -// Download file B from the sandbox to path '/home/user/my-file' -const contentB = await sbx.files.read('/home/user/my-file-b') -// Write file B to local path -fs.writeFileSync('/local/file/b', contentB) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() -# Download file A from the sandbox to path '/home/user/my-file-a' -contentA = sbx.files.read('/home/user/my-file-a') -# Write file A to local path -with open('/local/file/a', 'w') as file: - file.write(contentA) - -# Download file B from the sandbox to path '/home/user/my-file-b' -contentB = sbx.files.read('/home/user/my-file-b') -# Write file B to local path -with open('/local/file/b', 'w') as file: - file.write(contentB) -``` - - - -## Download directory -We currently don't support an easy way to download a whole directory. -You need to download each file separately. - -We're working on a better solution. diff --git a/src/features/client-providers.tsx b/src/features/client-providers.tsx index 8b56776de..8c9499e11 100644 --- a/src/features/client-providers.tsx +++ b/src/features/client-providers.tsx @@ -2,7 +2,7 @@ import { ToastProvider } from '@/ui/primitives/toast' import { TooltipProvider } from '@/ui/primitives/tooltip' -import { RootProvider } from 'fumadocs-ui/provider' +import { ThemeProvider } from 'next-themes' import posthog from 'posthog-js' import { PostHogProvider as PHProvider } from 'posthog-js/react' import { useEffect } from 'react' @@ -14,18 +14,16 @@ interface ClientProvidersProps { export default function ClientProviders({ children }: ClientProvidersProps) { return ( - {children} - + ) } diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx new file mode 100644 index 000000000..9cf82e17e --- /dev/null +++ b/src/features/dashboard/sandbox/context.tsx @@ -0,0 +1,38 @@ +'use client' + +import React, { createContext, useContext, ReactNode } from 'react' +import { SandboxInfo } from '@/types/api' + +interface SandboxContextValue { + sandboxInfo: SandboxInfo +} + +const SandboxContext = createContext(null) + +export function useSandboxContext() { + const context = useContext(SandboxContext) + if (!context) { + throw new Error('useSandboxContext must be used within a SandboxProvider') + } + return context +} + +interface SandboxProviderProps { + children: ReactNode + sandboxInfo: SandboxInfo +} + +export function SandboxProvider({ + children, + sandboxInfo, +}: SandboxProviderProps) { + return ( + + {children} + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx new file mode 100644 index 000000000..ff61e6024 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -0,0 +1,238 @@ +'use client' + +import React, { + createContext, + useContext, + useRef, + ReactNode, + useLayoutEffect, + useMemo, +} from 'react' +import { createFilesystemStore, type FilesystemStore } from './filesystem/store' +import { FilesystemNode, FilesystemOperations } from './filesystem/types' +import { SandboxManager } from './sandbox-manager' +import { getParentPath, normalizePath } from '@/lib/utils/filesystem' +import { useSandboxContext } from '../context' +import Sandbox, { EntryInfo, FileType } from 'e2b' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { supabase } from '@/lib/clients/supabase/client' +import { useRouter } from 'next/navigation' +import { AUTH_URLS } from '@/configs/urls' + +interface SandboxInspectContextValue { + store: FilesystemStore + operations: FilesystemOperations +} + +const SandboxInspectContext = createContext( + null +) + +interface SandboxInspectProviderProps { + children: ReactNode + rootPath: string + teamId: string + seedEntries?: EntryInfo[] +} + +export function SandboxInspectProvider({ + children, + rootPath, + seedEntries, + teamId, +}: SandboxInspectProviderProps) { + const { sandboxInfo } = useSandboxContext() + const storeRef = useRef(null) + const sandboxManagerRef = useRef(null) + const operationsRef = useRef(null) + + const router = useRouter() + + /* + * ---------- synchronous store initialisation ---------- + * We want the tree to render immediately using the "seedEntries" streamed from the + * server component (see page.tsx). We therefore build / populate the Zustand store + * right here during render, instead of doing it later inside an effect. + */ + { + const normalizedRoot = normalizePath(rootPath) + const needsNewStore = + !storeRef.current || + storeRef.current.getState().rootPath !== normalizedRoot + + if (needsNewStore) { + // stop previous watcher (if any) + if (sandboxManagerRef.current) { + sandboxManagerRef.current.stopWatching() + sandboxManagerRef.current = null + } + + storeRef.current = createFilesystemStore(rootPath) + + const state = storeRef.current.getState() + + const rootName = + normalizedRoot === '/' ? '/' : normalizedRoot.split('/').pop() || '' + + state.addNodes(getParentPath(normalizedRoot), [ + { + name: rootName, + path: normalizedRoot, + type: FileType.DIR, + isExpanded: true, + children: [], + }, + ]) + + state.setLoaded(normalizedRoot, true) + + if (seedEntries && seedEntries.length) { + const seedNodes: FilesystemNode[] = seedEntries.map((entry) => { + const base = { + name: entry.name, + path: normalizePath(entry.path), + } + + if (entry.type === FileType.DIR) { + state.setLoaded(base.path, false) + + return { + ...base, + type: FileType.DIR, + isExpanded: false, + children: [], + } + } + + return { + ...base, + type: FileType.FILE, + } + }) + + state.addNodes(normalizedRoot, seedNodes) + } + + const store = storeRef.current + operationsRef.current = { + loadDirectory: async (path: string) => { + await sandboxManagerRef.current?.loadDirectory(path) + }, + selectNode: async (path: string) => { + const node = store.getState().getNode(path) + + if (!node) return + + if (node.type === FileType.FILE && !store.getState().isLoaded(path)) { + await sandboxManagerRef.current?.readFile(path) + } + + store.getState().setSelected(path) + }, + resetSelected: () => { + store.getState().setSelected(undefined) + }, + toggleDirectory: async (path: string) => { + const normalizedPath = normalizePath(path) + const state = store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.DIR) return + + const newExpandedState = !node.isExpanded + state.setExpanded(normalizedPath, newExpandedState) + + if (newExpandedState && !state.isLoaded(normalizedPath)) { + await sandboxManagerRef.current?.loadDirectory(normalizedPath) + } + }, + refreshDirectory: async (path: string) => { + await sandboxManagerRef.current?.refreshDirectory(path) + }, + refreshFile: async (path: string) => { + await sandboxManagerRef.current?.readFile(path) + }, + downloadFile: async (path: string) => { + const downloadUrl = + await sandboxManagerRef.current?.getDownloadUrl(path) + + if (!downloadUrl) return + + const node = store.getState().getNode(path) + + const a = document.createElement('a') + a.href = downloadUrl + a.download = node?.name || '' + a.target = '_blank' + a.click() + }, + } + } + } + + /* + * ---------- watcher (side-effect) initialisation / cleanup ---------- + */ + useLayoutEffect(() => { + const connectSandbox = async () => { + if (!storeRef.current) return + + // (re)create the sandbox-manager when sandbox / team / root changes + if (sandboxManagerRef.current) { + sandboxManagerRef.current.stopWatching() + } + + const { data } = await supabase.auth.getSession() + + if (!data || !data.session) { + router.replace(AUTH_URLS.SIGN_IN) + return + } + + const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { + domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, + headers: { + ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), + }, + }) + + sandboxManagerRef.current = new SandboxManager( + storeRef.current, + sandbox, + rootPath, + sandboxInfo.envdAccessToken !== undefined + ) + } + + connectSandbox() + + return () => { + sandboxManagerRef.current?.stopWatching() + } + }, [sandboxInfo.sandboxID, teamId, rootPath, router]) + + if (!storeRef.current || !operationsRef.current) { + return null // should never happen, but satisfies type-checker + } + + const contextValue: SandboxInspectContextValue = { + store: storeRef.current, + operations: operationsRef.current, + } + + return ( + + {children} + + ) +} + +export function useSandboxInspectContext(): SandboxInspectContextValue { + const context = useContext(SandboxInspectContext) + if (!context) { + throw new Error( + 'useSandboxInspectContext must be used within a SandboxInspectProvider' + ) + } + return context +} diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts new file mode 100644 index 000000000..2f3719578 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -0,0 +1,368 @@ +'use client' + +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { enableMapSet } from 'immer' +import { + normalizePath, + getParentPath, + isChildPath, + getBasename, +} from '@/lib/utils/filesystem' +import { FilesystemNode } from './types' +import { FileType } from 'e2b' + +enableMapSet() + +interface FilesystemStatics { + rootPath: string +} + +interface ContentFileContentState { + text: string + type: 'text' +} + +interface UnreadableFileContentState { + type: 'unreadable' +} + +interface ImageFileContentState { + dataUri: string + type: 'image' +} + +export type FileContentState = + | ContentFileContentState + | UnreadableFileContentState + | ImageFileContentState + +export type FileContentStateType = FileContentState['type'] + +// mutable state +export interface FilesystemState { + nodes: Map + selectedPath?: string + loadingPaths: Set + loadedPaths: Set + errorPaths: Map + sortingDirection: 'asc' | 'desc' + fileContents: Map +} + +// mutations/actions that modify state +export interface FilesystemMutations { + addNodes: (parentPath: string, nodes: FilesystemNode[]) => void + removeNode: (path: string) => void + updateNode: (path: string, updates: Partial) => void + setExpanded: (path: string, expanded: boolean) => void + setSelected: (path?: string) => void + setLoading: (path: string, loading: boolean) => void + setLoaded: (path: string, loaded: boolean) => void + setError: (path: string, error?: string) => void + setFileContent: (path: string, updates: FileContentState) => void + resetFileContent: (path: string) => void + reset: () => void +} + +// computed/derived values +export interface FilesystemComputed { + getChildren: (path: string) => FilesystemNode[] + getNode: (path: string) => FilesystemNode | undefined + isExpanded: (path: string) => boolean + isSelected: (path: string) => boolean + isLoaded: (path: string) => boolean + hasChildren: (path: string) => boolean + getFileContent: (path: string) => FileContentState | undefined +} + +// combined store type +export type FilesystemStoreData = FilesystemStatics & + FilesystemState & + FilesystemMutations & + FilesystemComputed + +// Retain reference-stable arrays of children per directory path. A cached array +// is only reused while the underlying `children` array reference on the node +// stays the same; any mutation that replaces `children` with a new array +// automatically invalidates the cache. +const childrenCache: Map = + new Map() + +function compareFilesystemNodes( + nodeA: FilesystemNode | undefined, + nodeB: FilesystemNode | undefined, + direction: 'asc' | 'desc' = 'asc' +): number { + if (!nodeA || !nodeB) return 0 + + if (nodeA.type === FileType.DIR && nodeB.type === FileType.FILE) return -1 + if (nodeA.type === FileType.FILE && nodeB.type === FileType.DIR) return 1 + + const cmp = nodeA.name.localeCompare(nodeB.name, undefined, { + sensitivity: 'base', + numeric: true, + }) + + return direction === 'asc' ? cmp : -cmp +} + +export const createFilesystemStore = (rootPath: string) => + create()( + immer((set, get) => ({ + rootPath: normalizePath(rootPath), + + nodes: new Map(), + loadingPaths: new Set(), + loadedPaths: new Set(), + errorPaths: new Map(), + sortingDirection: 'asc' as 'asc' | 'desc', + fileContents: new Map(), + + addNodes: (parentPath: string, nodes: FilesystemNode[]) => { + const normalizedParentPath = normalizePath(parentPath) + + set((state: FilesystemState) => { + let parentNode = state.nodes.get(normalizedParentPath) + + if (!parentNode) { + const parentName = getBasename(normalizedParentPath) + + parentNode = { + name: parentName, + path: normalizedParentPath, + type: FileType.DIR, + isExpanded: false, + children: [], + } + state.nodes.set(normalizedParentPath, parentNode) + } + + if (parentNode.type === FileType.FILE) { + throw new Error('Parent node is a file') + } + + const childrenSet = new Set(parentNode.children) + + for (const node of nodes) { + const normalizedPath = normalizePath(node.path) + + state.nodes.set(normalizedPath, { + ...node, + path: normalizedPath, + }) + + if (normalizedPath !== normalizedParentPath) { + childrenSet.add(normalizedPath) + } + } + + const newChildren = Array.from(childrenSet) + newChildren.sort((a: string, b: string) => + compareFilesystemNodes( + state.nodes.get(a), + state.nodes.get(b), + state.sortingDirection + ) + ) + + parentNode.children = newChildren + + childrenCache.delete(normalizedParentPath) + }) + }, + + removeNode: (path: string) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemStoreData) => { + const node = state.nodes.get(normalizedPath) + if (!node) return + + const parentPath = getParentPath(normalizedPath) + const parentNode = state.nodes.get(parentPath) + if (parentNode && parentNode.type === FileType.DIR) { + parentNode.children = parentNode.children.filter( + (childPath: string) => childPath !== normalizedPath + ) + + childrenCache.delete(parentPath) + } + + for (const [nodePath] of state.nodes) { + if ( + nodePath === normalizedPath || + isChildPath(normalizedPath, nodePath) + ) { + state.nodes.delete(nodePath) + state.loadingPaths.delete(nodePath) + state.errorPaths.delete(nodePath) + + if (state.selectedPath === nodePath) { + state.selectedPath = undefined + } + } + } + }) + }, + + updateNode: (path: string, updates: Partial) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + const node = state.nodes.get(normalizedPath) + if (node) { + Object.assign(node, updates) + } + }) + }, + + setExpanded: (path: string, expanded: boolean) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + const node = state.nodes.get(normalizedPath) + + if (!node) return + + if (node?.type === FileType.FILE) { + console.error('Cannot expand file', node) + return + } + + node.isExpanded = expanded + }) + }, + + setSelected: (path) => { + const normalizedPath = path ? normalizePath(path) : undefined + + set((state: FilesystemState) => { + state.selectedPath = normalizedPath + }) + }, + + setLoading: (path: string, loading: boolean) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + if (loading) { + state.loadingPaths.add(normalizedPath) + } else { + state.loadingPaths.delete(normalizedPath) + } + }) + }, + + setLoaded: (path: string, loaded: boolean) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + if (loaded) { + state.loadedPaths.add(normalizedPath) + } else { + state.loadedPaths.delete(normalizedPath) + } + }) + }, + + setError: (path: string, error?: string) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + if (error) { + state.errorPaths.set(normalizedPath, error) + } else { + state.errorPaths.delete(normalizedPath) + } + }) + }, + + setFileContent: (path, updates) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + state.fileContents.set(normalizedPath, updates) + }) + }, + + resetFileContent: (path: string) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + state.fileContents.delete(normalizedPath) + }) + }, + + reset: () => { + set((state: FilesystemState) => { + state.nodes.clear() + state.selectedPath = undefined + state.loadingPaths.clear() + state.errorPaths.clear() + state.fileContents.clear() + }) + }, + + getChildren: (path: string) => { + const normalizedPath = normalizePath(path) + const state = get() + const node = state.nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return [] + + const cached = childrenCache.get(normalizedPath) + if (cached && cached.ref === node.children) { + return cached.result + } + + const result = node.children + .map((childPath) => state.nodes.get(childPath)) + .filter((child): child is FilesystemNode => child !== undefined) + + childrenCache.set(normalizedPath, { ref: node.children, result }) + return result + }, + + getNode: (path: string) => { + const normalizedPath = normalizePath(path) + return get().nodes.get(normalizedPath) + }, + + isExpanded: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return false + + return !!node.isExpanded + }, + + isSelected: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node) return false + + return get().selectedPath === normalizedPath + }, + + isLoaded: (path: string) => { + const normalizedPath = normalizePath(path) + return get().loadedPaths.has(normalizedPath) + }, + + hasChildren: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return false + + return node.children.length > 0 + }, + + getFileContent: (path: string) => { + const normalizedPath = normalizePath(path) + return get().fileContents.get(normalizedPath) + }, + })) + ) + +export type FilesystemStore = ReturnType diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts new file mode 100644 index 000000000..c55b20c6e --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -0,0 +1,27 @@ +import { FileType } from 'e2b' + +interface FilesystemDir { + type: FileType.DIR + name: string + path: string + children: string[] // paths of children + isExpanded?: boolean +} + +interface FilesystemFile { + type: FileType.FILE + name: string + path: string +} + +export type FilesystemNode = FilesystemDir | FilesystemFile + +export interface FilesystemOperations { + loadDirectory: (path: string) => Promise + toggleDirectory: (path: string) => Promise + refreshDirectory: (path: string) => Promise + selectNode: (path: string) => void + resetSelected: () => void + refreshFile: (path: string) => Promise + downloadFile: (path: string) => Promise +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts new file mode 100644 index 000000000..b462f7781 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts @@ -0,0 +1,18 @@ +import { useStore } from 'zustand/react' +import { useSandboxInspectContext } from '../context' +import { useCallback } from 'react' + +export function useContent(path: string) { + const { store, operations } = useSandboxInspectContext() + + const contentState = useStore(store, (state) => state.getFileContent(path)) + + const refresh = useCallback(async () => { + await operations.refreshFile(path) + }, [path, operations]) + + return { + state: contentState, + refresh, + } +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts new file mode 100644 index 000000000..2d71462cc --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -0,0 +1,72 @@ +'use client' + +import { useMemo } from 'react' +import { useSandboxInspectContext } from '../context' +import { FilesystemNode } from '../filesystem/types' +import { useStore } from 'zustand' + +/** + * Hook for accessing directory children with automatic updates + */ +export function useDirectoryChildren(path: string): FilesystemNode[] { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.getChildren(path)) +} + +/** + * Hook for accessing directory state (expanded, loading, error) + */ +export function useDirectoryState(path: string) { + const { store } = useSandboxInspectContext() + + const isExpanded = useStore(store, (state) => state.isExpanded(path)) + const isLoading = useStore(store, (state) => state.loadingPaths.has(path)) + const hasError = useStore(store, (state) => state.errorPaths.has(path)) + const error = useStore(store, (state) => state.errorPaths.get(path)) + const isLoaded = useStore(store, (state) => state.isLoaded(path)) + const hasChildren = useStore(store, (state) => state.hasChildren(path)) + + return useMemo( + () => ({ + isExpanded, + isLoading, + hasError, + error, + isLoaded, + hasChildren, + }), + [isExpanded, isLoading, hasError, error, isLoaded, hasChildren] + ) +} + +/** + * Hook for directory operations + */ +export function useDirectoryOperations(path: string) { + const { operations } = useSandboxInspectContext() + + return useMemo( + () => ({ + toggle: () => operations.toggleDirectory(path), + load: () => operations.loadDirectory(path), + refresh: () => operations.refreshDirectory(path), + }), + [operations, path] + ) +} + +/** + * Combined hook for directory data and operations + */ +export function useDirectory(path: string) { + const children = useDirectoryChildren(path) + const state = useDirectoryState(path) + const ops = useDirectoryOperations(path) + + return { + children, + ...state, + ...ops, + } +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx new file mode 100644 index 000000000..8d82bdcc9 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useMemo } from 'react' +import { useSandboxInspectContext } from '../context' +import { useStore } from 'zustand' +import { useFilesystemNode, useSelectedPath } from './use-node' +import { FileType } from 'e2b' + +/** + * Hook for accessing file state (loading, error) + */ +export function useFileState(path: string) { + const { store } = useSandboxInspectContext() + + const isLoading = useStore(store, (state) => state.loadingPaths.has(path)) + const hasError = useStore(store, (state) => state.errorPaths.has(path)) + const error = useStore(store, (state) => state.errorPaths.get(path)) + const isSelected = useStore(store, (state) => state.isSelected(path)) + + return useMemo( + () => ({ + isLoading, + hasError, + error, + isSelected, + }), + [isLoading, hasError, error, isSelected] + ) +} + +/** + * Hook for file operations + */ +export function useFileOperations(path: string) { + const { operations } = useSandboxInspectContext() + const selectedPath = useSelectedPath() + + return useMemo( + () => ({ + refresh: () => operations.refreshFile(path), + toggle: () => { + if (selectedPath === path) { + operations.resetSelected() + } else { + operations.selectNode(path) + } + }, + download: () => operations.downloadFile(path), + }), + [operations, path, selectedPath] + ) +} + +/** + * Combined hook for file data and operations + */ +export function useFile(path: string) { + const node = useFilesystemNode(path) + const state = useFileState(path) + const ops = useFileOperations(path) + + return { + ...(node && { + name: node.name, + type: node.type, + path: node.path, + }), + ...state, + ...ops, + } +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts new file mode 100644 index 000000000..95879835b --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -0,0 +1,85 @@ +'use client' + +import { useMemo } from 'react' +import { useSandboxInspectContext } from '../context' +import type { FilesystemNode } from '../filesystem/types' +import { useStore } from 'zustand' + +/** + * Hook for accessing a specific filesystem node + */ +export function useFilesystemNode(path: string): FilesystemNode | undefined { + const { store } = useSandboxInspectContext() + + const node = useStore(store, (state) => state.getNode(path)) + + return node +} + +/** + * Hook for accessing node selection state + */ +export function useNodeSelection(path: string) { + const { store, operations } = useSandboxInspectContext() + + const isSelected = useStore(store, (state) => state.isSelected(path)) + + const select = useMemo( + () => () => operations.selectNode(path), + [operations, path] + ) + + return { + isSelected, + select, + } +} + +/** + * Combined hook for node data and operations + */ +export function useNode(path: string) { + const node = useFilesystemNode(path) + const selection = useNodeSelection(path) + + return { + node, + ...selection, + } +} + +/** + * Hook for getting root directory children (commonly used) + */ +export function useRootChildren() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.getChildren(state.rootPath)) +} + +/** + * Hook for getting selected node path + */ +export function useSelectedPath() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.selectedPath) +} + +/** + * Hook for getting all loading paths + */ +export function useLoadingPaths() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.loadingPaths) +} + +/** + * Hook for getting all error paths and their messages + */ +export function useErrorPaths() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.errorPaths) +} diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts new file mode 100644 index 000000000..f26697289 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -0,0 +1,384 @@ +import { + FileType, + type Sandbox, + type FilesystemEvent, + type WatchHandle, + type EntryInfo, + FilesystemEventType, +} from 'e2b' +import type { FilesystemStore } from './filesystem/store' +import { FilesystemNode } from './filesystem/types' +import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' +import { determineFileContentState } from '@/lib/utils/filesystem' + +export const HANDLED_ERRORS = { + 'signal timed out': 'The operation timed out. Please try again later.', + 'user aborted a request': 'The request was cancelled. Try downloading the file.', +} as const + +export class SandboxManager { + private watchHandle?: WatchHandle + private readonly rootPath: string + private store: FilesystemStore + private sandbox: Sandbox + private readonly isSandboxSecure: boolean = false + + private static readonly LOAD_DEBOUNCE_MS = 250 + private static readonly READ_DEBOUNCE_MS = 250 + + + private loadTimers: Map> = new Map() + private pendingLoads: Map< + string, + { + promise: Promise + resolve: () => void + reject: (err: unknown) => void + } + > = new Map() + + private readTimers: Map> = new Map() + private pendingReads: Map< + string, + { + promise: Promise + resolve: () => void + reject: (err: unknown) => void + } + > = new Map() + + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string, isSandboxSecure: boolean) { + this.store = store + this.sandbox = sandbox + this.rootPath = normalizePath(rootPath) + this.isSandboxSecure = isSandboxSecure + + // immediately start a single recursive watcher at the root + void this.startRootWatcher() + } + + private async startRootWatcher(): Promise { + if (this.watchHandle) return + + try { + this.watchHandle = await this.sandbox.files.watchDir( + this.rootPath, + (event) => this.handleFilesystemEvent(event), + { recursive: true, timeoutMs: 0 } + ) + } catch (error) { + console.error(`Failed to start root watcher on ${this.rootPath}:`, error) + throw error + } + } + + stopWatching(): void { + if (this.watchHandle) { + this.watchHandle.stop() + this.watchHandle = undefined + } + } + + private handleFilesystemEvent(event: FilesystemEvent): void { + const { type, name } = event + + // "name" is relative to the watched root; construct absolute path + const normalizedPath = normalizePath(joinPath(this.rootPath, name)) + const parentDir = normalizePath( + joinPath(this.rootPath, getParentPath(name)) + ) + + const state = this.store.getState() + const parentNode = state.getNode(parentDir) + + switch (type) { + case FilesystemEventType.CREATE: + case FilesystemEventType.RENAME: + if (parentNode && state.isLoaded(parentDir)) { + void this.refreshDirectory(parentDir) + } + break + + case FilesystemEventType.REMOVE: + this.handleRemoveEvent(normalizedPath) + break + + case FilesystemEventType.WRITE: + void this.readFile(normalizedPath) + break + + case FilesystemEventType.CHMOD: + break + + default: + console.warn(`Unknown filesystem event type: ${type}`) + break + } + } + + private handleRemoveEvent(removedPath: string): void { + const state = this.store.getState() + const node = state.getNode(removedPath) + + if (!node) return + + state.removeNode(removedPath) + + if (node?.type === FileType.FILE) { + state.resetFileContent(removedPath) + } + } + + async loadDirectory(path: string): Promise { + const normalizedPath = normalizePath(path) + + const node = this.store.getState().getNode(normalizedPath) + + if (node?.type === FileType.FILE) { + return + } + + let pending = this.pendingLoads.get(normalizedPath) + if (!pending) { + pending = SandboxManager.createDeferred() + this.pendingLoads.set(normalizedPath, pending) + } + + const state = this.store.getState() + + const isAlreadyLoading = state.loadingPaths.has(normalizedPath) + const existingTimer = this.loadTimers.get(normalizedPath) + + if (isAlreadyLoading || existingTimer) { + if (existingTimer) clearTimeout(existingTimer) + + const timer = setTimeout(async () => { + this.loadTimers.delete(normalizedPath) + try { + await this.loadDirectoryImmediate(normalizedPath) + pending.resolve() + } catch (err) { + pending.reject(err) + } finally { + this.pendingLoads.delete(normalizedPath) + } + }, SandboxManager.LOAD_DEBOUNCE_MS) + + this.loadTimers.set(normalizedPath, timer) + return pending.promise + } + + void this.loadDirectoryImmediate(normalizedPath) + .then(() => pending.resolve()) + .catch((err) => pending.reject(err)) + .finally(() => this.pendingLoads.delete(normalizedPath)) + + return pending.promise + } + + private async loadDirectoryImmediate(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if ( + !node || + node.type !== FileType.DIR || + state.loadingPaths.has(normalizedPath) + ) + return + + state.setLoading(normalizedPath, true) + state.setError(normalizedPath) // clear any previous errors + + try { + const entries = await this.sandbox.files.list(normalizedPath) + + const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { + if (entry.type === FileType.DIR) { + return { + name: entry.name, + path: entry.path, + type: FileType.DIR, + isExpanded: false, + isSelected: false, + children: [], + } + } else { + return { + name: entry.name, + path: entry.path, + type: FileType.FILE, + isSelected: false, + } + } + }) + + state.addNodes(normalizedPath, nodes) + + const newChildrenSet = new Set(nodes.map((n) => normalizePath(n.path))) + + for (const childPath of [...node.children]) { + if (!newChildrenSet.has(childPath)) { + state.removeNode(childPath) + } + } + } catch (error) { + const errorMessage = SandboxManager.pipeError( + error, + 'Failed to load directory' + ) + state.setError(normalizedPath, errorMessage) + console.error(`Failed to load directory ${normalizedPath}:`, error) + } finally { + state.setLoading(normalizedPath, false) + state.setLoaded(normalizedPath, true) + } + } + + async refreshDirectory(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + + const node = state.getNode(normalizedPath) + if (!node || node.type !== FileType.DIR) return + + await this.loadDirectory(normalizedPath) + } + + async readFile(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.FILE) return + + let pending = this.pendingReads.get(normalizedPath) + if (!pending) { + pending = SandboxManager.createDeferred() + this.pendingReads.set(normalizedPath, pending) + } + + const isAlreadyLoading = state.loadingPaths.has(normalizedPath) + const existingTimer = this.readTimers.get(normalizedPath) + + if (isAlreadyLoading || existingTimer) { + if (existingTimer) clearTimeout(existingTimer) + + const timer = setTimeout(async () => { + this.readTimers.delete(normalizedPath) + try { + await this.readFileImmediate(normalizedPath) + pending.resolve() + } catch (err) { + pending.reject(err) + } finally { + this.pendingReads.delete(normalizedPath) + } + }, SandboxManager.READ_DEBOUNCE_MS) + + this.readTimers.set(normalizedPath, timer) + return pending.promise + } + + void this.readFileImmediate(normalizedPath) + .then(() => pending.resolve()) + .catch((err) => pending.reject(err)) + .finally(() => this.pendingReads.delete(normalizedPath)) + + return pending.promise + } + + private async readFileImmediate(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.FILE) return + + try { + state.setLoading(normalizedPath, true) + + const blob = await this.sandbox.files.read(normalizedPath, { + format: 'blob', + requestTimeoutMs: 30_000, + }) + + const contentState = await determineFileContentState(blob) + + state.setFileContent(normalizedPath, contentState) + } catch (err) { + const errorMessage = SandboxManager.pipeError(err, 'Failed to read file') + + console.error(`Failed to read file ${normalizedPath}:`, err) + + state.setError(normalizedPath, errorMessage) + state.setFileContent(normalizedPath, { type: 'unreadable' }) + } finally { + state.setLoading(normalizedPath, false) + state.setLoaded(normalizedPath, true) + } + } + + async getDownloadUrl(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.FILE) { + console.error( + `Failed to get download URL for file. Invalid node: ${node} ${normalizedPath}` + ) + state.setError(normalizedPath, 'Node is not a directory.') + + return '' + } + + const downloadUrl = await this.sandbox.downloadUrl(normalizedPath, { + user: 'root', + useSignature: this.isSandboxSecure || undefined, + }) + + console.log('downloadUrl', downloadUrl) + + return downloadUrl + } + + /** + * Small utility to create a deferred promise (aka Promise with exposed + * resolve/reject). + */ + private static createDeferred() { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise: Promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } + } + + /** + * Returns a user-friendly message for a given error. It checks the error's + * message against known substrings in `errorMap` and falls back to the + * supplied default message if no match is found. + */ + private static pipeError(error: unknown, defaultMessage: string): string { + const originalMessage = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : '' + + const lowerOriginal = originalMessage.toLowerCase() + + for (const [search, msg] of Object.entries(HANDLED_ERRORS)) { + if (lowerOriginal.includes(search.toLowerCase())) { + return msg + } + } + + return originalMessage || defaultMessage + } +} diff --git a/src/features/dashboard/sandboxes/table-cells.tsx b/src/features/dashboard/sandboxes/table-cells.tsx index 5b0b28dd2..b08894f68 100644 --- a/src/features/dashboard/sandboxes/table-cells.tsx +++ b/src/features/dashboard/sandboxes/table-cells.tsx @@ -1,7 +1,7 @@ 'use client' import { PROTECTED_URLS } from '@/configs/urls' -import { useServerContext } from '@/lib/hooks/use-server-context' +import { useServerContext } from '@/features/dashboard/server-context' import { cn } from '@/lib/utils' import { Template } from '@/types/api' import { JsonPopover } from '@/ui/json-popover' diff --git a/src/lib/hooks/use-server-context.tsx b/src/features/dashboard/server-context.tsx similarity index 100% rename from src/lib/hooks/use-server-context.tsx rename to src/features/dashboard/server-context.tsx diff --git a/src/lib/clients/api.ts b/src/lib/clients/api.ts index 320c371b0..18fff4992 100644 --- a/src/lib/clients/api.ts +++ b/src/lib/clients/api.ts @@ -8,10 +8,9 @@ export const infra = createClient({ headers, body, method, - // @ts-expect-error -- duplex not on type, keep it for now duplex: !!body ? 'half' : undefined, ...options, - }) + } as RequestInit) }, querySerializer: { array: { style: 'form', explode: false }, diff --git a/src/lib/clients/envd/filesystem/filesystem_connect.ts b/src/lib/clients/envd/filesystem/filesystem_connect.ts deleted file mode 100644 index ba5a7636c..000000000 --- a/src/lib/clients/envd/filesystem/filesystem_connect.ts +++ /dev/null @@ -1,118 +0,0 @@ -// @generated by protoc-gen-connect-es v1.6.1 with parameter "target=ts" -// @generated from file filesystem/filesystem.proto (package filesystem, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import { MethodKind } from '@bufbuild/protobuf' -import { - CreateWatcherRequest, - CreateWatcherResponse, - GetWatcherEventsRequest, - GetWatcherEventsResponse, - ListDirRequest, - ListDirResponse, - MakeDirRequest, - MakeDirResponse, - MoveRequest, - MoveResponse, - RemoveRequest, - RemoveResponse, - RemoveWatcherRequest, - RemoveWatcherResponse, - StatRequest, - StatResponse, - WatchDirRequest, - WatchDirResponse, -} from './filesystem_pb.js' - -/** - * @generated from service filesystem.Filesystem - */ -export const Filesystem = { - typeName: 'filesystem.Filesystem', - methods: { - /** - * @generated from rpc filesystem.Filesystem.Stat - */ - stat: { - name: 'Stat', - I: StatRequest, - O: StatResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.MakeDir - */ - makeDir: { - name: 'MakeDir', - I: MakeDirRequest, - O: MakeDirResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.Move - */ - move: { - name: 'Move', - I: MoveRequest, - O: MoveResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.ListDir - */ - listDir: { - name: 'ListDir', - I: ListDirRequest, - O: ListDirResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.Remove - */ - remove: { - name: 'Remove', - I: RemoveRequest, - O: RemoveResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.WatchDir - */ - watchDir: { - name: 'WatchDir', - I: WatchDirRequest, - O: WatchDirResponse, - kind: MethodKind.ServerStreaming, - }, - /** - * Non-streaming versions of WatchDir - * - * @generated from rpc filesystem.Filesystem.CreateWatcher - */ - createWatcher: { - name: 'CreateWatcher', - I: CreateWatcherRequest, - O: CreateWatcherResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.GetWatcherEvents - */ - getWatcherEvents: { - name: 'GetWatcherEvents', - I: GetWatcherEventsRequest, - O: GetWatcherEventsResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.RemoveWatcher - */ - removeWatcher: { - name: 'RemoveWatcher', - I: RemoveWatcherRequest, - O: RemoveWatcherResponse, - kind: MethodKind.Unary, - }, - }, -} as const diff --git a/src/lib/clients/envd/filesystem/filesystem_pb.ts b/src/lib/clients/envd/filesystem/filesystem_pb.ts deleted file mode 100644 index 618d11098..000000000 --- a/src/lib/clients/envd/filesystem/filesystem_pb.ts +++ /dev/null @@ -1,616 +0,0 @@ -// @generated by protoc-gen-es v2.5.2 with parameter "target=ts" -// @generated from file filesystem/filesystem.proto (package filesystem, syntax proto3) -/* eslint-disable */ - -import type { Message } from '@bufbuild/protobuf' -import type { - GenEnum, - GenFile, - GenMessage, - GenService, -} from '@bufbuild/protobuf/codegenv2' -import { - enumDesc, - fileDesc, - messageDesc, - serviceDesc, -} from '@bufbuild/protobuf/codegenv2' - -/** - * Describes the file filesystem/filesystem.proto. - */ -export const file_filesystem_filesystem: GenFile = - /*@__PURE__*/ - fileDesc( - 'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSItCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJEg0KBWRlcHRoGAIgASgNIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iMgoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIkQKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50IjcKFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIisKFUNyZWF0ZVdhdGNoZXJSZXNwb25zZRISCgp3YXRjaGVyX2lkGAEgASgJIi0KF0dldFdhdGNoZXJFdmVudHNSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiRwoYR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlEisKBmV2ZW50cxgBIAMoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50IioKFFJlbW92ZVdhdGNoZXJSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiFwoVUmVtb3ZlV2F0Y2hlclJlc3BvbnNlKlIKCEZpbGVUeXBlEhkKFUZJTEVfVFlQRV9VTlNQRUNJRklFRBAAEhIKDkZJTEVfVFlQRV9GSUxFEAESFwoTRklMRV9UWVBFX0RJUkVDVE9SWRACKpgBCglFdmVudFR5cGUSGgoWRVZFTlRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUVWRU5UX1RZUEVfQ1JFQVRFEAESFAoQRVZFTlRfVFlQRV9XUklURRACEhUKEUVWRU5UX1RZUEVfUkVNT1ZFEAMSFQoRRVZFTlRfVFlQRV9SRU5BTUUQBBIUChBFVkVOVF9UWVBFX0NITU9EEAUynwUKCkZpbGVzeXN0ZW0SOQoEU3RhdBIXLmZpbGVzeXN0ZW0uU3RhdFJlcXVlc3QaGC5maWxlc3lzdGVtLlN0YXRSZXNwb25zZRJCCgdNYWtlRGlyEhouZmlsZXN5c3RlbS5NYWtlRGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTWFrZURpclJlc3BvbnNlEjkKBE1vdmUSFy5maWxlc3lzdGVtLk1vdmVSZXF1ZXN0GhguZmlsZXN5c3RlbS5Nb3ZlUmVzcG9uc2USQgoHTGlzdERpchIaLmZpbGVzeXN0ZW0uTGlzdERpclJlcXVlc3QaGy5maWxlc3lzdGVtLkxpc3REaXJSZXNwb25zZRI/CgZSZW1vdmUSGS5maWxlc3lzdGVtLlJlbW92ZVJlcXVlc3QaGi5maWxlc3lzdGVtLlJlbW92ZVJlc3BvbnNlEkcKCFdhdGNoRGlyEhsuZmlsZXN5c3RlbS5XYXRjaERpclJlcXVlc3QaHC5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UwARJUCg1DcmVhdGVXYXRjaGVyEiAuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEl0KEEdldFdhdGNoZXJFdmVudHMSIy5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXF1ZXN0GiQuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USVAoNUmVtb3ZlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXNwb25zZUJpCg5jb20uZmlsZXN5c3RlbUIPRmlsZXN5c3RlbVByb3RvUAGiAgNGWFiqAgpGaWxlc3lzdGVtygIKRmlsZXN5c3RlbeICFkZpbGVzeXN0ZW1cR1BCTWV0YWRhdGHqAgpGaWxlc3lzdGVtYgZwcm90bzM' - ) - -/** - * @generated from message filesystem.MoveRequest - */ -export type MoveRequest = Message<'filesystem.MoveRequest'> & { - /** - * @generated from field: string source = 1; - */ - source: string - - /** - * @generated from field: string destination = 2; - */ - destination: string -} - -/** - * Describes the message filesystem.MoveRequest. - * Use `create(MoveRequestSchema)` to create a new message. - */ -export const MoveRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 0) - -/** - * @generated from message filesystem.MoveResponse - */ -export type MoveResponse = Message<'filesystem.MoveResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.MoveResponse. - * Use `create(MoveResponseSchema)` to create a new message. - */ -export const MoveResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 1) - -/** - * @generated from message filesystem.MakeDirRequest - */ -export type MakeDirRequest = Message<'filesystem.MakeDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.MakeDirRequest. - * Use `create(MakeDirRequestSchema)` to create a new message. - */ -export const MakeDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 2) - -/** - * @generated from message filesystem.MakeDirResponse - */ -export type MakeDirResponse = Message<'filesystem.MakeDirResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.MakeDirResponse. - * Use `create(MakeDirResponseSchema)` to create a new message. - */ -export const MakeDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 3) - -/** - * @generated from message filesystem.RemoveRequest - */ -export type RemoveRequest = Message<'filesystem.RemoveRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.RemoveRequest. - * Use `create(RemoveRequestSchema)` to create a new message. - */ -export const RemoveRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 4) - -/** - * @generated from message filesystem.RemoveResponse - */ -export type RemoveResponse = Message<'filesystem.RemoveResponse'> & {} - -/** - * Describes the message filesystem.RemoveResponse. - * Use `create(RemoveResponseSchema)` to create a new message. - */ -export const RemoveResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 5) - -/** - * @generated from message filesystem.StatRequest - */ -export type StatRequest = Message<'filesystem.StatRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.StatRequest. - * Use `create(StatRequestSchema)` to create a new message. - */ -export const StatRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 6) - -/** - * @generated from message filesystem.StatResponse - */ -export type StatResponse = Message<'filesystem.StatResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.StatResponse. - * Use `create(StatResponseSchema)` to create a new message. - */ -export const StatResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 7) - -/** - * @generated from message filesystem.EntryInfo - */ -export type EntryInfo = Message<'filesystem.EntryInfo'> & { - /** - * @generated from field: string name = 1; - */ - name: string - - /** - * @generated from field: filesystem.FileType type = 2; - */ - type: FileType - - /** - * @generated from field: string path = 3; - */ - path: string -} - -/** - * Describes the message filesystem.EntryInfo. - * Use `create(EntryInfoSchema)` to create a new message. - */ -export const EntryInfoSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 8) - -/** - * @generated from message filesystem.ListDirRequest - */ -export type ListDirRequest = Message<'filesystem.ListDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: uint32 depth = 2; - */ - depth: number -} - -/** - * Describes the message filesystem.ListDirRequest. - * Use `create(ListDirRequestSchema)` to create a new message. - */ -export const ListDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 9) - -/** - * @generated from message filesystem.ListDirResponse - */ -export type ListDirResponse = Message<'filesystem.ListDirResponse'> & { - /** - * @generated from field: repeated filesystem.EntryInfo entries = 1; - */ - entries: EntryInfo[] -} - -/** - * Describes the message filesystem.ListDirResponse. - * Use `create(ListDirResponseSchema)` to create a new message. - */ -export const ListDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 10) - -/** - * @generated from message filesystem.WatchDirRequest - */ -export type WatchDirRequest = Message<'filesystem.WatchDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: bool recursive = 2; - */ - recursive: boolean -} - -/** - * Describes the message filesystem.WatchDirRequest. - * Use `create(WatchDirRequestSchema)` to create a new message. - */ -export const WatchDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 11) - -/** - * @generated from message filesystem.FilesystemEvent - */ -export type FilesystemEvent = Message<'filesystem.FilesystemEvent'> & { - /** - * @generated from field: string name = 1; - */ - name: string - - /** - * @generated from field: filesystem.EventType type = 2; - */ - type: EventType -} - -/** - * Describes the message filesystem.FilesystemEvent. - * Use `create(FilesystemEventSchema)` to create a new message. - */ -export const FilesystemEventSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 12) - -/** - * @generated from message filesystem.WatchDirResponse - */ -export type WatchDirResponse = Message<'filesystem.WatchDirResponse'> & { - /** - * @generated from oneof filesystem.WatchDirResponse.event - */ - event: - | { - /** - * @generated from field: filesystem.WatchDirResponse.StartEvent start = 1; - */ - value: WatchDirResponse_StartEvent - case: 'start' - } - | { - /** - * @generated from field: filesystem.FilesystemEvent filesystem = 2; - */ - value: FilesystemEvent - case: 'filesystem' - } - | { - /** - * @generated from field: filesystem.WatchDirResponse.KeepAlive keepalive = 3; - */ - value: WatchDirResponse_KeepAlive - case: 'keepalive' - } - | { case: undefined; value?: undefined } -} - -/** - * Describes the message filesystem.WatchDirResponse. - * Use `create(WatchDirResponseSchema)` to create a new message. - */ -export const WatchDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13) - -/** - * @generated from message filesystem.WatchDirResponse.StartEvent - */ -export type WatchDirResponse_StartEvent = - Message<'filesystem.WatchDirResponse.StartEvent'> & {} - -/** - * Describes the message filesystem.WatchDirResponse.StartEvent. - * Use `create(WatchDirResponse_StartEventSchema)` to create a new message. - */ -export const WatchDirResponse_StartEventSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13, 0) - -/** - * @generated from message filesystem.WatchDirResponse.KeepAlive - */ -export type WatchDirResponse_KeepAlive = - Message<'filesystem.WatchDirResponse.KeepAlive'> & {} - -/** - * Describes the message filesystem.WatchDirResponse.KeepAlive. - * Use `create(WatchDirResponse_KeepAliveSchema)` to create a new message. - */ -export const WatchDirResponse_KeepAliveSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13, 1) - -/** - * @generated from message filesystem.CreateWatcherRequest - */ -export type CreateWatcherRequest = - Message<'filesystem.CreateWatcherRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: bool recursive = 2; - */ - recursive: boolean - } - -/** - * Describes the message filesystem.CreateWatcherRequest. - * Use `create(CreateWatcherRequestSchema)` to create a new message. - */ -export const CreateWatcherRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 14) - -/** - * @generated from message filesystem.CreateWatcherResponse - */ -export type CreateWatcherResponse = - Message<'filesystem.CreateWatcherResponse'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.CreateWatcherResponse. - * Use `create(CreateWatcherResponseSchema)` to create a new message. - */ -export const CreateWatcherResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 15) - -/** - * @generated from message filesystem.GetWatcherEventsRequest - */ -export type GetWatcherEventsRequest = - Message<'filesystem.GetWatcherEventsRequest'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.GetWatcherEventsRequest. - * Use `create(GetWatcherEventsRequestSchema)` to create a new message. - */ -export const GetWatcherEventsRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 16) - -/** - * @generated from message filesystem.GetWatcherEventsResponse - */ -export type GetWatcherEventsResponse = - Message<'filesystem.GetWatcherEventsResponse'> & { - /** - * @generated from field: repeated filesystem.FilesystemEvent events = 1; - */ - events: FilesystemEvent[] - } - -/** - * Describes the message filesystem.GetWatcherEventsResponse. - * Use `create(GetWatcherEventsResponseSchema)` to create a new message. - */ -export const GetWatcherEventsResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 17) - -/** - * @generated from message filesystem.RemoveWatcherRequest - */ -export type RemoveWatcherRequest = - Message<'filesystem.RemoveWatcherRequest'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.RemoveWatcherRequest. - * Use `create(RemoveWatcherRequestSchema)` to create a new message. - */ -export const RemoveWatcherRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 18) - -/** - * @generated from message filesystem.RemoveWatcherResponse - */ -export type RemoveWatcherResponse = - Message<'filesystem.RemoveWatcherResponse'> & {} - -/** - * Describes the message filesystem.RemoveWatcherResponse. - * Use `create(RemoveWatcherResponseSchema)` to create a new message. - */ -export const RemoveWatcherResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 19) - -/** - * @generated from enum filesystem.FileType - */ -export enum FileType { - /** - * @generated from enum value: FILE_TYPE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: FILE_TYPE_FILE = 1; - */ - FILE = 1, - - /** - * @generated from enum value: FILE_TYPE_DIRECTORY = 2; - */ - DIRECTORY = 2, -} - -/** - * Describes the enum filesystem.FileType. - */ -export const FileTypeSchema: GenEnum = - /*@__PURE__*/ - enumDesc(file_filesystem_filesystem, 0) - -/** - * @generated from enum filesystem.EventType - */ -export enum EventType { - /** - * @generated from enum value: EVENT_TYPE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: EVENT_TYPE_CREATE = 1; - */ - CREATE = 1, - - /** - * @generated from enum value: EVENT_TYPE_WRITE = 2; - */ - WRITE = 2, - - /** - * @generated from enum value: EVENT_TYPE_REMOVE = 3; - */ - REMOVE = 3, - - /** - * @generated from enum value: EVENT_TYPE_RENAME = 4; - */ - RENAME = 4, - - /** - * @generated from enum value: EVENT_TYPE_CHMOD = 5; - */ - CHMOD = 5, -} - -/** - * Describes the enum filesystem.EventType. - */ -export const EventTypeSchema: GenEnum = - /*@__PURE__*/ - enumDesc(file_filesystem_filesystem, 1) - -/** - * @generated from service filesystem.Filesystem - */ -export const Filesystem: GenService<{ - /** - * @generated from rpc filesystem.Filesystem.Stat - */ - stat: { - methodKind: 'unary' - input: typeof StatRequestSchema - output: typeof StatResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.MakeDir - */ - makeDir: { - methodKind: 'unary' - input: typeof MakeDirRequestSchema - output: typeof MakeDirResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.Move - */ - move: { - methodKind: 'unary' - input: typeof MoveRequestSchema - output: typeof MoveResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.ListDir - */ - listDir: { - methodKind: 'unary' - input: typeof ListDirRequestSchema - output: typeof ListDirResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.Remove - */ - remove: { - methodKind: 'unary' - input: typeof RemoveRequestSchema - output: typeof RemoveResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.WatchDir - */ - watchDir: { - methodKind: 'server_streaming' - input: typeof WatchDirRequestSchema - output: typeof WatchDirResponseSchema - } - /** - * Non-streaming versions of WatchDir - * - * @generated from rpc filesystem.Filesystem.CreateWatcher - */ - createWatcher: { - methodKind: 'unary' - input: typeof CreateWatcherRequestSchema - output: typeof CreateWatcherResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.GetWatcherEvents - */ - getWatcherEvents: { - methodKind: 'unary' - input: typeof GetWatcherEventsRequestSchema - output: typeof GetWatcherEventsResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.RemoveWatcher - */ - removeWatcher: { - methodKind: 'unary' - input: typeof RemoveWatcherRequestSchema - output: typeof RemoveWatcherResponseSchema - } -}> = /*@__PURE__*/ serviceDesc(file_filesystem_filesystem, 0) diff --git a/src/lib/env.ts b/src/lib/env.ts index e74da865a..05c49c88f 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,6 +5,7 @@ export const serverSchema = z.object({ INFRA_API_URL: z.string().url(), KV_REST_API_TOKEN: z.string().min(1), KV_REST_API_URL: z.string().url(), + NEXT_PUBLIC_E2B_DOMAIN: z.string(), BILLING_API_URL: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().optional(), diff --git a/src/lib/hooks/use-teams.ts b/src/lib/hooks/use-teams.ts index bc6faf6e1..67e0eb46e 100644 --- a/src/lib/hooks/use-teams.ts +++ b/src/lib/hooks/use-teams.ts @@ -1,4 +1,4 @@ -import { useServerContext } from './use-server-context' +import { useServerContext } from '../../features/dashboard/server-context' export const useTeams = () => { const { teams } = useServerContext() diff --git a/src/lib/hooks/use-user.ts b/src/lib/hooks/use-user.ts index e470b3ffa..6ee8baa27 100644 --- a/src/lib/hooks/use-user.ts +++ b/src/lib/hooks/use-user.ts @@ -1,6 +1,6 @@ 'use client' -import { useServerContext } from './use-server-context' +import { useServerContext } from '../../features/dashboard/server-context' export const useUser = () => { const { user } = useServerContext() diff --git a/src/lib/source.ts b/src/lib/source.ts deleted file mode 100644 index 0afbb0515..000000000 --- a/src/lib/source.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { docs } from '@/../.source' -import { IconContainer } from '@/ui/icons' -import type { InferMetaType, InferPageType } from 'fumadocs-core/source' -import { loader } from 'fumadocs-core/source' -import { icons } from 'lucide-react' -import { createElement } from 'react' - -export const source = loader({ - baseUrl: '/docs', - icon(icon) { - if (icon && icon in icons) - return createElement(IconContainer, { - icon: icons[icon as keyof typeof icons], - }) - }, - source: docs.toFumadocsSource(), -}) - -export type Page = InferPageType -export type Meta = InferMetaType diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts new file mode 100644 index 000000000..af7717d8d --- /dev/null +++ b/src/lib/utils/filesystem.ts @@ -0,0 +1,91 @@ +import { FileContentState } from '@/features/dashboard/sandbox/inspect/filesystem/store' + +// Leverage pathe (a tiny, browser-friendly path replacement) +import { normalize, dirname, basename, join } from 'pathe' + +/** + * Normalize a path so that it: + * • always starts with "/" (root-relative) + * • has duplicate slashes removed + * • resolves . and .. segments + */ +export function normalizePath(path: string): string { + if (!path) return '/' + + return normalize(path) +} + +/** Get the parent directory of a path */ +export function getParentPath(path: string): string { + const norm = normalizePath(path) + + return norm === '/' ? '/' : dirname(norm) || '/' +} + +/** Get the basename (filename) of a path */ +export function getBasename(path: string): string { + const norm = normalizePath(path) + + return norm === '/' ? '/' : basename(norm) +} + +/** Join path segments together */ +export function joinPath(...segments: (string | null | undefined)[]): string { + if (segments.length === 0) return '/' + const filtered = segments.filter( + (s): s is string => s !== '' && s !== null && s !== undefined + ) + return normalizePath(join(...filtered)) +} + +/** Check if a path is a strict child of another path */ +export function isChildPath(parentPath: string, childPath: string): boolean { + const parent = normalizePath(parentPath) + const child = normalizePath(childPath) + if (parent === child) return false + + const parentWithSlash = parent === '/' ? '/' : `${parent}/` + return child.startsWith(parentWithSlash) +} + +/** Get the depth of a path (number of directory levels) */ +export function getPathDepth(path: string): number { + const norm = normalizePath(path) + return norm === '/' ? 0 : norm.split('/').length - 1 +} + +/** Check if a path is the root path */ +export function isRootPath(path: string): boolean { + return normalizePath(path) === '/' +} + +// --------------------------------------------------------------------------- +// Binary/text blob helpers (unchanged) +// --------------------------------------------------------------------------- + +export async function determineFileContentState( + blob: Blob +): Promise { + const mimeType = blob.type ?? '' + + try { + if (mimeType.startsWith('image/')) { + const dataUri = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(blob) + }) + + return { type: 'image', dataUri } + } + + const buffer = await blob.arrayBuffer() + const data = new Uint8Array(buffer) + + const content = new TextDecoder('utf-8', { fatal: true }).decode(data) + return { type: 'text', text: content } + } catch { + return { type: 'unreadable' } + } +} diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts new file mode 100644 index 000000000..deaf52ede --- /dev/null +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -0,0 +1,36 @@ +import { z } from 'zod' +import { authActionClient } from '@/lib/clients/action' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { returnServerError } from '@/lib/utils/action' +import Sandbox from 'e2b' +import { l } from '@/lib/clients/logger' + +export const GetSandboxRootSchema = z.object({ + teamId: z.string().uuid(), + sandboxId: z.string(), + rootPath: z.string().default('/'), +}) + +export const getSandboxRoot = authActionClient + .schema(GetSandboxRootSchema) + .metadata({ serverFunctionName: 'getSandboxRoot' }) + .action(async ({ parsedInput, ctx }) => { + const { teamId, sandboxId, rootPath } = parsedInput + const { session } = ctx + + const headers = SUPABASE_AUTH_HEADERS(session.access_token, teamId) + + try { + const sandbox = await Sandbox.connect(sandboxId, { + domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, + headers, + }) + + return { + entries: await sandbox.files.list(rootPath), + } + } catch (err) { + l.error('get_sandbox_root:unexpected_error', err) + return returnServerError('Failed to list root directory.') + } + }) diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 9319a504a..a2cbb8fd5 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -2,6 +2,8 @@ import { components as InfraComponents } from '@/types/infra-api' type Sandbox = InfraComponents['schemas']['ListedSandbox'] +type SandboxInfo = InfraComponents['schemas']['SandboxDetail'] + type Sandboxes = InfraComponents['schemas']['ListedSandbox'][] type SandboxMetric = InfraComponents['schemas']['SandboxMetric'] @@ -33,6 +35,7 @@ export type { DefaultTemplate, IdentifierMaskingDetails, Sandbox, + SandboxInfo, Sandboxes, SandboxesMetricsRecord, SandboxMetric, diff --git a/tsconfig.json b/tsconfig.json index 211eb95eb..525414d80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ "moduleResolution": "bundler", "noUncheckedIndexedAccess": true, "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ @@ -22,7 +21,8 @@ ], "paths": { "@/*": ["./src/*"] - } + }, + "isolatedModules": true }, "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], "exclude": ["node_modules"]