@watasu/sdk 0.1.25 → 0.1.40

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/dist/sandbox.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Commands } from './commands.js';
2
2
  import { ConnectionConfig, SESSION_OPERATION_REQUEST_TIMEOUT_MS } from './connectionConfig.js';
3
- import { DataPlaneClient, ControlClient } from './transport.js';
3
+ import { DataPlaneClient, ControlClient, withQuery } from './transport.js';
4
4
  import { ConflictError, FileNotFoundError, NotFoundError, SandboxError, unsupported } from './errors.js';
5
5
  import { Filesystem } from './filesystem.js';
6
6
  import { Git } from './git.js';
@@ -24,6 +24,7 @@ export class SnapshotPaginator {
24
24
  const control = new ControlClient(config);
25
25
  const payload = await control.get(snapshotListPath(this.opts, this.nextToken), {
26
26
  requestTimeoutMs: opts.requestTimeoutMs,
27
+ signal: opts.signal,
27
28
  });
28
29
  this.nextToken = stringValue(payload.next_token ?? payload.nextToken);
29
30
  this.hasNext = this.nextToken !== undefined;
@@ -57,6 +58,7 @@ export class SandboxPaginator {
57
58
  const control = new ControlClient(config);
58
59
  const payload = await control.get(sandboxListPath(this.opts, this.nextToken), {
59
60
  requestTimeoutMs: opts.requestTimeoutMs,
61
+ signal: opts.signal,
60
62
  });
61
63
  this.nextToken = stringValue(payload.next_token ?? payload.nextToken);
62
64
  this.hasNext = this.nextToken !== undefined;
@@ -80,14 +82,12 @@ export class Sandbox {
80
82
  /** Default sandbox lifetime in milliseconds. */
81
83
  static defaultSandboxTimeoutMs = 300_000;
82
84
  files;
83
- filesystem;
84
85
  commands;
85
86
  process;
86
87
  pty;
87
88
  terminal;
88
89
  git;
89
90
  cwd;
90
- envVars;
91
91
  sandboxId;
92
92
  mcpPort = 50005;
93
93
  mcpToken;
@@ -101,12 +101,10 @@ export class Sandbox {
101
101
  this.config = opts.connectionConfig;
102
102
  this.control = opts.control ?? new ControlClient(this.config);
103
103
  this.envs = opts.envs ?? {};
104
- this.envVars = this.envs;
105
104
  this.sandbox = opts.sandbox ?? {};
106
105
  const dataPlane = dataPlaneFromSession(opts.session, this.config);
107
106
  this.dataPlane = dataPlane;
108
107
  this.files = new Filesystem(dataPlane);
109
- this.filesystem = this.files;
110
108
  this.commands = new Commands(dataPlane, this.config, this.envs);
111
109
  this.process = new ProcessManager(this.commands);
112
110
  this.pty = new Pty(dataPlane, this.config);
@@ -123,8 +121,6 @@ export class Sandbox {
123
121
  const template = typeof templateOrOpts === 'string'
124
122
  ? templateOrOpts
125
123
  : templateOrOpts?.template ?? (sandboxOpts.mcp === undefined ? this.defaultTemplate : undefined);
126
- if (sandboxOpts.volumeMounts !== undefined)
127
- unsupported('volumeMounts');
128
124
  const config = new ConnectionConfig(sandboxOpts);
129
125
  const control = new ControlClient(config);
130
126
  const sandboxPayload = {
@@ -136,11 +132,14 @@ export class Sandbox {
136
132
  };
137
133
  putIfPresent(sandboxPayload, 'template_id', template);
138
134
  putIfPresent(sandboxPayload, 'mcp', sandboxOpts.mcp);
135
+ putIfPresent(sandboxPayload, 'lifecycle', lifecyclePayload(sandboxOpts.lifecycle));
136
+ putIfPresent(sandboxPayload, 'volume_mounts', volumeMountsPayload(sandboxOpts.volumeMounts));
139
137
  Object.assign(sandboxPayload, networkUpdatePayload(sandboxOpts.network));
140
138
  putIfPresent(sandboxPayload, 'team', sandboxOpts.team);
141
139
  const response = await control.post('/sandboxes', {
142
140
  json: sandboxPayload,
143
141
  requestTimeoutMs: sessionOperationRequestTimeout(config, sandboxOpts),
142
+ signal: sandboxOpts.signal,
144
143
  });
145
144
  const sandbox = record(response.sandbox ?? response);
146
145
  const sandboxId = sandbox.id ?? sandbox.sandbox_id;
@@ -160,10 +159,14 @@ export class Sandbox {
160
159
  static async connect(sandboxId, opts = {}) {
161
160
  const config = new ConnectionConfig(opts);
162
161
  const control = new ControlClient(config);
163
- const info = await control.get(`/sandboxes/${sandboxId}`);
162
+ const info = await control.get(`/sandboxes/${sandboxId}`, {
163
+ requestTimeoutMs: opts.requestTimeoutMs,
164
+ signal: opts.signal,
165
+ });
164
166
  const response = await control.post(`/sandboxes/${sandboxId}/resume`, {
165
167
  json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
166
168
  requestTimeoutMs: sessionOperationRequestTimeout(config, opts),
169
+ signal: opts.signal,
167
170
  });
168
171
  return new this({
169
172
  sandboxId,
@@ -173,22 +176,17 @@ export class Sandbox {
173
176
  sandbox: record(response.sandbox ?? info.sandbox ?? {}),
174
177
  });
175
178
  }
176
- static async reconnect(sandboxOrOpts, opts = {}) {
177
- if (typeof sandboxOrOpts === 'string')
178
- return this.connect(sandboxOrOpts, opts);
179
- return this.connect(sandboxOrOpts.sandboxID, sandboxOrOpts);
180
- }
181
179
  /** Refresh this sandbox's data-plane session in place. */
182
180
  async connect(opts = {}) {
183
181
  const response = await this.control.post(`/sandboxes/${this.sandboxId}/resume`, {
184
182
  json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
185
183
  requestTimeoutMs: sessionOperationRequestTimeout(this.config, opts),
184
+ signal: opts.signal,
186
185
  });
187
186
  this.sandbox = record(response.sandbox ?? this.sandbox);
188
187
  const dataPlane = dataPlaneFromSession(response.session, this.config);
189
188
  this.dataPlane = dataPlane;
190
189
  this.files = new Filesystem(dataPlane);
191
- this.filesystem = this.files;
192
190
  this.commands = new Commands(dataPlane, this.config, this.envs);
193
191
  this.process = new ProcessManager(this.commands);
194
192
  this.pty = new Pty(dataPlane, this.config);
@@ -207,6 +205,7 @@ export class Sandbox {
207
205
  try {
208
206
  await control.post(`/sandboxes/${sandboxId}/pause`, {
209
207
  requestTimeoutMs: opts.requestTimeoutMs,
208
+ signal: opts.signal,
210
209
  });
211
210
  return true;
212
211
  }
@@ -216,21 +215,26 @@ export class Sandbox {
216
215
  throw error;
217
216
  }
218
217
  }
219
- /** Alias for `betaPause`. */
218
+ /** Pause a sandbox by id. */
220
219
  static async pause(sandboxId, opts = {}) {
221
220
  return this.betaPause(sandboxId, opts);
222
221
  }
223
222
  /** Destroy a sandbox by id. */
224
223
  static async kill(sandboxId, opts = {}) {
224
+ const requestOpts = typeof opts === 'string' ? {} : opts;
225
225
  const control = new ControlClient(new ConnectionConfig(typeof opts === 'string' ? { apiKey: opts } : opts));
226
- await control.delete(`/sandboxes/${sandboxId}`);
226
+ await control.delete(`/sandboxes/${sandboxId}`, {
227
+ requestTimeoutMs: requestOpts.requestTimeoutMs,
228
+ signal: requestOpts.signal,
229
+ });
227
230
  return true;
228
231
  }
229
232
  /** Fetch sandbox metrics by id. */
230
233
  static async getMetrics(sandboxId, opts = {}) {
231
234
  const control = new ControlClient(new ConnectionConfig(opts));
232
- const payload = await control.get(`/sandboxes/${sandboxId}/metrics`, {
235
+ const payload = await control.get(metricsPath(sandboxId, opts), {
233
236
  requestTimeoutMs: opts.requestTimeoutMs,
237
+ signal: opts.signal,
234
238
  });
235
239
  return metricsList(payload.metrics ?? payload);
236
240
  }
@@ -243,19 +247,17 @@ export class Sandbox {
243
247
  const response = await control.put(`/sandboxes/${sandboxId}/network`, {
244
248
  json: networkUpdatePayload(network),
245
249
  requestTimeoutMs: opts.requestTimeoutMs,
250
+ signal: opts.signal,
246
251
  });
247
252
  return response.sandbox === undefined ? undefined : record(response.sandbox);
248
253
  }
249
- /** Deprecated alias for `getInfo`. */
250
- static async getFullInfo(sandboxId, opts = {}) {
251
- return this.getInfo(sandboxId, opts);
252
- }
253
254
  /** Create a Watasu checkpoint using snapshot naming. */
254
255
  static async createSnapshot(sandboxId, opts = {}) {
255
256
  const control = new ControlClient(new ConnectionConfig(opts));
256
257
  const payload = await control.post(`/sandboxes/${sandboxId}/snapshots`, {
257
258
  json: snapshotPayload(opts),
258
259
  requestTimeoutMs: opts.requestTimeoutMs,
260
+ signal: opts.signal,
259
261
  });
260
262
  return snapshotInfo(record(payload.sandbox_checkpoint ?? payload.snapshot ?? payload));
261
263
  }
@@ -269,6 +271,7 @@ export class Sandbox {
269
271
  try {
270
272
  await control.delete(`/sandbox_snapshots/${snapshotId}`, {
271
273
  requestTimeoutMs: opts.requestTimeoutMs,
274
+ signal: opts.signal,
272
275
  });
273
276
  return true;
274
277
  }
@@ -279,8 +282,11 @@ export class Sandbox {
279
282
  }
280
283
  }
281
284
  /** Destroy this sandbox. */
282
- async kill() {
283
- await this.control.delete(`/sandboxes/${this.sandboxId}`);
285
+ async kill(opts = {}) {
286
+ await this.control.delete(`/sandboxes/${this.sandboxId}`, {
287
+ requestTimeoutMs: opts.requestTimeoutMs,
288
+ signal: opts.signal,
289
+ });
284
290
  return true;
285
291
  }
286
292
  /** Check if this sandbox is in a runtime-active lifecycle state. */
@@ -288,6 +294,7 @@ export class Sandbox {
288
294
  try {
289
295
  const payload = await this.control.get(`/sandboxes/${this.sandboxId}`, {
290
296
  requestTimeoutMs: opts.requestTimeoutMs,
297
+ signal: opts.signal,
291
298
  });
292
299
  const item = record(payload.sandbox ?? payload);
293
300
  return ['creating', 'ready', 'checkpointing', 'restoring', 'stopping'].includes(String(item.state ?? ''));
@@ -303,27 +310,33 @@ export class Sandbox {
303
310
  const control = new ControlClient(new ConnectionConfig(opts));
304
311
  await control.post(`/sandboxes/${sandboxId}/timeout`, {
305
312
  json: { timeout: Math.ceil(timeoutMs / 1000) },
313
+ requestTimeoutMs: opts.requestTimeoutMs,
314
+ signal: opts.signal,
306
315
  });
307
316
  }
308
317
  /** Set this sandbox's lifetime. */
309
- async setTimeout(timeoutMs) {
318
+ async setTimeout(timeoutMs, opts = {}) {
310
319
  await this.control.post(`/sandboxes/${this.sandboxId}/timeout`, {
311
320
  json: { timeout: Math.ceil(timeoutMs / 1000) },
321
+ requestTimeoutMs: opts.requestTimeoutMs,
322
+ signal: opts.signal,
312
323
  });
313
324
  }
314
- /** Keep the sandbox alive for `duration` milliseconds. */
315
- async keepAlive(duration) {
316
- await this.setTimeout(duration);
317
- }
318
325
  /** Fetch control-plane metadata for a sandbox by id. */
319
326
  static async getInfo(sandboxId, opts = {}) {
320
327
  const control = new ControlClient(new ConnectionConfig(opts));
321
- const payload = await control.get(`/sandboxes/${sandboxId}`);
328
+ const payload = await control.get(`/sandboxes/${sandboxId}`, {
329
+ requestTimeoutMs: opts.requestTimeoutMs,
330
+ signal: opts.signal,
331
+ });
322
332
  return sandboxInfo(record(payload.sandbox ?? payload));
323
333
  }
324
334
  /** Fetch the latest control-plane metadata for this sandbox. */
325
- async getInfo() {
326
- const payload = await this.control.get(`/sandboxes/${this.sandboxId}`);
335
+ async getInfo(opts = {}) {
336
+ const payload = await this.control.get(`/sandboxes/${this.sandboxId}`, {
337
+ requestTimeoutMs: opts.requestTimeoutMs,
338
+ signal: opts.signal,
339
+ });
327
340
  return sandboxInfo(record(payload.sandbox ?? payload));
328
341
  }
329
342
  /** Fetch latest sandbox metrics. */
@@ -355,13 +368,12 @@ export class Sandbox {
355
368
  if (checkpointId === undefined)
356
369
  throw new SandboxError('checkpointId or snapshotId is required');
357
370
  const payload = { checkpoint_id: checkpointId };
358
- if (restoreOpts.timeout !== undefined)
359
- payload.timeout_seconds = restoreOpts.timeout;
360
371
  if (restoreOpts.timeoutMs !== undefined)
361
372
  payload.timeout_seconds = Math.ceil(restoreOpts.timeoutMs / 1000);
362
373
  const response = await this.control.post(`/sandboxes/${this.sandboxId}/restore`, {
363
374
  json: payload,
364
375
  requestTimeoutMs: restoreOpts.requestTimeoutMs,
376
+ signal: restoreOpts.signal,
365
377
  });
366
378
  return sandboxInfo(record(response.sandbox ?? response));
367
379
  }
@@ -379,12 +391,6 @@ export class Sandbox {
379
391
  throw new SandboxError('port response did not include host or url');
380
392
  return `p${port}-${routeToken}.sandbox.${this.config.dataPlaneDomain}`;
381
393
  }
382
- /** Return the public hostname for the sandbox or an exposed sandbox port. */
383
- getHostname(port) {
384
- if (port !== undefined)
385
- return this.getHost(port);
386
- return new URL(this.dataPlane.baseUrl).host;
387
- }
388
394
  /** Return the conventional MCP URL for this sandbox. */
389
395
  getMcpUrl() {
390
396
  return `https://${this.getHost(this.mcpPort)}/mcp`;
@@ -404,12 +410,6 @@ export class Sandbox {
404
410
  throw error;
405
411
  }
406
412
  }
407
- /** Return a protocol string for a secure or insecure sandbox URL. */
408
- getProtocol(baseProtocol = 'http', secure = true) {
409
- return `${baseProtocol}${secure ? 's' : ''}`;
410
- }
411
- /** Close the local SDK attachment. This does not destroy the sandbox. */
412
- async close() { }
413
413
  /** Get a signed URL that accepts a POST upload for a sandbox file path. */
414
414
  async uploadUrl(path = '', opts = {}) {
415
415
  const fileUrl = await this.fileUrl('/upload_url', path, opts);
@@ -437,7 +437,7 @@ export class Sandbox {
437
437
  async betaPause(opts = {}) {
438
438
  return Sandbox.betaPause(this.sandboxId, { ...this.configOptions(), ...opts });
439
439
  }
440
- /** Alias for `betaPause`. */
440
+ /** Pause this sandbox. Returns false when it was already paused. */
441
441
  async pause(opts = {}) {
442
442
  return this.betaPause(opts);
443
443
  }
@@ -455,6 +455,7 @@ export class Sandbox {
455
455
  expires_in_seconds: opts.expiresInSeconds,
456
456
  }),
457
457
  requestTimeoutMs: opts.requestTimeoutMs,
458
+ signal: opts.signal,
458
459
  });
459
460
  return fileUrlInfo(record(payload.file_url ?? payload));
460
461
  }
@@ -463,21 +464,42 @@ export class Sandbox {
463
464
  return this.dataPlane.postJson(path, {
464
465
  json,
465
466
  requestTimeoutMs: opts.requestTimeoutMs,
467
+ signal: opts.signal,
468
+ });
469
+ }
470
+ /** GET JSON from the sandbox data-plane runtime API. */
471
+ async runtimeGetJson(path, opts = {}) {
472
+ return this.dataPlane.getJson(path, {
473
+ requestTimeoutMs: opts.requestTimeoutMs,
474
+ signal: opts.signal,
475
+ });
476
+ }
477
+ /** DELETE JSON from the sandbox data-plane runtime API. */
478
+ async runtimeDeleteJson(path, opts = {}) {
479
+ return this.dataPlane.deleteJson(path, {
480
+ requestTimeoutMs: opts.requestTimeoutMs,
481
+ signal: opts.signal,
466
482
  });
467
483
  }
468
484
  configOptions() {
469
485
  return {
470
486
  apiKey: this.config.apiKey,
471
487
  apiUrl: this.config.apiUrl,
488
+ sandboxUrl: this.config.sandboxUrl,
472
489
  dataPlaneDomain: this.config.dataPlaneDomain,
473
490
  requestTimeoutMs: this.config.requestTimeoutMs,
491
+ headers: this.config.headers,
492
+ apiHeaders: this.config.apiHeaders,
493
+ debug: this.config.debug,
494
+ signal: this.config.signal,
495
+ proxy: this.config.proxy,
474
496
  };
475
497
  }
476
498
  }
477
499
  function dataPlaneFromSession(session, config) {
478
500
  const item = record(session);
479
501
  const token = item.token ?? item.access_token;
480
- const url = item.data_plane_url;
502
+ const url = config.sandboxUrl ?? item.data_plane_url;
481
503
  if (!session)
482
504
  throw new SandboxError('sandbox session is required for data-plane operations');
483
505
  if (typeof token !== 'string' || typeof url !== 'string') {
@@ -487,8 +509,8 @@ function dataPlaneFromSession(session, config) {
487
509
  }
488
510
  function sandboxListPath(opts, nextToken) {
489
511
  const params = new URLSearchParams();
490
- if (opts.team)
491
- params.set('team', opts.team);
512
+ if (opts.team !== undefined)
513
+ params.set('team', String(opts.team));
492
514
  if (opts.limit !== undefined)
493
515
  params.set('limit', String(opts.limit));
494
516
  if (nextToken)
@@ -515,6 +537,15 @@ function snapshotListPath(opts, nextToken) {
515
537
  const query = params.toString();
516
538
  return query ? `/sandbox_snapshots?${query}` : '/sandbox_snapshots';
517
539
  }
540
+ function metricsPath(sandboxId, opts) {
541
+ return withQuery(`/sandboxes/${sandboxId}/metrics`, {
542
+ start: dateTimestampSeconds(opts.start),
543
+ end: dateTimestampSeconds(opts.end),
544
+ });
545
+ }
546
+ function dateTimestampSeconds(value) {
547
+ return value === undefined ? undefined : Math.round(value.getTime() / 1000);
548
+ }
518
549
  function fileUrlInfo(payload) {
519
550
  return {
520
551
  method: String(payload.method ?? ''),
@@ -538,6 +569,8 @@ function sandboxInfo(payload) {
538
569
  templateId: typeof payload.template_id === 'string' ? payload.template_id : templateSlug(payload.template),
539
570
  name: typeof payload.name === 'string' ? payload.name : undefined,
540
571
  state: typeof payload.state === 'string' ? payload.state : undefined,
572
+ lifecycle: sandboxLifecycleInfo(payload.lifecycle),
573
+ volumeMounts: volumeMountsInfo(payload.volume_mounts ?? payload.volumeMounts),
541
574
  metadata: recordOfStrings(payload.metadata),
542
575
  startedAt: typeof payload.started_at === 'string'
543
576
  ? payload.started_at
@@ -547,6 +580,45 @@ function sandboxInfo(payload) {
547
580
  : typeof payload.deadline_at === 'string' ? payload.deadline_at : undefined,
548
581
  };
549
582
  }
583
+ function lifecyclePayload(lifecycle) {
584
+ if (lifecycle === undefined)
585
+ return undefined;
586
+ const onTimeout = lifecycle.onTimeout ?? 'kill';
587
+ const autoResume = lifecycle.autoResume ?? false;
588
+ if (autoResume && onTimeout !== 'pause') {
589
+ throw new SandboxError("lifecycle.autoResume can only be true when lifecycle.onTimeout is 'pause'");
590
+ }
591
+ return { on_timeout: onTimeout, auto_resume: autoResume };
592
+ }
593
+ function volumeMountsPayload(volumeMounts) {
594
+ if (volumeMounts === undefined)
595
+ return undefined;
596
+ return Object.entries(volumeMounts).map(([path, volume]) => ({
597
+ path,
598
+ name: typeof volume === 'string' ? volume : volume.name,
599
+ }));
600
+ }
601
+ function volumeMountsInfo(value) {
602
+ if (!Array.isArray(value))
603
+ return undefined;
604
+ return value
605
+ .map((item) => {
606
+ const entry = record(item);
607
+ return { name: String(entry.name ?? ''), path: String(entry.path ?? '') };
608
+ })
609
+ .filter((entry) => entry.name !== '' && entry.path !== '');
610
+ }
611
+ function sandboxLifecycleInfo(value) {
612
+ const lifecycle = record(value);
613
+ const onTimeout = stringValue(lifecycle.on_timeout ?? lifecycle.onTimeout);
614
+ const autoResume = booleanValue(lifecycle.auto_resume ?? lifecycle.autoResume);
615
+ if (onTimeout === undefined && autoResume === undefined)
616
+ return undefined;
617
+ return {
618
+ onTimeout: onTimeout ?? 'kill',
619
+ autoResume: autoResume ?? false,
620
+ };
621
+ }
550
622
  function metricsList(value) {
551
623
  if (Array.isArray(value))
552
624
  return value.map((item) => metricsInfo(record(item)));
@@ -596,6 +668,15 @@ function stringValue(value) {
596
668
  function numberValue(value) {
597
669
  return typeof value === 'number' ? value : undefined;
598
670
  }
671
+ function booleanValue(value) {
672
+ if (typeof value === 'boolean')
673
+ return value;
674
+ if (value === 'true' || value === '1')
675
+ return true;
676
+ if (value === 'false' || value === '0')
677
+ return false;
678
+ return undefined;
679
+ }
599
680
  function templateSlug(value) {
600
681
  const template = record(value);
601
682
  return typeof template.slug === 'string' ? template.slug : undefined;
@@ -102,8 +102,6 @@ export declare class ReadyCmd {
102
102
  export declare function waitForPort(port: number): ReadyCmd;
103
103
  /** Return a ready check that waits for a URL to return an HTTP status code. */
104
104
  export declare function waitForURL(url: string, statusCode?: number): ReadyCmd;
105
- /** Alias for `waitForURL`. */
106
- export declare const waitForUrl: typeof waitForURL;
107
105
  /** Return a ready check that waits for a process name. */
108
106
  export declare function waitForProcess(processName: string): ReadyCmd;
109
107
  /** Return a ready check that waits for a file to exist. */
@@ -204,10 +202,13 @@ export declare class TemplateBase {
204
202
  }): TemplateBuilder;
205
203
  setStartCmd(startCommand: string, readyCommand: ReadyCommand): TemplateFinal;
206
204
  setReadyCmd(readyCommand: ReadyCommand): TemplateFinal;
205
+ betaDevContainerPrebuild(devcontainerDirectory: string): TemplateBuilder;
206
+ betaSetDevContainerStart(devcontainerDirectory: string): TemplateFinal;
207
207
  setEnvs(envs: Record<string, string>): TemplateBuilder;
208
208
  skipCache(): TemplateBuilder;
209
209
  toBuildSpec(): BuildSpec;
210
210
  private addPackages;
211
+ private requireDevContainerTemplate;
211
212
  private addCopySource;
212
213
  private addFileSpec;
213
214
  private resolveContextPath;
package/dist/template.js CHANGED
@@ -22,8 +22,6 @@ export function waitForPort(port) {
22
22
  export function waitForURL(url, statusCode = 200) {
23
23
  return new ReadyCmd(`curl -s -o /dev/null -w "%{http_code}" ${url} | grep -q "${statusCode}"`);
24
24
  }
25
- /** Alias for `waitForURL`. */
26
- export const waitForUrl = waitForURL;
27
25
  /** Return a ready check that waits for a process name. */
28
26
  export function waitForProcess(processName) {
29
27
  return new ReadyCmd(`pgrep ${processName} > /dev/null`);
@@ -286,6 +284,14 @@ export class TemplateBase {
286
284
  this.readyCmd = readyCommandText(readyCommand);
287
285
  return this;
288
286
  }
287
+ betaDevContainerPrebuild(devcontainerDirectory) {
288
+ this.requireDevContainerTemplate('betaDevContainerPrebuild');
289
+ return this.runCmd(`devcontainer build --workspace-folder ${devcontainerDirectory}`, { user: 'root' });
290
+ }
291
+ betaSetDevContainerStart(devcontainerDirectory) {
292
+ this.requireDevContainerTemplate('betaSetDevContainerStart');
293
+ return this.setStartCmd(`sudo devcontainer up --workspace-folder ${devcontainerDirectory} && sudo /prepare-exec.sh ${devcontainerDirectory} | sudo tee /devcontainer.sh > /dev/null && sudo chmod +x /devcontainer.sh && sudo touch /devcontainer.up`, waitForFile('/devcontainer.up'));
294
+ }
289
295
  setEnvs(envs) {
290
296
  Object.assign(this.env, envs);
291
297
  return this;
@@ -315,6 +321,11 @@ export class TemplateBase {
315
321
  addPackages(manager, packages) {
316
322
  this.packages[manager] = [...(this.packages[manager] ?? []), ...packages];
317
323
  }
324
+ requireDevContainerTemplate(method) {
325
+ if (this.base !== 'devcontainer') {
326
+ throw new SandboxError(`${method} can only be used with the devcontainer template`);
327
+ }
328
+ }
318
329
  addCopySource(source, dest, options, multipleSources) {
319
330
  const sourcePath = this.resolveContextPath(source);
320
331
  const stat = fs.statSync(sourcePath);
@@ -13,9 +13,8 @@ export type TerminalOpts = {
13
13
  terminalID?: string;
14
14
  cmd?: string;
15
15
  cwd?: string;
16
- rootDir?: string;
17
- envVars?: Record<string, string>;
18
- timeout?: number;
16
+ envs?: Record<string, string>;
17
+ timeoutMs?: number;
19
18
  };
20
19
  /** A running terminal session in a sandbox. */
21
20
  export declare class Terminal {
package/dist/terminal.js CHANGED
@@ -59,10 +59,9 @@ export class TerminalManager {
59
59
  const handle = await this.pty.create({
60
60
  cmd: opts.cmd,
61
61
  cwd: opts.cwd,
62
- rootDir: opts.rootDir,
63
- envVars: opts.envVars,
62
+ envs: opts.envs,
64
63
  size: opts.size,
65
- timeout: opts.timeout,
64
+ timeoutMs: opts.timeoutMs,
66
65
  onData: async (bytes) => {
67
66
  const data = new TextDecoder().decode(bytes);
68
67
  output.addData(data);
@@ -15,6 +15,7 @@ export declare class DataPlaneClient {
15
15
  readonly token: string;
16
16
  private readonly config;
17
17
  constructor(baseUrl: string, token: string, config: ConnectionConfig);
18
+ get headers(): Record<string, string>;
18
19
  getJson(path: string, opts?: RequestOpts): Promise<JsonRecord>;
19
20
  postJson(path: string, opts?: RequestOpts): Promise<JsonRecord>;
20
21
  deleteJson(path: string, opts?: RequestOpts): Promise<JsonRecord>;
@@ -29,6 +30,7 @@ export interface RequestOpts {
29
30
  body?: BodyInit | Uint8Array;
30
31
  headers?: Record<string, string>;
31
32
  requestTimeoutMs?: number;
33
+ signal?: AbortSignal;
32
34
  }
33
35
  export declare function withQuery(path: string, params: Record<string, string | number | boolean | undefined>): string;
34
36
  export {};
package/dist/transport.js CHANGED
@@ -28,9 +28,10 @@ export class ControlClient {
28
28
  headers: {
29
29
  ...this.config.authHeaders,
30
30
  ...(opts.json ? { 'content-type': 'application/json' } : {}),
31
+ ...(opts.headers ?? {}),
31
32
  },
32
33
  body: opts.json ? JSON.stringify(opts.json) : undefined,
33
- }, opts.requestTimeoutMs ?? this.config.requestTimeoutMs);
34
+ }, opts.requestTimeoutMs ?? this.config.requestTimeoutMs, opts.signal ?? this.config.signal);
34
35
  return parseJsonResponse(response);
35
36
  }
36
37
  }
@@ -43,6 +44,9 @@ export class DataPlaneClient {
43
44
  this.token = token;
44
45
  this.config = config;
45
46
  }
47
+ get headers() {
48
+ return this.config.headers;
49
+ }
46
50
  getJson(path, opts = {}) {
47
51
  return this.request(path, { ...opts, method: 'GET' });
48
52
  }
@@ -68,12 +72,13 @@ export class DataPlaneClient {
68
72
  const response = await fetchWithTimeout(joinUrl(this.baseUrl, path), {
69
73
  method: opts.method,
70
74
  headers: {
75
+ ...this.config.headers,
71
76
  Authorization: `Bearer ${this.token}`,
72
77
  ...(opts.json ? { 'content-type': 'application/json' } : {}),
73
78
  ...(opts.headers ?? {}),
74
79
  },
75
80
  body: opts.json ? JSON.stringify(opts.json) : opts.body,
76
- }, opts.requestTimeoutMs ?? this.config.requestTimeoutMs);
81
+ }, opts.requestTimeoutMs ?? this.config.requestTimeoutMs, opts.signal ?? this.config.signal);
77
82
  if (!response.ok) {
78
83
  throw errorFromResponse(response.status, await readJsonOrText(response));
79
84
  }
@@ -109,14 +114,26 @@ async function readJsonOrText(response) {
109
114
  return { message: text };
110
115
  }
111
116
  }
112
- function fetchWithTimeout(url, init, timeoutMs) {
117
+ function fetchWithTimeout(url, init, timeoutMs, signal) {
113
118
  const controller = new AbortController();
114
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
119
+ let timedOut = false;
120
+ const abortFromCaller = () => controller.abort();
121
+ if (signal?.aborted)
122
+ controller.abort();
123
+ else
124
+ signal?.addEventListener('abort', abortFromCaller, { once: true });
125
+ const timeout = setTimeout(() => {
126
+ timedOut = true;
127
+ controller.abort();
128
+ }, timeoutMs);
115
129
  return fetch(url, { ...init, signal: controller.signal }).catch((error) => {
116
- if (error?.name === 'AbortError')
130
+ if (error?.name === 'AbortError' && timedOut)
117
131
  throw new TimeoutError();
118
132
  throw error;
119
- }).finally(() => clearTimeout(timeout));
133
+ }).finally(() => {
134
+ clearTimeout(timeout);
135
+ signal?.removeEventListener('abort', abortFromCaller);
136
+ });
120
137
  }
121
138
  function joinUrl(base, path) {
122
139
  const normalizedBase = base.replace(/\/+$/, '');