@demicodes/shell 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as HostFileSystem, c as HostProcessOutputChunk, d as HostSpawnParams, f as HostStore, i as HostFileStat, l as HostSpawnExit, m as createLogicalHostCwd, n as HostCwd, o as HostIdentity, p as SpawnErrorKind, r as HostDirent, s as HostProcess, t as Host, u as HostSpawnHandle } from "./host-BupAFtK6.mjs";
2
2
  import { HostBackedFileSystem, HostBackedFileSystemOptions } from "./host-fs.mjs";
3
- import { _ as runRegisteredCommand, a as CommandIO, c as CommandRegistry, d as CommandStdin, f as CommandStorage, g as renderCommandHelp, h as parseCommandInput, i as CommandExecutionContext, l as CommandRunContext, m as emptyStdin, n as COMMAND_HELP_DEFAULTS, o as CommandInputSpec, p as ParsedCommandInput, r as Command, s as CommandOutputSpec, t as AgentSessionCommandStorage, u as CommandRunResult } from "./storage-CAc1N6MN.mjs";
3
+ import { _ as runRegisteredCommand, a as CommandIO, c as CommandRegistry, d as CommandStdin, f as CommandStorage, g as renderCommandHelp, h as parseCommandInput, i as CommandExecutionContext, l as CommandRunContext, m as emptyStdin, n as COMMAND_HELP_DEFAULTS, o as CommandInputSpec, p as ParsedCommandInput, r as Command, s as CommandOutputSpec, t as AgentSessionCommandStorage, u as CommandRunResult } from "./storage-BDRwHNOB.mjs";
4
4
  import { CommandName } from "@demicodes/just-bash/commands";
5
5
  import { Interpreter, InterpreterState } from "@demicodes/just-bash/interpreter";
6
6
  import { CommandRegistry as CommandRegistry$1, ExecResult } from "@demicodes/just-bash/types";
package/dist/index.mjs CHANGED
@@ -128,6 +128,7 @@ function emptyStdin() {
128
128
  bytes: /* @__PURE__ */ new Uint8Array(0)
129
129
  };
130
130
  }
131
+ async function* emptyStdinStream() {}
131
132
  const EXECUTION_ONLY_FIELDS = [
132
133
  "successOutput",
133
134
  "failureOutput",
@@ -221,7 +222,7 @@ function parseArgs(command, path, argv, startIndex, stdin) {
221
222
  setParsedValue(values, field, token);
222
223
  positionalIndex += 1;
223
224
  }
224
- if (command.stdinField) values[command.stdinField] = stdin.text;
225
+ if (command.stdinField && values[command.stdinField] === void 0) values[command.stdinField] = stdin.text;
225
226
  return {
226
227
  path: [...path],
227
228
  help: false,
@@ -262,7 +263,9 @@ async function runRegisteredCommand(root, ctx) {
262
263
  cwd: ctx.cwd,
263
264
  io: parsed.json ? capture : ctx.io,
264
265
  storage: ctx.storage,
265
- host: ctx.host
266
+ host: ctx.host,
267
+ signal: ctx.signal ?? new AbortController().signal,
268
+ stdinStream: ctx.stdinStream ?? emptyStdinStream()
266
269
  });
267
270
  if (parsed.json && result.exitCode === 0) {
268
271
  const raw = capture.stdoutText();
@@ -696,24 +699,34 @@ var CommandArtifactStore = class {
696
699
  };
697
700
  //#endregion
698
701
  //#region src/registered-command-adapter.ts
699
- function commandToForkCommand(session, command, storage, host) {
702
+ /**
703
+ * Runs a registered command as a shell foreground job: it exposes the same
704
+ * control surface as a host process (abort signal, live stdout/stderr view,
705
+ * stdin as a post-start chunk stream) through a virtual process handle, so
706
+ * `shell_status` / `shell_write` / `shell_abort` apply uniformly.
707
+ */
708
+ function commandToForkCommand(session, command, storage, host, captureLimitBytes) {
700
709
  return {
701
710
  name: command.name,
702
711
  consumesStdin: treeConsumesStdin(command),
703
712
  execute: async (args, ctx) => {
704
713
  const stdin = decodeForkStdin(ctx.stdin);
705
- const io = createForwardingIO();
706
714
  const argv = [command.name, ...args];
715
+ const job = new VirtualForegroundJob(session, command.name, args, ctx.cwd, captureLimitBytes);
716
+ job.install();
707
717
  try {
708
- const result = await runRegisteredCommand(command, {
718
+ const result = await Promise.race([runRegisteredCommand(command, {
709
719
  argv,
710
720
  stdin,
711
721
  env: mapToRecord(ctx.env),
712
722
  cwd: ctx.cwd,
713
- io,
723
+ io: job.io,
714
724
  storage,
715
- host
716
- });
725
+ host,
726
+ signal: job.signal,
727
+ stdinStream: job.stdinChunks()
728
+ }), job.killedResult()]);
729
+ if (job.foreground.captureOverflowed) return job.overflowResult();
717
730
  session.accumulator.audit.push({
718
731
  kind: "registered-command",
719
732
  name: command.name,
@@ -727,12 +740,13 @@ function commandToForkCommand(session, command, storage, host) {
727
740
  metadata: result.metadata
728
741
  });
729
742
  return {
730
- stdout: io.stdoutLatin1(),
743
+ stdout: job.stdoutLatin1(),
731
744
  stdoutKind: "bytes",
732
- stderr: io.stderrText(),
745
+ stderr: job.stderrText(),
733
746
  exitCode: result.exitCode
734
747
  };
735
748
  } catch (error) {
749
+ if (job.foreground.captureOverflowed) return job.overflowResult();
736
750
  const message = error instanceof Error ? error.message : String(error);
737
751
  session.accumulator.audit.push({
738
752
  kind: "registered-command",
@@ -741,35 +755,171 @@ function commandToForkCommand(session, command, storage, host) {
741
755
  exitCode: 1
742
756
  });
743
757
  return {
744
- stdout: io.stdoutLatin1(),
758
+ stdout: job.stdoutLatin1(),
745
759
  stdoutKind: "bytes",
746
- stderr: `${io.stderrText()}${command.name}: ${message}\n`,
760
+ stderr: `${job.stderrText()}${command.name}: ${message}\n`,
747
761
  exitCode: 1
748
762
  };
763
+ } finally {
764
+ job.release();
749
765
  }
750
766
  }
751
767
  };
752
768
  }
753
- /** Collects command output as raw bytes; stdout stays byte-clean for the pipe. */
754
- var ForwardingIO = class {
755
- stdoutChunks = [];
756
- stderrChunks = [];
757
- async stdout(data) {
758
- this.stdoutChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
769
+ /**
770
+ * In-process stand-in for an OS process. `kill` maps to the abort signal
771
+ * (there is no OS process to signal); a SIGKILL additionally abandons the
772
+ * run so the shell never waits on an in-process function that ignores its
773
+ * signal.
774
+ */
775
+ var VirtualForegroundJob = class {
776
+ session;
777
+ captureLimitBytes;
778
+ foreground;
779
+ io;
780
+ abortController = new AbortController();
781
+ stdinQueue = new StdinChunkQueue();
782
+ settleExit;
783
+ killed;
784
+ killedPromise;
785
+ exitPromise;
786
+ installed = false;
787
+ constructor(session, command, args, cwd, captureLimitBytes) {
788
+ this.session = session;
789
+ this.captureLimitBytes = captureLimitBytes;
790
+ this.exitPromise = new Promise((resolve) => {
791
+ this.settleExit = resolve;
792
+ });
793
+ this.killedPromise = new Promise((_resolve, reject) => {
794
+ this.killed = reject;
795
+ });
796
+ this.killedPromise.catch(() => {});
797
+ const handle = {
798
+ stdout: emptyByteStream(),
799
+ stderr: emptyByteStream(),
800
+ writeStdin: async (data) => this.stdinQueue.push(data),
801
+ closeStdin: async () => this.stdinQueue.close(),
802
+ kill: async (signal) => {
803
+ this.abortController.abort();
804
+ this.stdinQueue.close();
805
+ if (signal === "SIGKILL") {
806
+ this.settleExit({
807
+ exitCode: 137,
808
+ signal: "SIGKILL"
809
+ });
810
+ this.killed(/* @__PURE__ */ new Error(`${command}: killed`));
811
+ }
812
+ },
813
+ wait: () => this.exitPromise
814
+ };
815
+ const startedAt = Date.now();
816
+ this.foreground = {
817
+ commandId: session.activeCommandId ?? "",
818
+ command,
819
+ args,
820
+ cwd,
821
+ handle,
822
+ startedAt,
823
+ lastOutputAt: startedAt,
824
+ rawStdoutBuffer: "",
825
+ rawStdoutBytes: [],
826
+ rawStderrBuffer: "",
827
+ stdoutBuffer: "",
828
+ stderrBuffer: "",
829
+ outputChunks: [],
830
+ outputBytes: 0,
831
+ capturedBytes: 0,
832
+ captureOverflowed: false,
833
+ audit: [],
834
+ stdoutPump: Promise.resolve(),
835
+ stderrPump: Promise.resolve(),
836
+ exitPromise: this.exitPromise,
837
+ outputSinks: createOutputSinks(session.fs, cwd, void 0),
838
+ abortController: this.abortController
839
+ };
840
+ this.io = {
841
+ stdout: (data) => {
842
+ recordForegroundChunk(this.foreground, 1, toBytes(data), this.captureLimitBytes);
843
+ },
844
+ stderr: (data) => {
845
+ recordForegroundChunk(this.foreground, 2, toBytes(data), this.captureLimitBytes);
846
+ }
847
+ };
759
848
  }
760
- async stderr(data) {
761
- this.stderrChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
849
+ get signal() {
850
+ return this.abortController.signal;
851
+ }
852
+ stdinChunks() {
853
+ return this.stdinQueue.stream();
854
+ }
855
+ killedResult() {
856
+ return this.killedPromise;
857
+ }
858
+ /** Registers this job as the session foreground so shell control verbs route here. */
859
+ install() {
860
+ if (this.session.foreground || !this.foreground.commandId) return;
861
+ this.session.foreground = this.foreground;
862
+ this.installed = true;
863
+ notifyForegroundWaiters(this.session.foregroundWaiters, this.foreground);
864
+ }
865
+ release() {
866
+ this.stdinQueue.close();
867
+ this.settleExit({ exitCode: 0 });
868
+ if (this.installed && this.session.foreground === this.foreground) this.session.foreground = void 0;
762
869
  }
763
870
  stdoutLatin1() {
764
- return decodeLatin1(concatBytes(this.stdoutChunks));
871
+ return decodeLatin1(concatBytes(this.foreground.rawStdoutBytes));
765
872
  }
766
873
  stderrText() {
767
- return decodeUtf8(concatBytes(this.stderrChunks));
874
+ return this.foreground.rawStderrBuffer;
875
+ }
876
+ overflowResult() {
877
+ return {
878
+ stdout: "",
879
+ stdoutKind: "bytes",
880
+ stderr: `${this.foreground.command}: output exceeded the ${this.captureLimitBytes}-byte capture limit and the command was stopped; the shell buffers whole command outputs in memory — narrow the output at the source (filters, head, tighter paths)\n`,
881
+ exitCode: 137
882
+ };
883
+ }
884
+ };
885
+ /** Async chunk queue: each pushed chunk is delivered once, in order; close ends the stream. */
886
+ var StdinChunkQueue = class {
887
+ chunks = [];
888
+ waiter = null;
889
+ isClosed = false;
890
+ push(data) {
891
+ if (this.isClosed) return;
892
+ this.chunks.push(data);
893
+ this.wake();
894
+ }
895
+ close() {
896
+ if (this.isClosed) return;
897
+ this.isClosed = true;
898
+ this.wake();
899
+ }
900
+ async *stream() {
901
+ while (true) {
902
+ const chunk = this.chunks.shift();
903
+ if (chunk) {
904
+ yield chunk;
905
+ continue;
906
+ }
907
+ if (this.isClosed) return;
908
+ await new Promise((resolve) => {
909
+ this.waiter = resolve;
910
+ });
911
+ }
912
+ }
913
+ wake() {
914
+ const waiter = this.waiter;
915
+ this.waiter = null;
916
+ waiter?.();
768
917
  }
769
918
  };
770
- function createForwardingIO() {
771
- return new ForwardingIO();
919
+ function toBytes(data) {
920
+ return typeof data === "string" ? encodeUtf8(data) : data;
772
921
  }
922
+ async function* emptyByteStream() {}
773
923
  function treeConsumesStdin(command) {
774
924
  if (command.stdinField) return true;
775
925
  return command.subcommands?.some(treeConsumesStdin) ?? false;
@@ -1082,7 +1232,7 @@ var BashEnvironment = class {
1082
1232
  };
1083
1233
  for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
1084
1234
  const storage = new AgentSessionCommandStorage(this.host.store, commandStorageId);
1085
- for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
1235
+ for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host, this.captureLimitBytes));
1086
1236
  session.abortController = new AbortController();
1087
1237
  const limits = resolveLimits({
1088
1238
  maxOutputSize: this.captureLimitBytes,
@@ -47,6 +47,13 @@ interface CommandRunContext {
47
47
  storage: CommandStorage;
48
48
  /** Host of the BashEnvironment executing this command. */
49
49
  host: Host;
50
+ /** Aborted when the shell command is aborted (shell_abort, shell teardown). */
51
+ signal: AbortSignal;
52
+ /**
53
+ * Stdin written after the command started: each `shell_write` call arrives
54
+ * as one chunk. Ends when the command's shell job is released.
55
+ */
56
+ stdinStream: AsyncIterable<Uint8Array>;
50
57
  }
51
58
  interface CommandRunResult {
52
59
  exitCode: number;
@@ -77,6 +84,8 @@ interface CommandExecutionContext {
77
84
  io: CommandIO;
78
85
  storage: CommandStorage;
79
86
  host: Host;
87
+ signal?: AbortSignal;
88
+ stdinStream?: AsyncIterable<Uint8Array>;
80
89
  }
81
90
  declare class CommandRegistry {
82
91
  private readonly commands;
@@ -1,2 +1,2 @@
1
- import { t as AgentSessionCommandStorage } from "./storage-CAc1N6MN.mjs";
1
+ import { t as AgentSessionCommandStorage } from "./storage-BDRwHNOB.mjs";
2
2
  export { AgentSessionCommandStorage };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/shell",
3
3
  "description": "Sandboxable bash engine and Host contract for Demi.",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@demicodes/just-bash": "^3.1.0-demi.4",
23
- "@demicodes/utils": "^0.18.0",
23
+ "@demicodes/utils": "^0.19.0",
24
24
  "zod": "^4.0.0"
25
25
  },
26
26
  "license": "Apache-2.0",