@computesdk/cocoonstack 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,630 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CocoonstackApiError: () => CocoonstackApiError,
34
+ cocoonstack: () => cocoonstack,
35
+ default: () => index_default
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_node_http = __toESM(require("http"));
39
+ var import_node_http2 = __toESM(require("http2"));
40
+ var import_node_https = __toESM(require("https"));
41
+ var import_node_perf_hooks = require("perf_hooks");
42
+ var import_node_string_decoder = require("string_decoder");
43
+ var import_node_tls = __toESM(require("tls"));
44
+ var import_provider = require("@computesdk/provider");
45
+ var PROVIDER = "cocoonstack";
46
+ var DEFAULT_TEMPLATE = "node-rt:24.04";
47
+ var DEFAULT_TTL_SECONDS = 300;
48
+ var MAX_TTL_SECONDS = 86400;
49
+ var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
50
+ var COMMAND_HTTP_TIMEOUT_BUFFER_MS = 5e3;
51
+ var MAX_RESPONSE_BYTES = 16 << 20;
52
+ var SIZES = [
53
+ { size: "small", cpu: 1, memoryMb: 512 },
54
+ { size: "medium", cpu: 2, memoryMb: 1024 },
55
+ { size: "large", cpu: 4, memoryMb: 4096 },
56
+ { size: "xlarge", cpu: 4, memoryMb: 8192 },
57
+ { size: "2xlarge", cpu: 8, memoryMb: 16384 }
58
+ ];
59
+ var CocoonstackApiError = class extends Error {
60
+ constructor(status, message) {
61
+ super(message);
62
+ this.status = status;
63
+ this.name = "CocoonstackApiError";
64
+ }
65
+ };
66
+ var claimTokens = /* @__PURE__ */ new Map();
67
+ var sessions = /* @__PURE__ */ new Map();
68
+ var sessionStreams = /* @__PURE__ */ new WeakMap();
69
+ var secureContext = import_node_tls.default.createSecureContext();
70
+ var httpAgent = new import_node_http.default.Agent({ keepAlive: true });
71
+ var httpsAgent = new import_node_https.default.Agent({ keepAlive: true });
72
+ function env(name) {
73
+ return typeof process === "undefined" ? void 0 : process.env[name];
74
+ }
75
+ function resolve(config) {
76
+ const baseUrl = config.baseUrl || env("COCOONSTACK_API_URL") || "";
77
+ if (!baseUrl) {
78
+ throw new Error(
79
+ "Missing Cocoon Stack endpoint. Pass baseUrl or set COCOONSTACK_API_URL to your sandboxd address."
80
+ );
81
+ }
82
+ return {
83
+ baseUrl: baseUrl.replace(/\/$/, ""),
84
+ apiKey: config.apiKey || env("COCOONSTACK_API_KEY") || "",
85
+ template: config.template ?? DEFAULT_TEMPLATE,
86
+ net: config.net ?? "none",
87
+ size: config.size ?? "small",
88
+ ttlSeconds: config.ttlSeconds ?? DEFAULT_TTL_SECONDS,
89
+ requestTimeoutMs: config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
90
+ };
91
+ }
92
+ function claimKey(resolved, id) {
93
+ return `${resolved.baseUrl} ${id}`;
94
+ }
95
+ function rememberClaim(resolved, id, token, deadline) {
96
+ const now = Date.now();
97
+ for (const [key, claim] of claimTokens) {
98
+ if (claim.deadline < now) claimTokens.delete(key);
99
+ }
100
+ claimTokens.set(claimKey(resolved, id), { token, deadline: deadline.getTime() });
101
+ }
102
+ function retainedToken(resolved, id) {
103
+ const key = claimKey(resolved, id);
104
+ const claim = claimTokens.get(key);
105
+ if (!claim) return void 0;
106
+ if (claim.deadline < Date.now()) {
107
+ claimTokens.delete(key);
108
+ return void 0;
109
+ }
110
+ return claim.token;
111
+ }
112
+ function detail(body) {
113
+ const match = /"error"\s*:\s*"([^"]*)"/.exec(body);
114
+ return match?.[1] ?? body;
115
+ }
116
+ function session(origin, lane) {
117
+ const key = `${lane} ${origin}`;
118
+ const live = sessions.get(key);
119
+ if (live && !live.closed && !live.destroyed) return live;
120
+ const created = import_node_http2.default.connect(origin, { secureContext });
121
+ applyHold(created);
122
+ const drop = () => {
123
+ if (sessions.get(key) === created) sessions.delete(key);
124
+ };
125
+ created.on("connect", () => applyHold(created));
126
+ created.on("close", drop);
127
+ created.on("goaway", drop);
128
+ created.on("error", drop);
129
+ sessions.set(key, created);
130
+ return created;
131
+ }
132
+ function holdSession(live) {
133
+ sessionStreams.set(live, (sessionStreams.get(live) ?? 0) + 1);
134
+ applyHold(live);
135
+ return () => {
136
+ sessionStreams.set(live, Math.max(0, (sessionStreams.get(live) ?? 1) - 1));
137
+ applyHold(live);
138
+ };
139
+ }
140
+ function applyHold(live) {
141
+ const socket = live.socket;
142
+ if (!socket) return;
143
+ if ((sessionStreams.get(live) ?? 0) > 0) socket.ref?.();
144
+ else socket.unref?.();
145
+ }
146
+ function h2Request(origin, lane, method, path, headers, payload, timeoutMs, signal) {
147
+ return new Promise((fulfill, reject) => {
148
+ const live = session(origin, lane);
149
+ const release = holdSession(live);
150
+ const stream = live.request({ ":method": method, ":path": path, ...headers });
151
+ let status = 0;
152
+ let bytes = 0;
153
+ const chunks = [];
154
+ const abort = () => stream.close(import_node_http2.default.constants.NGHTTP2_CANCEL);
155
+ signal?.addEventListener("abort", abort, { once: true });
156
+ let released = false;
157
+ const done = () => {
158
+ if (released) return;
159
+ released = true;
160
+ signal?.removeEventListener("abort", abort);
161
+ release();
162
+ };
163
+ stream.setTimeout(timeoutMs, () => {
164
+ stream.close(import_node_http2.default.constants.NGHTTP2_CANCEL);
165
+ reject(new Error("sandboxd request timed out"));
166
+ });
167
+ stream.on("response", (responseHeaders) => {
168
+ status = Number(responseHeaders[":status"] ?? 0);
169
+ });
170
+ stream.on("data", (chunk) => {
171
+ bytes += chunk.length;
172
+ if (bytes > MAX_RESPONSE_BYTES) {
173
+ stream.close(import_node_http2.default.constants.NGHTTP2_CANCEL);
174
+ reject(new Error(`sandboxd response exceeds ${MAX_RESPONSE_BYTES >> 20} MiB`));
175
+ return;
176
+ }
177
+ chunks.push(chunk);
178
+ });
179
+ stream.on("end", () => {
180
+ done();
181
+ fulfill({ status, text: Buffer.concat(chunks).toString("utf8") });
182
+ });
183
+ stream.on("error", (error) => {
184
+ done();
185
+ reject(signal?.aborted ? signal.reason instanceof Error ? signal.reason : new Error("aborted") : error);
186
+ });
187
+ stream.on("close", done);
188
+ stream.end(payload);
189
+ });
190
+ }
191
+ function h1Request(url, method, headers, payload, timeoutMs, signal) {
192
+ return new Promise((fulfill, reject) => {
193
+ const secure = url.protocol === "https:";
194
+ const client = (secure ? import_node_https.default : import_node_http.default).request(
195
+ url,
196
+ {
197
+ method,
198
+ agent: secure ? httpsAgent : httpAgent,
199
+ timeout: timeoutMs,
200
+ signal,
201
+ headers: {
202
+ ...headers,
203
+ ...payload === void 0 ? {} : { "content-length": Buffer.byteLength(payload) }
204
+ }
205
+ },
206
+ (response) => {
207
+ let bytes = 0;
208
+ const chunks = [];
209
+ response.on("data", (chunk) => {
210
+ bytes += chunk.length;
211
+ if (bytes > MAX_RESPONSE_BYTES) {
212
+ client.destroy(new Error(`sandboxd response exceeds ${MAX_RESPONSE_BYTES >> 20} MiB`));
213
+ return;
214
+ }
215
+ chunks.push(chunk);
216
+ });
217
+ response.on("error", reject);
218
+ response.on("end", () => fulfill({ status: response.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
219
+ }
220
+ );
221
+ client.on("timeout", () => client.destroy(new Error("sandboxd request timed out")));
222
+ client.on("error", reject);
223
+ client.end(payload);
224
+ });
225
+ }
226
+ async function request(resolved, token, method, path, body, signal, timeoutMs = resolved.requestTimeoutMs, lane = "main") {
227
+ const url = new URL(`${resolved.baseUrl}${path}`);
228
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
229
+ const headers = {
230
+ "content-type": "application/json",
231
+ ...token ? { authorization: `Bearer ${token}` } : {}
232
+ };
233
+ const { status, text } = url.protocol === "https:" ? await h2Request(url.origin, lane, method, `${url.pathname}${url.search}`, headers, payload, timeoutMs, signal) : await h1Request(url, method, headers, payload, timeoutMs, signal);
234
+ if (status < 200 || status >= 300) {
235
+ throw new CocoonstackApiError(status, `sandboxd ${method} ${path} failed (${status}): ${detail(text)}`);
236
+ }
237
+ return text ? JSON.parse(text) : void 0;
238
+ }
239
+ function isSize(value) {
240
+ return SIZES.some((tier) => tier.size === value);
241
+ }
242
+ function sizeFor(resolved, options) {
243
+ if (options?.size !== void 0) {
244
+ if (!isSize(options.size)) {
245
+ throw new Error(
246
+ `Unknown Cocoon Stack size ${JSON.stringify(options.size)}; one of ${SIZES.map((tier2) => tier2.size).join(", ")}.`
247
+ );
248
+ }
249
+ return options.size;
250
+ }
251
+ const cpu = options?.vcpus ?? options?.cpus ?? options?.cpu;
252
+ const memoryMb = options?.memoryMb ?? options?.memoryMiB ?? options?.memMiB ?? options?.memory;
253
+ if (cpu === void 0 && memoryMb === void 0) return resolved.size;
254
+ const tier = SIZES.find((candidate) => candidate.cpu >= (cpu ?? 0) && candidate.memoryMb >= (memoryMb ?? 0));
255
+ if (!tier) {
256
+ throw new Error(
257
+ `No Cocoon Stack size tier offers ${cpu ?? "-"} CPU / ${memoryMb ?? "-"} MB; the largest is 2xlarge (8 CPU / 16384 MB).`
258
+ );
259
+ }
260
+ return tier.size;
261
+ }
262
+ function ttlFor(resolved, options) {
263
+ if (!options?.timeout) return resolved.ttlSeconds;
264
+ return Math.min(MAX_TTL_SECONDS, Math.ceil(options.timeout / 1e3));
265
+ }
266
+ function fromRow(config, row, token) {
267
+ return {
268
+ id: row.id,
269
+ token,
270
+ template: row.key.template,
271
+ net: row.key.net,
272
+ size: row.key.size,
273
+ createdAt: /* @__PURE__ */ new Date(),
274
+ deadline: new Date(row.deadline),
275
+ config
276
+ };
277
+ }
278
+ async function index(resolved) {
279
+ const listing = await request(resolved, resolved.apiKey, "GET", "/v1/sandboxes");
280
+ return listing.sandboxes;
281
+ }
282
+ function relay(sandbox, resolved, rpc, options) {
283
+ return new Promise((fulfill, reject) => {
284
+ const url = new URL(`${resolved.baseUrl}/v1/sandboxes/${encodeURIComponent(sandbox.id)}/agent`);
285
+ const secure = url.protocol === "https:";
286
+ const client = (secure ? import_node_https.default : import_node_http.default).request(url, {
287
+ method: "GET",
288
+ agent: secure ? httpsAgent : httpAgent,
289
+ headers: { upgrade: "silkd", connection: "Upgrade", authorization: `Bearer ${sandbox.token}` }
290
+ });
291
+ let settled = false;
292
+ let timer;
293
+ const finish = (outcome) => {
294
+ if (settled) return;
295
+ settled = true;
296
+ clearTimeout(timer);
297
+ outcome();
298
+ };
299
+ if (options.timeoutMs) {
300
+ timer = setTimeout(() => {
301
+ finish(() => reject(new Error(`Command timed out after ${options.timeoutMs} ms`)));
302
+ client.destroy();
303
+ }, options.timeoutMs);
304
+ }
305
+ client.on("response", (response) => {
306
+ let body = "";
307
+ response.setEncoding("utf8");
308
+ response.on("data", (chunk) => {
309
+ body += chunk;
310
+ });
311
+ response.on(
312
+ "end",
313
+ () => finish(
314
+ () => reject(
315
+ new CocoonstackApiError(
316
+ response.statusCode ?? 0,
317
+ `sandboxd agent relay refused (${response.statusCode}): ${detail(body)}`
318
+ )
319
+ )
320
+ )
321
+ );
322
+ });
323
+ client.on("upgrade", (_response, socket, head) => {
324
+ const out = new import_node_string_decoder.StringDecoder("utf8");
325
+ const err = new import_node_string_decoder.StringDecoder("utf8");
326
+ let stdout = "";
327
+ let stderr = "";
328
+ let frames = 0;
329
+ let lastOut = 0;
330
+ let lastErr = 0;
331
+ let pending = head.toString("utf8");
332
+ const handle = (frame) => {
333
+ switch (frame.type) {
334
+ case "started":
335
+ if (options.detach) {
336
+ finish(() => fulfill({ exitCode: 0, stdout: String(frame.pid ?? ""), stderr: "" }));
337
+ socket.destroy();
338
+ }
339
+ break;
340
+ case "stdout": {
341
+ lastOut = ++frames;
342
+ const text = out.write(Buffer.from(frame.data ?? "", "base64"));
343
+ stdout += text;
344
+ if (text) options.onStdout?.(text);
345
+ break;
346
+ }
347
+ case "stderr": {
348
+ lastErr = ++frames;
349
+ const text = err.write(Buffer.from(frame.data ?? "", "base64"));
350
+ stderr += text;
351
+ if (text) options.onStderr?.(text);
352
+ break;
353
+ }
354
+ case "exit": {
355
+ const tailOut = out.end();
356
+ const tailErr = err.end();
357
+ stdout += tailOut;
358
+ stderr += tailErr;
359
+ const tails = [
360
+ [lastOut, tailOut, options.onStdout],
361
+ [lastErr, tailErr, options.onStderr]
362
+ ];
363
+ for (const [, text, notify] of tails.sort((a, b) => a[0] - b[0])) {
364
+ if (text) notify?.(text);
365
+ }
366
+ finish(() => fulfill({ exitCode: frame.code ?? 0, stdout, stderr }));
367
+ socket.destroy();
368
+ break;
369
+ }
370
+ case "error":
371
+ finish(() => reject(new Error(`silkd ${frame.kind ?? "error"}: ${frame.message ?? ""}`)));
372
+ socket.destroy();
373
+ break;
374
+ default:
375
+ break;
376
+ }
377
+ };
378
+ socket.setEncoding("utf8");
379
+ socket.on("data", (chunk) => {
380
+ pending += chunk;
381
+ let newline = pending.indexOf("\n");
382
+ while (newline >= 0) {
383
+ const line = pending.slice(0, newline);
384
+ pending = pending.slice(newline + 1);
385
+ if (line.trim()) {
386
+ try {
387
+ handle(JSON.parse(line));
388
+ } catch (error) {
389
+ finish(() => reject(error instanceof Error ? error : new Error(String(error))));
390
+ socket.destroy();
391
+ return;
392
+ }
393
+ }
394
+ newline = pending.indexOf("\n");
395
+ }
396
+ });
397
+ socket.on("error", (error) => finish(() => reject(error)));
398
+ socket.on("close", () => finish(() => reject(new Error("Agent relay closed before the command exited"))));
399
+ socket.write(`${JSON.stringify({ v: 1, ...rpc })}
400
+ `);
401
+ });
402
+ client.on("error", (error) => finish(() => reject(error)));
403
+ client.end();
404
+ });
405
+ }
406
+ async function exec(sandbox, command, options) {
407
+ const started = import_node_perf_hooks.performance.now();
408
+ const resolved = resolve(sandbox.config);
409
+ const argv = ["bash", "-c", command];
410
+ if (options?.background || options?.onStdout || options?.onStderr) {
411
+ const result = await relay(
412
+ sandbox,
413
+ resolved,
414
+ {
415
+ op: "exec",
416
+ argv,
417
+ ...options.cwd ? { cwd: options.cwd } : {},
418
+ ...options.env ? { env: options.env } : {},
419
+ ...options.background ? { detach: true } : {}
420
+ },
421
+ {
422
+ timeoutMs: options.timeout ?? resolved.requestTimeoutMs,
423
+ detach: options.background,
424
+ onStdout: options.onStdout,
425
+ onStderr: options.onStderr
426
+ }
427
+ );
428
+ return { ...result, durationMs: import_node_perf_hooks.performance.now() - started };
429
+ }
430
+ try {
431
+ const result = await request(
432
+ resolved,
433
+ sandbox.token,
434
+ "POST",
435
+ `/v1/sandboxes/${encodeURIComponent(sandbox.id)}/exec`,
436
+ {
437
+ argv,
438
+ ...options?.cwd ? { cwd: options.cwd } : {},
439
+ ...options?.env ? { env: options.env } : {},
440
+ ...options?.timeout ? { timeout_seconds: Math.ceil(options.timeout / 1e3) } : {}
441
+ },
442
+ void 0,
443
+ options?.timeout ? options.timeout + COMMAND_HTTP_TIMEOUT_BUFFER_MS : resolved.requestTimeoutMs
444
+ );
445
+ return {
446
+ exitCode: result.exit_code,
447
+ stdout: result.stdout,
448
+ stderr: result.stderr,
449
+ durationMs: import_node_perf_hooks.performance.now() - started
450
+ };
451
+ } catch (error) {
452
+ if (error instanceof CocoonstackApiError && error.status === 504) {
453
+ throw new Error(`Command timed out after ${options?.timeout} ms`);
454
+ }
455
+ throw error;
456
+ }
457
+ }
458
+ async function shell(runCommand, sandbox, command, failure) {
459
+ const result = await runCommand(sandbox, command);
460
+ if (result.exitCode !== 0) {
461
+ throw new Error(`${failure}: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
462
+ }
463
+ return result.stdout;
464
+ }
465
+ var provider = (0, import_provider.defineProvider)({
466
+ name: PROVIDER,
467
+ methods: {
468
+ sandbox: {
469
+ create: async (config, options) => {
470
+ const resolved = resolve(config);
471
+ const template = options?.templateId ?? options?.image ?? resolved.template;
472
+ const size = sizeFor(resolved, options);
473
+ const claimed = await request(
474
+ resolved,
475
+ resolved.apiKey,
476
+ "POST",
477
+ "/v1/claim",
478
+ { template, net: resolved.net, size, ttl_seconds: ttlFor(resolved, options) },
479
+ options?.signal
480
+ );
481
+ if (claimed.redirect) {
482
+ throw new Error(
483
+ `sandboxd redirected the claim to ${claimed.redirect.join(", ")}; point baseUrl at a node that serves the pool.`
484
+ );
485
+ }
486
+ const deadline = new Date(claimed.deadline);
487
+ rememberClaim(resolved, claimed.id, claimed.token, deadline);
488
+ const sandbox = {
489
+ id: claimed.id,
490
+ token: claimed.token,
491
+ template,
492
+ net: resolved.net,
493
+ size,
494
+ createdAt: /* @__PURE__ */ new Date(),
495
+ deadline,
496
+ config
497
+ };
498
+ return { sandbox, sandboxId: sandbox.id };
499
+ },
500
+ getById: async (config, sandboxId) => {
501
+ const resolved = resolve(config);
502
+ const token = retainedToken(resolved, sandboxId);
503
+ if (token === void 0) return null;
504
+ const row = (await index(resolved)).find((candidate) => candidate.id === sandboxId);
505
+ return row ? { sandbox: fromRow(config, row, token), sandboxId } : null;
506
+ },
507
+ list: async (config) => {
508
+ const resolved = resolve(config);
509
+ return (await index(resolved)).flatMap((row) => {
510
+ const token = retainedToken(resolved, row.id);
511
+ return token === void 0 ? [] : [{ sandbox: fromRow(config, row, token), sandboxId: row.id }];
512
+ });
513
+ },
514
+ destroy: async (config, sandboxId) => {
515
+ const resolved = resolve(config);
516
+ try {
517
+ await request(
518
+ resolved,
519
+ retainedToken(resolved, sandboxId) ?? resolved.apiKey,
520
+ "POST",
521
+ `/v1/sandboxes/${encodeURIComponent(sandboxId)}/release`,
522
+ void 0,
523
+ void 0,
524
+ resolved.requestTimeoutMs,
525
+ "release"
526
+ );
527
+ } catch (error) {
528
+ if (!(error instanceof CocoonstackApiError && error.status === 404)) throw error;
529
+ }
530
+ claimTokens.delete(claimKey(resolved, sandboxId));
531
+ },
532
+ runCommand: exec,
533
+ streamCommand: exec,
534
+ getInfo: async (sandbox) => ({
535
+ id: sandbox.id,
536
+ provider: PROVIDER,
537
+ status: "running",
538
+ createdAt: sandbox.createdAt,
539
+ timeout: Math.max(0, sandbox.deadline.getTime() - Date.now()),
540
+ metadata: {
541
+ template: sandbox.template,
542
+ net: sandbox.net,
543
+ size: sandbox.size,
544
+ deadline: sandbox.deadline.toISOString()
545
+ }
546
+ }),
547
+ getUrl: async (sandbox, options) => {
548
+ const resolved = resolve(sandbox.config);
549
+ const preview = await request(
550
+ resolved,
551
+ resolved.apiKey,
552
+ "POST",
553
+ `/v1/sandboxes/${encodeURIComponent(sandbox.id)}/preview`,
554
+ { token: sandbox.token, port: options.port, ttl_seconds: 0 }
555
+ );
556
+ return preview.url;
557
+ },
558
+ filesystem: {
559
+ readFile: async (sandbox, path, runCommand) => shell(runCommand, sandbox, `cat "${shellPath(path)}"`, `Failed to read ${path}`),
560
+ writeFile: async (sandbox, path, content, runCommand) => {
561
+ const encoded = Buffer.from(content, "utf8").toString("base64");
562
+ const cut = path.lastIndexOf("/");
563
+ const directory = cut < 0 ? "." : path.slice(0, cut) || "/";
564
+ await shell(
565
+ runCommand,
566
+ sandbox,
567
+ `mkdir -p "${shellPath(directory)}" && printf %s "${(0, import_provider.escapeShellArg)(encoded)}" | base64 -d > "${shellPath(path)}"`,
568
+ `Failed to write ${path}`
569
+ );
570
+ },
571
+ mkdir: async (sandbox, path, runCommand) => {
572
+ await shell(runCommand, sandbox, `mkdir -p "${shellPath(path)}"`, `Failed to create ${path}`);
573
+ },
574
+ readdir: async (sandbox, path, runCommand) => {
575
+ const listing = await shell(
576
+ runCommand,
577
+ sandbox,
578
+ `find "${shellPath(path)}" -mindepth 1 -maxdepth 1 -printf '%y\\t%s\\t%T@\\t%f\\0'`,
579
+ `Failed to list ${path}`
580
+ );
581
+ return listing.split("\0").flatMap((record) => {
582
+ if (!record) return [];
583
+ const [kind, size, modified, ...rest] = record.split(" ");
584
+ const name = rest.join(" ");
585
+ if (!name) return [];
586
+ return [
587
+ {
588
+ name,
589
+ type: kind === "d" ? "directory" : "file",
590
+ size: Number.parseInt(size, 10) || 0,
591
+ modified: new Date(Number.parseFloat(modified) * 1e3)
592
+ }
593
+ ];
594
+ });
595
+ },
596
+ exists: async (sandbox, path, runCommand) => {
597
+ const result = await runCommand(sandbox, `test -e "${shellPath(path)}"`);
598
+ return result.exitCode === 0;
599
+ },
600
+ remove: async (sandbox, path, runCommand) => {
601
+ await shell(runCommand, sandbox, `rm -rf "${shellPath(path)}"`, `Failed to remove ${path}`);
602
+ }
603
+ },
604
+ getInstance: (sandbox) => sandbox
605
+ }
606
+ }
607
+ });
608
+ var cocoonstack = (config) => {
609
+ if (config.preconnect) preconnect(config.baseUrl || env("COCOONSTACK_API_URL") || "");
610
+ return provider(config);
611
+ };
612
+ var index_default = cocoonstack;
613
+ function shellPath(path) {
614
+ return (0, import_provider.escapeShellArg)(path.startsWith("/") ? path : `./${path}`);
615
+ }
616
+ function preconnect(baseUrl) {
617
+ let url;
618
+ try {
619
+ url = new URL(baseUrl);
620
+ } catch {
621
+ return;
622
+ }
623
+ if (url.protocol === "https:") session(url.origin, "main");
624
+ }
625
+ // Annotate the CommonJS export names for ESM import in node:
626
+ 0 && (module.exports = {
627
+ CocoonstackApiError,
628
+ cocoonstack
629
+ });
630
+ //# sourceMappingURL=index.js.map