looply-cli 0.5.2 → 0.5.3

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.
Files changed (126) hide show
  1. package/dist/.src-hash +1 -1
  2. package/dist/agent-contained.d.ts +2 -0
  3. package/dist/agent-contained.d.ts.map +1 -1
  4. package/dist/agent-contained.js +22 -5
  5. package/dist/agent-contained.js.map +1 -1
  6. package/dist/agent-exec.d.ts.map +1 -1
  7. package/dist/agent-exec.js +1 -0
  8. package/dist/agent-exec.js.map +1 -1
  9. package/dist/autonomy-tick.d.ts +46 -0
  10. package/dist/autonomy-tick.d.ts.map +1 -1
  11. package/dist/autonomy-tick.js +44 -2
  12. package/dist/autonomy-tick.js.map +1 -1
  13. package/dist/bin.d.ts.map +1 -1
  14. package/dist/bin.js +131 -93
  15. package/dist/bin.js.map +1 -1
  16. package/dist/brain-advice.js +2 -2
  17. package/dist/brain-advice.js.map +1 -1
  18. package/dist/command-runner.d.ts.map +1 -1
  19. package/dist/command-runner.js +1 -0
  20. package/dist/command-runner.js.map +1 -1
  21. package/dist/connect-node.d.ts.map +1 -1
  22. package/dist/connect-node.js +8 -4
  23. package/dist/connect-node.js.map +1 -1
  24. package/dist/connect.d.ts +8 -0
  25. package/dist/connect.d.ts.map +1 -1
  26. package/dist/connect.js +22 -0
  27. package/dist/connect.js.map +1 -1
  28. package/dist/contained-node.d.ts +51 -0
  29. package/dist/contained-node.d.ts.map +1 -0
  30. package/dist/contained-node.js +116 -0
  31. package/dist/contained-node.js.map +1 -0
  32. package/dist/daemon-cmd.d.ts.map +1 -1
  33. package/dist/daemon-cmd.js +43 -1
  34. package/dist/daemon-cmd.js.map +1 -1
  35. package/dist/daemon.d.ts +9 -0
  36. package/dist/daemon.d.ts.map +1 -1
  37. package/dist/daemon.js +11 -1
  38. package/dist/daemon.js.map +1 -1
  39. package/dist/exec-domains.d.ts +4 -0
  40. package/dist/exec-domains.d.ts.map +1 -1
  41. package/dist/exec-domains.js +7 -2
  42. package/dist/exec-domains.js.map +1 -1
  43. package/dist/gate-draft.d.ts +99 -0
  44. package/dist/gate-draft.d.ts.map +1 -0
  45. package/dist/gate-draft.js +544 -0
  46. package/dist/gate-draft.js.map +1 -0
  47. package/dist/git-driver.d.ts.map +1 -1
  48. package/dist/git-driver.js +1 -0
  49. package/dist/git-driver.js.map +1 -1
  50. package/dist/init-detect.d.ts +20 -10
  51. package/dist/init-detect.d.ts.map +1 -1
  52. package/dist/init-detect.js +27 -85
  53. package/dist/init-detect.js.map +1 -1
  54. package/dist/model-transport.d.ts.map +1 -1
  55. package/dist/model-transport.js +25 -8
  56. package/dist/model-transport.js.map +1 -1
  57. package/dist/notify-channel.d.ts.map +1 -1
  58. package/dist/notify-channel.js +1 -0
  59. package/dist/notify-channel.js.map +1 -1
  60. package/dist/provider-cmd.d.ts +56 -0
  61. package/dist/provider-cmd.d.ts.map +1 -0
  62. package/dist/provider-cmd.js +260 -0
  63. package/dist/provider-cmd.js.map +1 -0
  64. package/dist/provider-discovery.d.ts +75 -0
  65. package/dist/provider-discovery.d.ts.map +1 -0
  66. package/dist/provider-discovery.js +235 -0
  67. package/dist/provider-discovery.js.map +1 -0
  68. package/dist/provider-identity.d.ts +3 -3
  69. package/dist/provider-identity.d.ts.map +1 -1
  70. package/dist/provider-identity.js +4 -5
  71. package/dist/provider-identity.js.map +1 -1
  72. package/dist/provider-preflight.d.ts.map +1 -1
  73. package/dist/provider-preflight.js +1 -0
  74. package/dist/provider-preflight.js.map +1 -1
  75. package/dist/provider-ready.d.ts +66 -0
  76. package/dist/provider-ready.d.ts.map +1 -0
  77. package/dist/provider-ready.js +105 -0
  78. package/dist/provider-ready.js.map +1 -0
  79. package/dist/provider-state.d.ts.map +1 -1
  80. package/dist/provider-state.js +1 -0
  81. package/dist/provider-state.js.map +1 -1
  82. package/dist/refresh-gates.d.ts +47 -0
  83. package/dist/refresh-gates.d.ts.map +1 -0
  84. package/dist/refresh-gates.js +128 -0
  85. package/dist/refresh-gates.js.map +1 -0
  86. package/dist/repo-init.d.ts +5 -1
  87. package/dist/repo-init.d.ts.map +1 -1
  88. package/dist/repo-init.js +19 -3
  89. package/dist/repo-init.js.map +1 -1
  90. package/dist/run-cmd.js +4 -4
  91. package/dist/run-cmd.js.map +1 -1
  92. package/dist/run-launcher.d.ts.map +1 -1
  93. package/dist/run-launcher.js +2 -0
  94. package/dist/run-launcher.js.map +1 -1
  95. package/dist/serve-cmd.d.ts.map +1 -1
  96. package/dist/serve-cmd.js +14 -5
  97. package/dist/serve-cmd.js.map +1 -1
  98. package/dist/serve.d.ts +19 -7
  99. package/dist/serve.d.ts.map +1 -1
  100. package/dist/serve.js +28 -12
  101. package/dist/serve.js.map +1 -1
  102. package/dist/server-client.d.ts +12 -1
  103. package/dist/server-client.d.ts.map +1 -1
  104. package/dist/server-client.js +10 -2
  105. package/dist/server-client.js.map +1 -1
  106. package/dist/session-checkpoint-runtime.d.ts.map +1 -1
  107. package/dist/session-checkpoint-runtime.js +1 -0
  108. package/dist/session-checkpoint-runtime.js.map +1 -1
  109. package/dist/setup.d.ts +7 -1
  110. package/dist/setup.d.ts.map +1 -1
  111. package/dist/setup.js +13 -13
  112. package/dist/setup.js.map +1 -1
  113. package/dist/shared-grants.js +2 -2
  114. package/dist/shared-grants.js.map +1 -1
  115. package/dist/toolchain-mirror.js +1 -1
  116. package/dist/toolchain-mirror.js.map +1 -1
  117. package/dist/untrusted-exec.d.ts +23 -0
  118. package/dist/untrusted-exec.d.ts.map +1 -1
  119. package/dist/untrusted-exec.js +18 -1
  120. package/dist/untrusted-exec.js.map +1 -1
  121. package/dist/untrusted-gate.d.ts.map +1 -1
  122. package/dist/untrusted-gate.js +4 -0
  123. package/dist/untrusted-gate.js.map +1 -1
  124. package/package.json +2 -2
  125. package/win/appcontainer.ps1 +97 -4
  126. package/win/contained-node.cjs +530 -0
@@ -0,0 +1,530 @@
1
+ 'use strict';
2
+ /*
3
+ WHAT NODE NEEDS TO WORK INSIDE A WINDOWS APPCONTAINER, AND CANNOT DO ITSELF.
4
+
5
+ Loaded into every Node process a contained gate or agent starts, through
6
+ `NODE_OPTIONS=--require` (see `contained-node.ts`). It changes nothing about
7
+ what the container may reach — it is plain JavaScript running with the
8
+ container's own token, so it can do nothing the project's code could not do
9
+ itself. It exists so the project's code does not HANG or FAIL on two
10
+ properties of the boundary that were measured, not assumed:
11
+
12
+ 1. PIPED CHILD PROCESSES. libuv implements piped stdio as a named pipe
13
+ called `\\?\pipe\uv\<ptr>-<pid>`, and an AppContainer may not create a
14
+ pipe outside its own `\\.\pipe\LOCAL\` namespace. libuv reads the
15
+ resulting ERROR_ACCESS_DENIED as a name COLLISION and retries with the
16
+ next name, for ever — so `spawn(…, {stdio:'pipe'})` never returns and
17
+ the process spins. That is why esbuild (a Go binary driven over its
18
+ stdin/stdout) hung, and with it Vite and Vitest.
19
+
20
+ The repair keeps the API and moves the pipe: each piped slot gets a pipe
21
+ under `\\.\pipe\LOCAL\`, which the container IS allowed to create; the
22
+ child is handed the client end as an inherited handle (which has always
23
+ worked), and the parent reads and writes the server end through a stream
24
+ with the same surface `child.stdout` always had. `spawnSync` and the
25
+ `exec*Sync` family go through temporary files instead — a synchronous
26
+ call cannot wait for an asynchronous accept.
27
+
28
+ `fork()`'s IPC channel cannot be moved: its pipe is created inside
29
+ libuv and set up by Node internals no preload can reach. So it FAILS
30
+ FAST, naming the cause, instead of hanging until the gate's timeout —
31
+ a test runner then needs its worker-thread pool (`vitest --pool=threads`,
32
+ jest `--runInBand` or `workerThreads`).
33
+
34
+ 2. REALPATH. `fs.realpathSync.native` fails with EPERM on EVERY path — it
35
+ maps the final path back to a drive letter, which a container is not
36
+ allowed to ask — and the JavaScript `fs.realpath` walks up to `lstat C:\`,
37
+ which the container cannot read either. Jest and Vite both call it at
38
+ startup. On EPERM/EACCES only, the path is resolved again component by
39
+ component, following the links the container CAN see and taking the
40
+ ancestors it cannot see as they are. Any other error is unchanged.
41
+
42
+ Installed only when `LOOPLY_CONTAINED_NODE=1`, so requiring the file to test
43
+ it changes nothing.
44
+ */
45
+
46
+ const BS = String.fromCharCode(92);
47
+ const LOCAL_PIPE_PREFIX = BS + BS + '.' + BS + 'pipe' + BS + 'LOCAL' + BS + 'looply-';
48
+
49
+ const IPC_REFUSAL =
50
+ 'child_process IPC (fork, or stdio "ipc") cannot work inside the Looply Windows sandbox: ' +
51
+ 'Node creates its IPC pipe where an AppContainer may not, and would hang for ever. ' +
52
+ 'Use a worker-thread pool instead (vitest --pool=threads; jest --runInBand or workerThreads).';
53
+
54
+ /* ─────────────────────────────────────────────────────────── realpath ── */
55
+
56
+ /**
57
+ * A realpath that tolerates ancestors it may not read.
58
+ *
59
+ * Walks the absolute path one component at a time. A component the container
60
+ * cannot `lstat` is taken as it is (it is an ancestor above the grant, and the
61
+ * container could not follow a link there anyway); a component it CAN see is
62
+ * followed if it is a link. A component that does not exist throws ENOENT, as
63
+ * realpath does.
64
+ */
65
+ function lenientRealpath(fs, path, p) {
66
+ // The root as the platform spells it — `C:\`, or `\\server\share\` — and
67
+ // the components below it.
68
+ const split = (abs) => {
69
+ const root = path.parse(abs).root;
70
+ return [root, abs.slice(root.length).split(/[\\/]+/)];
71
+ };
72
+ let [current, pending] = split(path.resolve(p));
73
+ let links = 0;
74
+ while (pending.length > 0) {
75
+ const part = pending.shift();
76
+ if (part === '' || part === '.') continue;
77
+ if (part === '..') {
78
+ current = path.dirname(current);
79
+ continue;
80
+ }
81
+ const next = path.join(current, part);
82
+ let st = null;
83
+ try {
84
+ st = fs.lstatSync(next);
85
+ } catch (e) {
86
+ if (!e || (e.code !== 'EPERM' && e.code !== 'EACCES')) throw e;
87
+ }
88
+ if (st !== null && st.isSymbolicLink()) {
89
+ if (++links > 40) {
90
+ const err = new Error(`ELOOP: too many symbolic links encountered, realpath '${p}'`);
91
+ err.code = 'ELOOP';
92
+ throw err;
93
+ }
94
+ const [root, parts] = split(path.resolve(current, fs.readlinkSync(next)));
95
+ current = root;
96
+ pending = parts.concat(pending);
97
+ continue;
98
+ }
99
+ current = next;
100
+ }
101
+ return current;
102
+ }
103
+
104
+ function denied(e) {
105
+ return !!e && (e.code === 'EPERM' || e.code === 'EACCES');
106
+ }
107
+
108
+ function toPathString(p) {
109
+ if (typeof p === 'string') return p;
110
+ if (Buffer.isBuffer(p)) return p.toString('utf8');
111
+ if (p instanceof URL) return require('url').fileURLToPath(p);
112
+ return String(p);
113
+ }
114
+
115
+ function encode(result, options) {
116
+ const enc = typeof options === 'string' ? options : options && options.encoding;
117
+ return enc === 'buffer' ? Buffer.from(result, 'utf8') : result;
118
+ }
119
+
120
+ function installRealpath(fs, path) {
121
+ const lenient = (p, options) => encode(lenientRealpath(fs, path, toPathString(p)), options);
122
+
123
+ const wrapSync = (orig) =>
124
+ function realpathSync(p, options) {
125
+ try {
126
+ return orig.call(fs, p, options);
127
+ } catch (e) {
128
+ if (!denied(e)) throw e;
129
+ return lenient(p, options);
130
+ }
131
+ };
132
+ const wrapCallback = (orig) =>
133
+ function realpath(p, options, callback) {
134
+ if (typeof options === 'function') {
135
+ callback = options;
136
+ options = undefined;
137
+ }
138
+ orig.call(fs, p, options, (e, resolved) => {
139
+ if (!denied(e)) return callback(e, resolved);
140
+ let out;
141
+ try {
142
+ out = lenient(p, options);
143
+ } catch (e2) {
144
+ return callback(e2);
145
+ }
146
+ callback(null, out);
147
+ });
148
+ };
149
+
150
+ const sync = wrapSync(fs.realpathSync);
151
+ sync.native = wrapSync(fs.realpathSync.native);
152
+ fs.realpathSync = sync;
153
+ const cb = wrapCallback(fs.realpath);
154
+ cb.native = wrapCallback(fs.realpath.native);
155
+ fs.realpath = cb;
156
+
157
+ const origPromise = fs.promises.realpath;
158
+ fs.promises.realpath = async function realpath(p, options) {
159
+ try {
160
+ return await origPromise.call(fs.promises, p, options);
161
+ } catch (e) {
162
+ if (!denied(e)) throw e;
163
+ return lenient(p, options);
164
+ }
165
+ };
166
+ }
167
+
168
+ /* ───────────────────────────────────────────────────── piped children ── */
169
+
170
+ /** Every piped slot normalised the way Node normalises it. */
171
+ function normaliseStdio(stdio) {
172
+ if (stdio === undefined || stdio === null) stdio = 'pipe';
173
+ if (typeof stdio === 'string') return [stdio, stdio, stdio];
174
+ if (!Array.isArray(stdio)) return null; // Node will throw its own error for this
175
+ const out = stdio.slice();
176
+ for (let i = 0; i < out.length; i += 1) {
177
+ if (out[i] === undefined || out[i] === null) out[i] = i < 3 ? 'pipe' : 'ignore';
178
+ }
179
+ return out;
180
+ }
181
+
182
+ const isPipe = (s) => s === 'pipe' || s === 'overlapped';
183
+
184
+ /**
185
+ * A pipe pair under `\\.\pipe\LOCAL\`: the child's end as a file descriptor,
186
+ * opened NOW, and the parent's end as a stream that is wired the moment the
187
+ * server accepts it — on the next turn of the loop.
188
+ *
189
+ * The server must stay open until it has accepted: closing it first closes the
190
+ * pending instance the child is connected to, and the child's first write fails
191
+ * with EPIPE.
192
+ */
193
+ function pipePair(deps, end) {
194
+ const { fs, net, crypto } = deps;
195
+ const name = LOCAL_PIPE_PREFIX + process.pid + '-' + crypto.randomBytes(8).toString('hex');
196
+ const server = net.createServer();
197
+ server.on('error', (e) => end.destroy(e));
198
+ server.once('connection', (socket) => {
199
+ server.close();
200
+ end._attach(socket);
201
+ });
202
+ // Binding a pipe server is synchronous; only the accept is not.
203
+ server.listen(name);
204
+ // Never keep a process alive on its own: the child is already connected, so
205
+ // the accept completes on the next loop turn whenever anything else runs.
206
+ server.unref();
207
+ let fd;
208
+ try {
209
+ fd = fs.openSync(name, 'r+');
210
+ } catch (e) {
211
+ server.close();
212
+ throw e;
213
+ }
214
+ return fd;
215
+ }
216
+
217
+ function makePipeEnd(Duplex) {
218
+ /**
219
+ * The parent's end of one piped slot.
220
+ *
221
+ * A Duplex over a socket that does not exist yet: writes queue until it does,
222
+ * reads are pushed as they arrive and pause the socket when nobody is reading.
223
+ * `ref`/`unref` are kept and applied, because esbuild unrefs its service's
224
+ * stdio so a process using it can exit.
225
+ */
226
+ return class PipeEnd extends Duplex {
227
+ constructor(options) {
228
+ super(options);
229
+ this._socket = null;
230
+ this._queued = [];
231
+ this._finalCallback = null;
232
+ this._refed = true;
233
+ }
234
+
235
+ _attach(socket) {
236
+ if (this.destroyed) {
237
+ socket.destroy();
238
+ return;
239
+ }
240
+ this._socket = socket;
241
+ if (!this._refed) socket.unref();
242
+ socket.on('data', (chunk) => {
243
+ if (!this.push(chunk)) socket.pause();
244
+ });
245
+ socket.on('end', () => this.push(null));
246
+ socket.on('error', (e) => this.destroy(e));
247
+ for (const [chunk, encoding, callback] of this._queued) socket.write(chunk, encoding, callback);
248
+ this._queued = [];
249
+ if (this._finalCallback !== null) {
250
+ const callback = this._finalCallback;
251
+ this._finalCallback = null;
252
+ this._finish(callback);
253
+ }
254
+ }
255
+
256
+ _read() {
257
+ if (this._socket !== null) this._socket.resume();
258
+ }
259
+
260
+ _write(chunk, encoding, callback) {
261
+ if (this._socket !== null) this._socket.write(chunk, encoding, callback);
262
+ else this._queued.push([chunk, encoding, callback]);
263
+ }
264
+
265
+ _final(callback) {
266
+ if (this._socket !== null) this._finish(callback);
267
+ else this._finalCallback = callback;
268
+ }
269
+
270
+ /** A named pipe has no half-close: the child sees EOF when the handle closes. */
271
+ _finish(callback) {
272
+ const socket = this._socket;
273
+ socket.end(() => {
274
+ socket.destroy();
275
+ callback();
276
+ });
277
+ }
278
+
279
+ _destroy(error, callback) {
280
+ if (this._socket !== null) this._socket.destroy();
281
+ for (const [, , cb] of this._queued) cb(error || new Error('stream destroyed'));
282
+ this._queued = [];
283
+ callback(error);
284
+ }
285
+
286
+ ref() {
287
+ this._refed = true;
288
+ if (this._socket !== null) this._socket.ref();
289
+ return this;
290
+ }
291
+
292
+ unref() {
293
+ this._refed = false;
294
+ if (this._socket !== null) this._socket.unref();
295
+ return this;
296
+ }
297
+ };
298
+ }
299
+
300
+ function installSpawn(deps) {
301
+ const { cp, fs } = deps;
302
+ const PipeEnd = makePipeEnd(require('stream').Duplex);
303
+ const original = cp.ChildProcess.prototype.spawn;
304
+
305
+ cp.ChildProcess.prototype.spawn = function spawn(options) {
306
+ const stdio = options && typeof options === 'object' ? normaliseStdio(options.stdio) : null;
307
+ if (stdio === null) return original.call(this, options);
308
+ if (stdio.includes('ipc')) {
309
+ const err = new Error(IPC_REFUSAL);
310
+ err.code = 'ERR_LOOPLY_CONTAINED_IPC';
311
+ throw err;
312
+ }
313
+ if (!stdio.some(isPipe)) return original.call(this, options);
314
+
315
+ const ends = [];
316
+ const fds = [];
317
+ try {
318
+ for (let i = 0; i < stdio.length; i += 1) {
319
+ if (!isPipe(stdio[i])) continue;
320
+ // Node's own shape: stdin is written, every other slot is read.
321
+ const end = new PipeEnd(i === 0 ? { readable: false } : { writable: false });
322
+ const fd = pipePair(deps, end);
323
+ ends.push([i, end]);
324
+ fds.push(fd);
325
+ stdio[i] = fd;
326
+ }
327
+ } catch (e) {
328
+ for (const fd of fds) fs.closeSync(fd);
329
+ for (const [, end] of ends) end.destroy();
330
+ throw e;
331
+ }
332
+
333
+ let result;
334
+ try {
335
+ result = original.call(this, Object.assign({}, options, { stdio }));
336
+ } finally {
337
+ // The child holds its own copies now. Ours must go, or the parent keeps
338
+ // the pipe open and never sees the child's EOF.
339
+ for (const fd of fds) fs.closeSync(fd);
340
+ }
341
+
342
+ const spawned = typeof this.pid === 'number' && this.pid !== 0;
343
+ for (const [i, end] of ends) {
344
+ if (!spawned) {
345
+ end.destroy();
346
+ continue;
347
+ }
348
+ this.stdio[i] = end;
349
+ if (i === 0) this.stdin = end;
350
+ if (i === 1) this.stdout = end;
351
+ if (i === 2) this.stderr = end;
352
+ if (i > 0) {
353
+ // Node waits for every READ slot to close before 'close', so the last
354
+ // of a child's output is never lost to an early 'close'. Same here,
355
+ // through the same counters its own `maybeClose` uses.
356
+ this._closesNeeded += 1;
357
+ end.on('close', () => {
358
+ this._closesGot += 1;
359
+ if (this._closesGot === this._closesNeeded) this.emit('close', this.exitCode, this.signalCode);
360
+ });
361
+ }
362
+ }
363
+ return result;
364
+ };
365
+ }
366
+
367
+ /* ─────────────────────────────────────────────────── synchronous calls ── */
368
+
369
+ function installSync(deps) {
370
+ const { cp, fs, path, os, crypto } = deps;
371
+ const originalSpawnSync = cp.spawnSync;
372
+ const DEFAULT_MAX_BUFFER = 1024 * 1024;
373
+
374
+ function spawnSync(file, args, options) {
375
+ if (!Array.isArray(args)) {
376
+ options = args;
377
+ args = [];
378
+ }
379
+ options = Object.assign({}, options);
380
+ const stdio = normaliseStdio(options.stdio);
381
+ if (stdio === null) return originalSpawnSync(file, args, options);
382
+ if (stdio.includes('ipc')) {
383
+ const err = new Error(IPC_REFUSAL);
384
+ err.code = 'ERR_LOOPLY_CONTAINED_IPC';
385
+ throw err;
386
+ }
387
+ if (!stdio.some(isPipe)) return originalSpawnSync(file, args, options);
388
+
389
+ const box = fs.mkdtempSync(path.join(os.tmpdir(), 'looply-sync-'));
390
+ const opened = [];
391
+ const outputs = [];
392
+ try {
393
+ for (let i = 0; i < stdio.length; i += 1) {
394
+ if (!isPipe(stdio[i])) continue;
395
+ const file_ = path.join(box, String(i) + '-' + crypto.randomBytes(4).toString('hex'));
396
+ if (i === 0) {
397
+ const input = options.input;
398
+ const bytes =
399
+ input === undefined || input === null
400
+ ? Buffer.alloc(0)
401
+ : typeof input === 'string'
402
+ ? Buffer.from(input, options.encoding && options.encoding !== 'buffer' ? options.encoding : 'utf8')
403
+ : Buffer.from(input.buffer, input.byteOffset, input.byteLength);
404
+ fs.writeFileSync(file_, bytes);
405
+ const fd = fs.openSync(file_, 'r');
406
+ opened.push(fd);
407
+ stdio[i] = fd;
408
+ } else {
409
+ const fd = fs.openSync(file_, 'w');
410
+ opened.push(fd);
411
+ outputs.push([i, file_]);
412
+ stdio[i] = fd;
413
+ }
414
+ }
415
+ delete options.input;
416
+ const ret = originalSpawnSync(file, args, Object.assign(options, { stdio }));
417
+ for (const fd of opened.splice(0)) fs.closeSync(fd);
418
+
419
+ const maxBuffer = options.maxBuffer === undefined ? DEFAULT_MAX_BUFFER : options.maxBuffer;
420
+ const encoding = options.encoding && options.encoding !== 'buffer' ? options.encoding : null;
421
+ const output = Array.isArray(ret.output) ? ret.output.slice() : new Array(stdio.length).fill(null);
422
+ for (const [i, file_] of outputs) {
423
+ let bytes = fs.readFileSync(file_);
424
+ if (Number.isFinite(maxBuffer) && bytes.length > maxBuffer) {
425
+ bytes = bytes.subarray(0, maxBuffer);
426
+ if (!ret.error) {
427
+ const err = new Error(`spawnSync ${file} ENOBUFS`);
428
+ err.code = 'ENOBUFS';
429
+ err.errno = -4060;
430
+ err.syscall = `spawnSync ${file}`;
431
+ err.path = file;
432
+ err.spawnargs = args;
433
+ ret.error = err;
434
+ }
435
+ }
436
+ output[i] = encoding === null ? bytes : bytes.toString(encoding);
437
+ }
438
+ ret.output = output;
439
+ ret.stdout = output[1] === undefined ? null : output[1];
440
+ ret.stderr = output[2] === undefined ? null : output[2];
441
+ return ret;
442
+ } finally {
443
+ for (const fd of opened) fs.closeSync(fd);
444
+ try {
445
+ fs.rmSync(box, { recursive: true, force: true });
446
+ } catch {
447
+ /* a temp directory is not this call's verdict */
448
+ }
449
+ }
450
+ }
451
+
452
+ /** Node's own `checkExecSyncError`, which is not exported. */
453
+ function execSyncError(ret, command) {
454
+ if (ret.error) return Object.assign(ret.error, ret);
455
+ if (ret.status !== 0) {
456
+ let message = 'Command failed: ' + command;
457
+ if (ret.stderr && ret.stderr.length > 0) message += '\n' + ret.stderr.toString();
458
+ return Object.assign(new Error(message), ret);
459
+ }
460
+ return null;
461
+ }
462
+
463
+ function execFileSync(file, args, options) {
464
+ if (!Array.isArray(args)) {
465
+ options = args;
466
+ args = [];
467
+ }
468
+ options = Object.assign({}, options);
469
+ const inheritStderr = !options.stdio;
470
+ const ret = spawnSync(file, args, options);
471
+ if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr);
472
+ const err = execSyncError(ret, [options.argv0 || file].concat(args).join(' '));
473
+ if (err) throw err;
474
+ return ret.stdout;
475
+ }
476
+
477
+ function execSync(command, options) {
478
+ options = Object.assign({}, options);
479
+ options.shell = typeof options.shell === 'string' ? options.shell : true;
480
+ const inheritStderr = !options.stdio;
481
+ const ret = spawnSync(command, [], options);
482
+ if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr);
483
+ const err = execSyncError(ret, command);
484
+ if (err) throw err;
485
+ return ret.stdout;
486
+ }
487
+
488
+ cp.spawnSync = spawnSync;
489
+ cp.execFileSync = execFileSync;
490
+ cp.execSync = execSync;
491
+ }
492
+
493
+ /* ───────────────────────────────────────────────────────────── install ── */
494
+
495
+ function install() {
496
+ if (process.platform !== 'win32') return false;
497
+ if (global.__looplyContainedNode) return false;
498
+ global.__looplyContainedNode = true;
499
+ const deps = {
500
+ cp: require('child_process'),
501
+ fs: require('fs'),
502
+ net: require('net'),
503
+ os: require('os'),
504
+ path: require('path'),
505
+ crypto: require('crypto'),
506
+ };
507
+ /*
508
+ * Windows REWRITES `TEMP`/`TMP` for an AppContainer process, to
509
+ * `<LOCALAPPDATA>\Packages\<container>\AC\Temp` — whatever the environment
510
+ * block said — and nothing creates that directory, so `os.tmpdir()` names a
511
+ * path that is not there and every temp file fails with ENOENT (measured).
512
+ * It is under the contained root's granted `local`, so it can be made.
513
+ */
514
+ try {
515
+ deps.fs.mkdirSync(deps.os.tmpdir(), { recursive: true });
516
+ } catch {
517
+ /* a temp directory we cannot make is a failure for whoever needs it */
518
+ }
519
+ installRealpath(deps.fs, deps.path);
520
+ installSpawn(deps);
521
+ installSync(deps);
522
+ // ESM `import { spawn } from 'node:child_process'` reads a snapshot of the
523
+ // CommonJS exports; refresh it so both module systems see the same functions.
524
+ require('module').syncBuiltinESMExports();
525
+ return true;
526
+ }
527
+
528
+ module.exports = { install, lenientRealpath, normaliseStdio, IPC_REFUSAL, LOCAL_PIPE_PREFIX };
529
+
530
+ if (process.env.LOOPLY_CONTAINED_NODE === '1') install();