@demicodes/shell 0.3.3 → 0.6.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 HostProcess, c as HostSpawnHandle, i as HostFileSystem, l as HostSpawnParams, n as HostDirent, o as HostProcessOutputChunk, r as HostFileStat, s as HostSpawnExit, t as Host, u as HostStore } from "./host-DIg4RxcL.mjs";
2
2
  import { HostBackedFileSystem, VirtualFileSystemNode, VirtualFileSystemProvider, virtualDirectory, virtualFile } 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-MeB35Df6.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-DB9hjZ2G.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";
@@ -13,7 +13,8 @@ interface ExecAccumulator {
13
13
  }
14
14
  interface ShellSession {
15
15
  id: string;
16
- commandScopeId: string;
16
+ agentSessionId: string | null;
17
+ commandStorageId: string;
17
18
  state: InterpreterState;
18
19
  fs: HostBackedFileSystem;
19
20
  interpreter: Interpreter;
@@ -90,14 +91,6 @@ interface BashEnvironmentOptions {
90
91
  shellIdFactory?: () => string;
91
92
  commandIdFactory?: () => string;
92
93
  initialEnv?: Record<string, string>;
93
- /**
94
- * Per-exec env injection for agent-owned shells. Evaluated on every exec with
95
- * the owning agent session id; returned vars are set and exported, so both
96
- * registered commands (ctx.env) and spawned external processes observe them.
97
- * Lets a product harness expose session-scoped context (identity, routing)
98
- * that changes between execs, which static initialEnv cannot express.
99
- */
100
- execEnv?: (agentSessionId: string) => Record<string, string>;
101
94
  maxOutputBytes?: number;
102
95
  }
103
96
  interface ShellExecInput {
@@ -235,7 +228,6 @@ declare class BashEnvironment {
235
228
  private readonly shellIdFactory;
236
229
  private readonly commandIdFactory;
237
230
  private readonly initialEnv;
238
- private readonly execEnv?;
239
231
  private readonly defaultOutputLimitBytes;
240
232
  private readonly shells;
241
233
  private readonly defaultShellByAgentSessionId;
@@ -243,6 +235,7 @@ declare class BashEnvironment {
243
235
  private readonly artifacts;
244
236
  constructor(options: BashEnvironmentOptions);
245
237
  getShell(shellId: string): ShellSession | null;
238
+ hasCommand(commandId: string): boolean;
246
239
  registerCommand(command: Command): void;
247
240
  registeredCommands(): Command[];
248
241
  exec(input: ShellExecInput): Promise<ShellCommandStatus>;
package/dist/index.mjs CHANGED
@@ -249,7 +249,8 @@ async function runRegisteredCommand(root, ctx) {
249
249
  env: ctx.env,
250
250
  cwd: ctx.cwd,
251
251
  io: parsed.json ? capture : ctx.io,
252
- storage: ctx.storage
252
+ storage: ctx.storage,
253
+ host: ctx.host
253
254
  });
254
255
  if (parsed.json && result.exitCode === 0) {
255
256
  const raw = capture.stdoutText();
@@ -620,47 +621,47 @@ function appendVisibleChunk(foreground, targetFd, text, byteLength) {
620
621
  //#endregion
621
622
  //#region src/command-artifact-store.ts
622
623
  /**
623
- * Owns persistence of shell command artifacts: a per-scope storage cache plus the
624
+ * Owns persistence of shell command artifacts: a per-storage-id cache plus the
624
625
  * set of released (tombstoned) commands. `BashEnvironment` delegates the storage
625
626
  * and release-tracking side of the `/@` virtual filesystem here, keeping only the
626
627
  * in-memory record lookups that need live command state.
627
628
  */
628
629
  var CommandArtifactStore = class {
629
630
  store;
630
- storageByScope = /* @__PURE__ */ new Map();
631
+ storageById = /* @__PURE__ */ new Map();
631
632
  released = /* @__PURE__ */ new Set();
632
633
  constructor(store) {
633
634
  this.store = store;
634
635
  }
635
- /** The artifact storage for a command scope, created (and cached) on first use. */
636
- storageFor(scopeId) {
637
- const existing = this.storageByScope.get(scopeId);
636
+ /** The artifact storage for one agent session or anonymous shell. */
637
+ storageFor(commandStorageId) {
638
+ const existing = this.storageById.get(commandStorageId);
638
639
  if (existing) return existing;
639
- const storage = new AgentSessionCommandStorage(this.store, scopeId);
640
- this.storageByScope.set(scopeId, storage);
640
+ const storage = new AgentSessionCommandStorage(this.store, commandStorageId);
641
+ this.storageById.set(commandStorageId, storage);
641
642
  return storage;
642
643
  }
643
644
  /** Whether a command's artifact has been released (tombstoned). */
644
- isReleased(scopeId, commandId) {
645
- return this.released.has(this.key(scopeId, commandId));
645
+ isReleased(commandStorageId, commandId) {
646
+ return this.released.has(this.key(commandStorageId, commandId));
646
647
  }
647
648
  /** Persists an artifact unless its command has already been released. */
648
- persist(scopeId, commandId, artifact) {
649
- if (this.isReleased(scopeId, commandId)) return;
650
- this.storageFor(scopeId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
649
+ persist(commandStorageId, commandId, artifact) {
650
+ if (this.isReleased(commandStorageId, commandId)) return;
651
+ this.storageFor(commandStorageId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
651
652
  }
652
653
  /** Tombstones a command and removes its persisted artifact. */
653
- async release(scopeId, commandId) {
654
- this.released.add(this.key(scopeId, commandId));
655
- await this.storageFor(scopeId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
654
+ async release(commandStorageId, commandId) {
655
+ this.released.add(this.key(commandStorageId, commandId));
656
+ await this.storageFor(commandStorageId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
656
657
  }
657
- key(scopeId, commandId) {
658
- return `${scopeId}\0${commandId}`;
658
+ key(commandStorageId, commandId) {
659
+ return `${commandStorageId}\0${commandId}`;
659
660
  }
660
661
  };
661
662
  //#endregion
662
663
  //#region src/registered-command-adapter.ts
663
- function commandToForkCommand(session, command, storage) {
664
+ function commandToForkCommand(session, command, storage, host) {
664
665
  return {
665
666
  name: command.name,
666
667
  consumesStdin: treeConsumesStdin(command),
@@ -675,7 +676,8 @@ function commandToForkCommand(session, command, storage) {
675
676
  env: mapToRecord(ctx.env),
676
677
  cwd: ctx.cwd,
677
678
  io,
678
- storage
679
+ storage,
680
+ host
679
681
  });
680
682
  session.accumulator.audit.push({
681
683
  kind: "registered-command",
@@ -790,7 +792,6 @@ var BashEnvironment = class {
790
792
  shellIdFactory;
791
793
  commandIdFactory;
792
794
  initialEnv;
793
- execEnv;
794
795
  defaultOutputLimitBytes;
795
796
  shells = /* @__PURE__ */ new Map();
796
797
  defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
@@ -803,12 +804,14 @@ var BashEnvironment = class {
803
804
  this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
804
805
  this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
805
806
  this.initialEnv = options.initialEnv ?? {};
806
- this.execEnv = options.execEnv;
807
807
  this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
808
808
  }
809
809
  getShell(shellId) {
810
810
  return this.shells.get(shellId) ?? null;
811
811
  }
812
+ hasCommand(commandId) {
813
+ return this.commandsById.has(commandId);
814
+ }
812
815
  registerCommand(command) {
813
816
  if (this.commands.get(command.name)) return;
814
817
  this.commands.register(command);
@@ -825,17 +828,6 @@ var BashEnvironment = class {
825
828
  }
826
829
  const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
827
830
  if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
828
- if (input.agentSessionId) {
829
- const extraEnv = this.execEnv?.(input.agentSessionId);
830
- if (extraEnv) {
831
- const exported = session.state.exportedVars ?? /* @__PURE__ */ new Set();
832
- session.state.exportedVars = exported;
833
- for (const [key, value] of Object.entries(extraEnv)) {
834
- session.state.env.set(key, value);
835
- exported.add(key);
836
- }
837
- }
838
- }
839
831
  if (session.pendingExec || session.foreground) {
840
832
  const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
841
833
  throw new Error(`Shell session "${session.id}" is already running command "${commandId}"`);
@@ -873,7 +865,7 @@ var BashEnvironment = class {
873
865
  const record = this.commandsById.get(commandId);
874
866
  if (!record || record.status === "running") return false;
875
867
  this.commandsById.delete(commandId);
876
- await this.artifacts.release(record.commandScopeId, commandId);
868
+ await this.artifacts.release(record.commandStorageId, commandId);
877
869
  return true;
878
870
  }
879
871
  async disposeShell(shellId) {
@@ -941,23 +933,20 @@ var BashEnvironment = class {
941
933
  }
942
934
  createShell(agentSessionId, initialCwd) {
943
935
  const id = this.shellIdFactory();
944
- const commandScopeId = agentSessionId ?? id;
936
+ const commandStorageId = agentSessionId ?? id;
945
937
  const cwd = initialCwd ?? this.host.defaultCwd;
946
- const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandScopeId, path) });
938
+ const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandStorageId, path) });
947
939
  const env = /* @__PURE__ */ new Map();
948
940
  for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
949
941
  env.set("PWD", cwd);
950
- env.set("DEMI_SESSION_ID", commandScopeId);
942
+ if (agentSessionId) env.set("DEMI_SESSION_ID", agentSessionId);
951
943
  env.set("DEMI_SHELL_ID", id);
952
944
  if (!env.has("IFS")) env.set("IFS", " \n");
953
945
  if (!env.has("PS1")) env.set("PS1", "");
954
946
  if (!env.has("PS2")) env.set("PS2", "> ");
955
947
  if (!env.has("SHLVL")) env.set("SHLVL", "1");
956
- const exportedVars = /* @__PURE__ */ new Set([
957
- "PWD",
958
- "DEMI_SESSION_ID",
959
- "DEMI_SHELL_ID"
960
- ]);
948
+ const exportedVars = /* @__PURE__ */ new Set(["PWD", "DEMI_SHELL_ID"]);
949
+ if (agentSessionId) exportedVars.add("DEMI_SESSION_ID");
961
950
  for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
962
951
  for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
963
952
  const state = {
@@ -1018,7 +1007,8 @@ var BashEnvironment = class {
1018
1007
  const forkCommands = /* @__PURE__ */ new Map();
1019
1008
  const session = {
1020
1009
  id,
1021
- commandScopeId,
1010
+ agentSessionId: agentSessionId ?? null,
1011
+ commandStorageId,
1022
1012
  state,
1023
1013
  fs,
1024
1014
  interpreter: void 0,
@@ -1035,8 +1025,8 @@ var BashEnvironment = class {
1035
1025
  exited: false
1036
1026
  };
1037
1027
  for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
1038
- const storage = new AgentSessionCommandStorage(this.host.store, commandScopeId);
1039
- for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage));
1028
+ const storage = new AgentSessionCommandStorage(this.host.store, commandStorageId);
1029
+ for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
1040
1030
  session.abortController = new AbortController();
1041
1031
  session.interpreter = new Interpreter({
1042
1032
  fs,
@@ -1109,7 +1099,7 @@ var BashEnvironment = class {
1109
1099
  const record = {
1110
1100
  id,
1111
1101
  shellId: session.id,
1112
- commandScopeId: session.commandScopeId,
1102
+ commandStorageId: session.commandStorageId,
1113
1103
  script,
1114
1104
  startedAt: now,
1115
1105
  lastOutputAt: now,
@@ -1550,12 +1540,12 @@ var BashEnvironment = class {
1550
1540
  status: "running"
1551
1541
  };
1552
1542
  }
1553
- async lookupVirtualArtifact(scopeId, path) {
1543
+ async lookupVirtualArtifact(commandStorageId, path) {
1554
1544
  const parts = path.split("/").filter(Boolean);
1555
1545
  if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
1556
- if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(scopeId));
1546
+ if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(commandStorageId));
1557
1547
  if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
1558
- const artifact = await this.commandArtifact(scopeId, parts[2]);
1548
+ const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1559
1549
  if (!artifact) return null;
1560
1550
  const entries = [
1561
1551
  "meta.json",
@@ -1566,7 +1556,7 @@ var BashEnvironment = class {
1566
1556
  return virtualDirectory(entries);
1567
1557
  }
1568
1558
  if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
1569
- const artifact = await this.commandArtifact(scopeId, parts[2]);
1559
+ const artifact = await this.commandArtifact(commandStorageId, parts[2]);
1570
1560
  if (!artifact) return null;
1571
1561
  const fileName = parts[3];
1572
1562
  if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
@@ -1575,31 +1565,31 @@ var BashEnvironment = class {
1575
1565
  if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
1576
1566
  return null;
1577
1567
  }
1578
- async commandArtifactIds(scopeId) {
1568
+ async commandArtifactIds(commandStorageId) {
1579
1569
  const ids = /* @__PURE__ */ new Set();
1580
- for (const record of this.commandsById.values()) if (record.commandScopeId === scopeId && !this.artifacts.isReleased(scopeId, record.id)) ids.add(record.id);
1581
- const keys = await this.artifacts.storageFor(scopeId).list("commands").catch(() => []);
1570
+ for (const record of this.commandsById.values()) if (record.commandStorageId === commandStorageId && !this.artifacts.isReleased(commandStorageId, record.id)) ids.add(record.id);
1571
+ const keys = await this.artifacts.storageFor(commandStorageId).list("commands").catch(() => []);
1582
1572
  for (const key of keys) {
1583
1573
  const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
1584
- if (match && !this.artifacts.isReleased(scopeId, match[1])) ids.add(match[1]);
1574
+ if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
1585
1575
  }
1586
1576
  return [...ids];
1587
1577
  }
1588
- async commandArtifact(scopeId, commandId) {
1589
- if (this.artifacts.isReleased(scopeId, commandId)) return null;
1578
+ async commandArtifact(commandStorageId, commandId) {
1579
+ if (this.artifacts.isReleased(commandStorageId, commandId)) return null;
1590
1580
  const record = this.commandsById.get(commandId);
1591
- if (record?.commandScopeId === scopeId) {
1581
+ if (record?.commandStorageId === commandStorageId) {
1592
1582
  this.syncRunningRecord(record);
1593
1583
  return commandArtifactFromRecord(record);
1594
1584
  }
1595
- const value = await this.artifacts.storageFor(scopeId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
1585
+ const value = await this.artifacts.storageFor(commandStorageId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
1596
1586
  return isCommandArtifact(value) ? value : null;
1597
1587
  }
1598
1588
  persistCommandArtifact(record) {
1599
1589
  const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
1600
1590
  if (record.persistedFingerprint === fingerprint) return;
1601
1591
  record.persistedFingerprint = fingerprint;
1602
- this.artifacts.persist(record.commandScopeId, record.id, commandArtifactFromRecord(record));
1592
+ this.artifacts.persist(record.commandStorageId, record.id, commandArtifactFromRecord(record));
1603
1593
  }
1604
1594
  syncRunningRecord(record) {
1605
1595
  const foreground = this.shells.get(record.shellId)?.foreground;
@@ -1,4 +1,4 @@
1
- import { u as HostStore } from "./host-DIg4RxcL.mjs";
1
+ import { t as Host, u as HostStore } from "./host-DIg4RxcL.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/command.d.ts
4
4
  type CommandInputSpec = Record<string, z.ZodType>;
@@ -48,6 +48,8 @@ interface CommandRunContext {
48
48
  cwd: string;
49
49
  io: CommandIO;
50
50
  storage: CommandStorage;
51
+ /** Host of the BashEnvironment executing this command. */
52
+ host: Host;
51
53
  }
52
54
  interface CommandRunResult {
53
55
  exitCode: number;
@@ -77,6 +79,7 @@ interface CommandExecutionContext {
77
79
  cwd: string;
78
80
  io: CommandIO;
79
81
  storage: CommandStorage;
82
+ host: Host;
80
83
  }
81
84
  declare class CommandRegistry {
82
85
  private readonly commands;
@@ -1,2 +1,2 @@
1
- import { t as AgentSessionCommandStorage } from "./storage-MeB35Df6.mjs";
1
+ import { t as AgentSessionCommandStorage } from "./storage-DB9hjZ2G.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.3.3",
4
+ "version": "0.6.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -19,7 +19,7 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@demicodes/utils": "^0.4.0",
22
+ "@demicodes/utils": "^0.6.0",
23
23
  "@demicodes/just-bash": "^3.0.1-demi.5",
24
24
  "zod": "^4.0.0"
25
25
  },