@capsule-run/sdk 0.7.2 → 0.8.1
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/README.md +5 -1
- package/dist/app.d.ts +7 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +7 -0
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/polyfills/fs.d.ts +81 -11
- package/dist/polyfills/fs.d.ts.map +1 -1
- package/dist/polyfills/fs.js +290 -11
- package/dist/polyfills/fs.js.map +1 -1
- package/dist/polyfills/process.d.ts +17 -4
- package/dist/polyfills/process.d.ts.map +1 -1
- package/dist/polyfills/process.js +118 -19
- package/dist/polyfills/process.js.map +1 -1
- package/dist/run.d.ts +3 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js.map +1 -1
- package/dist/task.d.ts +11 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +6 -3
- package/dist/task.js.map +1 -1
- package/package.json +4 -10
- package/src/app.ts +8 -0
- package/src/index.ts +1 -2
- package/src/polyfills/fs.ts +317 -12
- package/src/polyfills/process.ts +119 -20
- package/src/run.ts +3 -0
- package/src/task.ts +32 -14
- package/dist/polyfills/buffer.d.ts +0 -8
- package/dist/polyfills/buffer.d.ts.map +0 -1
- package/dist/polyfills/buffer.js +0 -9
- package/dist/polyfills/buffer.js.map +0 -1
- package/dist/polyfills/events.d.ts +0 -8
- package/dist/polyfills/events.d.ts.map +0 -1
- package/dist/polyfills/events.js +0 -9
- package/dist/polyfills/events.js.map +0 -1
- package/dist/polyfills/path.d.ts +0 -8
- package/dist/polyfills/path.d.ts.map +0 -1
- package/dist/polyfills/path.js +0 -8
- package/dist/polyfills/path.js.map +0 -1
- package/dist/polyfills/stream-web.d.ts +0 -74
- package/dist/polyfills/stream-web.d.ts.map +0 -1
- package/dist/polyfills/stream-web.js +0 -23
- package/dist/polyfills/stream-web.js.map +0 -1
- package/dist/polyfills/stream.d.ts +0 -16
- package/dist/polyfills/stream.d.ts.map +0 -1
- package/dist/polyfills/stream.js +0 -16
- package/dist/polyfills/stream.js.map +0 -1
- package/dist/polyfills/url.d.ts +0 -47
- package/dist/polyfills/url.d.ts.map +0 -1
- package/dist/polyfills/url.js +0 -68
- package/dist/polyfills/url.js.map +0 -1
- package/src/polyfills/buffer.ts +0 -11
- package/src/polyfills/events.ts +0 -11
- package/src/polyfills/path.ts +0 -21
- package/src/polyfills/stream-web.ts +0 -24
- package/src/polyfills/stream.ts +0 -28
- package/src/polyfills/url.ts +0 -73
package/src/polyfills/fs.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Provides both Node.js fs API
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getCwd } from './process.js';
|
|
7
|
+
|
|
6
8
|
declare const globalThis: {
|
|
7
9
|
'wasi:filesystem/types': any;
|
|
8
10
|
'wasi:filesystem/preopens': any;
|
|
@@ -76,11 +78,24 @@ function normalizePath(path: string): string {
|
|
|
76
78
|
return path;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Absolute paths pass through unchanged.
|
|
83
|
+
*/
|
|
84
|
+
function getEffectivePath(path: string): string {
|
|
85
|
+
if (path.startsWith('/')) return path;
|
|
86
|
+
|
|
87
|
+
const cwd = getCwd();
|
|
88
|
+
if (cwd === '.' || cwd === '') return path;
|
|
89
|
+
if (path === '.') return cwd;
|
|
90
|
+
if (path.startsWith('./')) return cwd.replace(/\/+$/, '') + '/' + path.slice(2);
|
|
91
|
+
return cwd.replace(/\/+$/, '') + '/' + path;
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function resolvePath(path: string): { dir: Descriptor; relativePath: string } | null {
|
|
80
95
|
const preopens = getPreopenedDirs();
|
|
81
96
|
if (preopens.length === 0) return null;
|
|
82
97
|
|
|
83
|
-
const normalizedPath = normalizePath(path);
|
|
98
|
+
const normalizedPath = normalizePath(getEffectivePath(path));
|
|
84
99
|
|
|
85
100
|
let catchAll: { dir: Descriptor; relativePath: string } | null = null;
|
|
86
101
|
|
|
@@ -296,12 +311,287 @@ export function readdir(
|
|
|
296
311
|
.catch((err) => cb?.(err instanceof Error ? err : new Error(String(err))));
|
|
297
312
|
}
|
|
298
313
|
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Sync implementations
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
function enoent(path: string): Error {
|
|
319
|
+
return Object.assign(
|
|
320
|
+
new Error(`ENOENT: no such file or directory, open '${path}'`),
|
|
321
|
+
{ code: 'ENOENT' }
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Read file contents synchronously.
|
|
327
|
+
*/
|
|
328
|
+
export function readFileSync(path: string, options?: ReadFileOptions | Encoding): string | Uint8Array {
|
|
329
|
+
const resolved = resolvePath(path);
|
|
330
|
+
if (!resolved) throw enoent(path);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const fd = resolved.dir.openAt({ symlinkFollow: false }, resolved.relativePath, {}, { read: true });
|
|
334
|
+
const stat = fd.stat();
|
|
335
|
+
const [data] = fd.read(stat.size, BigInt(0));
|
|
336
|
+
const encoding = typeof options === 'string' ? options : options?.encoding;
|
|
337
|
+
return (encoding === 'utf8' || encoding === 'utf-8') ? new TextDecoder().decode(data) : data;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (e instanceof Error && (e as any).code) throw e;
|
|
340
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), { code: 'ENOENT' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Write data to a file synchronously.
|
|
346
|
+
*/
|
|
347
|
+
export function writeFileSync(path: string, data: string | Uint8Array, _options?: WriteFileOptions | Encoding): void {
|
|
348
|
+
const resolved = resolvePath(path);
|
|
349
|
+
if (!resolved) throw enoent(path);
|
|
350
|
+
|
|
351
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
352
|
+
try {
|
|
353
|
+
const fd = resolved.dir.openAt(
|
|
354
|
+
{ symlinkFollow: false },
|
|
355
|
+
resolved.relativePath,
|
|
356
|
+
{ create: true, truncate: true },
|
|
357
|
+
{ write: true, mutateDirectory: true }
|
|
358
|
+
);
|
|
359
|
+
fd.write(bytes, BigInt(0));
|
|
360
|
+
} catch (e) {
|
|
361
|
+
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Append data to a file synchronously, creating it if it doesn't exist.
|
|
367
|
+
*/
|
|
368
|
+
export function appendFileSync(path: string, data: string | Uint8Array, _options?: WriteFileOptions | Encoding): void {
|
|
369
|
+
const resolved = resolvePath(path);
|
|
370
|
+
if (!resolved) throw enoent(path);
|
|
371
|
+
|
|
372
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
373
|
+
try {
|
|
374
|
+
const fd = resolved.dir.openAt(
|
|
375
|
+
{ symlinkFollow: false },
|
|
376
|
+
resolved.relativePath,
|
|
377
|
+
{ create: true },
|
|
378
|
+
{ write: true, mutateDirectory: true }
|
|
379
|
+
);
|
|
380
|
+
const stat = fd.stat();
|
|
381
|
+
fd.write(bytes, stat.size);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
throw new Error(`Failed to append to file '${path}': ${e}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Read directory contents synchronously.
|
|
389
|
+
*/
|
|
390
|
+
export function readdirSync(path: string, _options?: any): string[] {
|
|
391
|
+
const resolved = resolvePath(path);
|
|
392
|
+
if (!resolved) throw enoent(path);
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
let targetDir = resolved.dir;
|
|
396
|
+
if (resolved.relativePath !== '.') {
|
|
397
|
+
targetDir = resolved.dir.openAt(
|
|
398
|
+
{ symlinkFollow: false },
|
|
399
|
+
resolved.relativePath,
|
|
400
|
+
{ directory: true },
|
|
401
|
+
{ read: true }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const stream = targetDir.readDirectory();
|
|
405
|
+
const entries: string[] = [];
|
|
406
|
+
let entry;
|
|
407
|
+
while ((entry = stream.readDirectoryEntry()) && entry) {
|
|
408
|
+
if (entry.name) entries.push(entry.name);
|
|
409
|
+
}
|
|
410
|
+
return entries;
|
|
411
|
+
} catch (e) {
|
|
412
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, scandir '${path}'`), { code: 'ENOENT' });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface StatResult {
|
|
417
|
+
isFile: () => boolean;
|
|
418
|
+
isDirectory: () => boolean;
|
|
419
|
+
isSymbolicLink: () => boolean;
|
|
420
|
+
size: number;
|
|
421
|
+
mtimeMs: number;
|
|
422
|
+
atimeMs: number;
|
|
423
|
+
ctimeMs: number;
|
|
424
|
+
mode: number;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function makeStatResult(type: 'file' | 'directory' | 'notfound', size: bigint = BigInt(0)): StatResult {
|
|
428
|
+
return {
|
|
429
|
+
isFile: () => type === 'file',
|
|
430
|
+
isDirectory: () => type === 'directory',
|
|
431
|
+
isSymbolicLink: () => false,
|
|
432
|
+
size: Number(size),
|
|
433
|
+
mtimeMs: 0,
|
|
434
|
+
atimeMs: 0,
|
|
435
|
+
ctimeMs: 0,
|
|
436
|
+
mode: type === 'directory' ? 0o40755 : 0o100644,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get file stats synchronously.
|
|
442
|
+
*/
|
|
443
|
+
export function statSync(path: string): StatResult {
|
|
444
|
+
const resolved = resolvePath(path);
|
|
445
|
+
if (!resolved) throw enoent(path);
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const fd = resolved.dir.openAt({ symlinkFollow: false }, resolved.relativePath, {}, { read: true });
|
|
449
|
+
const s = fd.stat();
|
|
450
|
+
const type = s.type === 'directory' ? 'directory' : 'file';
|
|
451
|
+
return makeStatResult(type, s.size);
|
|
452
|
+
} catch (e) {
|
|
453
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, stat '${path}'`), { code: 'ENOENT' });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get file stats synchronously (no symlink follow — same as stat in WASI 0.2).
|
|
459
|
+
*/
|
|
460
|
+
export function lstatSync(path: string): StatResult {
|
|
461
|
+
return statSync(path);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create a directory synchronously.
|
|
466
|
+
*/
|
|
467
|
+
export function mkdirSync(path: string, options?: MkdirOptions): void {
|
|
468
|
+
if (options?.recursive) {
|
|
469
|
+
const normalized = normalizePath(path);
|
|
470
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
471
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
472
|
+
const partial = parts.slice(0, i).join('/');
|
|
473
|
+
const resolved = resolvePath(partial);
|
|
474
|
+
if (!resolved) continue;
|
|
475
|
+
try { resolved.dir.createDirectoryAt(resolved.relativePath); } catch { /* already exists */ }
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
const resolved = resolvePath(path);
|
|
479
|
+
if (!resolved) throw enoent(path);
|
|
480
|
+
try {
|
|
481
|
+
resolved.dir.createDirectoryAt(resolved.relativePath);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
throw Object.assign(new Error(`EEXIST: file already exists, mkdir '${path}'`), { code: 'EEXIST' });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Remove a directory synchronously.
|
|
490
|
+
*/
|
|
491
|
+
export function rmdirSync(path: string, _options?: RmdirOptions): void {
|
|
492
|
+
const resolved = resolvePath(path);
|
|
493
|
+
if (!resolved) throw enoent(path);
|
|
494
|
+
try {
|
|
495
|
+
resolved.dir.removeDirectoryAt(resolved.relativePath);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, rmdir '${path}'`), { code: 'ENOENT' });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Remove a file or directory synchronously.
|
|
503
|
+
*/
|
|
504
|
+
export function rmSync(path: string, options?: RmOptions): void {
|
|
505
|
+
const resolved = resolvePath(path);
|
|
506
|
+
if (!resolved) {
|
|
507
|
+
if (options?.force) return;
|
|
508
|
+
throw enoent(path);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const fd = resolved.dir.openAt({ symlinkFollow: false }, resolved.relativePath, {}, { read: true });
|
|
513
|
+
const s = fd.stat();
|
|
514
|
+
if (s.type === 'directory') {
|
|
515
|
+
if (!options?.recursive) {
|
|
516
|
+
throw Object.assign(
|
|
517
|
+
new Error(`EISDIR: illegal operation on a directory, rm '${path}'`),
|
|
518
|
+
{ code: 'EISDIR' }
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
resolved.dir.removeDirectoryAt(resolved.relativePath);
|
|
522
|
+
} else {
|
|
523
|
+
resolved.dir.unlinkFileAt(resolved.relativePath);
|
|
524
|
+
}
|
|
525
|
+
} catch (e) {
|
|
526
|
+
if (options?.force) return;
|
|
527
|
+
if (e instanceof Error && (e as any).code) throw e;
|
|
528
|
+
throw enoent(path);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Remove a file synchronously.
|
|
534
|
+
*/
|
|
535
|
+
export function unlinkSync(path: string): void {
|
|
536
|
+
const resolved = resolvePath(path);
|
|
537
|
+
if (!resolved) throw enoent(path);
|
|
538
|
+
try {
|
|
539
|
+
resolved.dir.unlinkFileAt(resolved.relativePath);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, unlink '${path}'`), { code: 'ENOENT' });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Copy a file synchronously.
|
|
547
|
+
*/
|
|
548
|
+
export function copyFileSync(src: string, dest: string): void {
|
|
549
|
+
const data = readFileSync(src) as Uint8Array;
|
|
550
|
+
writeFileSync(dest, data);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Rename a file or directory synchronously.
|
|
555
|
+
*/
|
|
556
|
+
export function renameSync(oldPath: string, newPath: string): void {
|
|
557
|
+
try {
|
|
558
|
+
const data = readFileSync(oldPath);
|
|
559
|
+
writeFileSync(newPath, data as Uint8Array);
|
|
560
|
+
unlinkSync(oldPath);
|
|
561
|
+
} catch (e) {
|
|
562
|
+
throw Object.assign(
|
|
563
|
+
new Error(`ENOENT: no such file or directory, rename '${oldPath}' -> '${newPath}'`),
|
|
564
|
+
{ code: 'ENOENT' }
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Check file accessibility synchronously.
|
|
571
|
+
* Throws if the path does not exist.
|
|
572
|
+
*/
|
|
573
|
+
export function accessSync(path: string, _mode?: number): void {
|
|
574
|
+
const resolved = resolvePath(path);
|
|
575
|
+
if (!resolved) throw enoent(path);
|
|
576
|
+
try {
|
|
577
|
+
resolved.dir.openAt({ symlinkFollow: false }, resolved.relativePath, {}, { read: true });
|
|
578
|
+
} catch (e) {
|
|
579
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, access '${path}'`), { code: 'ENOENT' });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
299
583
|
/**
|
|
300
|
-
* Check if
|
|
584
|
+
* Check if a path exists synchronously.
|
|
301
585
|
*/
|
|
302
|
-
export function existsSync(
|
|
303
|
-
|
|
304
|
-
return false;
|
|
586
|
+
export function existsSync(path: string): boolean {
|
|
587
|
+
const resolved = resolvePath(path);
|
|
588
|
+
if (!resolved) return false;
|
|
589
|
+
try {
|
|
590
|
+
resolved.dir.openAt({ symlinkFollow: false }, resolved.relativePath, {}, { read: true });
|
|
591
|
+
return true;
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
305
595
|
}
|
|
306
596
|
|
|
307
597
|
/**
|
|
@@ -533,15 +823,15 @@ export const promises = {
|
|
|
533
823
|
await unlink(path);
|
|
534
824
|
},
|
|
535
825
|
|
|
536
|
-
async stat(path: string): Promise<
|
|
826
|
+
async stat(path: string): Promise<StatResult> {
|
|
537
827
|
const kind = await statPath(path);
|
|
538
828
|
if (kind === 'notfound') {
|
|
539
|
-
throw
|
|
829
|
+
throw Object.assign(
|
|
830
|
+
new Error(`ENOENT: no such file or directory, stat '${path}'`),
|
|
831
|
+
{ code: 'ENOENT' }
|
|
832
|
+
);
|
|
540
833
|
}
|
|
541
|
-
return
|
|
542
|
-
isFile: () => kind === 'file',
|
|
543
|
-
isDirectory: () => kind === 'directory',
|
|
544
|
-
};
|
|
834
|
+
return makeStatResult(kind);
|
|
545
835
|
},
|
|
546
836
|
|
|
547
837
|
async rmdir(path: string, options?: RmdirOptions): Promise<void> {
|
|
@@ -566,16 +856,31 @@ export const promises = {
|
|
|
566
856
|
};
|
|
567
857
|
|
|
568
858
|
const fs = {
|
|
859
|
+
// Async / callback
|
|
569
860
|
readFile,
|
|
570
861
|
writeFile,
|
|
571
862
|
readdir,
|
|
572
|
-
existsSync,
|
|
573
863
|
unlink,
|
|
574
864
|
rmdir,
|
|
575
865
|
rm,
|
|
576
866
|
mkdir,
|
|
577
867
|
copyFile,
|
|
578
868
|
cp,
|
|
869
|
+
readFileSync,
|
|
870
|
+
writeFileSync,
|
|
871
|
+
appendFileSync,
|
|
872
|
+
readdirSync,
|
|
873
|
+
statSync,
|
|
874
|
+
lstatSync,
|
|
875
|
+
mkdirSync,
|
|
876
|
+
rmdirSync,
|
|
877
|
+
rmSync,
|
|
878
|
+
unlinkSync,
|
|
879
|
+
copyFileSync,
|
|
880
|
+
renameSync,
|
|
881
|
+
accessSync,
|
|
882
|
+
existsSync,
|
|
883
|
+
// Promises API
|
|
579
884
|
promises,
|
|
580
885
|
};
|
|
581
886
|
|
package/src/polyfills/process.ts
CHANGED
|
@@ -9,6 +9,16 @@ declare const globalThis: {
|
|
|
9
9
|
getArguments(): string[];
|
|
10
10
|
initialCwd(): string | null;
|
|
11
11
|
};
|
|
12
|
+
'wasi:cli/stdin': {
|
|
13
|
+
getStdin(): {
|
|
14
|
+
blockingRead(len: bigint): [Uint8Array, boolean];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
'wasi:cli/stdout': {
|
|
18
|
+
getStdout(): {
|
|
19
|
+
blockingWriteAndFlush(buf: Uint8Array): void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
/**
|
|
@@ -60,22 +70,44 @@ function getArgv(): string[] {
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/**
|
|
63
|
-
*
|
|
73
|
+
* Initialized from wasi:cli/environment initialCwd(), falls back to '.'.
|
|
64
74
|
*/
|
|
65
|
-
|
|
66
|
-
const bindings = getEnvBindings();
|
|
67
|
-
if (!bindings || typeof bindings.initialCwd !== 'function') {
|
|
68
|
-
return '/';
|
|
69
|
-
}
|
|
70
|
-
|
|
75
|
+
let _virtualCwd: string = (() => {
|
|
71
76
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const env = (globalThis as any)['wasi:cli/environment'];
|
|
78
|
+
if (env && typeof env.initialCwd === 'function') {
|
|
79
|
+
return env.initialCwd() ?? '.';
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
return '.';
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Internal setter for virtual CWD — handles relative and absolute paths.
|
|
87
|
+
*/
|
|
88
|
+
function setCwd(directory: string): void {
|
|
89
|
+
if (!directory) return;
|
|
90
|
+
if (directory.startsWith('/')) {
|
|
91
|
+
_virtualCwd = directory.replace(/\/+$/, '') || '/';
|
|
92
|
+
} else if (directory === '..') {
|
|
93
|
+
const parts = _virtualCwd.split('/').filter(Boolean);
|
|
94
|
+
parts.pop();
|
|
95
|
+
_virtualCwd = parts.join('/') || '.';
|
|
96
|
+
} else {
|
|
97
|
+
_virtualCwd = (_virtualCwd === '.' || _virtualCwd === '')
|
|
98
|
+
? directory.replace(/\/+$/, '')
|
|
99
|
+
: _virtualCwd.replace(/\/+$/, '') + '/' + directory.replace(/\/+$/, '');
|
|
76
100
|
}
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Returns the current virtual CWD.
|
|
105
|
+
* Exported so the fs polyfill can resolve relative paths against it.
|
|
106
|
+
*/
|
|
107
|
+
export function getCwd(): string {
|
|
108
|
+
return _virtualCwd;
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
const process = {
|
|
80
112
|
/**
|
|
81
113
|
* Environment variables
|
|
@@ -92,17 +124,20 @@ const process = {
|
|
|
92
124
|
},
|
|
93
125
|
|
|
94
126
|
/**
|
|
95
|
-
* Returns the current working directory
|
|
127
|
+
* Returns the current virtual working directory.
|
|
128
|
+
* Backed by in-process state, same as CPython's WASI libc.
|
|
96
129
|
*/
|
|
97
130
|
cwd(): string {
|
|
98
131
|
return getCwd();
|
|
99
132
|
},
|
|
100
133
|
|
|
101
134
|
/**
|
|
102
|
-
* Change
|
|
135
|
+
* Change the virtual working directory.
|
|
136
|
+
* Affects all relative path resolution in the fs polyfill.
|
|
137
|
+
* Does not perform any real syscall — mirrors CPython WASI behaviour.
|
|
103
138
|
*/
|
|
104
139
|
chdir(directory: string): void {
|
|
105
|
-
|
|
140
|
+
setCwd(directory);
|
|
106
141
|
},
|
|
107
142
|
|
|
108
143
|
/**
|
|
@@ -178,24 +213,88 @@ const process = {
|
|
|
178
213
|
},
|
|
179
214
|
|
|
180
215
|
/**
|
|
181
|
-
* Standard
|
|
216
|
+
* Standard input — backed by wasi:cli/stdin@0.2.0.
|
|
217
|
+
* Exposes a minimal EventEmitter-compatible Readable so that
|
|
218
|
+
* MCP's StdioServerTransport can call stdin.on('data', ...).
|
|
182
219
|
*/
|
|
183
|
-
get
|
|
220
|
+
get stdin(): any {
|
|
221
|
+
const listeners: Array<(chunk: Uint8Array | string) => void> = [];
|
|
222
|
+
let reading = false;
|
|
223
|
+
|
|
224
|
+
function startReading() {
|
|
225
|
+
if (reading) return;
|
|
226
|
+
reading = true;
|
|
227
|
+
(async () => {
|
|
228
|
+
try {
|
|
229
|
+
const api = globalThis['wasi:cli/stdin'];
|
|
230
|
+
if (!api) return;
|
|
231
|
+
const stream = api.getStdin();
|
|
232
|
+
while (true) {
|
|
233
|
+
const [chunk, done] = stream.blockingRead(BigInt(4096));
|
|
234
|
+
if (chunk && chunk.length > 0) {
|
|
235
|
+
const buf = chunk;
|
|
236
|
+
listeners.forEach(fn => fn(buf));
|
|
237
|
+
}
|
|
238
|
+
if (done) {
|
|
239
|
+
endListeners.forEach(fn => fn());
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch { /* stdin not available */ }
|
|
244
|
+
})();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const endListeners: Array<() => void> = [];
|
|
248
|
+
|
|
184
249
|
return {
|
|
185
|
-
write: (data: string) => console.log(data),
|
|
186
250
|
isTTY: false,
|
|
251
|
+
readable: true,
|
|
252
|
+
on(event: string, fn: (...args: any[]) => void) {
|
|
253
|
+
if (event === 'data') { listeners.push(fn); startReading(); }
|
|
254
|
+
if (event === 'end') { endListeners.push(fn); }
|
|
255
|
+
return this;
|
|
256
|
+
},
|
|
257
|
+
once(event: string, fn: (...args: any[]) => void) {
|
|
258
|
+
return this.on(event, fn);
|
|
259
|
+
},
|
|
260
|
+
pause() { return this; },
|
|
261
|
+
resume() { startReading(); return this; },
|
|
262
|
+
pipe(dest: any) { return dest; },
|
|
263
|
+
setEncoding(_enc: string) { return this; },
|
|
187
264
|
};
|
|
188
265
|
},
|
|
189
266
|
|
|
190
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Standard output — backed by wasi:cli/stdout@0.2.0.
|
|
269
|
+
*/
|
|
270
|
+
get stdout(): any {
|
|
191
271
|
return {
|
|
192
|
-
write: (data: string) => console.error(data),
|
|
193
272
|
isTTY: false,
|
|
273
|
+
write(data: string | Uint8Array, _enc?: string, cb?: () => void): boolean {
|
|
274
|
+
try {
|
|
275
|
+
const api = globalThis['wasi:cli/stdout'];
|
|
276
|
+
if (api) {
|
|
277
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
278
|
+
api.getStdout().blockingWriteAndFlush(bytes);
|
|
279
|
+
} else {
|
|
280
|
+
console.log(typeof data === 'string' ? data : new TextDecoder().decode(data));
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
console.log(typeof data === 'string' ? data : new TextDecoder().decode(data));
|
|
284
|
+
}
|
|
285
|
+
cb?.();
|
|
286
|
+
return true;
|
|
287
|
+
},
|
|
288
|
+
end(data?: string | Uint8Array, cb?: () => void): void {
|
|
289
|
+
if (data) this.write(data);
|
|
290
|
+
cb?.();
|
|
291
|
+
},
|
|
194
292
|
};
|
|
195
293
|
},
|
|
196
294
|
|
|
197
|
-
get
|
|
295
|
+
get stderr(): any {
|
|
198
296
|
return {
|
|
297
|
+
write: (data: string) => console.error(data),
|
|
199
298
|
isTTY: false,
|
|
200
299
|
};
|
|
201
300
|
},
|
package/src/run.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { execFile } from 'child_process';
|
|
9
9
|
import { resolve, extname } from 'path';
|
|
10
10
|
import { existsSync } from 'fs';
|
|
11
|
+
import { HostRequest } from './task';
|
|
11
12
|
|
|
12
13
|
export interface RunnerOptions {
|
|
13
14
|
file: string;
|
|
@@ -26,6 +27,8 @@ export interface RunnerResult {
|
|
|
26
27
|
duration_ms: number;
|
|
27
28
|
retries: number;
|
|
28
29
|
fuel_consumed: number;
|
|
30
|
+
ram_used: number;
|
|
31
|
+
host_requests: HostRequest[];
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
|
package/src/task.ts
CHANGED
|
@@ -50,13 +50,18 @@ interface TaskExecution {
|
|
|
50
50
|
duration_ms: number;
|
|
51
51
|
retries: number;
|
|
52
52
|
fuel_consumed: number;
|
|
53
|
+
ram_used: number;
|
|
54
|
+
host_requests: HostRequest[];
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
export interface HostRequest {
|
|
58
|
+
method: string;
|
|
59
|
+
url: string;
|
|
60
|
+
headers?: string[] | null;
|
|
61
|
+
body?: string | null;
|
|
62
|
+
status: number;
|
|
63
|
+
}
|
|
56
64
|
|
|
57
|
-
type TaskReturnType<T> = T extends Promise<infer U>
|
|
58
|
-
? Promise<TaskResult<U>>
|
|
59
|
-
: TaskResult<T>;
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
67
|
* Define a Capsule task with configuration.
|
|
@@ -121,10 +126,20 @@ function normalizeAllowedFile(entry: string | AllowedFile): string {
|
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
export function task<TArgs extends
|
|
129
|
+
export function task<TArgs extends unknown[], TReturn>(
|
|
130
|
+
options: TaskOptions,
|
|
131
|
+
fn: (...args: TArgs) => Promise<TReturn>
|
|
132
|
+
): (...args: TArgs) => Promise<TaskResult<TReturn>>;
|
|
133
|
+
|
|
134
|
+
export function task<TArgs extends unknown[], TReturn>(
|
|
125
135
|
options: TaskOptions,
|
|
126
136
|
fn: (...args: TArgs) => TReturn
|
|
127
|
-
): (...args: TArgs) =>
|
|
137
|
+
): (...args: TArgs) => TaskResult<TReturn>;
|
|
138
|
+
|
|
139
|
+
export function task<TArgs extends unknown[], TReturn>(
|
|
140
|
+
options: TaskOptions,
|
|
141
|
+
fn: (...args: TArgs) => TReturn | Promise<TReturn>
|
|
142
|
+
): (...args: TArgs) => TaskResult<TReturn> | Promise<TaskResult<TReturn>> {
|
|
128
143
|
const taskName = options.name;
|
|
129
144
|
let compute = options.compute?.toString().toUpperCase() ?? "MEDIUM";
|
|
130
145
|
let allowedHosts = options.allowedHosts ?? [];
|
|
@@ -140,45 +155,48 @@ export function task<TArgs extends any[], TReturn>(
|
|
|
140
155
|
envVariables: options.envVariables,
|
|
141
156
|
};
|
|
142
157
|
|
|
143
|
-
const wrapper = (...args: TArgs):
|
|
158
|
+
const wrapper = (...args: TArgs): TaskResult<TReturn> | Promise<TaskResult<TReturn>> => {
|
|
144
159
|
if (!isWasmMode()) {
|
|
145
160
|
const result = fn(...args);
|
|
146
161
|
|
|
147
162
|
if (result instanceof Promise) {
|
|
148
163
|
return result.then((value) => ({
|
|
149
164
|
success: true,
|
|
150
|
-
result: value,
|
|
165
|
+
result: value as TReturn,
|
|
151
166
|
error: null,
|
|
152
167
|
execution: {
|
|
153
168
|
task_name: taskName,
|
|
154
169
|
duration_ms: 0,
|
|
155
170
|
retries: 0,
|
|
156
171
|
fuel_consumed: 0,
|
|
172
|
+
ram_used: 0,
|
|
173
|
+
host_requests: [],
|
|
157
174
|
},
|
|
158
|
-
}))
|
|
175
|
+
}));
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
return {
|
|
162
179
|
success: true,
|
|
163
|
-
result,
|
|
180
|
+
result: result as TReturn,
|
|
164
181
|
error: null,
|
|
165
182
|
execution: {
|
|
166
183
|
task_name: taskName,
|
|
167
184
|
duration_ms: 0,
|
|
168
185
|
retries: 0,
|
|
169
186
|
fuel_consumed: 0,
|
|
187
|
+
ram_used: 0,
|
|
188
|
+
host_requests: [],
|
|
170
189
|
},
|
|
171
|
-
}
|
|
190
|
+
};
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
const resultJson = callHost(taskName, args, taskConfig);
|
|
175
194
|
|
|
176
195
|
try {
|
|
177
|
-
|
|
178
|
-
return result as TaskReturnType<TReturn>;
|
|
196
|
+
return JSON.parse(resultJson) as TaskResult<TReturn>;
|
|
179
197
|
} catch (e) {
|
|
180
198
|
if (e instanceof SyntaxError) {
|
|
181
|
-
return resultJson as unknown as
|
|
199
|
+
return resultJson as unknown as TaskResult<TReturn>;
|
|
182
200
|
}
|
|
183
201
|
throw e;
|
|
184
202
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Buffer polyfill for WASI environment
|
|
3
|
-
* Provides Node.js-compatible Buffer class using the buffer package
|
|
4
|
-
*/
|
|
5
|
-
import { Buffer as BufferPolyfill } from 'buffer';
|
|
6
|
-
export declare const Buffer: BufferConstructor;
|
|
7
|
-
export default BufferPolyfill;
|
|
8
|
-
//# sourceMappingURL=buffer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/polyfills/buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAC;AAIlD,eAAO,MAAM,MAAM,mBAAiB,CAAC;AACrC,eAAe,cAAc,CAAC"}
|