pi-bridge 0.1.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.
@@ -0,0 +1,639 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const fsp = require('fs/promises');
5
+ const http = require('http');
6
+ const path = require('path');
7
+ const { isFakePath, toRemotePath } = require('./path-mapper');
8
+ const { httpGet, httpPost } = require('./http-client');
9
+
10
+ /**
11
+ * Patch Node.js built-in fs methods to intercept calls on fake-root paths
12
+ * and redirect them to the remote file server.
13
+ *
14
+ * Covers both sync (readFileSync, readdirSync, …) and async (fs.promises.*)
15
+ * variants so that both pi's startup resource loading and its read tool work.
16
+ *
17
+ * Non-fake paths are forwarded to the original fs implementations unchanged.
18
+ *
19
+ * @param {string} fakeRoot - Local fake root directory (real path, no symlinks)
20
+ * @param {number} port - Remote file server port (127.0.0.1)
21
+ * @param {string} token - Auth token
22
+ */
23
+
24
+ // --- Error helpers ---
25
+
26
+ function makeEnoent(p, syscall) {
27
+ syscall = syscall || 'open';
28
+ return Object.assign(
29
+ new Error("ENOENT: no such file or directory, " + syscall + " '" + p + "'"),
30
+ { code: 'ENOENT', syscall, path: p }
31
+ );
32
+ }
33
+
34
+ function makeEisdir(p) {
35
+ return Object.assign(
36
+ new Error("EISDIR: illegal operation on a directory, read"),
37
+ { code: 'EISDIR', syscall: 'read', path: p }
38
+ );
39
+ }
40
+
41
+ function rethrowRemote(e, p) {
42
+ if (!e || !e.message) throw e;
43
+ if (e.message.includes('403')) throw makeEnoent(p);
44
+ if (e.message.includes('EISDIR')) throw makeEisdir(p);
45
+ if (e.message.includes('ENOENT')) throw makeEnoent(p);
46
+ throw e;
47
+ }
48
+
49
+ // --- Async HTTP helper (used for fs.promises patching) ---
50
+
51
+ function httpGetAsync(port, token, urlPath) {
52
+ return new Promise(function(resolve, reject) {
53
+ const req = http.get(
54
+ { hostname: '127.0.0.1', port: port, path: urlPath, headers: { 'x-token': token } },
55
+ function(res) {
56
+ var body = '';
57
+ res.setEncoding('utf8');
58
+ res.on('data', function(d) { body += d; });
59
+ res.on('end', function() {
60
+ if (res.statusCode >= 200 && res.statusCode < 300) {
61
+ resolve(body);
62
+ } else {
63
+ var err = new Error('HTTP ' + res.statusCode + ': ' + body);
64
+ err.statusCode = res.statusCode;
65
+ reject(err);
66
+ }
67
+ });
68
+ }
69
+ );
70
+ req.on('error', reject);
71
+ });
72
+ }
73
+
74
+ function patchFs(fakeRoot, port, token) {
75
+ // --- Sync originals ---
76
+ const orig = {
77
+ readdirSync: fs.readdirSync.bind(fs),
78
+ readFileSync: fs.readFileSync.bind(fs),
79
+ existsSync: fs.existsSync.bind(fs),
80
+ accessSync: fs.accessSync.bind(fs),
81
+ statSync: fs.statSync.bind(fs),
82
+ lstatSync: fs.lstatSync.bind(fs),
83
+ writeFileSync: fs.writeFileSync.bind(fs),
84
+ appendFileSync: fs.appendFileSync.bind(fs),
85
+ mkdirSync: fs.mkdirSync.bind(fs),
86
+ rmSync: fs.rmSync.bind(fs),
87
+ unlinkSync: fs.unlinkSync.bind(fs),
88
+ renameSync: fs.renameSync.bind(fs),
89
+ copyFileSync: fs.copyFileSync.bind(fs),
90
+ realpathSync: fs.realpathSync.bind(fs),
91
+ createWriteStream: fs.createWriteStream.bind(fs),
92
+ createReadStream: fs.createReadStream.bind(fs),
93
+ };
94
+
95
+ // --- Async (promises) originals ---
96
+ const origP = {
97
+ access: fs.promises.access.bind(fs.promises),
98
+ readFile: fs.promises.readFile.bind(fs.promises),
99
+ readdir: fs.promises.readdir.bind(fs.promises),
100
+ stat: fs.promises.stat.bind(fs.promises),
101
+ lstat: fs.promises.lstat.bind(fs.promises),
102
+ writeFile: fs.promises.writeFile.bind(fs.promises),
103
+ appendFile: fs.promises.appendFile.bind(fs.promises),
104
+ mkdir: fs.promises.mkdir.bind(fs.promises),
105
+ rm: fs.promises.rm.bind(fs.promises),
106
+ unlink: fs.promises.unlink.bind(fs.promises),
107
+ rename: fs.promises.rename.bind(fs.promises),
108
+ copyFile: fs.promises.copyFile.bind(fs.promises),
109
+ open: fs.promises.open.bind(fs.promises),
110
+ };
111
+
112
+ // Resolve any path (relative or absolute) to absolute before checking fake root
113
+ function abs(p) {
114
+ return typeof p === 'string' ? path.resolve(p) : null;
115
+ }
116
+
117
+ function rpath(fakePath) {
118
+ return toRemotePath(fakePath, fakeRoot);
119
+ }
120
+
121
+ // Build a minimal fs.Stats-like object from the remote /stat response
122
+ function makeStats(info) {
123
+ return {
124
+ isFile: function() { return info.isFile; },
125
+ isDirectory: function() { return info.isDirectory; },
126
+ isSymbolicLink: function() { return false; },
127
+ isBlockDevice: function() { return false; },
128
+ isCharacterDevice: function() { return false; },
129
+ isFIFO: function() { return false; },
130
+ isSocket: function() { return false; },
131
+ size: info.size,
132
+ mtimeMs: info.mtimeMs,
133
+ mtime: new Date(info.mtimeMs),
134
+ mode: info.isDirectory ? 0o40755 : 0o100644,
135
+ nlink: 1,
136
+ uid: 0, gid: 0, dev: 0, ino: 0, rdev: 0, blksize: 4096, blocks: 0,
137
+ atime: new Date(info.mtimeMs), ctime: new Date(info.mtimeMs), birthtime: new Date(info.mtimeMs),
138
+ atimeMs: info.mtimeMs, ctimeMs: info.mtimeMs, birthtimeMs: info.mtimeMs,
139
+ };
140
+ }
141
+
142
+ // ── Sync patches ────────────────────────────────────────────────────────────
143
+
144
+ fs.readdirSync = function patchedReaddirSync(p, opts) {
145
+ const s = abs(p) || p.toString();
146
+ if (!isFakePath(s, fakeRoot)) return orig.readdirSync(p, opts);
147
+ try {
148
+ const withFileTypes = opts && opts.withFileTypes;
149
+ const qs = '/list?path=' + encodeURIComponent(rpath(s)) + (withFileTypes ? '&withFileTypes=1' : '');
150
+ const parsed = JSON.parse(httpGet(port, token, qs));
151
+ if (!withFileTypes) return parsed;
152
+ return parsed.map(function(e) {
153
+ return {
154
+ name: e.name,
155
+ isFile: function() { return e.isFile; },
156
+ isDirectory: function() { return e.isDirectory; },
157
+ isSymbolicLink: function() { return e.isSymbolicLink; },
158
+ isBlockDevice: function() { return false; },
159
+ isCharacterDevice: function() { return false; },
160
+ isFIFO: function() { return false; },
161
+ isSocket: function() { return false; },
162
+ };
163
+ });
164
+ } catch (e) { rethrowRemote(e, s); }
165
+ };
166
+
167
+ fs.readFileSync = function patchedReadFileSync(p, opts) {
168
+ const s = abs(p);
169
+ if (!s || !isFakePath(s, fakeRoot)) return orig.readFileSync(p, opts);
170
+ try {
171
+ const body = httpGet(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
172
+ const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
173
+ return enc ? body : Buffer.from(body, 'utf8');
174
+ } catch (e) { rethrowRemote(e, s); }
175
+ };
176
+
177
+ fs.existsSync = function patchedExistsSync(p) {
178
+ const s = abs(p);
179
+ if (!s || !isFakePath(s, fakeRoot)) return orig.existsSync(p);
180
+ try {
181
+ return httpGet(port, token, '/exists?path=' + encodeURIComponent(rpath(s))).trim() === '1';
182
+ } catch (e) {
183
+ if (e.message && (e.message.includes('403') || e.message.includes('ENOENT'))) return false;
184
+ throw e;
185
+ }
186
+ };
187
+
188
+ fs.accessSync = function patchedAccessSync(p, mode) {
189
+ const s = abs(p);
190
+ if (!s || !isFakePath(s, fakeRoot)) return orig.accessSync(p, mode);
191
+ try {
192
+ const exists = httpGet(port, token, '/exists?path=' + encodeURIComponent(rpath(s))).trim() === '1';
193
+ if (!exists) throw makeEnoent(s, 'access');
194
+ } catch (e) {
195
+ if (e.code === 'ENOENT') throw e;
196
+ if (e.message && e.message.includes('403')) throw makeEnoent(s, 'access');
197
+ throw e;
198
+ }
199
+ };
200
+
201
+ fs.statSync = function patchedStatSync(p, opts) {
202
+ const s = abs(p);
203
+ if (!s || !isFakePath(s, fakeRoot)) return orig.statSync(p, opts);
204
+ try {
205
+ return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
206
+ } catch (e) { rethrowRemote(e, s); }
207
+ };
208
+
209
+ fs.lstatSync = function patchedLstatSync(p, opts) {
210
+ const s = abs(p);
211
+ if (!s || !isFakePath(s, fakeRoot)) return orig.lstatSync(p, opts);
212
+ try {
213
+ return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
214
+ } catch (e) { rethrowRemote(e, s); }
215
+ };
216
+
217
+ fs.writeFileSync = function patchedWriteFileSync(p, data, opts) {
218
+ const s = abs(p);
219
+ if (!s || !isFakePath(s, fakeRoot)) return orig.writeFileSync(p, data, opts);
220
+ const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
221
+ httpPost(port, token, '/write?path=' + encodeURIComponent(rpath(s)), content);
222
+ };
223
+
224
+ fs.mkdirSync = function patchedMkdirSync(p, opts) {
225
+ const s = abs(p);
226
+ if (!s || !isFakePath(s, fakeRoot)) return orig.mkdirSync(p, opts);
227
+ httpPost(port, token, '/mkdir?path=' + encodeURIComponent(rpath(s)), '');
228
+ };
229
+
230
+ fs.unlinkSync = function patchedUnlinkSync(p) {
231
+ const s = abs(p);
232
+ if (!s || !isFakePath(s, fakeRoot)) return orig.unlinkSync(p);
233
+ httpPost(port, token, '/delete?path=' + encodeURIComponent(rpath(s)), '');
234
+ };
235
+
236
+ fs.renameSync = function patchedRenameSync(from, to) {
237
+ const fromS = abs(from);
238
+ const toS = abs(to);
239
+ if (!fromS || !isFakePath(fromS, fakeRoot)) return orig.renameSync(from, to);
240
+ httpPost(port, token,
241
+ '/rename?from=' + encodeURIComponent(rpath(fromS)) + '&to=' + encodeURIComponent(toS ? rpath(toS) : to.toString()),
242
+ '');
243
+ };
244
+
245
+ fs.appendFileSync = function patchedAppendFileSync(p, data, opts) {
246
+ const s = abs(p);
247
+ if (!s || !isFakePath(s, fakeRoot)) return orig.appendFileSync(p, data, opts);
248
+ const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
249
+ httpPost(port, token, '/append?path=' + encodeURIComponent(rpath(s)), content);
250
+ };
251
+
252
+ fs.rmSync = function patchedRmSync(p, opts) {
253
+ const s = abs(p);
254
+ if (!s || !isFakePath(s, fakeRoot)) return orig.rmSync(p, opts);
255
+ const recursive = (opts && opts.recursive) ? '1' : '0';
256
+ httpPost(port, token, '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive, '');
257
+ };
258
+
259
+ fs.copyFileSync = function patchedCopyFileSync(src, dest) {
260
+ const srcS = abs(src);
261
+ const destS = abs(dest);
262
+ if (!srcS || !isFakePath(srcS, fakeRoot)) return orig.copyFileSync(src, dest);
263
+ httpPost(port, token,
264
+ '/copy?from=' + encodeURIComponent(rpath(srcS)) + '&to=' + encodeURIComponent(destS ? rpath(destS) : dest.toString()),
265
+ '');
266
+ };
267
+
268
+ // realpathSync: fake paths are already real absolute paths (no symlinks in skeleton)
269
+ fs.realpathSync = function patchedRealpathSync(p, opts) {
270
+ const s = abs(p);
271
+ if (!s || !isFakePath(s, fakeRoot)) return orig.realpathSync(p, opts);
272
+ return s;
273
+ };
274
+ fs.realpathSync.native = function(p, opts) {
275
+ const s = abs(p);
276
+ if (!s || !isFakePath(s, fakeRoot)) return orig.realpathSync.native ? orig.realpathSync.native(p, opts) : orig.realpathSync(p, opts);
277
+ return s;
278
+ };
279
+
280
+ // createReadStream: read entire file from remote, pipe as stream
281
+ fs.createReadStream = function patchedCreateReadStream(p, opts) {
282
+ const s = typeof p === 'string' ? abs(p) : null;
283
+ if (!s || !isFakePath(s, fakeRoot)) return orig.createReadStream(p, opts);
284
+ const { Readable } = require('stream');
285
+ const stream = new Readable({ read() {} });
286
+ // Fetch async, push into stream
287
+ httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)))
288
+ .then(function(body) { stream.push(body); stream.push(null); })
289
+ .catch(function(e) { stream.destroy(e); });
290
+ return stream;
291
+ };
292
+
293
+ // createWriteStream: buffer writes, POST on finish
294
+ fs.createWriteStream = function patchedCreateWriteStream(p, opts) {
295
+ const s = typeof p === 'string' ? abs(p) : null;
296
+ if (!s || !isFakePath(s, fakeRoot)) return orig.createWriteStream(p, opts);
297
+ const { Writable } = require('stream');
298
+ const chunks = [];
299
+ return new Writable({
300
+ write(chunk, enc, cb) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, enc)); cb(); },
301
+ final(cb) {
302
+ const content = Buffer.concat(chunks).toString('utf8');
303
+ try { httpPost(port, token, '/write?path=' + encodeURIComponent(rpath(s)), content); cb(); }
304
+ catch(e) { cb(e); }
305
+ },
306
+ });
307
+ };
308
+
309
+ // ── Async (fs.promises) patches ─────────────────────────────────────────────
310
+
311
+ fs.promises.access = async function patchedAccess(p, mode) {
312
+ const s = abs(p);
313
+ if (!s || !isFakePath(s, fakeRoot)) return origP.access(p, mode);
314
+ try {
315
+ const exists = (await httpGetAsync(port, token, '/exists?path=' + encodeURIComponent(rpath(s)))).trim() === '1';
316
+ if (!exists) throw makeEnoent(s, 'access');
317
+ } catch (e) {
318
+ if (e.code === 'ENOENT') throw e;
319
+ if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s, 'access');
320
+ throw e;
321
+ }
322
+ };
323
+
324
+ fs.promises.readFile = async function patchedReadFile(p, opts) {
325
+ const s = typeof p === 'string' ? abs(p) : null;
326
+ if (!s || !isFakePath(s, fakeRoot)) return origP.readFile(p, opts);
327
+ try {
328
+ const body = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
329
+ const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
330
+ return enc ? body : Buffer.from(body, 'utf8');
331
+ } catch (e) {
332
+ if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
333
+ if (e.message && e.message.includes('EISDIR')) throw makeEisdir(s);
334
+ throw e;
335
+ }
336
+ };
337
+
338
+ fs.promises.readdir = async function patchedReaddir(p, opts) {
339
+ const s = typeof p === 'string' ? abs(p) : p.toString();
340
+ if (!isFakePath(s, fakeRoot)) return origP.readdir(p, opts);
341
+ try {
342
+ const withFileTypes = opts && opts.withFileTypes;
343
+ const qs = '/list?path=' + encodeURIComponent(rpath(s)) + (withFileTypes ? '&withFileTypes=1' : '');
344
+ const parsed = JSON.parse(await httpGetAsync(port, token, qs));
345
+ if (!withFileTypes) return parsed;
346
+ return parsed.map(function(e) {
347
+ return {
348
+ name: e.name,
349
+ isFile: function() { return e.isFile; },
350
+ isDirectory: function() { return e.isDirectory; },
351
+ isSymbolicLink: function() { return e.isSymbolicLink; },
352
+ isBlockDevice: function() { return false; },
353
+ isCharacterDevice: function() { return false; },
354
+ isFIFO: function() { return false; },
355
+ isSocket: function() { return false; },
356
+ };
357
+ });
358
+ } catch (e) {
359
+ if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
360
+ throw e;
361
+ }
362
+ };
363
+ fsp.readdir = fs.promises.readdir;
364
+
365
+ fs.promises.stat = async function patchedStat(p, opts) {
366
+ const s = abs(p);
367
+ if (!s || !isFakePath(s, fakeRoot)) return origP.stat(p, opts);
368
+ try {
369
+ return makeStats(JSON.parse(await httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
370
+ } catch (e) {
371
+ if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
372
+ throw e;
373
+ }
374
+ };
375
+
376
+ fs.promises.lstat = async function patchedLstat(p, opts) {
377
+ const s = abs(p);
378
+ if (!s || !isFakePath(s, fakeRoot)) return origP.lstat(p, opts);
379
+ try {
380
+ return makeStats(JSON.parse(await httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
381
+ } catch (e) {
382
+ if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
383
+ throw e;
384
+ }
385
+ };
386
+
387
+ fs.promises.writeFile = async function patchedWriteFile(p, data, opts) {
388
+ const s = typeof p === 'string' ? abs(p) : null;
389
+ if (!s || !isFakePath(s, fakeRoot)) return origP.writeFile(p, data, opts);
390
+ const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
391
+ await new Promise(function(resolve, reject) {
392
+ const body = Buffer.from(content, 'utf8');
393
+ const req = http.request(
394
+ { method: 'POST', hostname: '127.0.0.1', port: port,
395
+ path: '/write?path=' + encodeURIComponent(rpath(s)),
396
+ headers: { 'x-token': token, 'Content-Length': body.length } },
397
+ function(res) {
398
+ res.resume();
399
+ res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); });
400
+ }
401
+ );
402
+ req.on('error', reject);
403
+ req.end(body);
404
+ });
405
+ };
406
+
407
+ fs.promises.mkdir = async function patchedMkdir(p, opts) {
408
+ const s = abs(p);
409
+ if (!s || !isFakePath(s, fakeRoot)) return origP.mkdir(p, opts);
410
+ await new Promise(function(resolve, reject) {
411
+ const req = http.request(
412
+ { method: 'POST', hostname: '127.0.0.1', port: port,
413
+ path: '/mkdir?path=' + encodeURIComponent(rpath(s)),
414
+ headers: { 'x-token': token, 'Content-Length': 0 } },
415
+ function(res) {
416
+ res.resume();
417
+ res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); });
418
+ }
419
+ );
420
+ req.on('error', reject);
421
+ req.end();
422
+ });
423
+ };
424
+
425
+ fs.promises.unlink = async function patchedUnlink(p) {
426
+ const s = abs(p);
427
+ if (!s || !isFakePath(s, fakeRoot)) return origP.unlink(p);
428
+ await new Promise(function(resolve, reject) {
429
+ const req = http.request(
430
+ { method: 'POST', hostname: '127.0.0.1', port: port,
431
+ path: '/delete?path=' + encodeURIComponent(rpath(s)),
432
+ headers: { 'x-token': token, 'Content-Length': 0 } },
433
+ function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
434
+ );
435
+ req.on('error', reject); req.end();
436
+ });
437
+ };
438
+
439
+ fs.promises.appendFile = async function patchedAppendFile(p, data, opts) {
440
+ const s = typeof p === 'string' ? abs(p) : null;
441
+ if (!s || !isFakePath(s, fakeRoot)) return origP.appendFile(p, data, opts);
442
+ const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
443
+ const buf = Buffer.from(content, 'utf8');
444
+ await new Promise(function(resolve, reject) {
445
+ const req = http.request(
446
+ { method: 'POST', hostname: '127.0.0.1', port: port,
447
+ path: '/append?path=' + encodeURIComponent(rpath(s)),
448
+ headers: { 'x-token': token, 'Content-Length': buf.length } },
449
+ function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
450
+ );
451
+ req.on('error', reject); req.end(buf);
452
+ });
453
+ };
454
+
455
+ fs.promises.rm = async function patchedRm(p, opts) {
456
+ const s = abs(p);
457
+ if (!s || !isFakePath(s, fakeRoot)) return origP.rm(p, opts);
458
+ const recursive = (opts && opts.recursive) ? '1' : '0';
459
+ await new Promise(function(resolve, reject) {
460
+ const req = http.request(
461
+ { method: 'POST', hostname: '127.0.0.1', port: port,
462
+ path: '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive,
463
+ headers: { 'x-token': token, 'Content-Length': 0 } },
464
+ function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
465
+ );
466
+ req.on('error', reject); req.end();
467
+ });
468
+ };
469
+
470
+ fs.promises.copyFile = async function patchedCopyFile(src, dest) {
471
+ const srcS = abs(src);
472
+ const destS = abs(dest);
473
+ if (!srcS || !isFakePath(srcS, fakeRoot)) return origP.copyFile(src, dest);
474
+ await new Promise(function(resolve, reject) {
475
+ const req = http.request(
476
+ { method: 'POST', hostname: '127.0.0.1', port: port,
477
+ path: '/copy?from=' + encodeURIComponent(rpath(srcS)) + '&to=' + encodeURIComponent(destS ? rpath(destS) : String(dest)),
478
+ headers: { 'x-token': token, 'Content-Length': 0 } },
479
+ function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
480
+ );
481
+ req.on('error', reject); req.end();
482
+ });
483
+ };
484
+
485
+ // fs.promises.open: return a minimal FileHandle for reading
486
+ fs.promises.open = async function patchedOpen(p, flags, mode) {
487
+ const s = typeof p === 'string' ? abs(p) : null;
488
+ if (!s || !isFakePath(s, fakeRoot)) return origP.open(p, flags, mode);
489
+ // Fetch the file content eagerly; return a minimal FileHandle-like object
490
+ const content = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
491
+ const buf = Buffer.from(content, 'utf8');
492
+ let pos = 0;
493
+ return {
494
+ readFile: async function(opts) {
495
+ const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
496
+ return enc ? content : buf;
497
+ },
498
+ read: async function(buffer, offset, length, position) {
499
+ const start = position != null ? position : pos;
500
+ const copied = buf.copy(buffer, offset, start, start + length);
501
+ pos = start + copied;
502
+ return { bytesRead: copied, buffer };
503
+ },
504
+ close: async function() {},
505
+ [Symbol.asyncDispose]: async function() {},
506
+ };
507
+ };
508
+
509
+ // watch / watchFile: inotify can't cross machines, so we poll the remote
510
+ // /stat endpoint and emit 'change' when mtimeMs shifts.
511
+ const origWatch = fs.watch.bind(fs);
512
+ const origWatchFile = fs.watchFile ? fs.watchFile.bind(fs) : null;
513
+ const origUnwatchFile = fs.unwatchFile ? fs.unwatchFile.bind(fs) : null;
514
+
515
+ // Registry for watchFile pollers keyed by resolved path, so unwatchFile can stop them.
516
+ const watchFilePollers = new Map();
517
+
518
+ fs.watch = function patchedWatch(filename, opts, listener) {
519
+ const s = abs(typeof filename === 'string' ? filename : null);
520
+ if (!s || !isFakePath(s, fakeRoot)) return origWatch(filename, opts, listener);
521
+
522
+ if (typeof opts === 'function') { listener = opts; opts = {}; }
523
+
524
+ const { EventEmitter } = require('events');
525
+ const watcher = new EventEmitter();
526
+ if (listener) watcher.on('change', listener);
527
+
528
+ // file mtimes: '' → file itself, or filename → entry inside directory
529
+ var mtimes = new Map();
530
+ var isDir = null;
531
+ var closed = false;
532
+
533
+ function pollFile() {
534
+ return httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
535
+ .then(function(body) {
536
+ var info = JSON.parse(body);
537
+ var prev = mtimes.get('');
538
+ if (prev !== undefined && info.mtimeMs !== prev) {
539
+ watcher.emit('change', 'change', path.basename(s));
540
+ }
541
+ mtimes.set('', info.mtimeMs);
542
+ });
543
+ }
544
+
545
+ function pollDir() {
546
+ // Single request returns all entries with their mtimes
547
+ return httpGetAsync(port, token, '/liststat?path=' + encodeURIComponent(rpath(s)))
548
+ .then(function(body) {
549
+ var entries = JSON.parse(body);
550
+ entries.forEach(function(e) {
551
+ if (!e.isFile) return;
552
+ var prev = mtimes.get(e.name);
553
+ if (prev !== undefined && e.mtimeMs !== prev) {
554
+ watcher.emit('change', 'change', e.name);
555
+ }
556
+ mtimes.set(e.name, e.mtimeMs);
557
+ });
558
+ });
559
+ }
560
+
561
+ function poll() {
562
+ if (closed) return;
563
+ var work;
564
+ if (isDir === null) {
565
+ work = httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
566
+ .then(function(body) {
567
+ isDir = JSON.parse(body).isDirectory;
568
+ return isDir ? pollDir() : pollFile();
569
+ });
570
+ } else {
571
+ work = isDir ? pollDir() : pollFile();
572
+ }
573
+ work.catch(function() {}).then(function() { if (!closed) setTimeout(poll, 1000); });
574
+ }
575
+ poll();
576
+
577
+ watcher.close = function() { closed = true; };
578
+ watcher.ref = function() { return watcher; };
579
+ watcher.unref = function() { return watcher; };
580
+ return watcher;
581
+ };
582
+
583
+ fs.watchFile = function patchedWatchFile(filename, opts, listener) {
584
+ const s = abs(typeof filename === 'string' ? filename : null);
585
+ if (!s || !isFakePath(s, fakeRoot)) {
586
+ return origWatchFile ? origWatchFile(filename, opts, listener) : undefined;
587
+ }
588
+ if (typeof opts === 'function') { listener = opts; opts = {}; }
589
+ const interval = (opts && opts.interval) || 5007;
590
+
591
+ var lastStat = null;
592
+ var closed = false;
593
+
594
+ function poll() {
595
+ if (closed) return;
596
+ httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
597
+ .then(function(body) {
598
+ var info = JSON.parse(body);
599
+ var cur = makeStats(info);
600
+ if (lastStat !== null && cur.mtimeMs !== lastStat.mtimeMs) {
601
+ if (listener) listener(cur, lastStat);
602
+ }
603
+ lastStat = cur;
604
+ })
605
+ .catch(function() {})
606
+ .then(function() { if (!closed) setTimeout(poll, interval); });
607
+ }
608
+ poll();
609
+
610
+ watchFilePollers.set(s, function stop() { closed = true; });
611
+ };
612
+
613
+ fs.unwatchFile = function patchedUnwatchFile(filename, listener) {
614
+ const s = abs(typeof filename === 'string' ? filename : null);
615
+ if (!s || !isFakePath(s, fakeRoot)) {
616
+ return origUnwatchFile ? origUnwatchFile(filename, listener) : undefined;
617
+ }
618
+ const stop = watchFilePollers.get(s);
619
+ if (stop) { stop(); watchFilePollers.delete(s); }
620
+ };
621
+
622
+ // Mirror all async patches onto the standalone 'fs/promises' module object too.
623
+ // ES modules that do `import { readFile } from 'node:fs/promises'` get static
624
+ // bindings from this object — patching it ensures they see our interceptors.
625
+ fsp.access = fs.promises.access;
626
+ fsp.readFile = fs.promises.readFile;
627
+ fsp.readdir = fs.promises.readdir;
628
+ fsp.stat = fs.promises.stat;
629
+ fsp.lstat = fs.promises.lstat;
630
+ fsp.writeFile = fs.promises.writeFile;
631
+ fsp.appendFile = fs.promises.appendFile;
632
+ fsp.mkdir = fs.promises.mkdir;
633
+ fsp.rm = fs.promises.rm;
634
+ fsp.unlink = fs.promises.unlink;
635
+ fsp.copyFile = fs.promises.copyFile;
636
+ fsp.open = fs.promises.open;
637
+ }
638
+
639
+ module.exports = { patchFs };