@poe-platform/safe-bash 0.1.153 → 0.1.155

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.
@@ -19,6 +19,7 @@ export interface BrowserCommandsOptions {
19
19
  readonly replace?: boolean;
20
20
  readonly maxDirectoryEntries?: number;
21
21
  readonly maxTeeTargets?: number;
22
+ readonly maxTailFollowHandles?: number;
22
23
  }
23
24
  export declare function createBrowserCommands(options?: BrowserCommandsOptions): readonly CommandDefinition[];
24
25
  export declare function browserCommands(options?: BrowserCommandsOptions): VirtualShellPlugin;
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAMlF,OAAO,EAAqC,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAG3G,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AACjG,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACnG,cAAc,sCAAsC,CAAC;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,iBAAiB,EAAE,CAYxG;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,kBAAkB,CAaxF"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAMlF,OAAO,EAAqC,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAG3G,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AACjG,OAAO,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACnG,cAAc,sCAAsC,CAAC;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,iBAAiB,EAAE,CAYxG;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,kBAAkB,CAaxF"}
@@ -4373,6 +4373,434 @@ async function openFileOutput(context, path, flag, incremental) {
4373
4373
  }
4374
4374
  }
4375
4375
 
4376
+ // packages/safe-bash/src/commands/tail-follow.ts
4377
+ init_platform();
4378
+ var tailFollowScheduler = {
4379
+ now: monotonicNow,
4380
+ setTimeout: (callback, milliseconds) => setTimeout(callback, milliseconds),
4381
+ clearTimeout: (handle) => clearTimeout(handle)
4382
+ };
4383
+ function parseTailFollow(arguments_) {
4384
+ const args = [];
4385
+ let mode;
4386
+ let idleMs;
4387
+ let ended = false;
4388
+ for (let index = 0; index < arguments_.length; index++) {
4389
+ const argument = arguments_[index];
4390
+ if (ended || argument === "-" || !argument.startsWith("-")) {
4391
+ args.push(argument);
4392
+ continue;
4393
+ }
4394
+ if (argument === "--") {
4395
+ ended = true;
4396
+ args.push(argument);
4397
+ continue;
4398
+ }
4399
+ if (argument === "--max-idle" || argument.startsWith("--max-idle=")) {
4400
+ const text = argument === "--max-idle" ? arguments_[++index] : argument.slice(11);
4401
+ if (text === void 0 || !/^(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$/u.test(text)) throw new UsageError("max-idle must be nonnegative decimal seconds");
4402
+ const [whole = "", fraction = ""] = text.split(".");
4403
+ if (fraction.slice(3).split("").some((digit2) => digit2 !== "0")) throw new UsageError("max-idle must resolve to integer milliseconds");
4404
+ const milliseconds = BigInt(whole || "0") * 1000n + BigInt(fraction.slice(0, 3).padEnd(3, "0"));
4405
+ if (milliseconds > BigInt(Number.MAX_SAFE_INTEGER)) throw new UsageError("max-idle exceeds safe milliseconds");
4406
+ idleMs = Number(milliseconds);
4407
+ } else if (argument.startsWith("--")) {
4408
+ args.push(argument);
4409
+ if ((argument === "--lines" || argument === "--bytes") && index + 1 < arguments_.length) args.push(arguments_[++index]);
4410
+ } else {
4411
+ let kept = "-";
4412
+ for (let offset = 1; offset < argument.length; offset++) {
4413
+ const flag = argument[offset];
4414
+ if (flag === "f" || flag === "F") mode = flag;
4415
+ else {
4416
+ kept += flag;
4417
+ if (flag === "n" || flag === "c") {
4418
+ kept += argument.slice(offset + 1);
4419
+ if (offset + 1 === argument.length && index + 1 < arguments_.length) {
4420
+ args.push(kept, arguments_[++index]);
4421
+ kept = "-";
4422
+ }
4423
+ break;
4424
+ }
4425
+ }
4426
+ }
4427
+ if (kept !== "-") args.push(kept);
4428
+ }
4429
+ }
4430
+ if (idleMs !== void 0 && mode === void 0) throw new UsageError("max-idle requires -f or -F");
4431
+ return { args, mode, idleMs };
4432
+ }
4433
+ var idleExpired = /* @__PURE__ */ Symbol("tail idle expired");
4434
+ var FollowSession = class {
4435
+ constructor(caller, cap, scheduler) {
4436
+ this.caller = caller;
4437
+ this.cap = cap;
4438
+ this.scheduler = scheduler;
4439
+ this.operation = createOutputOperation(caller, caller.stdout);
4440
+ this.context = { ...caller, stdout: this.operation.output, signal: this.operation.signal };
4441
+ this.operation.registerCleanup(() => this.close());
4442
+ }
4443
+ caller;
4444
+ cap;
4445
+ scheduler;
4446
+ operation;
4447
+ context;
4448
+ readers = /* @__PURE__ */ new Set();
4449
+ pending = /* @__PURE__ */ new Set();
4450
+ waiting = /* @__PURE__ */ new Set();
4451
+ failures = [];
4452
+ accepting = true;
4453
+ opening = 0;
4454
+ closeFailed = false;
4455
+ drain;
4456
+ stdin;
4457
+ assertOpen() {
4458
+ this.caller.signal.throwIfAborted();
4459
+ this.operation.signal.throwIfAborted();
4460
+ if (!this.accepting || this.closeFailed) throw new Error("Tail follow admission is closed");
4461
+ }
4462
+ wait(pending, milliseconds) {
4463
+ this.assertOpen();
4464
+ return new Promise((resolve, reject) => {
4465
+ let settled = false;
4466
+ let timer;
4467
+ let armed = false;
4468
+ const finish = (outcome) => {
4469
+ if (settled) return;
4470
+ settled = true;
4471
+ this.waiting.delete(cancel);
4472
+ this.operation.signal.removeEventListener("abort", cancel);
4473
+ try {
4474
+ if (armed) this.scheduler.clearTimeout(timer);
4475
+ } catch (reason) {
4476
+ this.failures.push(reason);
4477
+ if (!("reason" in outcome)) outcome = { reason };
4478
+ }
4479
+ if ("reason" in outcome) reject(outcome.reason);
4480
+ else resolve(outcome.value);
4481
+ };
4482
+ const cancel = () => finish({ reason: this.operation.signal.aborted ? this.operation.signal.reason : new Error("Tail follow is closed") });
4483
+ this.waiting.add(cancel);
4484
+ this.operation.signal.addEventListener("abort", cancel, { once: true });
4485
+ pending?.then((value2) => finish({ value: value2 }), (reason) => finish({ reason }));
4486
+ if (milliseconds !== void 0) {
4487
+ try {
4488
+ timer = this.scheduler.setTimeout(() => finish({ value: idleExpired }), Math.min(2147483647, Math.max(0, milliseconds)));
4489
+ armed = true;
4490
+ } catch (reason) {
4491
+ finish({ reason });
4492
+ }
4493
+ }
4494
+ if (this.operation.signal.aborted || !this.accepting) cancel();
4495
+ });
4496
+ }
4497
+ async work(start) {
4498
+ this.assertOpen();
4499
+ const pending = Promise.resolve().then(() => {
4500
+ this.assertOpen();
4501
+ return start();
4502
+ });
4503
+ const settled = pending.then(() => {
4504
+ }, () => {
4505
+ });
4506
+ this.pending.add(settled);
4507
+ void settled.then(() => {
4508
+ this.pending.delete(settled);
4509
+ });
4510
+ return await this.wait(pending);
4511
+ }
4512
+ async open(name2) {
4513
+ this.assertOpen();
4514
+ if (this.readers.size + this.opening >= this.cap) throw new FsError("EMFILE", { message: "maxTailFollowHandles exhausted" });
4515
+ this.opening++;
4516
+ try {
4517
+ return await this.work(async () => {
4518
+ const path = pathOf(this.context, name2);
4519
+ const capabilities = await this.caller.fs.capabilitiesFor?.(path, { signal: this.operation.signal }) ?? this.caller.fs.capabilities;
4520
+ this.assertOpen();
4521
+ if (capabilities.retainedRead !== true || typeof this.caller.fs.openReadFile !== "function") throw new FsError("ENOTSUP", { path, message: "tail follow requires retainedRead and openReadFile" });
4522
+ const handle = await this.caller.fs.openReadFile(path, { signal: this.operation.signal });
4523
+ const reader = { handle };
4524
+ this.readers.add(reader);
4525
+ if (!this.accepting || this.operation.signal.aborted) {
4526
+ await this.release(reader);
4527
+ this.assertOpen();
4528
+ }
4529
+ return reader;
4530
+ });
4531
+ } finally {
4532
+ this.opening--;
4533
+ }
4534
+ }
4535
+ release(reader) {
4536
+ reader.closing ??= Promise.resolve().then(() => reader.handle.close()).then(() => {
4537
+ this.readers.delete(reader);
4538
+ }, (reason) => {
4539
+ this.closeFailed = true;
4540
+ this.failures.push(reason);
4541
+ throw reason;
4542
+ });
4543
+ void reader.closing.catch(() => {
4544
+ });
4545
+ return reader.closing;
4546
+ }
4547
+ async stat(reader) {
4548
+ const stat = await this.work(() => reader.handle.stat({ signal: this.operation.signal }));
4549
+ if (stat.type !== "file") throw new FsError("EISDIR");
4550
+ if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new FsError("EIO", { message: "invalid retained file size" });
4551
+ return stat;
4552
+ }
4553
+ async read(reader, offset, end) {
4554
+ const maximum = Math.min(64 * 1024, end - offset);
4555
+ const chunk = await this.work(() => reader.handle.read(offset, maximum, { signal: this.operation.signal }));
4556
+ if (!(chunk instanceof Uint8Array) || chunk.byteLength > maximum) throw new FsError("EIO", { message: "invalid retained read result" });
4557
+ return chunk;
4558
+ }
4559
+ closeInput() {
4560
+ const stdin = this.stdin;
4561
+ if (!stdin) return Promise.resolve();
4562
+ stdin.closing ??= Promise.resolve().then(async () => {
4563
+ if (!stdin.finished) await stdin.iterator.return?.();
4564
+ }).catch((reason) => {
4565
+ this.failures.push(reason);
4566
+ throw reason;
4567
+ });
4568
+ void stdin.closing.catch(() => {
4569
+ });
4570
+ return stdin.closing;
4571
+ }
4572
+ async *input(idleMs) {
4573
+ this.assertOpen();
4574
+ const stdin = { iterator: this.caller.stdin[Symbol.asyncIterator](), finished: false };
4575
+ this.stdin = stdin;
4576
+ let deadline = idleMs === void 0 ? void 0 : this.scheduler.now() + idleMs;
4577
+ let failed = false;
4578
+ try {
4579
+ while (true) {
4580
+ const pending = Promise.resolve().then(() => {
4581
+ this.assertOpen();
4582
+ return stdin.iterator.next();
4583
+ });
4584
+ const result = await this.wait(pending, deadline === void 0 ? void 0 : Math.max(0, deadline - this.scheduler.now()));
4585
+ if (result === idleExpired) break;
4586
+ if (result.done) {
4587
+ stdin.finished = true;
4588
+ break;
4589
+ }
4590
+ if (!(result.value instanceof Uint8Array)) throw new TypeError("Shell stdin must yield Uint8Array");
4591
+ yield result.value;
4592
+ if (result.value.byteLength && idleMs !== void 0) deadline = this.scheduler.now() + idleMs;
4593
+ await yieldTurn(this.caller.signal);
4594
+ }
4595
+ } catch (reason) {
4596
+ failed = true;
4597
+ throw reason;
4598
+ } finally {
4599
+ await this.closeInput().catch((reason) => {
4600
+ if (!failed) throw reason;
4601
+ });
4602
+ }
4603
+ }
4604
+ close() {
4605
+ if (!this.drain) {
4606
+ this.accepting = false;
4607
+ for (const cancel of this.waiting) cancel();
4608
+ this.drain = Promise.resolve().then(async () => {
4609
+ await Promise.allSettled([...this.pending, ...[...this.readers].map((reader) => this.release(reader)), this.closeInput()]);
4610
+ if (this.failures.length === 1) throw this.failures[0];
4611
+ if (this.failures.length) throw new AggregateError(this.failures, "Tail follow cleanup failed");
4612
+ });
4613
+ }
4614
+ return this.drain;
4615
+ }
4616
+ };
4617
+ var FollowReadError = class {
4618
+ constructor(reason) {
4619
+ this.reason = reason;
4620
+ }
4621
+ reason;
4622
+ };
4623
+ async function followTail(caller, selection, cap, select, scheduler = tailFollowScheduler) {
4624
+ const named = selection.names.filter((name2) => name2 !== "-").length;
4625
+ if (named + Number(selection.mode === "F" && named > 0) > cap) throw new UsageError(`maxTailFollowHandles exceeded (limit ${cap})`);
4626
+ if (selection.mode === "F" && selection.names.includes("-")) throw new FsError("ENOTSUP", { message: "cannot follow standard input by name" });
4627
+ const session = new FollowSession(caller, cap, scheduler);
4628
+ const context = session.context;
4629
+ const entries = selection.names.map((name2) => ({ name: name2, reader: void 0, identity: void 0, offset: 0, error: void 0, active: name2 !== "-" }));
4630
+ let lastHeader;
4631
+ let exitCode = 0;
4632
+ let primary;
4633
+ let acknowledged = scheduler.now();
4634
+ const header = async (entry) => {
4635
+ if (selection.headers && lastHeader !== entry) {
4636
+ await output(context, `${lastHeader ? "\n" : ""}==> ${entry.name === "-" ? "standard input" : entry.name} <==
4637
+ `);
4638
+ lastHeader = entry;
4639
+ }
4640
+ };
4641
+ const failure = async (entry, reason, initial) => {
4642
+ session.assertOpen();
4643
+ if (!(reason instanceof FsError) || !["ENOENT", "ENOTDIR", "EACCES", "EPERM", "EISDIR"].includes(reason.code)) throw reason;
4644
+ if (initial) exitCode = 1;
4645
+ if (initial && reason.code === "EISDIR" || !initial && reason.code === "EACCES" && entry.reader) await header(entry);
4646
+ if (entry.error !== reason.code) await diagnostic(context, reason);
4647
+ entry.error = reason.code;
4648
+ if (entry.reader) {
4649
+ await session.release(entry.reader);
4650
+ entry.reader = void 0;
4651
+ }
4652
+ if (selection.mode === "f") {
4653
+ entry.active = false;
4654
+ exitCode = 1;
4655
+ }
4656
+ };
4657
+ const acquire = async (entry, initial) => {
4658
+ let candidate;
4659
+ let stat;
4660
+ try {
4661
+ candidate = await session.open(entry.name);
4662
+ stat = await session.stat(candidate);
4663
+ if (selection.mode === "F" && compareCopyIdentity(stat, stat) === "unknown") throw new FsError("ENOTSUP", { path: entry.name, message: "name follow requires complete retained identity" });
4664
+ } catch (reason) {
4665
+ if (candidate) {
4666
+ try {
4667
+ await session.release(candidate);
4668
+ } catch {
4669
+ throw reason;
4670
+ }
4671
+ }
4672
+ await failure(entry, reason, initial);
4673
+ return void 0;
4674
+ }
4675
+ if (entry.reader && compareCopyIdentity(entry.identity, stat) === "same") {
4676
+ await session.release(candidate);
4677
+ return stat;
4678
+ }
4679
+ const replaced = entry.reader !== void 0;
4680
+ if (entry.reader) await session.release(entry.reader);
4681
+ entry.reader = candidate;
4682
+ entry.identity = stat;
4683
+ entry.offset = 0;
4684
+ if (!initial) {
4685
+ const message2 = replaced ? "has been replaced; following new file" : entry.error === "ENOENT" ? "has appeared; following new file" : "has become accessible";
4686
+ await diagnostic(context, new Error(`'${entry.name}' ${message2}`));
4687
+ }
4688
+ entry.error = void 0;
4689
+ return stat;
4690
+ };
4691
+ try {
4692
+ for (const entry of entries) {
4693
+ if (entry.name === "-") {
4694
+ await header(entry);
4695
+ await select(context, session.input(selection.idleMs));
4696
+ continue;
4697
+ }
4698
+ const stat = await acquire(entry, true);
4699
+ if (!stat) {
4700
+ if (entry.error === "EISDIR" && selection.positive && !selection.bytes) break;
4701
+ continue;
4702
+ }
4703
+ await header(entry);
4704
+ if (!selection.positive && selection.count === 0) entry.offset = stat.size;
4705
+ else {
4706
+ if (selection.bytes && !selection.positive) entry.offset = Math.max(0, stat.size - selection.count);
4707
+ const source = (async function* () {
4708
+ while (entry.offset < stat.size) {
4709
+ let chunk;
4710
+ try {
4711
+ chunk = await session.read(entry.reader, entry.offset, stat.size);
4712
+ } catch (reason) {
4713
+ throw new FollowReadError(reason);
4714
+ }
4715
+ if (!chunk.byteLength) break;
4716
+ yield chunk;
4717
+ entry.offset += chunk.byteLength;
4718
+ await yieldTurn(caller.signal);
4719
+ }
4720
+ })();
4721
+ try {
4722
+ await select(context, source);
4723
+ } catch (reason) {
4724
+ if (!(reason instanceof FollowReadError)) throw reason;
4725
+ await failure(entry, reason.reason, true);
4726
+ }
4727
+ if (selection.bytes && selection.positive) entry.offset = Math.max(entry.offset, selection.count - 1);
4728
+ }
4729
+ }
4730
+ acknowledged = scheduler.now();
4731
+ if (selection.idleMs !== 0 && !(selection.positive && !selection.bytes && entries.some((entry) => entry.error === "EISDIR"))) {
4732
+ while (entries.some((entry) => entry.active)) {
4733
+ session.assertOpen();
4734
+ if (selection.idleMs !== void 0 && scheduler.now() - acknowledged >= selection.idleMs) break;
4735
+ const round = [];
4736
+ for (const entry of entries) {
4737
+ if (!entry.active) continue;
4738
+ let stat;
4739
+ if (selection.mode === "F") stat = await acquire(entry, false);
4740
+ else {
4741
+ try {
4742
+ stat = await session.stat(entry.reader);
4743
+ } catch (reason) {
4744
+ await failure(entry, reason, false);
4745
+ }
4746
+ }
4747
+ if (!stat) continue;
4748
+ if (stat.size < entry.offset) {
4749
+ await diagnostic(context, new Error(`${entry.name}: file truncated`));
4750
+ entry.offset = 0;
4751
+ }
4752
+ if (stat.size > entry.offset) round.push({ entry, end: stat.size });
4753
+ }
4754
+ let progressed = false;
4755
+ while (round.some((item) => item.entry.reader && item.entry.offset < item.end)) {
4756
+ for (const item of round) {
4757
+ const { entry } = item;
4758
+ if (!entry.reader || entry.offset >= item.end) continue;
4759
+ let chunk;
4760
+ try {
4761
+ chunk = await session.read(entry.reader, entry.offset, item.end);
4762
+ } catch (reason) {
4763
+ await failure(entry, reason, false);
4764
+ item.end = entry.offset;
4765
+ continue;
4766
+ }
4767
+ if (!chunk.byteLength) {
4768
+ item.end = entry.offset;
4769
+ continue;
4770
+ }
4771
+ await header(entry);
4772
+ await output(context, chunk);
4773
+ entry.offset += chunk.byteLength;
4774
+ acknowledged = scheduler.now();
4775
+ progressed = true;
4776
+ await yieldTurn(caller.signal);
4777
+ }
4778
+ }
4779
+ await yieldTurn(caller.signal);
4780
+ if (!progressed && entries.some((entry) => entry.active)) {
4781
+ const remaining = selection.idleMs === void 0 ? 100 : Math.min(100, selection.idleMs - (scheduler.now() - acknowledged));
4782
+ if (remaining <= 0) break;
4783
+ await session.wait(void 0, remaining);
4784
+ }
4785
+ }
4786
+ }
4787
+ } catch (reason) {
4788
+ primary = { reason };
4789
+ } finally {
4790
+ try {
4791
+ await session.operation.close();
4792
+ } catch (reason) {
4793
+ primary ??= { reason };
4794
+ }
4795
+ }
4796
+ caller.signal.throwIfAborted();
4797
+ if (primary) {
4798
+ if (session.operation.signal.aborted && session.operation.signal.reason instanceof FsError && session.operation.signal.reason.code === "EPIPE") return { exitCode: 141 };
4799
+ throw primary.reason;
4800
+ }
4801
+ return { exitCode };
4802
+ }
4803
+
4376
4804
  // packages/safe-bash/src/commands/streams.ts
4377
4805
  var inspectedInputRequirements = inputRequirements.map((mode) => mode.id === "file" ? { ...mode, capabilities: ["stat", "access"] } : mode);
4378
4806
  var countRequirements = [
@@ -4500,10 +4928,11 @@ function wcUtf8(consume) {
4500
4928
  function wcSpace(point, posix) {
4501
4929
  return point === 32 || point >= 9 && point <= 13 || point === 160 || point === 5760 || point >= 8192 && point <= 8202 || point === 8232 || point === 8233 || point === 8239 || point === 8287 || point === 12288 || !posix && point === 8288;
4502
4930
  }
4503
- function headTail(name2) {
4931
+ function headTail(name2, maxTailFollowHandles = 64) {
4504
4932
  return define(name2, async (context) => {
4505
4933
  const args = context.args[0] && /^-[0-9]+$/u.test(context.args[0]) ? ["-n", context.args[0].slice(1), ...context.args.slice(1)] : context.args;
4506
- const parsed = options(args, "n:c:qv", { lines: "n", bytes: "c", quiet: "q", silent: "q", verbose: "v" });
4934
+ const follow = name2 === "tail" ? parseTailFollow(args) : void 0;
4935
+ const parsed = options(follow?.args ?? args, "n:c:qv", { lines: "n", bytes: "c", quiet: "q", silent: "q", verbose: "v" });
4507
4936
  if (parsed.flags.has("n") && parsed.flags.has("c")) throw new UsageError("cannot combine line and byte counts");
4508
4937
  const bytes2 = parsed.flags.has("c");
4509
4938
  const amount = value(parsed, bytes2 ? "c" : "n") ?? "10";
@@ -4511,6 +4940,15 @@ function headTail(name2) {
4511
4940
  const negative = amount.startsWith("-");
4512
4941
  const count2 = integer(amount.replace(/^[+-]/u, ""));
4513
4942
  const names = parsed.operands.length ? parsed.operands : ["-"];
4943
+ if (follow?.mode) return followTail(context, {
4944
+ names,
4945
+ mode: follow.mode,
4946
+ idleMs: follow.idleMs,
4947
+ count: count2,
4948
+ bytes: bytes2,
4949
+ positive,
4950
+ headers: parsed.flags.has("v") || names.length > 1 && !parsed.flags.has("q")
4951
+ }, maxTailFollowHandles, (target, source) => positive ? prefix(target, source, Math.max(0, count2 - 1), bytes2, true) : suffix(target, source, count2, bytes2, false));
4514
4952
  await assertInputRequirements(context, names);
4515
4953
  assertCommandRequirements(context, inspectedInputRequirements, [names.some((name3) => name3 !== "-") ? "file" : "stdin"]);
4516
4954
  let exitCode = 0;
@@ -4590,10 +5028,13 @@ function characterSet(specification) {
4590
5028
  }
4591
5029
  return result;
4592
5030
  }
4593
- function streamCommands(maxTeeTargets = 64) {
5031
+ function streamCommands(maxTeeTargets = 64, maxTailFollowHandles = 64) {
4594
5032
  if (!Number.isSafeInteger(maxTeeTargets) || maxTeeTargets < 0) {
4595
5033
  throw new RangeError("maxTeeTargets must be a nonnegative safe integer");
4596
5034
  }
5035
+ if (!Number.isSafeInteger(maxTailFollowHandles) || maxTailFollowHandles < 0) {
5036
+ throw new RangeError("maxTailFollowHandles must be a nonnegative safe integer");
5037
+ }
4597
5038
  return [
4598
5039
  define("cat", async (context) => {
4599
5040
  const parsed = options(context.args, "nbsvETAute", { number: "n", "number-nonblank": "b", "squeeze-blank": "s", "show-ends": "E", "show-tabs": "T", "show-nonprinting": "v", "show-all": "A" });
@@ -4670,7 +5111,7 @@ function streamCommands(maxTeeTargets = 64) {
4670
5111
  }
4671
5112
  }),
4672
5113
  headTail("head"),
4673
- headTail("tail"),
5114
+ headTail("tail", maxTailFollowHandles),
4674
5115
  define("wc", async (context) => {
4675
5116
  const parsed = options(context.args, "lwcm", { lines: "l", words: "w", bytes: "c", chars: "m" });
4676
5117
  if (!parsed.flags.size) for (const flag of ["l", "w", "c"]) parsed.flags.add(flag);
@@ -12101,7 +12542,9 @@ var Runtime = class _Runtime {
12101
12542
  this.cancellationDepth = cancellationDepth;
12102
12543
  this.cancellationMaxDepth = cancellationMaxDepth;
12103
12544
  this.outcomeFrame = outcomeFrame;
12104
- registerYieldCheckpoint(this.signal, () => this.budget.cpuCheckpoint());
12545
+ const checkpoint = () => budget.cpuCheckpoint();
12546
+ registerYieldCheckpoint(signal, checkpoint);
12547
+ registerYieldCheckpoint(commandSignal, checkpoint);
12105
12548
  }
12106
12549
  fs;
12107
12550
  commands;
@@ -16551,7 +16994,6 @@ var InputCursor = class {
16551
16994
  #iterator;
16552
16995
  remainder;
16553
16996
  #read;
16554
- #readSettled = false;
16555
16997
  #readFailed = false;
16556
16998
  #turn = Promise.resolve();
16557
16999
  #returned;
@@ -16585,17 +17027,11 @@ var InputCursor = class {
16585
17027
  }
16586
17028
  if (this.#ended || this.#closed) return { value: void 0, done: true };
16587
17029
  if (!this.#read) {
16588
- this.#readSettled = false;
16589
17030
  this.#read = Promise.resolve().then(() => this.#closed ? { value: void 0, done: true } : this.#iterator.next()).then((result) => {
16590
17031
  if (result.done) return { value: void 0, done: true };
16591
17032
  if (!(result.value instanceof Uint8Array)) throw new TypeError("Shell stdin must yield Uint8Array");
16592
17033
  return { value: new Uint8Array(result.value), done: false };
16593
17034
  });
16594
- void this.#read.then(() => {
16595
- this.#readSettled = true;
16596
- }, () => {
16597
- this.#readSettled = true;
16598
- });
16599
17035
  }
16600
17036
  try {
16601
17037
  const result = await interruptible(this.#read, signal);
@@ -16619,15 +17055,12 @@ var InputCursor = class {
16619
17055
  }
16620
17056
  this.#closed = true;
16621
17057
  this.remainder = void 0;
16622
- const pendingRead = this.#read !== void 0 && !this.#readSettled;
16623
17058
  this.#returned ??= Promise.resolve().then(() => this.#iterator.return?.()).then(() => void 0);
16624
17059
  void this.#returned.catch(() => void 0);
16625
- if (!pendingRead) {
16626
- try {
16627
- await interruptible(this.#returned, signal);
16628
- } catch (error) {
16629
- if (!this.#readFailed) throw error;
16630
- }
17060
+ try {
17061
+ await interruptible(this.#returned, signal);
17062
+ } catch (error) {
17063
+ if (!this.#readFailed) throw error;
16631
17064
  }
16632
17065
  signal.throwIfAborted();
16633
17066
  }
@@ -20884,7 +21317,7 @@ function createBrowserCommands(options3 = {}) {
20884
21317
  ...basicCommands(),
20885
21318
  ...filesystemCommands(options3.maxDirectoryEntries),
20886
21319
  ...predicateCommands(),
20887
- ...streamCommands(options3.maxTeeTargets),
21320
+ ...streamCommands(options3.maxTeeTargets, options3.maxTailFollowHandles),
20888
21321
  ...textCommands()
20889
21322
  ];
20890
21323
  if (options3.execution !== void 0) commands.push(...executionCommands(directExecutor(async (context) => {