@hanzo/runtime 0.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ /*
2
+ * Copyright 2025 Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { ObjectStorageApi, SnapshotDto, SnapshotsApi, SnapshotState, CreateSnapshot } from '@daytonaio/api-client'
7
+ import { DaytonaError } from './errors/DaytonaError'
8
+ import { ObjectStorage } from './ObjectStorage'
9
+ import { Image } from './Image'
10
+ import { Resources } from './Daytona'
11
+ import { processStreamingResponse } from './utils/Stream'
12
+
13
+ const SNAPSHOTS_FETCH_LIMIT = 200
14
+
15
+ /**
16
+ * Represents a Daytona Snapshot which is a pre-configured sandbox.
17
+ *
18
+ * @property {string} id - Unique identifier for the Snapshot.
19
+ * @property {string} organizationId - Organization ID that owns the Snapshot.
20
+ * @property {boolean} general - Whether the Snapshot is general.
21
+ * @property {string} name - Name of the Snapshot.
22
+ * @property {string} imageName - Name of the Image of the Snapshot.
23
+ * @property {boolean} enabled - Whether the Snapshot is enabled.
24
+ * @property {SnapshotState} state - Current state of the Snapshot.
25
+ * @property {number} size - Size of the Snapshot.
26
+ * @property {string[]} entrypoint - Entrypoint of the Snapshot.
27
+ * @property {number} cpu - CPU of the Snapshot.
28
+ * @property {number} gpu - GPU of the Snapshot.
29
+ * @property {number} mem - Memory of the Snapshot in GiB.
30
+ * @property {number} disk - Disk of the Snapshot in GiB.
31
+ * @property {string} errorReason - Error reason of the Snapshot.
32
+ * @property {Date} createdAt - Timestamp when the Snapshot was created.
33
+ * @property {Date} updatedAt - Timestamp when the Snapshot was last updated.
34
+ * @property {Date} lastUsedAt - Timestamp when the Snapshot was last used.
35
+ */
36
+ export type Snapshot = SnapshotDto & { __brand: 'Snapshot' }
37
+
38
+ /**
39
+ * Parameters for creating a new snapshot.
40
+ *
41
+ * @property {string} name - Name of the snapshot.
42
+ * @property {string | Image} image - Image of the snapshot. If a string is provided, it should be available on some registry.
43
+ * If an Image instance is provided, it will be used to create a new image in Daytona.
44
+ * @property {Resources} resources - Resources of the snapshot.
45
+ * @property {string[]} entrypoint - Entrypoint of the snapshot.
46
+ */
47
+ export type CreateSnapshotParams = {
48
+ name: string
49
+ image: string | Image
50
+ resources?: Resources
51
+ entrypoint?: string[]
52
+ }
53
+
54
+ /**
55
+ * Service for managing Daytona Snapshots. Can be used to list, get, create and delete Snapshots.
56
+ *
57
+ * @class
58
+ */
59
+ export class SnapshotService {
60
+ constructor(
61
+ private snapshotsApi: SnapshotsApi,
62
+ private objectStorageApi: ObjectStorageApi,
63
+ ) {}
64
+
65
+ /**
66
+ * List all Snapshots.
67
+ *
68
+ * @returns {Promise<Snapshot[]>} List of all Snapshots accessible to the user
69
+ *
70
+ * @example
71
+ * const daytona = new Daytona();
72
+ * const snapshots = await daytona.snapshot.list();
73
+ * console.log(`Found ${snapshots.length} snapshots`);
74
+ * snapshots.forEach(snapshot => console.log(`${snapshot.name} (${snapshot.imageName})`));
75
+ */
76
+ async list(): Promise<Snapshot[]> {
77
+ let response = await this.snapshotsApi.getAllSnapshots(undefined, SNAPSHOTS_FETCH_LIMIT)
78
+ if (response.data.total > SNAPSHOTS_FETCH_LIMIT) {
79
+ response = await this.snapshotsApi.getAllSnapshots(undefined, response.data.total)
80
+ }
81
+ return response.data.items as Snapshot[]
82
+ }
83
+
84
+ /**
85
+ * Gets a Snapshot by its name.
86
+ *
87
+ * @param {string} name - Name of the Snapshot to retrieve
88
+ * @returns {Promise<Snapshot>} The requested Snapshot
89
+ * @throws {Error} If the Snapshot does not exist or cannot be accessed
90
+ *
91
+ * @example
92
+ * const daytona = new Daytona();
93
+ * const snapshot = await daytona.snapshot.get("snapshot-name");
94
+ * console.log(`Snapshot ${snapshot.name} is in state ${snapshot.state}`);
95
+ */
96
+ async get(name: string): Promise<Snapshot> {
97
+ const response = await this.snapshotsApi.getSnapshot(name)
98
+ return response.data as Snapshot
99
+ }
100
+
101
+ /**
102
+ * Deletes a Snapshot.
103
+ *
104
+ * @param {Snapshot} snapshot - Snapshot to delete
105
+ * @returns {Promise<void>}
106
+ * @throws {Error} If the Snapshot does not exist or cannot be deleted
107
+ *
108
+ * @example
109
+ * const daytona = new Daytona();
110
+ * const snapshot = await daytona.snapshot.get("snapshot-name");
111
+ * await daytona.snapshot.delete(snapshot);
112
+ * console.log("Snapshot deleted successfully");
113
+ */
114
+ async delete(snapshot: Snapshot): Promise<void> {
115
+ await this.snapshotsApi.removeSnapshot(snapshot.id)
116
+ }
117
+
118
+ /**
119
+ * Creates and registers a new snapshot from the given Image definition.
120
+ *
121
+ * @param {CreateSnapshotParams} params - Parameters for snapshot creation.
122
+ * @param {object} options - Options for the create operation.
123
+ * @param {boolean} options.onLogs - This callback function handles snapshot creation logs.
124
+ * @param {number} options.timeout - Default is no timeout. Timeout in seconds (0 means no timeout).
125
+ * @returns {Promise<void>}
126
+ *
127
+ * @example
128
+ * const image = Image.debianSlim('3.12').pipInstall('numpy');
129
+ * await daytona.snapshot.create({ name: 'my-snapshot', image: image }, { onLogs: console.log });
130
+ */
131
+ public async create(
132
+ params: CreateSnapshotParams,
133
+ options: { onLogs?: (chunk: string) => void; timeout?: number } = {},
134
+ ): Promise<Snapshot> {
135
+ const createSnapshotReq: CreateSnapshot = {
136
+ name: params.name,
137
+ }
138
+
139
+ if (typeof params.image === 'string') {
140
+ createSnapshotReq.imageName = params.image
141
+ createSnapshotReq.entrypoint = params.entrypoint
142
+ } else {
143
+ const contextHashes = await SnapshotService.processImageContext(this.objectStorageApi, params.image)
144
+ createSnapshotReq.buildInfo = {
145
+ contextHashes,
146
+ dockerfileContent: params.entrypoint
147
+ ? params.image.entrypoint(params.entrypoint).dockerfile
148
+ : params.image.dockerfile,
149
+ }
150
+ }
151
+
152
+ if (params.resources) {
153
+ createSnapshotReq.cpu = params.resources.cpu
154
+ createSnapshotReq.gpu = params.resources.gpu
155
+ createSnapshotReq.memory = params.resources.memory
156
+ createSnapshotReq.disk = params.resources.disk
157
+ }
158
+
159
+ let createdSnapshot = (
160
+ await this.snapshotsApi.createSnapshot(createSnapshotReq, undefined, {
161
+ timeout: (options.timeout || 0) * 1000,
162
+ })
163
+ ).data
164
+
165
+ if (!createdSnapshot) {
166
+ throw new DaytonaError("Failed to create snapshot. Didn't receive a snapshot from the server API.")
167
+ }
168
+
169
+ const terminalStates: SnapshotState[] = [SnapshotState.ACTIVE, SnapshotState.ERROR, SnapshotState.BUILD_FAILED]
170
+ const logTerminalStates: SnapshotState[] = [
171
+ ...terminalStates,
172
+ SnapshotState.PENDING_VALIDATION,
173
+ SnapshotState.VALIDATING,
174
+ ]
175
+ const snapshotRef = { createdSnapshot: createdSnapshot }
176
+ let streamPromise: Promise<void> | undefined
177
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
178
+ const startLogStreaming = async (onChunk: (chunk: string) => void = () => {}) => {
179
+ if (!streamPromise) {
180
+ streamPromise = processStreamingResponse(
181
+ () => this.snapshotsApi.getSnapshotBuildLogs(createdSnapshot.id, undefined, true, { responseType: 'stream' }),
182
+ (chunk) => onChunk(chunk.trimEnd()),
183
+ async () => logTerminalStates.includes(snapshotRef.createdSnapshot.state),
184
+ )
185
+ }
186
+ }
187
+
188
+ if (options.onLogs) {
189
+ options.onLogs(`Creating snapshot ${createdSnapshot.name} (${createdSnapshot.state})`)
190
+
191
+ if (createdSnapshot.state !== SnapshotState.BUILD_PENDING) {
192
+ await startLogStreaming(options.onLogs)
193
+ }
194
+ }
195
+
196
+ let previousState = createdSnapshot.state
197
+ while (!terminalStates.includes(createdSnapshot.state)) {
198
+ if (options.onLogs && previousState !== createdSnapshot.state) {
199
+ if (createdSnapshot.state !== SnapshotState.BUILD_PENDING && !streamPromise) {
200
+ await startLogStreaming(options.onLogs)
201
+ }
202
+ options.onLogs(`Creating snapshot ${createdSnapshot.name} (${createdSnapshot.state})`)
203
+ previousState = createdSnapshot.state
204
+ }
205
+ await new Promise((resolve) => setTimeout(resolve, 1000))
206
+ createdSnapshot = await this.get(createdSnapshot.name)
207
+ snapshotRef.createdSnapshot = createdSnapshot
208
+ }
209
+
210
+ if (options.onLogs) {
211
+ if (streamPromise) {
212
+ await streamPromise
213
+ }
214
+ if (createdSnapshot.state === SnapshotState.ACTIVE) {
215
+ options.onLogs(`Created snapshot ${createdSnapshot.name} (${createdSnapshot.state})`)
216
+ }
217
+ }
218
+
219
+ if (createdSnapshot.state === SnapshotState.ERROR || createdSnapshot.state === SnapshotState.BUILD_FAILED) {
220
+ const errMsg = `Failed to create snapshot. Name: ${createdSnapshot.name} Reason: ${createdSnapshot.errorReason}`
221
+ throw new DaytonaError(errMsg)
222
+ }
223
+
224
+ return createdSnapshot as Snapshot
225
+ }
226
+
227
+ /**
228
+ * Processes the image contexts by uploading them to object storage
229
+ *
230
+ * @private
231
+ * @param {Image} image - The Image instance.
232
+ * @returns {Promise<string[]>} The list of context hashes stored in object storage.
233
+ */
234
+ static async processImageContext(objectStorageApi: ObjectStorageApi, image: Image): Promise<string[]> {
235
+ if (!image.contextList || !image.contextList.length) {
236
+ return []
237
+ }
238
+
239
+ const pushAccessCreds = (await objectStorageApi.getPushAccess()).data
240
+ const objectStorage = new ObjectStorage({
241
+ endpointUrl: pushAccessCreds.storageUrl,
242
+ accessKeyId: pushAccessCreds.accessKey,
243
+ secretAccessKey: pushAccessCreds.secret,
244
+ sessionToken: pushAccessCreds.sessionToken,
245
+ bucketName: pushAccessCreds.bucket,
246
+ })
247
+
248
+ const contextHashes = []
249
+ for (const context of image.contextList) {
250
+ const contextHash = await objectStorage.upload(
251
+ context.sourcePath,
252
+ pushAccessCreds.organizationId,
253
+ context.archivePath,
254
+ )
255
+ contextHashes.push(contextHash)
256
+ }
257
+
258
+ return contextHashes
259
+ }
260
+ }
package/src/Volume.ts ADDED
@@ -0,0 +1,110 @@
1
+ /*
2
+ * Copyright 2025 Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { VolumeDto, VolumesApi } from '@daytonaio/api-client'
7
+ import { DaytonaNotFoundError } from './errors/DaytonaError'
8
+
9
+ /**
10
+ * Represents a Daytona Volume which is a shared storage volume for Sandboxes.
11
+ *
12
+ * @property {string} id - Unique identifier for the Volume
13
+ * @property {string} name - Name of the Volume
14
+ * @property {string} organizationId - Organization ID that owns the Volume
15
+ * @property {string} state - Current state of the Volume
16
+ * @property {string} createdAt - Date and time when the Volume was created
17
+ * @property {string} updatedAt - Date and time when the Volume was last updated
18
+ * @property {string} lastUsedAt - Date and time when the Volume was last used
19
+ */
20
+ export type Volume = VolumeDto & { __brand: 'Volume' }
21
+
22
+ /**
23
+ * Service for managing Daytona Volumes.
24
+ *
25
+ * This service provides methods to list, get, create, and delete Volumes.
26
+ *
27
+ * @class
28
+ */
29
+ export class VolumeService {
30
+ constructor(private volumesApi: VolumesApi) {}
31
+
32
+ /**
33
+ * Lists all available Volumes.
34
+ *
35
+ * @returns {Promise<Volume[]>} List of all Volumes accessible to the user
36
+ *
37
+ * @example
38
+ * const daytona = new Daytona();
39
+ * const volumes = await daytona.volume.list();
40
+ * console.log(`Found ${volumes.length} volumes`);
41
+ * volumes.forEach(vol => console.log(`${vol.name} (${vol.id})`));
42
+ */
43
+ async list(): Promise<Volume[]> {
44
+ const response = await this.volumesApi.listVolumes()
45
+ return response.data as Volume[]
46
+ }
47
+
48
+ /**
49
+ * Gets a Volume by its name.
50
+ *
51
+ * @param {string} name - Name of the Volume to retrieve
52
+ * @param {boolean} create - Whether to create the Volume if it does not exist
53
+ * @returns {Promise<Volume>} The requested Volume
54
+ * @throws {Error} If the Volume does not exist or cannot be accessed
55
+ *
56
+ * @example
57
+ * const daytona = new Daytona();
58
+ * const volume = await daytona.volume.get("volume-name", true);
59
+ * console.log(`Volume ${volume.name} is in state ${volume.state}`);
60
+ */
61
+ async get(name: string, create = false): Promise<Volume> {
62
+ try {
63
+ const response = await this.volumesApi.getVolumeByName(name)
64
+ return response.data as Volume
65
+ } catch (error) {
66
+ if (
67
+ error instanceof DaytonaNotFoundError &&
68
+ create &&
69
+ error.message.match(/Volume with name ([\w-]+) not found/)
70
+ ) {
71
+ return await this.create(name)
72
+ }
73
+ throw error
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Creates a new Volume with the specified name.
79
+ *
80
+ * @param {string} name - Name for the new Volume
81
+ * @returns {Promise<Volume>} The newly created Volume
82
+ * @throws {Error} If the Volume cannot be created
83
+ *
84
+ * @example
85
+ * const daytona = new Daytona();
86
+ * const volume = await daytona.volume.create("my-data-volume");
87
+ * console.log(`Created volume ${volume.name} with ID ${volume.id}`);
88
+ */
89
+ async create(name: string): Promise<Volume> {
90
+ const response = await this.volumesApi.createVolume({ name })
91
+ return response.data as Volume
92
+ }
93
+
94
+ /**
95
+ * Deletes a Volume.
96
+ *
97
+ * @param {Volume} volume - Volume to delete
98
+ * @returns {Promise<void>}
99
+ * @throws {Error} If the Volume does not exist or cannot be deleted
100
+ *
101
+ * @example
102
+ * const daytona = new Daytona();
103
+ * const volume = await daytona.volume.get("volume-name");
104
+ * await daytona.volume.delete(volume);
105
+ * console.log("Volume deleted successfully");
106
+ */
107
+ async delete(volume: Volume): Promise<void> {
108
+ await this.volumesApi.deleteVolume(volume.id)
109
+ }
110
+ }