teleportation-cli 1.0.0 → 1.0.2
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.
- package/.claude/hooks/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/stop.mjs +205 -1
- package/.claude/hooks/user_prompt_submit.mjs +111 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/daemon/teleportation-daemon.js +131 -41
- package/lib/install/installer.js +22 -7
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daytona Provider for Remote Development Environments
|
|
3
|
+
*
|
|
4
|
+
* Uses Daytona API for ephemeral, cloud-native development workspaces.
|
|
5
|
+
* Workspaces can be created from git repos and automatically configured.
|
|
6
|
+
*
|
|
7
|
+
* Ideal for:
|
|
8
|
+
* - Quick tasks (feature branches, bug fixes, PR reviews)
|
|
9
|
+
* - Clean, isolated environments
|
|
10
|
+
* - Auto-stop to save costs when inactive
|
|
11
|
+
* - Snapshot support for state preservation
|
|
12
|
+
*
|
|
13
|
+
* @see https://daytona.io/docs/api
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { BaseProvider } from './base-provider.js';
|
|
17
|
+
|
|
18
|
+
export class DaytonaProvider extends BaseProvider {
|
|
19
|
+
/**
|
|
20
|
+
* Initialize DaytonaProvider
|
|
21
|
+
* @param {Object} config - Provider configuration
|
|
22
|
+
* @param {VaultClient} config.vaultClient - Mech Vault client
|
|
23
|
+
* @param {LivePortClient} config.livePortClient - LivePort client
|
|
24
|
+
* @param {string} [config.daytonaApiKey] - Daytona API key (or DAYTONA_API_KEY env var)
|
|
25
|
+
* @param {string} [config.apiUrl] - Daytona API URL (or DAYTONA_API_URL env var, defaults to https://app.daytona.io/api)
|
|
26
|
+
* @param {string} [config.profileId] - Daytona profile ID (or DAYTONA_PROFILE_ID env var, defaults to 'default')
|
|
27
|
+
* @param {string} [config.organizationId] - Daytona organization ID (or DAYTONA_ORGANIZATION_ID env var)
|
|
28
|
+
*/
|
|
29
|
+
constructor(config) {
|
|
30
|
+
super(config);
|
|
31
|
+
this.apiKey = config.daytonaApiKey || process.env.DAYTONA_API_KEY;
|
|
32
|
+
this.apiUrl = config.apiUrl || process.env.DAYTONA_API_URL || 'https://app.daytona.io/api';
|
|
33
|
+
this.profileId = config.profileId || process.env.DAYTONA_PROFILE_ID || 'default';
|
|
34
|
+
this.organizationId = config.organizationId || process.env.DAYTONA_ORGANIZATION_ID || '';
|
|
35
|
+
this.timeout = config.timeout || 30000; // 30 second default timeout
|
|
36
|
+
|
|
37
|
+
if (!this.apiKey) {
|
|
38
|
+
throw new Error('DAYTONA_API_KEY is required for DaytonaProvider');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create headers for Daytona API
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
_headers() {
|
|
47
|
+
const headers = {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (this.organizationId) {
|
|
53
|
+
headers['X-Daytona-Organization-ID'] = this.organizationId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return headers;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _formatApiError(response, url) {
|
|
60
|
+
const contentType = response.headers?.get?.('content-type') || '';
|
|
61
|
+
|
|
62
|
+
let raw = '';
|
|
63
|
+
try {
|
|
64
|
+
const bodyReader = typeof response.clone === 'function' ? response.clone() : response;
|
|
65
|
+
raw = await bodyReader.text();
|
|
66
|
+
} catch {
|
|
67
|
+
raw = '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trimmed = raw?.trim?.() || '';
|
|
71
|
+
|
|
72
|
+
const truncated = trimmed.length > 1000
|
|
73
|
+
? `${trimmed.slice(0, 1000)}…`
|
|
74
|
+
: trimmed;
|
|
75
|
+
|
|
76
|
+
const contentTypeLower = String(contentType).toLowerCase();
|
|
77
|
+
const isHtml = contentTypeLower.includes('text/html')
|
|
78
|
+
|| /<\s*!doctype\s+html/i.test(truncated)
|
|
79
|
+
|| /<\s*html/i.test(truncated);
|
|
80
|
+
const hint = isHtml
|
|
81
|
+
? ' (received HTML; check DAYTONA_API_URL and ensure DAYTONA_API_KEY is valid for API access)'
|
|
82
|
+
: '';
|
|
83
|
+
|
|
84
|
+
return `Daytona API request failed: ${response.status} ${response.statusText} (${url})${hint}\n${truncated}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Escape shell argument by wrapping in single quotes and escaping any single quotes
|
|
89
|
+
* @private
|
|
90
|
+
* @param {string} arg - Argument to escape
|
|
91
|
+
* @returns {string} Shell-safe escaped argument
|
|
92
|
+
*/
|
|
93
|
+
_escapeShellArg(arg) {
|
|
94
|
+
if (!arg) return "''";
|
|
95
|
+
// Replace any single quotes with '\'' (end quote, escaped quote, start quote)
|
|
96
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch with timeout support
|
|
101
|
+
* @private
|
|
102
|
+
* @param {string} url - URL to fetch
|
|
103
|
+
* @param {Object} options - Fetch options
|
|
104
|
+
* @returns {Promise<Response>} Fetch response
|
|
105
|
+
* @throws {Error} If request times out or fails
|
|
106
|
+
*/
|
|
107
|
+
async _fetchWithTimeout(url, options = {}) {
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
...options,
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
return response;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
if (error.name === 'AbortError') {
|
|
121
|
+
throw new Error(`Request timed out after ${this.timeout}ms: ${url}`);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a remote Daytona workspace
|
|
129
|
+
*
|
|
130
|
+
* Provisions a new Daytona workspace with:
|
|
131
|
+
* - Bun runtime (oven/bun:latest)
|
|
132
|
+
* - Git repository cloned automatically
|
|
133
|
+
* - SSH access configured
|
|
134
|
+
* - LivePort tunnel setup
|
|
135
|
+
* - All secrets stored in Vault
|
|
136
|
+
* - Auto-stop when inactive
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} sessionConfig - Session configuration (see BaseProvider)
|
|
139
|
+
* @returns {Promise<Object>} Workspace details with IDs for cleanup
|
|
140
|
+
* @throws {Error} If workspace provisioning fails
|
|
141
|
+
*/
|
|
142
|
+
async createMachine(sessionConfig) {
|
|
143
|
+
const sessionId = sessionConfig.sessionId;
|
|
144
|
+
const namespace = `teleportationremote${String(sessionId).replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
145
|
+
|
|
146
|
+
// 1. Setup SSH access
|
|
147
|
+
const ssh = await this._setupSSHAccess(sessionId, namespace);
|
|
148
|
+
|
|
149
|
+
// 2. Setup LivePort tunnel
|
|
150
|
+
const tunnel = await this._setupTunnel(sessionId, sessionId);
|
|
151
|
+
|
|
152
|
+
// 3. Store necessary secrets in Vault
|
|
153
|
+
const secrets = {
|
|
154
|
+
RELAY_API_URL: sessionConfig.relayApiUrl,
|
|
155
|
+
RELAY_API_KEY: sessionConfig.relayApiKey,
|
|
156
|
+
GITHUB_TOKEN: sessionConfig.githubToken,
|
|
157
|
+
MECH_API_KEY: sessionConfig.mechApiKey,
|
|
158
|
+
LIVEPORT_BRIDGE_KEY: tunnel.bridgeKey,
|
|
159
|
+
...sessionConfig.additionalSecrets,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const secretIds = await this._storeSecrets(namespace, secrets);
|
|
163
|
+
|
|
164
|
+
// 4. Create workspace configuration
|
|
165
|
+
const workspaceConfig = {
|
|
166
|
+
name: `teleportation-${sessionId}`,
|
|
167
|
+
profileId: this.profileId,
|
|
168
|
+
git: {
|
|
169
|
+
repository: sessionConfig.repoUrl,
|
|
170
|
+
branch: sessionConfig.branch || 'main',
|
|
171
|
+
},
|
|
172
|
+
envVars: {
|
|
173
|
+
SESSION_ID: sessionId,
|
|
174
|
+
VAULT_NAMESPACE: namespace,
|
|
175
|
+
MECH_VAULT_URL: this.vaultClient.apiUrl,
|
|
176
|
+
MECH_API_KEY: this.vaultClient.apiKey,
|
|
177
|
+
MECH_APP_ID: this.vaultClient.appId,
|
|
178
|
+
TUNNEL_URL: tunnel.tunnelUrl,
|
|
179
|
+
LIVEPORT_BRIDGE_KEY: tunnel.bridgeKey,
|
|
180
|
+
},
|
|
181
|
+
devcontainer: {
|
|
182
|
+
image: 'oven/bun:latest',
|
|
183
|
+
postCreateCommand: this._generatePostCreateScript(sessionConfig, tunnel, ssh),
|
|
184
|
+
},
|
|
185
|
+
ideConfig: {
|
|
186
|
+
defaultIde: 'none', // No IDE needed, using CLI
|
|
187
|
+
},
|
|
188
|
+
resources: {
|
|
189
|
+
cpus: sessionConfig.cpus || 1,
|
|
190
|
+
memory: sessionConfig.memory || 2048, // MB
|
|
191
|
+
disk: sessionConfig.disk || 10, // GB
|
|
192
|
+
},
|
|
193
|
+
autoStop: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
inactivityMinutes: sessionConfig.autoStopMinutes || 60,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const createUrl = `${this.apiUrl}/workspace`;
|
|
200
|
+
const response = await this._fetchWithTimeout(createUrl, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: this._headers(),
|
|
203
|
+
body: JSON.stringify(workspaceConfig),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
throw new Error(await this._formatApiError(response, createUrl));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const workspace = await response.json();
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
machineId: workspace.id,
|
|
214
|
+
workspaceId: workspace.id,
|
|
215
|
+
sshHost: workspace.sshHost || `${workspace.id}.daytona.io`,
|
|
216
|
+
sshPort: workspace.sshPort || 22,
|
|
217
|
+
tunnelUrl: tunnel.tunnelUrl,
|
|
218
|
+
namespace,
|
|
219
|
+
secretIds,
|
|
220
|
+
sshKeyId: ssh.keyId,
|
|
221
|
+
bridgeKeyId: tunnel.keyId,
|
|
222
|
+
ideUrl: workspace.ideUrl,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate post-create script for Daytona workspace
|
|
228
|
+
*
|
|
229
|
+
* This script runs after the workspace is created and sets up:
|
|
230
|
+
* - SSH authorized keys
|
|
231
|
+
* - Teleportation CLI installation
|
|
232
|
+
* - LivePort tunnel
|
|
233
|
+
* - Teleportation daemon
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
* @param {Object} sessionConfig - Session configuration
|
|
237
|
+
* @param {Object} tunnel - Tunnel configuration with tunnelCommand
|
|
238
|
+
* @param {Object} ssh - SSH key with publicKey
|
|
239
|
+
* @returns {string} Shell script for workspace initialization
|
|
240
|
+
*/
|
|
241
|
+
_generatePostCreateScript(sessionConfig, tunnel, ssh) {
|
|
242
|
+
// Escape all user-provided values to prevent command injection
|
|
243
|
+
const safePublicKey = this._escapeShellArg(ssh.publicKey);
|
|
244
|
+
const safeSessionId = this._escapeShellArg(sessionConfig.sessionId);
|
|
245
|
+
const safeTask = this._escapeShellArg(sessionConfig.task || '');
|
|
246
|
+
|
|
247
|
+
return `
|
|
248
|
+
set -e
|
|
249
|
+
|
|
250
|
+
# Setup SSH key
|
|
251
|
+
mkdir -p ~/.ssh
|
|
252
|
+
echo ${safePublicKey} >> ~/.ssh/authorized_keys
|
|
253
|
+
chmod 600 ~/.ssh/authorized_keys
|
|
254
|
+
|
|
255
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
256
|
+
cd "$REPO_ROOT"
|
|
257
|
+
|
|
258
|
+
# Install repo dependencies
|
|
259
|
+
bun install
|
|
260
|
+
|
|
261
|
+
# Start LivePort tunnel in background
|
|
262
|
+
${tunnel.tunnelCommand} &
|
|
263
|
+
|
|
264
|
+
TUNNEL_PID=$!
|
|
265
|
+
|
|
266
|
+
# Wait briefly for the tunnel process to stay alive (avoid blind sleep)
|
|
267
|
+
for i in 1 2 3 4 5; do
|
|
268
|
+
if ! kill -0 "$TUNNEL_PID" >/dev/null 2>&1; then
|
|
269
|
+
echo "LivePort tunnel failed to start" >&2
|
|
270
|
+
exit 1
|
|
271
|
+
fi
|
|
272
|
+
sleep 1
|
|
273
|
+
done
|
|
274
|
+
|
|
275
|
+
# Start teleportation daemon
|
|
276
|
+
bun teleportation-cli.cjs daemon start --session-id ${safeSessionId} --task ${safeTask}
|
|
277
|
+
`.trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Destroy the Daytona workspace
|
|
282
|
+
*
|
|
283
|
+
* Permanently deletes the workspace and all its data.
|
|
284
|
+
* This action cannot be undone.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
287
|
+
* @returns {Promise<boolean>} True if destroyed successfully
|
|
288
|
+
* @throws {Error} If workspace deletion fails
|
|
289
|
+
*/
|
|
290
|
+
async destroyMachine(workspaceId) {
|
|
291
|
+
this._validateMachineId(workspaceId);
|
|
292
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}`;
|
|
293
|
+
const response = await this._fetchWithTimeout(url, {
|
|
294
|
+
method: 'DELETE',
|
|
295
|
+
headers: this._headers(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
throw new Error(await this._formatApiError(response, url));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get workspace status
|
|
307
|
+
*
|
|
308
|
+
* Returns current state of the Daytona workspace including auto-stop countdown.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
311
|
+
* @returns {Promise<Object>} Workspace status
|
|
312
|
+
* @returns {string} return.status - Workspace state (running, stopped, error)
|
|
313
|
+
* @returns {number} return.uptime - Uptime in seconds
|
|
314
|
+
* @returns {string} return.lastActivity - Last activity timestamp
|
|
315
|
+
* @returns {number} return.autoStopIn - Seconds until auto-stop
|
|
316
|
+
* @returns {Object} return.resources - Resource allocation (cpus, memory)
|
|
317
|
+
* @throws {Error} If status fetch fails
|
|
318
|
+
*/
|
|
319
|
+
async getMachineStatus(workspaceId) {
|
|
320
|
+
this._validateMachineId(workspaceId);
|
|
321
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}`;
|
|
322
|
+
const response = await this._fetchWithTimeout(url, {
|
|
323
|
+
method: 'GET',
|
|
324
|
+
headers: this._headers(),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
throw new Error(await this._formatApiError(response, url));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const workspace = await response.json();
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
status: workspace.state || workspace.status,
|
|
335
|
+
uptime: workspace.uptime,
|
|
336
|
+
lastActivity: workspace.lastActivity,
|
|
337
|
+
autoStopIn: workspace.autoStopIn,
|
|
338
|
+
resources: workspace.resources,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Execute command in workspace
|
|
344
|
+
*
|
|
345
|
+
* Runs a command directly in the workspace via Daytona exec API.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
348
|
+
* @param {string} command - Command to execute
|
|
349
|
+
* @returns {Promise<Object>} Command result
|
|
350
|
+
* @returns {string} return.stdout - Standard output
|
|
351
|
+
* @returns {string} return.stderr - Standard error
|
|
352
|
+
* @returns {number} return.exitCode - Exit code
|
|
353
|
+
* @throws {Error} If command execution fails
|
|
354
|
+
*/
|
|
355
|
+
async executeCommand(workspaceId, command) {
|
|
356
|
+
this._validateMachineId(workspaceId);
|
|
357
|
+
const url = `${this.apiUrl}/toolbox/${workspaceId}/toolbox/process/execute`;
|
|
358
|
+
const response = await this._fetchWithTimeout(url, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: this._headers(),
|
|
361
|
+
body: JSON.stringify({ command }),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
throw new Error(await this._formatApiError(response, url));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return response.json();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get workspace logs
|
|
373
|
+
*
|
|
374
|
+
* Retrieves logs from the workspace's processes.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
377
|
+
* @param {Object} [options] - Log retrieval options
|
|
378
|
+
* @param {number} [options.tail] - Number of recent lines to retrieve (default: 100)
|
|
379
|
+
* @param {boolean} [options.follow] - Stream logs continuously (default: false)
|
|
380
|
+
* @returns {Promise<Object>} Log data
|
|
381
|
+
* @returns {Array<string>} return.logs - Array of log lines
|
|
382
|
+
* @throws {Error} If log retrieval fails
|
|
383
|
+
*/
|
|
384
|
+
async getLogs(workspaceId, options = {}) {
|
|
385
|
+
this._validateMachineId(workspaceId);
|
|
386
|
+
const params = new URLSearchParams({
|
|
387
|
+
tail: options.tail || 100,
|
|
388
|
+
follow: options.follow || false,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}/logs?${params}`;
|
|
392
|
+
const response = await this._fetchWithTimeout(url, {
|
|
393
|
+
method: 'GET',
|
|
394
|
+
headers: this._headers(),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(await this._formatApiError(response, url));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return response.json();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Stop workspace (keeps it for later restart)
|
|
406
|
+
*
|
|
407
|
+
* Stops the workspace to save costs. Workspace state is preserved.
|
|
408
|
+
*
|
|
409
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
410
|
+
* @returns {Promise<boolean>} True if stopped successfully
|
|
411
|
+
* @throws {Error} If stop fails
|
|
412
|
+
*/
|
|
413
|
+
async stopMachine(workspaceId) {
|
|
414
|
+
this._validateMachineId(workspaceId);
|
|
415
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}/stop`;
|
|
416
|
+
const response = await this._fetchWithTimeout(url, {
|
|
417
|
+
method: 'POST',
|
|
418
|
+
headers: this._headers(),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
throw new Error(await this._formatApiError(response, url));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Start stopped workspace
|
|
430
|
+
*
|
|
431
|
+
* Resumes a stopped workspace. State is preserved from when it was stopped.
|
|
432
|
+
*
|
|
433
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
434
|
+
* @returns {Promise<boolean>} True if started successfully
|
|
435
|
+
* @throws {Error} If start fails
|
|
436
|
+
*/
|
|
437
|
+
async startMachine(workspaceId) {
|
|
438
|
+
this._validateMachineId(workspaceId);
|
|
439
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}/start`;
|
|
440
|
+
const response = await this._fetchWithTimeout(url, {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: this._headers(),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
throw new Error(await this._formatApiError(response, url));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Extend auto-stop timeout
|
|
454
|
+
*
|
|
455
|
+
* Adds additional time before the workspace auto-stops due to inactivity.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
458
|
+
* @param {number} additionalMinutes - Minutes to add to auto-stop timer
|
|
459
|
+
* @returns {Promise<Object>} Extended auto-stop information
|
|
460
|
+
* @throws {Error} If extension fails
|
|
461
|
+
*/
|
|
462
|
+
async extendAutoStop(workspaceId, additionalMinutes) {
|
|
463
|
+
this._validateMachineId(workspaceId);
|
|
464
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}/extend`;
|
|
465
|
+
const response = await this._fetchWithTimeout(url, {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: this._headers(),
|
|
468
|
+
body: JSON.stringify({
|
|
469
|
+
additionalMinutes,
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
throw new Error(await this._formatApiError(response, url));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return response.json();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Create snapshot of workspace
|
|
482
|
+
*
|
|
483
|
+
* Creates a snapshot of the workspace's current state.
|
|
484
|
+
* Snapshots can be used to restore workspace to a previous state.
|
|
485
|
+
*
|
|
486
|
+
* @param {string} workspaceId - Daytona workspace ID
|
|
487
|
+
* @param {string} name - Snapshot name
|
|
488
|
+
* @returns {Promise<Object>} Snapshot details
|
|
489
|
+
* @throws {Error} If snapshot creation fails
|
|
490
|
+
*/
|
|
491
|
+
async createSnapshot(workspaceId, name) {
|
|
492
|
+
this._validateMachineId(workspaceId);
|
|
493
|
+
const url = `${this.apiUrl}/workspace/${workspaceId}/snapshots`;
|
|
494
|
+
const response = await this._fetchWithTimeout(url, {
|
|
495
|
+
method: 'POST',
|
|
496
|
+
headers: this._headers(),
|
|
497
|
+
body: JSON.stringify({ name }),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
throw new Error(await this._formatApiError(response, url));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return response.json();
|
|
505
|
+
}
|
|
506
|
+
}
|