just-bash 2.6.0 → 2.7.0

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,1038 @@
1
+ // src/commands/python3/worker.ts
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { loadPyodide } from "pyodide";
4
+
5
+ // src/commands/python3/protocol.ts
6
+ var OpCode = {
7
+ NOOP: 0,
8
+ READ_FILE: 1,
9
+ WRITE_FILE: 2,
10
+ STAT: 3,
11
+ READDIR: 4,
12
+ MKDIR: 5,
13
+ RM: 6,
14
+ EXISTS: 7,
15
+ APPEND_FILE: 8,
16
+ SYMLINK: 9,
17
+ READLINK: 10,
18
+ LSTAT: 11,
19
+ CHMOD: 12,
20
+ REALPATH: 13,
21
+ // Special operations for Python I/O
22
+ WRITE_STDOUT: 100,
23
+ WRITE_STDERR: 101,
24
+ EXIT: 102,
25
+ // HTTP operations
26
+ HTTP_REQUEST: 200
27
+ };
28
+ var Status = {
29
+ PENDING: 0,
30
+ READY: 1,
31
+ SUCCESS: 2,
32
+ ERROR: 3
33
+ };
34
+ var ErrorCode = {
35
+ NONE: 0,
36
+ NOT_FOUND: 1,
37
+ IS_DIRECTORY: 2,
38
+ NOT_DIRECTORY: 3,
39
+ EXISTS: 4,
40
+ PERMISSION_DENIED: 5,
41
+ INVALID_PATH: 6,
42
+ IO_ERROR: 7,
43
+ TIMEOUT: 8,
44
+ NETWORK_ERROR: 9,
45
+ NETWORK_NOT_CONFIGURED: 10
46
+ };
47
+ var Offset = {
48
+ OP_CODE: 0,
49
+ STATUS: 4,
50
+ PATH_LENGTH: 8,
51
+ DATA_LENGTH: 12,
52
+ RESULT_LENGTH: 16,
53
+ ERROR_CODE: 20,
54
+ FLAGS: 24,
55
+ MODE: 28,
56
+ PATH_BUFFER: 32,
57
+ DATA_BUFFER: 4128
58
+ // 32 + 4096
59
+ };
60
+ var Size = {
61
+ CONTROL_REGION: 32,
62
+ PATH_BUFFER: 4096,
63
+ DATA_BUFFER: 1048576,
64
+ // 1MB (reduced from 16MB for faster tests)
65
+ TOTAL: 1052704
66
+ // 32 + 4096 + 1MB
67
+ };
68
+ var Flags = {
69
+ NONE: 0,
70
+ RECURSIVE: 1,
71
+ FORCE: 2,
72
+ MKDIR_RECURSIVE: 1
73
+ };
74
+ var StatLayout = {
75
+ IS_FILE: 0,
76
+ IS_DIRECTORY: 1,
77
+ IS_SYMLINK: 2,
78
+ MODE: 4,
79
+ SIZE: 8,
80
+ MTIME: 16,
81
+ TOTAL: 24
82
+ };
83
+ var ProtocolBuffer = class {
84
+ int32View;
85
+ uint8View;
86
+ dataView;
87
+ constructor(buffer) {
88
+ this.int32View = new Int32Array(buffer);
89
+ this.uint8View = new Uint8Array(buffer);
90
+ this.dataView = new DataView(buffer);
91
+ }
92
+ getOpCode() {
93
+ return Atomics.load(this.int32View, Offset.OP_CODE / 4);
94
+ }
95
+ setOpCode(code) {
96
+ Atomics.store(this.int32View, Offset.OP_CODE / 4, code);
97
+ }
98
+ getStatus() {
99
+ return Atomics.load(this.int32View, Offset.STATUS / 4);
100
+ }
101
+ setStatus(status) {
102
+ Atomics.store(this.int32View, Offset.STATUS / 4, status);
103
+ }
104
+ getPathLength() {
105
+ return Atomics.load(this.int32View, Offset.PATH_LENGTH / 4);
106
+ }
107
+ setPathLength(length) {
108
+ Atomics.store(this.int32View, Offset.PATH_LENGTH / 4, length);
109
+ }
110
+ getDataLength() {
111
+ return Atomics.load(this.int32View, Offset.DATA_LENGTH / 4);
112
+ }
113
+ setDataLength(length) {
114
+ Atomics.store(this.int32View, Offset.DATA_LENGTH / 4, length);
115
+ }
116
+ getResultLength() {
117
+ return Atomics.load(this.int32View, Offset.RESULT_LENGTH / 4);
118
+ }
119
+ setResultLength(length) {
120
+ Atomics.store(this.int32View, Offset.RESULT_LENGTH / 4, length);
121
+ }
122
+ getErrorCode() {
123
+ return Atomics.load(this.int32View, Offset.ERROR_CODE / 4);
124
+ }
125
+ setErrorCode(code) {
126
+ Atomics.store(this.int32View, Offset.ERROR_CODE / 4, code);
127
+ }
128
+ getFlags() {
129
+ return Atomics.load(this.int32View, Offset.FLAGS / 4);
130
+ }
131
+ setFlags(flags) {
132
+ Atomics.store(this.int32View, Offset.FLAGS / 4, flags);
133
+ }
134
+ getMode() {
135
+ return Atomics.load(this.int32View, Offset.MODE / 4);
136
+ }
137
+ setMode(mode) {
138
+ Atomics.store(this.int32View, Offset.MODE / 4, mode);
139
+ }
140
+ getPath() {
141
+ const length = this.getPathLength();
142
+ const bytes = this.uint8View.slice(
143
+ Offset.PATH_BUFFER,
144
+ Offset.PATH_BUFFER + length
145
+ );
146
+ return new TextDecoder().decode(bytes);
147
+ }
148
+ setPath(path) {
149
+ const encoded = new TextEncoder().encode(path);
150
+ if (encoded.length > Size.PATH_BUFFER) {
151
+ throw new Error(`Path too long: ${encoded.length} > ${Size.PATH_BUFFER}`);
152
+ }
153
+ this.uint8View.set(encoded, Offset.PATH_BUFFER);
154
+ this.setPathLength(encoded.length);
155
+ }
156
+ getData() {
157
+ const length = this.getDataLength();
158
+ return this.uint8View.slice(
159
+ Offset.DATA_BUFFER,
160
+ Offset.DATA_BUFFER + length
161
+ );
162
+ }
163
+ setData(data) {
164
+ if (data.length > Size.DATA_BUFFER) {
165
+ throw new Error(`Data too large: ${data.length} > ${Size.DATA_BUFFER}`);
166
+ }
167
+ this.uint8View.set(data, Offset.DATA_BUFFER);
168
+ this.setDataLength(data.length);
169
+ }
170
+ getDataAsString() {
171
+ const data = this.getData();
172
+ return new TextDecoder().decode(data);
173
+ }
174
+ setDataFromString(str) {
175
+ const encoded = new TextEncoder().encode(str);
176
+ this.setData(encoded);
177
+ }
178
+ getResult() {
179
+ const length = this.getResultLength();
180
+ return this.uint8View.slice(
181
+ Offset.DATA_BUFFER,
182
+ Offset.DATA_BUFFER + length
183
+ );
184
+ }
185
+ setResult(data) {
186
+ if (data.length > Size.DATA_BUFFER) {
187
+ throw new Error(`Result too large: ${data.length} > ${Size.DATA_BUFFER}`);
188
+ }
189
+ this.uint8View.set(data, Offset.DATA_BUFFER);
190
+ this.setResultLength(data.length);
191
+ }
192
+ getResultAsString() {
193
+ const result = this.getResult();
194
+ return new TextDecoder().decode(result);
195
+ }
196
+ setResultFromString(str) {
197
+ const encoded = new TextEncoder().encode(str);
198
+ this.setResult(encoded);
199
+ }
200
+ encodeStat(stat) {
201
+ this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_FILE] = stat.isFile ? 1 : 0;
202
+ this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_DIRECTORY] = stat.isDirectory ? 1 : 0;
203
+ this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_SYMLINK] = stat.isSymbolicLink ? 1 : 0;
204
+ this.dataView.setInt32(
205
+ Offset.DATA_BUFFER + StatLayout.MODE,
206
+ stat.mode,
207
+ true
208
+ );
209
+ const size = Math.min(stat.size, Number.MAX_SAFE_INTEGER);
210
+ this.dataView.setFloat64(Offset.DATA_BUFFER + StatLayout.SIZE, size, true);
211
+ this.dataView.setFloat64(
212
+ Offset.DATA_BUFFER + StatLayout.MTIME,
213
+ stat.mtime.getTime(),
214
+ true
215
+ );
216
+ this.setResultLength(StatLayout.TOTAL);
217
+ }
218
+ decodeStat() {
219
+ return {
220
+ isFile: this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_FILE] === 1,
221
+ isDirectory: this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_DIRECTORY] === 1,
222
+ isSymbolicLink: this.uint8View[Offset.DATA_BUFFER + StatLayout.IS_SYMLINK] === 1,
223
+ mode: this.dataView.getInt32(Offset.DATA_BUFFER + StatLayout.MODE, true),
224
+ size: this.dataView.getFloat64(
225
+ Offset.DATA_BUFFER + StatLayout.SIZE,
226
+ true
227
+ ),
228
+ mtime: new Date(
229
+ this.dataView.getFloat64(Offset.DATA_BUFFER + StatLayout.MTIME, true)
230
+ )
231
+ };
232
+ }
233
+ waitForReady(timeout) {
234
+ return Atomics.wait(
235
+ this.int32View,
236
+ Offset.STATUS / 4,
237
+ Status.PENDING,
238
+ timeout
239
+ );
240
+ }
241
+ waitForReadyAsync(timeout) {
242
+ return Atomics.waitAsync(
243
+ this.int32View,
244
+ Offset.STATUS / 4,
245
+ Status.PENDING,
246
+ timeout
247
+ );
248
+ }
249
+ /**
250
+ * Wait for status to become READY.
251
+ * Returns immediately if status is already READY, or waits until it changes.
252
+ */
253
+ async waitUntilReady(timeout) {
254
+ const startTime = Date.now();
255
+ while (true) {
256
+ const status = this.getStatus();
257
+ if (status === Status.READY) {
258
+ return true;
259
+ }
260
+ const elapsed = Date.now() - startTime;
261
+ if (elapsed >= timeout) {
262
+ return false;
263
+ }
264
+ const remainingMs = timeout - elapsed;
265
+ const result = Atomics.waitAsync(
266
+ this.int32View,
267
+ Offset.STATUS / 4,
268
+ status,
269
+ remainingMs
270
+ );
271
+ if (result.async) {
272
+ const waitResult = await result.value;
273
+ if (waitResult === "timed-out") {
274
+ return false;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ waitForResult(timeout) {
280
+ return Atomics.wait(
281
+ this.int32View,
282
+ Offset.STATUS / 4,
283
+ Status.READY,
284
+ timeout
285
+ );
286
+ }
287
+ notify() {
288
+ return Atomics.notify(this.int32View, Offset.STATUS / 4);
289
+ }
290
+ reset() {
291
+ this.setOpCode(OpCode.NOOP);
292
+ this.setStatus(Status.PENDING);
293
+ this.setPathLength(0);
294
+ this.setDataLength(0);
295
+ this.setResultLength(0);
296
+ this.setErrorCode(ErrorCode.NONE);
297
+ this.setFlags(Flags.NONE);
298
+ this.setMode(0);
299
+ }
300
+ };
301
+
302
+ // src/commands/python3/sync-fs-backend.ts
303
+ var SyncFsBackend = class {
304
+ protocol;
305
+ constructor(sharedBuffer) {
306
+ this.protocol = new ProtocolBuffer(sharedBuffer);
307
+ }
308
+ execSync(opCode, path, data, flags = 0, mode = 0) {
309
+ this.protocol.reset();
310
+ this.protocol.setOpCode(opCode);
311
+ this.protocol.setPath(path);
312
+ this.protocol.setFlags(flags);
313
+ this.protocol.setMode(mode);
314
+ if (data) {
315
+ this.protocol.setData(data);
316
+ }
317
+ this.protocol.setStatus(Status.READY);
318
+ this.protocol.notify();
319
+ const waitResult = this.protocol.waitForResult(5e3);
320
+ if (waitResult === "timed-out") {
321
+ return { success: false, error: "Operation timed out" };
322
+ }
323
+ const status = this.protocol.getStatus();
324
+ if (status === Status.SUCCESS) {
325
+ return { success: true, result: this.protocol.getResult() };
326
+ }
327
+ return {
328
+ success: false,
329
+ error: this.protocol.getResultAsString() || `Error code: ${this.protocol.getErrorCode()}`
330
+ };
331
+ }
332
+ readFile(path) {
333
+ const result = this.execSync(OpCode.READ_FILE, path);
334
+ if (!result.success) {
335
+ throw new Error(result.error || "Failed to read file");
336
+ }
337
+ return result.result ?? new Uint8Array(0);
338
+ }
339
+ writeFile(path, data) {
340
+ const result = this.execSync(OpCode.WRITE_FILE, path, data);
341
+ if (!result.success) {
342
+ throw new Error(result.error || "Failed to write file");
343
+ }
344
+ }
345
+ stat(path) {
346
+ const result = this.execSync(OpCode.STAT, path);
347
+ if (!result.success) {
348
+ throw new Error(result.error || "Failed to stat");
349
+ }
350
+ return this.protocol.decodeStat();
351
+ }
352
+ lstat(path) {
353
+ const result = this.execSync(OpCode.LSTAT, path);
354
+ if (!result.success) {
355
+ throw new Error(result.error || "Failed to lstat");
356
+ }
357
+ return this.protocol.decodeStat();
358
+ }
359
+ readdir(path) {
360
+ const result = this.execSync(OpCode.READDIR, path);
361
+ if (!result.success) {
362
+ throw new Error(result.error || "Failed to readdir");
363
+ }
364
+ return JSON.parse(this.protocol.getResultAsString());
365
+ }
366
+ mkdir(path, recursive = false) {
367
+ const flags = recursive ? Flags.MKDIR_RECURSIVE : 0;
368
+ const result = this.execSync(OpCode.MKDIR, path, void 0, flags);
369
+ if (!result.success) {
370
+ throw new Error(result.error || "Failed to mkdir");
371
+ }
372
+ }
373
+ rm(path, recursive = false, force = false) {
374
+ let flags = 0;
375
+ if (recursive) flags |= Flags.RECURSIVE;
376
+ if (force) flags |= Flags.FORCE;
377
+ const result = this.execSync(OpCode.RM, path, void 0, flags);
378
+ if (!result.success) {
379
+ throw new Error(result.error || "Failed to rm");
380
+ }
381
+ }
382
+ exists(path) {
383
+ const result = this.execSync(OpCode.EXISTS, path);
384
+ if (!result.success) {
385
+ return false;
386
+ }
387
+ return result.result?.[0] === 1;
388
+ }
389
+ appendFile(path, data) {
390
+ const result = this.execSync(OpCode.APPEND_FILE, path, data);
391
+ if (!result.success) {
392
+ throw new Error(result.error || "Failed to append file");
393
+ }
394
+ }
395
+ symlink(target, linkPath) {
396
+ const targetData = new TextEncoder().encode(target);
397
+ const result = this.execSync(OpCode.SYMLINK, linkPath, targetData);
398
+ if (!result.success) {
399
+ throw new Error(result.error || "Failed to symlink");
400
+ }
401
+ }
402
+ readlink(path) {
403
+ const result = this.execSync(OpCode.READLINK, path);
404
+ if (!result.success) {
405
+ throw new Error(result.error || "Failed to readlink");
406
+ }
407
+ return this.protocol.getResultAsString();
408
+ }
409
+ chmod(path, mode) {
410
+ const result = this.execSync(OpCode.CHMOD, path, void 0, 0, mode);
411
+ if (!result.success) {
412
+ throw new Error(result.error || "Failed to chmod");
413
+ }
414
+ }
415
+ realpath(path) {
416
+ const result = this.execSync(OpCode.REALPATH, path);
417
+ if (!result.success) {
418
+ throw new Error(result.error || "Failed to realpath");
419
+ }
420
+ return this.protocol.getResultAsString();
421
+ }
422
+ writeStdout(data) {
423
+ const encoded = new TextEncoder().encode(data);
424
+ this.execSync(OpCode.WRITE_STDOUT, "", encoded);
425
+ }
426
+ writeStderr(data) {
427
+ const encoded = new TextEncoder().encode(data);
428
+ this.execSync(OpCode.WRITE_STDERR, "", encoded);
429
+ }
430
+ exit(code) {
431
+ this.execSync(OpCode.EXIT, "", void 0, code);
432
+ }
433
+ /**
434
+ * Make an HTTP request through the main thread's secureFetch.
435
+ * Returns the response as a parsed object.
436
+ */
437
+ httpRequest(url, options) {
438
+ const requestData = options ? new TextEncoder().encode(JSON.stringify(options)) : void 0;
439
+ const result = this.execSync(OpCode.HTTP_REQUEST, url, requestData);
440
+ if (!result.success) {
441
+ throw new Error(result.error || "HTTP request failed");
442
+ }
443
+ const responseJson = new TextDecoder().decode(result.result);
444
+ return JSON.parse(responseJson);
445
+ }
446
+ };
447
+
448
+ // src/commands/python3/worker.ts
449
+ var pyodideInstance = null;
450
+ var pyodideLoading = null;
451
+ async function getPyodide() {
452
+ if (pyodideInstance) {
453
+ return pyodideInstance;
454
+ }
455
+ if (pyodideLoading) {
456
+ return pyodideLoading;
457
+ }
458
+ pyodideLoading = loadPyodide();
459
+ pyodideInstance = await pyodideLoading;
460
+ return pyodideInstance;
461
+ }
462
+ function createHOSTFS(backend, FS, PATH) {
463
+ const ERRNO_CODES = {
464
+ EPERM: 63,
465
+ ENOENT: 44,
466
+ EIO: 29,
467
+ EBADF: 8,
468
+ EAGAIN: 6,
469
+ EACCES: 2,
470
+ EBUSY: 10,
471
+ EEXIST: 20,
472
+ ENOTDIR: 54,
473
+ EISDIR: 31,
474
+ EINVAL: 28,
475
+ EMFILE: 33,
476
+ ENOSPC: 51,
477
+ ESPIPE: 70,
478
+ EROFS: 69,
479
+ ENOTEMPTY: 55,
480
+ ENOSYS: 52,
481
+ ENOTSUP: 138,
482
+ ENODATA: 42
483
+ };
484
+ function realPath(node) {
485
+ const parts = [];
486
+ while (node.parent !== node) {
487
+ parts.push(node.name);
488
+ node = node.parent;
489
+ }
490
+ parts.push(node.mount.opts.root);
491
+ parts.reverse();
492
+ return PATH.join(...parts);
493
+ }
494
+ function tryFSOperation(f) {
495
+ try {
496
+ return f();
497
+ } catch (e) {
498
+ const msg = e?.message?.toLowerCase() || (typeof e === "string" ? e.toLowerCase() : "");
499
+ let code = ERRNO_CODES.EIO;
500
+ if (msg.includes("no such file") || msg.includes("not found")) {
501
+ code = ERRNO_CODES.ENOENT;
502
+ } else if (msg.includes("is a directory")) {
503
+ code = ERRNO_CODES.EISDIR;
504
+ } else if (msg.includes("not a directory")) {
505
+ code = ERRNO_CODES.ENOTDIR;
506
+ } else if (msg.includes("already exists")) {
507
+ code = ERRNO_CODES.EEXIST;
508
+ } else if (msg.includes("permission")) {
509
+ code = ERRNO_CODES.EACCES;
510
+ } else if (msg.includes("not empty")) {
511
+ code = ERRNO_CODES.ENOTEMPTY;
512
+ }
513
+ throw new FS.ErrnoError(code);
514
+ }
515
+ }
516
+ function getMode(path) {
517
+ return tryFSOperation(() => {
518
+ const stat = backend.stat(path);
519
+ let mode = stat.mode & 511;
520
+ if (stat.isDirectory) {
521
+ mode |= 16384;
522
+ } else if (stat.isSymbolicLink) {
523
+ mode |= 40960;
524
+ } else {
525
+ mode |= 32768;
526
+ }
527
+ return mode;
528
+ });
529
+ }
530
+ const HOSTFS = {
531
+ mount(_mount) {
532
+ return HOSTFS.createNode(null, "/", 16877, 0);
533
+ },
534
+ createNode(parent, name, mode, dev) {
535
+ if (!FS.isDir(mode) && !FS.isFile(mode) && !FS.isLink(mode)) {
536
+ throw new FS.ErrnoError(ERRNO_CODES.EINVAL);
537
+ }
538
+ const node = FS.createNode(parent, name, mode, dev);
539
+ node.node_ops = HOSTFS.node_ops;
540
+ node.stream_ops = HOSTFS.stream_ops;
541
+ return node;
542
+ },
543
+ node_ops: {
544
+ getattr(node) {
545
+ const path = realPath(node);
546
+ return tryFSOperation(() => {
547
+ const stat = backend.stat(path);
548
+ let mode = stat.mode & 511;
549
+ if (stat.isDirectory) {
550
+ mode |= 16384;
551
+ } else if (stat.isSymbolicLink) {
552
+ mode |= 40960;
553
+ } else {
554
+ mode |= 32768;
555
+ }
556
+ return {
557
+ dev: 1,
558
+ ino: node.id,
559
+ mode,
560
+ nlink: 1,
561
+ uid: 0,
562
+ gid: 0,
563
+ rdev: 0,
564
+ size: stat.size,
565
+ atime: stat.mtime,
566
+ mtime: stat.mtime,
567
+ ctime: stat.mtime,
568
+ blksize: 4096,
569
+ blocks: Math.ceil(stat.size / 512)
570
+ };
571
+ });
572
+ },
573
+ setattr(node, attr) {
574
+ const path = realPath(node);
575
+ const mode = attr.mode;
576
+ if (mode !== void 0) {
577
+ tryFSOperation(() => backend.chmod(path, mode));
578
+ node.mode = mode;
579
+ }
580
+ if (attr.size !== void 0) {
581
+ tryFSOperation(() => {
582
+ const content = backend.readFile(path);
583
+ const newContent = content.slice(0, attr.size);
584
+ backend.writeFile(path, newContent);
585
+ });
586
+ }
587
+ },
588
+ lookup(parent, name) {
589
+ const path = PATH.join2(realPath(parent), name);
590
+ const mode = getMode(path);
591
+ return HOSTFS.createNode(parent, name, mode);
592
+ },
593
+ mknod(parent, name, mode, _dev) {
594
+ const node = HOSTFS.createNode(parent, name, mode, _dev);
595
+ const path = realPath(node);
596
+ tryFSOperation(() => {
597
+ if (FS.isDir(node.mode)) {
598
+ backend.mkdir(path, false);
599
+ } else {
600
+ backend.writeFile(path, new Uint8Array(0));
601
+ }
602
+ });
603
+ return node;
604
+ },
605
+ rename(oldNode, newDir, newName) {
606
+ const oldPath = realPath(oldNode);
607
+ const newPath = PATH.join2(realPath(newDir), newName);
608
+ tryFSOperation(() => {
609
+ const content = backend.readFile(oldPath);
610
+ backend.writeFile(newPath, content);
611
+ backend.rm(oldPath, false, false);
612
+ });
613
+ oldNode.name = newName;
614
+ },
615
+ unlink(parent, name) {
616
+ const path = PATH.join2(realPath(parent), name);
617
+ tryFSOperation(() => backend.rm(path, false, false));
618
+ },
619
+ rmdir(parent, name) {
620
+ const path = PATH.join2(realPath(parent), name);
621
+ tryFSOperation(() => backend.rm(path, false, false));
622
+ },
623
+ readdir(node) {
624
+ const path = realPath(node);
625
+ return tryFSOperation(() => backend.readdir(path));
626
+ },
627
+ symlink(parent, newName, oldPath) {
628
+ const newPath = PATH.join2(realPath(parent), newName);
629
+ tryFSOperation(() => backend.symlink(oldPath, newPath));
630
+ },
631
+ readlink(node) {
632
+ const path = realPath(node);
633
+ return tryFSOperation(() => backend.readlink(path));
634
+ }
635
+ },
636
+ stream_ops: {
637
+ open(stream) {
638
+ const path = realPath(stream.node);
639
+ const flags = stream.flags;
640
+ const O_WRONLY = 1;
641
+ const O_RDWR = 2;
642
+ const O_CREAT = 64;
643
+ const O_TRUNC = 512;
644
+ const O_APPEND = 1024;
645
+ const accessMode = flags & 3;
646
+ const isWrite = accessMode === O_WRONLY || accessMode === O_RDWR;
647
+ const isCreate = (flags & O_CREAT) !== 0;
648
+ const isTruncate = (flags & O_TRUNC) !== 0;
649
+ const isAppend = (flags & O_APPEND) !== 0;
650
+ if (FS.isDir(stream.node.mode)) {
651
+ return;
652
+ }
653
+ let content;
654
+ try {
655
+ if (isTruncate && isWrite) {
656
+ content = new Uint8Array(0);
657
+ } else {
658
+ content = backend.readFile(path);
659
+ }
660
+ } catch (_e) {
661
+ if (isCreate && isWrite) {
662
+ content = new Uint8Array(0);
663
+ } else {
664
+ throw new FS.ErrnoError(ERRNO_CODES.ENOENT);
665
+ }
666
+ }
667
+ stream.hostContent = content;
668
+ stream.hostModified = isTruncate && isWrite;
669
+ stream.hostPath = path;
670
+ if (isAppend) {
671
+ stream.position = content.length;
672
+ }
673
+ },
674
+ close(stream) {
675
+ const hostPath = stream.hostPath;
676
+ const hostContent = stream.hostContent;
677
+ if (stream.hostModified && hostContent && hostPath) {
678
+ tryFSOperation(() => backend.writeFile(hostPath, hostContent));
679
+ }
680
+ delete stream.hostContent;
681
+ delete stream.hostModified;
682
+ delete stream.hostPath;
683
+ },
684
+ read(stream, buffer, offset, length, position) {
685
+ const content = stream.hostContent;
686
+ if (!content) return 0;
687
+ const size = content.length;
688
+ if (position >= size) return 0;
689
+ const bytesToRead = Math.min(length, size - position);
690
+ buffer.set(content.subarray(position, position + bytesToRead), offset);
691
+ return bytesToRead;
692
+ },
693
+ write(stream, buffer, offset, length, position) {
694
+ let content = stream.hostContent || new Uint8Array(0);
695
+ const newSize = Math.max(content.length, position + length);
696
+ if (newSize > content.length) {
697
+ const newContent = new Uint8Array(newSize);
698
+ newContent.set(content);
699
+ content = newContent;
700
+ stream.hostContent = content;
701
+ }
702
+ content.set(buffer.subarray(offset, offset + length), position);
703
+ stream.hostModified = true;
704
+ return length;
705
+ },
706
+ llseek(stream, offset, whence) {
707
+ const SEEK_CUR = 1;
708
+ const SEEK_END = 2;
709
+ let position = offset;
710
+ if (whence === SEEK_CUR) {
711
+ position += stream.position;
712
+ } else if (whence === SEEK_END) {
713
+ if (FS.isFile(stream.node.mode)) {
714
+ const content = stream.hostContent;
715
+ position += content ? content.length : 0;
716
+ }
717
+ }
718
+ if (position < 0) {
719
+ throw new FS.ErrnoError(ERRNO_CODES.EINVAL);
720
+ }
721
+ return position;
722
+ }
723
+ }
724
+ };
725
+ return HOSTFS;
726
+ }
727
+ async function runPython(input) {
728
+ const backend = new SyncFsBackend(input.sharedBuffer);
729
+ let pyodide;
730
+ try {
731
+ pyodide = await getPyodide();
732
+ } catch (e) {
733
+ return {
734
+ success: false,
735
+ error: `Failed to load Pyodide: ${e.message}`
736
+ };
737
+ }
738
+ pyodide.setStdout({ batched: () => {
739
+ } });
740
+ pyodide.setStderr({ batched: () => {
741
+ } });
742
+ try {
743
+ pyodide.runPython(`
744
+ import sys
745
+ if hasattr(sys.stdout, 'flush'):
746
+ sys.stdout.flush()
747
+ if hasattr(sys.stderr, 'flush'):
748
+ sys.stderr.flush()
749
+ `);
750
+ } catch (_e) {
751
+ }
752
+ pyodide.setStdout({
753
+ batched: (text) => {
754
+ backend.writeStdout(`${text}
755
+ `);
756
+ }
757
+ });
758
+ pyodide.setStderr({
759
+ batched: (text) => {
760
+ backend.writeStderr(`${text}
761
+ `);
762
+ }
763
+ });
764
+ const FS = pyodide.FS;
765
+ const PATH = pyodide.PATH;
766
+ const HOSTFS = createHOSTFS(backend, FS, PATH);
767
+ try {
768
+ try {
769
+ pyodide.runPython(`import os; os.chdir('/')`);
770
+ } catch (_e) {
771
+ }
772
+ try {
773
+ FS.mkdir("/host");
774
+ } catch (_e) {
775
+ }
776
+ try {
777
+ FS.unmount("/host");
778
+ } catch (_e) {
779
+ }
780
+ FS.mount(HOSTFS, { root: "/" }, "/host");
781
+ } catch (e) {
782
+ return {
783
+ success: false,
784
+ error: `Failed to mount HOSTFS: ${e.message}`
785
+ };
786
+ }
787
+ try {
788
+ pyodide.runPython(`
789
+ import sys
790
+ if '_jb_http_bridge' in sys.modules:
791
+ del sys.modules['_jb_http_bridge']
792
+ if 'jb_http' in sys.modules:
793
+ del sys.modules['jb_http']
794
+ `);
795
+ } catch (_e) {
796
+ }
797
+ pyodide.registerJsModule("_jb_http_bridge", {
798
+ request: (url, method, headersJson, body) => {
799
+ try {
800
+ const headers = headersJson ? JSON.parse(headersJson) : void 0;
801
+ const result = backend.httpRequest(url, {
802
+ method: method || "GET",
803
+ headers,
804
+ body: body || void 0
805
+ });
806
+ return JSON.stringify(result);
807
+ } catch (e) {
808
+ return JSON.stringify({ error: e.message });
809
+ }
810
+ }
811
+ });
812
+ const envSetup = Object.entries(input.env).map(([key, value]) => {
813
+ return `os.environ[${JSON.stringify(key)}] = ${JSON.stringify(value)}`;
814
+ }).join("\n");
815
+ const argv0 = input.scriptPath || "python3";
816
+ const argvList = [argv0, ...input.args].map((arg) => JSON.stringify(arg)).join(", ");
817
+ try {
818
+ await pyodide.runPythonAsync(`
819
+ import os
820
+ import sys
821
+ import builtins
822
+ import json
823
+
824
+ ${envSetup}
825
+
826
+ sys.argv = [${argvList}]
827
+
828
+ # Create jb_http module for HTTP requests
829
+ class _JbHttpResponse:
830
+ """HTTP response object similar to requests.Response"""
831
+ def __init__(self, data):
832
+ self.status_code = data.get('status', 0)
833
+ self.reason = data.get('statusText', '')
834
+ self.headers = data.get('headers', {})
835
+ self.text = data.get('body', '')
836
+ self.url = data.get('url', '')
837
+ self._error = data.get('error')
838
+
839
+ @property
840
+ def ok(self):
841
+ return 200 <= self.status_code < 300
842
+
843
+ def json(self):
844
+ return json.loads(self.text)
845
+
846
+ def raise_for_status(self):
847
+ if self._error:
848
+ raise Exception(self._error)
849
+ if not self.ok:
850
+ raise Exception(f"HTTP {self.status_code}: {self.reason}")
851
+
852
+ class _JbHttp:
853
+ """HTTP client that bridges to just-bash's secureFetch"""
854
+ def request(self, method, url, headers=None, data=None, json_data=None):
855
+ # Import fresh each time to ensure we use the current bridge
856
+ # (important when worker is reused with different SharedArrayBuffer)
857
+ import _jb_http_bridge
858
+ if json_data is not None:
859
+ data = json.dumps(json_data)
860
+ headers = headers or {}
861
+ headers['Content-Type'] = 'application/json'
862
+ # Serialize headers to JSON to avoid PyProxy issues when passing to JS
863
+ headers_json = json.dumps(headers) if headers else None
864
+ result_json = _jb_http_bridge.request(url, method, headers_json, data)
865
+ result = json.loads(result_json)
866
+ # Check for errors from the bridge (network not configured, URL not allowed, etc.)
867
+ if 'error' in result and result.get('status') is None:
868
+ raise Exception(result['error'])
869
+ return _JbHttpResponse(result)
870
+
871
+ def get(self, url, headers=None, **kwargs):
872
+ return self.request('GET', url, headers=headers, **kwargs)
873
+
874
+ def post(self, url, headers=None, data=None, json=None, **kwargs):
875
+ return self.request('POST', url, headers=headers, data=data, json_data=json, **kwargs)
876
+
877
+ def put(self, url, headers=None, data=None, json=None, **kwargs):
878
+ return self.request('PUT', url, headers=headers, data=data, json_data=json, **kwargs)
879
+
880
+ def delete(self, url, headers=None, **kwargs):
881
+ return self.request('DELETE', url, headers=headers, **kwargs)
882
+
883
+ def head(self, url, headers=None, **kwargs):
884
+ return self.request('HEAD', url, headers=headers, **kwargs)
885
+
886
+ def patch(self, url, headers=None, data=None, json=None, **kwargs):
887
+ return self.request('PATCH', url, headers=headers, data=data, json_data=json, **kwargs)
888
+
889
+ # Register jb_http as an importable module
890
+ import types
891
+ jb_http = types.ModuleType('jb_http')
892
+ jb_http._client = _JbHttp()
893
+ jb_http.get = jb_http._client.get
894
+ jb_http.post = jb_http._client.post
895
+ jb_http.put = jb_http._client.put
896
+ jb_http.delete = jb_http._client.delete
897
+ jb_http.head = jb_http._client.head
898
+ jb_http.patch = jb_http._client.patch
899
+ jb_http.request = jb_http._client.request
900
+ jb_http.Response = _JbHttpResponse
901
+ sys.modules['jb_http'] = jb_http
902
+
903
+ # Redirect root paths to /host for file operations
904
+ # Only patch once - check if already patched
905
+ if not hasattr(builtins, '_jb_original_open'):
906
+ builtins._jb_original_open = builtins.open
907
+
908
+ def _redirected_open(path, mode='r', *args, **kwargs):
909
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
910
+ path = '/host' + path
911
+ return builtins._jb_original_open(path, mode, *args, **kwargs)
912
+ builtins.open = _redirected_open
913
+
914
+ os._jb_original_listdir = os.listdir
915
+ def _redirected_listdir(path='.'):
916
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
917
+ path = '/host' + path
918
+ return os._jb_original_listdir(path)
919
+ os.listdir = _redirected_listdir
920
+
921
+ os.path._jb_original_exists = os.path.exists
922
+ def _redirected_exists(path):
923
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
924
+ path = '/host' + path
925
+ return os.path._jb_original_exists(path)
926
+ os.path.exists = _redirected_exists
927
+
928
+ os.path._jb_original_isfile = os.path.isfile
929
+ def _redirected_isfile(path):
930
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
931
+ path = '/host' + path
932
+ return os.path._jb_original_isfile(path)
933
+ os.path.isfile = _redirected_isfile
934
+
935
+ os.path._jb_original_isdir = os.path.isdir
936
+ def _redirected_isdir(path):
937
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
938
+ path = '/host' + path
939
+ return os.path._jb_original_isdir(path)
940
+ os.path.isdir = _redirected_isdir
941
+
942
+ os._jb_original_stat = os.stat
943
+ def _redirected_stat(path, *args, **kwargs):
944
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
945
+ path = '/host' + path
946
+ return os._jb_original_stat(path, *args, **kwargs)
947
+ os.stat = _redirected_stat
948
+
949
+ os._jb_original_mkdir = os.mkdir
950
+ def _redirected_mkdir(path, *args, **kwargs):
951
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
952
+ path = '/host' + path
953
+ return os._jb_original_mkdir(path, *args, **kwargs)
954
+ os.mkdir = _redirected_mkdir
955
+
956
+ os._jb_original_makedirs = os.makedirs
957
+ def _redirected_makedirs(path, *args, **kwargs):
958
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
959
+ path = '/host' + path
960
+ return os._jb_original_makedirs(path, *args, **kwargs)
961
+ os.makedirs = _redirected_makedirs
962
+
963
+ os._jb_original_remove = os.remove
964
+ def _redirected_remove(path, *args, **kwargs):
965
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
966
+ path = '/host' + path
967
+ return os._jb_original_remove(path, *args, **kwargs)
968
+ os.remove = _redirected_remove
969
+
970
+ os._jb_original_rmdir = os.rmdir
971
+ def _redirected_rmdir(path, *args, **kwargs):
972
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
973
+ path = '/host' + path
974
+ return os._jb_original_rmdir(path, *args, **kwargs)
975
+ os.rmdir = _redirected_rmdir
976
+
977
+ # Patch os.getcwd to strip /host prefix
978
+ os._jb_original_getcwd = os.getcwd
979
+ def _redirected_getcwd():
980
+ cwd = os._jb_original_getcwd()
981
+ if cwd.startswith('/host'):
982
+ return cwd[5:] # Strip '/host' prefix
983
+ return cwd
984
+ os.getcwd = _redirected_getcwd
985
+
986
+ # Patch os.chdir to add /host prefix
987
+ os._jb_original_chdir = os.chdir
988
+ def _redirected_chdir(path):
989
+ if isinstance(path, str) and path.startswith('/') and not path.startswith('/lib') and not path.startswith('/proc') and not path.startswith('/host'):
990
+ path = '/host' + path
991
+ return os._jb_original_chdir(path)
992
+ os.chdir = _redirected_chdir
993
+
994
+ # Set cwd to host mount
995
+ os.chdir('/host' + ${JSON.stringify(input.cwd)})
996
+ `);
997
+ } catch (e) {
998
+ return {
999
+ success: false,
1000
+ error: `Failed to set up environment: ${e.message}`
1001
+ };
1002
+ }
1003
+ try {
1004
+ const wrappedCode = `
1005
+ import sys
1006
+ _jb_exit_code = 0
1007
+ try:
1008
+ ${input.pythonCode.split("\n").map((line) => ` ${line}`).join("\n")}
1009
+ except SystemExit as e:
1010
+ _jb_exit_code = e.code if isinstance(e.code, int) else (1 if e.code else 0)
1011
+ `;
1012
+ await pyodide.runPythonAsync(wrappedCode);
1013
+ const exitCode = pyodide.globals.get("_jb_exit_code");
1014
+ backend.exit(exitCode);
1015
+ return { success: true };
1016
+ } catch (e) {
1017
+ const error = e;
1018
+ backend.writeStderr(`${error.message}
1019
+ `);
1020
+ backend.exit(1);
1021
+ return { success: true };
1022
+ }
1023
+ }
1024
+ if (parentPort) {
1025
+ if (workerData) {
1026
+ runPython(workerData).then((result) => {
1027
+ parentPort?.postMessage(result);
1028
+ });
1029
+ }
1030
+ parentPort.on("message", async (input) => {
1031
+ try {
1032
+ const result = await runPython(input);
1033
+ parentPort?.postMessage(result);
1034
+ } catch (e) {
1035
+ parentPort?.postMessage({ success: false, error: e.message });
1036
+ }
1037
+ });
1038
+ }