Skip to content
Draft
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
76c2b4a
add templates SDK (JS)
mishushakov Aug 25, 2025
064e981
export template errors
mishushakov Aug 25, 2025
7ffa184
exported wait template
mishushakov Aug 25, 2025
19f6a0e
handle api error class
mishushakov Aug 25, 2025
9dbc7dd
added python template sdk
mishushakov Aug 25, 2025
61affb3
import from typing extensions instead of typing (python compatibility)
mishushakov Aug 25, 2025
6fc75ea
use union type instead of pipe syntax
mishushakov Aug 25, 2025
b6cd775
regenerated py api client
mishushakov Aug 25, 2025
b4acc93
Merge branch 'main' into merge-templates-sdk-with-e2b-sdk-e2b-2939
mishushakov Aug 25, 2025
0d66d00
type issue
mishushakov Aug 25, 2025
a1ac89a
typefix
mishushakov Aug 25, 2025
8a4cad9
moved template to a separate entrypoint (browser)
mishushakov Sep 1, 2025
93e5b83
reverse bundler change, use dynamic import instead
mishushakov Sep 1, 2025
8422ce7
browser quirks
mishushakov Sep 1, 2025
a2132a5
Merge branch 'main' into merge-templates-sdk-with-e2b-sdk-e2b-2939
mishushakov Sep 1, 2025
d6bf79c
updated lock file
mishushakov Sep 1, 2025
95104c3
Refactor imports in rpc.ts and remove unused defineConfig function in…
mishushakov Sep 1, 2025
08c9a9d
dynamic async import instead of require
mishushakov Sep 1, 2025
b5628d2
prefix node modulex with node for deno
mishushakov Sep 1, 2025
c08fb34
Refactor ApiClient initialization to conditionally include limits in …
mishushakov Sep 1, 2025
9a059d5
python lint
mishushakov Sep 1, 2025
6e490a9
python format
mishushakov Sep 1, 2025
28273b6
glob root dir python <3.10
mishushakov Sep 1, 2025
24b0b02
undo root dir change
mishushakov Sep 1, 2025
27fc4bf
updated python minimum version
mishushakov Sep 1, 2025
464c880
simulate root dir for tar
mishushakov Sep 1, 2025
cfdd611
make git clone path optional
mishushakov Sep 2, 2025
66d7e21
simplified ts codebase
mishushakov Sep 2, 2025
85be542
Refactor wait_for functions into class methods in TemplateBuilder; re…
mishushakov Sep 2, 2025
640c1a6
format python
mishushakov Sep 2, 2025
d6b6ed2
fix invalid package argument spread
mishushakov Sep 2, 2025
7b64e24
The 401 error handling in handle_api_exception directly accesses body…
mishushakov Sep 2, 2025
89dd382
removed unnecessary exports (python)
mishushakov Sep 2, 2025
5869278
remove async from test
mishushakov Sep 2, 2025
845f744
resolve circular dependencies
mishushakov Sep 2, 2025
859851d
remove unnecessary python exports
mishushakov Sep 2, 2025
e5c7057
Merge branch 'main' into merge-templates-sdk-with-e2b-sdk-e2b-2939
mishushakov Sep 3, 2025
005ae12
removed stdout from template test
mishushakov Sep 3, 2025
0113702
httpx client default limits
mishushakov Sep 4, 2025
3d83f8f
moved template consts up
mishushakov Sep 4, 2025
7c2035e
cursor suggestion
mishushakov Sep 4, 2025
8be695b
breaking: AuthenticationException to extend Exception for use in both…
mishushakov Sep 4, 2025
21e88e2
file upload exception inconsistency
mishushakov Sep 4, 2025
b4621ee
move mutable template consts back to self to avoid state conditions
mishushakov Sep 4, 2025
bc0e0d1
remove test mutable (cursor artifact)
mishushakov Sep 4, 2025
f7176a5
move it back again
mishushakov Sep 4, 2025
9fef33c
BuildError > FileUploadError on fileUploadLinkRes
mishushakov Sep 4, 2025
3a30c6e
fix incorrect file copy options in JS SDK
mishushakov Sep 4, 2025
4660304
cursor suggestions
mishushakov Sep 4, 2025
67d1b47
dynamic test folder
mishushakov Sep 4, 2025
c2dcead
unmount test folder
mishushakov Sep 4, 2025
278fc32
override existing test folder on failed run
mishushakov Sep 4, 2025
fbd6977
move __wait_for_build_finish to build api, move log class to types
mishushakov Sep 4, 2025
0c34d80
pritn warning on fromDockerFile
mishushakov Sep 4, 2025
16543b9
python format
mishushakov Sep 4, 2025
52e94fa
revert if/else for python compability in docker instruction matching
mishushakov Sep 4, 2025
52f6f47
cursor suggestion
mishushakov Sep 5, 2025
3db0078
added ansi escape source code ref to python utils
mishushakov Sep 5, 2025
88f71e2
exported template class
mishushakov Sep 5, 2025
da771bd
remove duration type
mishushakov Sep 5, 2025
4e33dde
timeout ms > timeout s in template timeout
mishushakov Sep 5, 2025
cfaeba0
pylint
mishushakov Sep 5, 2025
2acd36d
convert timeout ms to seconds for sleep
mishushakov Sep 5, 2025
64c7084
timeout in dockerfile parser ready command
mishushakov Sep 5, 2025
f6200e5
lint
mishushakov Sep 5, 2025
d5f9ca5
integer division for sleep
mishushakov Sep 5, 2025
119198e
update sleep in docker parser
mishushakov Sep 5, 2025
3db5d80
naming consistency, exported templatebase
mishushakov Sep 5, 2025
7f69bfb
ensure minimum timeout of 1s
mishushakov Sep 5, 2025
d5becc7
type error
mishushakov Sep 5, 2025
822d098
remove outdated comments
mishushakov Sep 5, 2025
9deb86a
types
mishushakov Sep 5, 2025
24bb3c8
export templatebase as type
mishushakov Sep 5, 2025
9f43bb8
fornat
mishushakov Sep 5, 2025
a805dab
switch to classes based approach in JavaScript SDK like Python, not i…
mishushakov Sep 5, 2025
5f1980d
lint
mishushakov Sep 5, 2025
16a4661
wip
mishushakov Sep 5, 2025
eddb7db
keep build, tojson, todockerfike protected
mishushakov Sep 5, 2025
1941631
same template accessor function as in javascript
mishushakov Sep 5, 2025
dd8e036
remove static duplicates in JS
mishushakov Sep 5, 2025
72ceab6
format
mishushakov Sep 5, 2025
131a636
lint
mishushakov Sep 5, 2025
3ad2f4c
remove cursor residue
mishushakov Sep 5, 2025
b0c2c56
return fromDockerfile pass template builder
mishushakov Sep 5, 2025
26175cc
remove unusued vars
mishushakov Sep 5, 2025
c3d7229
remove Any
mishushakov Sep 5, 2025
9addc88
format
mishushakov Sep 5, 2025
27e109d
moved ready commands back to where they were
mishushakov Sep 5, 2025
38ff225
remove duplicate setStartCmd
mishushakov Sep 5, 2025
1ac46eb
python static methods and from_dockerfile lint issue
mishushakov Sep 5, 2025
4616d23
parser final interface
mishushakov Sep 5, 2025
65123dd
removed unused import
mishushakov Sep 5, 2025
acccb35
docker file parser final interface (JS)
mishushakov Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/js-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@
"@connectrpc/connect": "2.0.0-rc.3",
"@connectrpc/connect-web": "2.0.0-rc.3",
"compare-versions": "^6.1.0",
"dockerfile-ast": "^0.7.1",
"glob": "^11.0.3",
"openapi-fetch": "^0.9.7",
"platform": "^1.3.6"
"platform": "^1.3.6",
"strip-ansi": "^7.1.0",
"tar": "^7.4.3"
},
"engines": {
"node": ">=18"
Expand Down
15 changes: 13 additions & 2 deletions packages/js-sdk/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,23 @@ import { AuthenticationError, RateLimitError, SandboxError } from '../errors'
import { createApiLogger } from '../logs'

export function handleApiError(
response: FetchResponse<any, any, any>
response: FetchResponse<any, any, any>,
errorClass: new (message: string) => Error = SandboxError
): Error | undefined {
if (!response.error) {
return
}

if (response.response.status === 401) {
const message = 'Unauthorized, please check your credentials.'
const content = response.error?.message ?? response.error

if (content) {
return new AuthenticationError(`${message} - ${content}`)
}
return new AuthenticationError(message)
}

if (response.response.status === 429) {
const message = 'Rate limit exceeded, please try again later'
const content = response.error?.message ?? response.error
Expand All @@ -24,7 +35,7 @@ export function handleApiError(
}

const message = response.error?.message ?? response.error
return new SandboxError(`${response.response.status}: ${message}`)
return new errorClass(`${response.response.status}: ${message}`)
}

/**
Expand Down
47 changes: 1 addition & 46 deletions packages/js-sdk/src/api/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,10 @@
import platform from 'platform'

import { version } from '../../package.json'
import { runtime, runtimeVersion } from '../utils'

export { version }

declare let window: any

type Runtime =
| 'node'
| 'browser'
| 'deno'
| 'bun'
| 'vercel-edge'
| 'cloudflare-worker'
| 'unknown'

function getRuntime(): { runtime: Runtime; version: string } {
// @ts-ignore
if ((globalThis as any).Bun) {
// @ts-ignore
return { runtime: 'bun', version: globalThis.Bun.version }
}

// @ts-ignore
if ((globalThis as any).Deno) {
// @ts-ignore
return { runtime: 'deno', version: globalThis.Deno.version.deno }
}

if ((globalThis as any).process?.release?.name === 'node') {
return { runtime: 'node', version: platform.version || 'unknown' }
}

// @ts-ignore
if (typeof EdgeRuntime === 'string') {
return { runtime: 'vercel-edge', version: 'unknown' }
}

if ((globalThis as any).navigator?.userAgent === 'Cloudflare-Workers') {
return { runtime: 'cloudflare-worker', version: 'unknown' }
}

if (typeof window !== 'undefined') {
return { runtime: 'browser', version: platform.version || 'unknown' }
}

return { runtime: 'unknown', version: 'unknown' }
}

export const { runtime, version: runtimeVersion } = getRuntime()

export const defaultHeaders = {
browser: (typeof window !== 'undefined' && platform.name) || 'unknown',
lang: 'js',
Expand Down
2 changes: 1 addition & 1 deletion packages/js-sdk/src/envd/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Code, ConnectError } from '@connectrpc/connect'
import { runtime } from '../api/metadata'
import { runtime } from '../utils'
import { defaultUsername } from '../connectionConfig'

import {
Expand Down
16 changes: 15 additions & 1 deletion packages/js-sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class NotFoundError extends SandboxError {
/**
* Thrown when authentication fails.
*/
export class AuthenticationError extends SandboxError {
export class AuthenticationError extends Error {
constructor(message: any) {
super(message)
this.name = 'AuthenticationError'
Expand All @@ -94,3 +94,17 @@ export class RateLimitError extends SandboxError {
this.name = 'RateLimitError'
}
}

export class BuildError extends Error {
constructor(message: string) {
super(message)
this.name = 'BuildError'
}
}

export class FileUploadError extends BuildError {
constructor(message: string) {
super(message)
this.name = 'FileUploadError'
}
}
4 changes: 4 additions & 0 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {
TemplateError,
TimeoutError,
RateLimitError,
BuildError,
FileUploadError,
} from './errors'
export type { Logger } from './logs'

Expand Down Expand Up @@ -58,3 +60,5 @@ export type {
export { Sandbox }
import { Sandbox } from './sandbox'
export default Sandbox

export { Template, type TemplateClass, type TemplateBase } from './template'
223 changes: 223 additions & 0 deletions packages/js-sdk/src/template/buildApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { ApiClient, paths, handleApiError } from '../api'
import { BuildError, FileUploadError } from '../errors'
import { LogEntry } from './types'
import { tarFileStreamUpload } from './utils'
import stripAnsi from 'strip-ansi'

type RequestBuildInput = {
alias: string
cpuCount: number
memoryMB: number
}

type GetFileUploadLinkInput = {
templateID: string
filesHash: string
}

type TriggerBuildInput = {
templateID: string
buildID: string
template: TriggerBuildTemplate
}

type GetBuildStatusInput = {
templateID: string
buildID: string
logsOffset: number
}

export type GetBuildStatusResponse =
paths['/templates/{templateID}/builds/{buildID}/status']['get']['responses']['200']['content']['application/json']

export type TriggerBuildTemplate =
paths['/v2/templates/{templateID}/builds/{buildID}']['post']['requestBody']['content']['application/json']

export async function requestBuild(
client: ApiClient,
{ alias, cpuCount, memoryMB }: RequestBuildInput
) {
const requestBuildRes = await client.api.POST('/v2/templates', {
body: {
alias,
cpuCount,
memoryMB,
},
})

const error = handleApiError(requestBuildRes, BuildError)
if (error) {
throw error
}

if (!requestBuildRes.data) {
throw new BuildError('Failed to request build')
}

return requestBuildRes.data
}

export async function getFileUploadLink(
client: ApiClient,
{ templateID, filesHash }: GetFileUploadLinkInput
) {
const fileUploadLinkRes = await client.api.GET(
'/templates/{templateID}/files/{hash}',
{
params: {
path: {
templateID,
hash: filesHash,
},
},
}
)

const error = handleApiError(fileUploadLinkRes, FileUploadError)
if (error) {
throw error
}

if (!fileUploadLinkRes.data) {
throw new FileUploadError('Failed to get file upload link')
}

return fileUploadLinkRes.data
}

export async function uploadFile(options: {
fileName: string
fileContextPath: string
url: string
}) {
const { fileName, url, fileContextPath } = options
const { contentLength, uploadStream } = await tarFileStreamUpload(
fileName,
fileContextPath
)

// The compiler assumes this is Web fetch API, but it's actually Node.js fetch API
const res = await fetch(url, {
method: 'PUT',
// @ts-expect-error
body: uploadStream,
headers: {
'Content-Length': contentLength.toString(),
},
duplex: 'half',
})

if (!res.ok) {
throw new FileUploadError(
`Failed to upload file: ${res.statusText} ${res.status}`
)
}
}

export async function triggerBuild(
client: ApiClient,
{ templateID, buildID, template }: TriggerBuildInput
) {
const triggerBuildRes = await client.api.POST(
'/v2/templates/{templateID}/builds/{buildID}',
{
params: {
path: {
templateID,
buildID,
},
},
body: template,
}
)

const error = handleApiError(triggerBuildRes, BuildError)
if (error) {
throw error
}
}

export async function getBuildStatus(
client: ApiClient,
{ templateID, buildID, logsOffset }: GetBuildStatusInput
) {
const buildStatusRes = await client.api.GET(
'/templates/{templateID}/builds/{buildID}/status',
{
params: {
path: {
templateID,
buildID,
},
query: {
logsOffset,
},
},
}
)

const error = handleApiError(buildStatusRes, BuildError)
if (error) {
throw error
}

if (!buildStatusRes.data) {
throw new BuildError('Failed to get build status')
}

return buildStatusRes.data
}

export async function waitForBuildFinish(
client: ApiClient,
{
templateID,
buildID,
onBuildLogs,
logsRefreshFrequency,
}: {
templateID: string
buildID: string
onBuildLogs?: (logEntry: InstanceType<typeof LogEntry>) => void
logsRefreshFrequency: number
}
): Promise<void> {
let logsOffset = 0
let status: GetBuildStatusResponse['status'] = 'building'

while (status === 'building') {
const buildStatus = await getBuildStatus(client, {
templateID,
buildID,
logsOffset,
})

logsOffset += buildStatus.logEntries.length

buildStatus.logEntries.forEach(
(logEntry: GetBuildStatusResponse['logEntries'][number]) =>
onBuildLogs?.(
new LogEntry(
new Date(logEntry.timestamp),
logEntry.level,
stripAnsi(logEntry.message)
)
)
)

status = buildStatus.status
switch (status) {
case 'ready': {
return
}
case 'error': {
throw new BuildError(buildStatus?.reason?.message ?? 'Unknown error')
}
}

// Wait for a short period before checking the status again
await new Promise((resolve) => setTimeout(resolve, logsRefreshFrequency))
}

throw new BuildError('Unknown build error occurred.')
}
Loading
Loading