@demicodes/shell 0.17.4 → 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/README.md +3 -1
- package/dist/{host-B-oKG7x5.d.mts → host-BupAFtK6.d.mts} +30 -1
- package/dist/host-fs.d.mts +1 -1
- package/dist/host-fs.mjs +8 -1
- package/dist/index.d.mts +11 -27
- package/dist/index.mjs +312 -67
- package/dist/{storage-DYvEwSc9.d.mts → storage-BDRwHNOB.d.mts} +10 -1
- package/dist/storage.d.mts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ same agent can target local, container, remote, or in-memory backends.
|
|
|
12
12
|
|
|
13
13
|
Subpaths: `@demicodes/shell/storage`, `@demicodes/shell/host-fs`.
|
|
14
14
|
See [docs/shell-yield-control-plan.md](../../docs/shell-yield-control-plan.md) for
|
|
15
|
-
the model-facing control surface and yield wakeups
|
|
15
|
+
the model-facing control surface and yield wakeups, and
|
|
16
|
+
[docs/bash-behavior.md](../../docs/bash-behavior.md) for Host-backed behavior
|
|
17
|
+
versus GNU bash.
|
|
16
18
|
|
|
17
19
|
Part of [Demi](../../README.md). Apache-2.0.
|
|
@@ -13,6 +13,12 @@ interface Host {
|
|
|
13
13
|
fs: HostFileSystem;
|
|
14
14
|
process: HostProcess;
|
|
15
15
|
store: HostStore;
|
|
16
|
+
identity: HostIdentity;
|
|
17
|
+
}
|
|
18
|
+
interface HostIdentity {
|
|
19
|
+
uid: number;
|
|
20
|
+
gid: number;
|
|
21
|
+
hostname: string;
|
|
16
22
|
}
|
|
17
23
|
interface HostFileSystem {
|
|
18
24
|
readFile(path: string, options?: {
|
|
@@ -80,6 +86,17 @@ interface HostFileSystem {
|
|
|
80
86
|
}
|
|
81
87
|
interface HostProcess {
|
|
82
88
|
spawn(params: HostSpawnParams): Promise<HostSpawnHandle>;
|
|
89
|
+
openCwd(path: string): Promise<HostCwd>;
|
|
90
|
+
}
|
|
91
|
+
type SpawnErrorKind = 'executable_not_found' | 'permission_denied' | 'cwd_unusable' | 'is_directory' | 'other';
|
|
92
|
+
interface HostCwd {
|
|
93
|
+
readonly path: string;
|
|
94
|
+
spawnPath(): string;
|
|
95
|
+
chdir(path: string): Promise<void>;
|
|
96
|
+
snapshot(): Promise<{
|
|
97
|
+
restore(): void;
|
|
98
|
+
}>;
|
|
99
|
+
close(): Promise<void>;
|
|
83
100
|
}
|
|
84
101
|
/**
|
|
85
102
|
* Keyed JSON state storage. Implementations must round-trip `Uint8Array` and
|
|
@@ -99,6 +116,13 @@ interface HostFileStat {
|
|
|
99
116
|
mode: number;
|
|
100
117
|
size: number;
|
|
101
118
|
mtime: Date;
|
|
119
|
+
uid?: number;
|
|
120
|
+
gid?: number;
|
|
121
|
+
ino?: number;
|
|
122
|
+
dev?: number;
|
|
123
|
+
nlink?: number;
|
|
124
|
+
isCharacterDevice?: boolean;
|
|
125
|
+
isFIFO?: boolean;
|
|
102
126
|
}
|
|
103
127
|
interface HostDirent {
|
|
104
128
|
name: string;
|
|
@@ -129,6 +153,11 @@ interface HostProcessOutputChunk {
|
|
|
129
153
|
interface HostSpawnExit {
|
|
130
154
|
exitCode: number | null;
|
|
131
155
|
signal?: string;
|
|
156
|
+
spawnError?: {
|
|
157
|
+
kind: SpawnErrorKind;
|
|
158
|
+
};
|
|
132
159
|
}
|
|
160
|
+
/** Path-string cwd for test doubles and Hosts that cannot hold a directory fd. */
|
|
161
|
+
declare function createLogicalHostCwd(initialPath: string): HostCwd;
|
|
133
162
|
//#endregion
|
|
134
|
-
export {
|
|
163
|
+
export { HostFileSystem as a, HostProcessOutputChunk as c, HostSpawnParams as d, HostStore as f, HostFileStat as i, HostSpawnExit as l, createLogicalHostCwd as m, HostCwd as n, HostIdentity as o, SpawnErrorKind as p, HostDirent as r, HostProcess as s, Host as t, HostSpawnHandle as u };
|
package/dist/host-fs.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as Host } from "./host-
|
|
1
|
+
import { t as Host } from "./host-BupAFtK6.mjs";
|
|
2
2
|
import { BufferEncoding, CpOptions, DirentEntry, FileContent, FsStat, IFileSystem, MkdirOptions, ReadFileOptions, RmOptions, WriteFileOptions } from "@demicodes/just-bash/fs/interface";
|
|
3
3
|
//#region src/host-fs.d.ts
|
|
4
4
|
interface HostBackedFileSystemOptions {
|
package/dist/host-fs.mjs
CHANGED
|
@@ -134,7 +134,14 @@ function toFsStat(value) {
|
|
|
134
134
|
isSymbolicLink: value.isSymbolicLink,
|
|
135
135
|
mode: value.mode,
|
|
136
136
|
size: value.size,
|
|
137
|
-
mtime: value.mtime
|
|
137
|
+
mtime: value.mtime,
|
|
138
|
+
uid: value.uid,
|
|
139
|
+
gid: value.gid,
|
|
140
|
+
ino: value.ino,
|
|
141
|
+
dev: value.dev,
|
|
142
|
+
nlink: value.nlink,
|
|
143
|
+
isCharacterDevice: value.isCharacterDevice,
|
|
144
|
+
isFIFO: value.isFIFO
|
|
138
145
|
};
|
|
139
146
|
}
|
|
140
147
|
function toDirentEntry(value) {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
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-
|
|
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";
|
|
@@ -19,6 +19,7 @@ interface ShellSession {
|
|
|
19
19
|
fs: HostBackedFileSystem;
|
|
20
20
|
interpreter: Interpreter;
|
|
21
21
|
forkCommands: CommandRegistry$1;
|
|
22
|
+
cwdHandle: HostCwd;
|
|
22
23
|
accumulator: ExecAccumulator;
|
|
23
24
|
foreground?: ForegroundProcess;
|
|
24
25
|
activeCommandId?: string;
|
|
@@ -43,10 +44,7 @@ interface BackgroundJob {
|
|
|
43
44
|
droppedStderrChars: number;
|
|
44
45
|
stdoutPump: Promise<void>;
|
|
45
46
|
stderrPump: Promise<void>;
|
|
46
|
-
exitPromise: Promise<
|
|
47
|
-
exitCode: number | null;
|
|
48
|
-
signal?: string;
|
|
49
|
-
}>;
|
|
47
|
+
exitPromise: Promise<HostSpawnExit>;
|
|
50
48
|
}
|
|
51
49
|
interface ForegroundProcess {
|
|
52
50
|
commandId: string;
|
|
@@ -76,10 +74,7 @@ interface ForegroundProcess {
|
|
|
76
74
|
audit: BashAuditEvent[];
|
|
77
75
|
stdoutPump: Promise<void>;
|
|
78
76
|
stderrPump: Promise<void>;
|
|
79
|
-
exitPromise: Promise<
|
|
80
|
-
exitCode: number | null;
|
|
81
|
-
signal?: string;
|
|
82
|
-
}>;
|
|
77
|
+
exitPromise: Promise<HostSpawnExit>;
|
|
83
78
|
outputSinks: Record<1 | 2, ForegroundSink>;
|
|
84
79
|
abortController: AbortController;
|
|
85
80
|
}
|
|
@@ -303,6 +298,7 @@ declare class BashEnvironment {
|
|
|
303
298
|
private raceForeground;
|
|
304
299
|
private waitForBoundary;
|
|
305
300
|
private hostSpawn;
|
|
301
|
+
private hostResolveCommand;
|
|
306
302
|
private collectExited;
|
|
307
303
|
private finishExited;
|
|
308
304
|
private collectAborted;
|
|
@@ -320,24 +316,12 @@ declare class BashEnvironment {
|
|
|
320
316
|
* copy: `CommandName` is just-bash's own "safe to run anywhere" set — it
|
|
321
317
|
* already excludes the commands that need real process/network access
|
|
322
318
|
* (curl → NetworkCommandName, python3/python → PythonCommandName, js-exec/node
|
|
323
|
-
* → JavaScriptCommandName all live in separate, non-portable type unions).
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* tar, split, sqlite3, xan, and more — all already categorized as safe
|
|
327
|
-
* upstream, just never backported here). Deriving it removes that drift
|
|
328
|
-
* entirely: everything just-bash calls portable (minus the exceptions above),
|
|
329
|
-
* we call portable.
|
|
319
|
+
* → JavaScriptCommandName all live in separate, non-portable type unions).
|
|
320
|
+
* Unix names `hostSpawn` first (`preferHostSpawn`); portable is fallback for
|
|
321
|
+
* `executable_not_found` only.
|
|
330
322
|
*/
|
|
331
323
|
declare const DEMI_PORTABLE_COMMANDS: readonly CommandName[];
|
|
332
|
-
|
|
333
|
-
* Portable commands routed real-binary-first (`preferHostSpawn`): recursive
|
|
334
|
-
* tree scanners whose in-process implementations read files whole and burn
|
|
335
|
-
* the embedding host's main thread for minutes on large trees. A real
|
|
336
|
-
* process runs off-thread with its output bounded by the capture limit;
|
|
337
|
-
* hosts without the binary fall back to the portable implementation on
|
|
338
|
-
* first use.
|
|
339
|
-
*/
|
|
340
|
-
declare const HOST_PREFERRED_SCAN_COMMANDS: ReadonlySet<string>;
|
|
324
|
+
declare function shouldPreferHostSpawn(name: string): boolean;
|
|
341
325
|
/**
|
|
342
326
|
* Names registered commands must not shadow, derived from the actual portable
|
|
343
327
|
* command set plus interpreter builtins and pass-through system tools — not a
|
|
@@ -351,4 +335,4 @@ declare function shellQuote(value: string): string;
|
|
|
351
335
|
/** Picks a heredoc delimiter that does not collide with any line already in `body`. */
|
|
352
336
|
declare function heredocDelimiter(body: string): string;
|
|
353
337
|
//#endregion
|
|
354
|
-
export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, BinaryStdout, COMMAND_HELP_DEFAULTS, Command, CommandExecutionContext, CommandIO, CommandInputSpec, CommandMetadataRecord, CommandOutputSpec, CommandRegistry, CommandRunContext, CommandRunResult, CommandStdin, CommandStorage, DEMI_PORTABLE_COMMANDS,
|
|
338
|
+
export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, BinaryStdout, COMMAND_HELP_DEFAULTS, Command, CommandExecutionContext, CommandIO, CommandInputSpec, CommandMetadataRecord, CommandOutputSpec, CommandRegistry, CommandRunContext, CommandRunResult, CommandStdin, CommandStorage, DEMI_PORTABLE_COMMANDS, Host, HostBackedFileSystem, HostBackedFileSystemOptions, HostCwd, HostDirent, HostFileStat, HostFileSystem, HostIdentity, HostProcess, HostProcessOutputChunk, HostSpawnExit, HostSpawnHandle, HostSpawnParams, HostStore, MAX_TIMEOUT_MS, ParsedCommandInput, RESERVED_COMMAND_NAMES, ShellAbortInput, ShellCommandStatus, ShellExecInput, ShellOutputChunk, ShellOutputRecordChunk, ShellOutputView, ShellStatusInput, ShellStreamView, ShellWriteInput, SpawnErrorKind, createLogicalHostCwd, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote, shouldPreferHostSpawn };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HostBackedFileSystem } from "./host-fs.mjs";
|
|
2
2
|
import { AgentSessionCommandStorage } from "./storage.mjs";
|
|
3
|
-
import { asError, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
|
|
3
|
+
import { asError, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, isAbsolutePath, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
|
|
4
4
|
import { createLazyCommands, getCommandNames } from "@demicodes/just-bash/commands";
|
|
5
5
|
import { ArithmeticError, BadSubstitutionError, ExecutionLimitError, ExitError, Interpreter } from "@demicodes/just-bash/interpreter";
|
|
6
6
|
import { decodeBytesToUtf8, unsafeBytesFromLatin1 } from "@demicodes/just-bash/encoding";
|
|
@@ -30,6 +30,21 @@ const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
30
30
|
"sleep"
|
|
31
31
|
]);
|
|
32
32
|
/**
|
|
33
|
+
* just-bash portable names that implement bash builtins (or shell state).
|
|
34
|
+
* They stay in-process. `true`/`false` are omitted from the registry so the
|
|
35
|
+
* interpreter builtins run.
|
|
36
|
+
*/
|
|
37
|
+
const IN_PROCESS_PORTABLE_COMMANDS = /* @__PURE__ */ new Set([
|
|
38
|
+
"echo",
|
|
39
|
+
"printf",
|
|
40
|
+
"pwd",
|
|
41
|
+
"alias",
|
|
42
|
+
"unalias",
|
|
43
|
+
"history",
|
|
44
|
+
"help",
|
|
45
|
+
"time"
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
33
48
|
* Fork portable commands BashEnvironment registers in every shell, so
|
|
34
49
|
* cat/ls/grep-class tools work without local coreutils on any Host backend.
|
|
35
50
|
*
|
|
@@ -37,28 +52,14 @@ const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
37
52
|
* copy: `CommandName` is just-bash's own "safe to run anywhere" set — it
|
|
38
53
|
* already excludes the commands that need real process/network access
|
|
39
54
|
* (curl → NetworkCommandName, python3/python → PythonCommandName, js-exec/node
|
|
40
|
-
* → JavaScriptCommandName all live in separate, non-portable type unions).
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* tar, split, sqlite3, xan, and more — all already categorized as safe
|
|
44
|
-
* upstream, just never backported here). Deriving it removes that drift
|
|
45
|
-
* entirely: everything just-bash calls portable (minus the exceptions above),
|
|
46
|
-
* we call portable.
|
|
47
|
-
*/
|
|
48
|
-
const DEMI_PORTABLE_COMMANDS = getCommandNames().filter((name) => !REAL_SPAWN_DEPENDENT_COMMANDS.has(name));
|
|
49
|
-
/**
|
|
50
|
-
* Portable commands routed real-binary-first (`preferHostSpawn`): recursive
|
|
51
|
-
* tree scanners whose in-process implementations read files whole and burn
|
|
52
|
-
* the embedding host's main thread for minutes on large trees. A real
|
|
53
|
-
* process runs off-thread with its output bounded by the capture limit;
|
|
54
|
-
* hosts without the binary fall back to the portable implementation on
|
|
55
|
-
* first use.
|
|
55
|
+
* → JavaScriptCommandName all live in separate, non-portable type unions).
|
|
56
|
+
* Unix names `hostSpawn` first (`preferHostSpawn`); portable is fallback for
|
|
57
|
+
* `executable_not_found` only.
|
|
56
58
|
*/
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
]);
|
|
59
|
+
const DEMI_PORTABLE_COMMANDS = getCommandNames().filter((name) => !REAL_SPAWN_DEPENDENT_COMMANDS.has(name) && name !== "true" && name !== "false");
|
|
60
|
+
function shouldPreferHostSpawn(name) {
|
|
61
|
+
return !IN_PROCESS_PORTABLE_COMMANDS.has(name);
|
|
62
|
+
}
|
|
62
63
|
/** Shell language words and builtins the interpreter itself owns. */
|
|
63
64
|
const SHELL_BUILTIN_NAMES = [
|
|
64
65
|
".",
|
|
@@ -82,17 +83,17 @@ const SHELL_BUILTIN_NAMES = [
|
|
|
82
83
|
"shift",
|
|
83
84
|
"source",
|
|
84
85
|
"test",
|
|
86
|
+
"true",
|
|
87
|
+
"false",
|
|
85
88
|
"unset",
|
|
86
89
|
"wait"
|
|
87
90
|
];
|
|
88
91
|
/**
|
|
89
92
|
* Ecosystem tools the model expects to reach through Host.process.spawn.
|
|
90
93
|
*
|
|
91
|
-
* xargs and yq
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* is ever consulted — listing them here too would claim they fall through to
|
|
95
|
-
* a real spawn, which no longer happens.
|
|
94
|
+
* xargs and yq stay out of this list: they are already in the portable set
|
|
95
|
+
* (`preferHostSpawn`, then the in-process implementation if the Host has no
|
|
96
|
+
* binary). Listing them here would duplicate a reserved name.
|
|
96
97
|
*/
|
|
97
98
|
const SYSTEM_TOOL_NAMES = [
|
|
98
99
|
"bun",
|
|
@@ -127,6 +128,7 @@ function emptyStdin() {
|
|
|
127
128
|
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
128
129
|
};
|
|
129
130
|
}
|
|
131
|
+
async function* emptyStdinStream() {}
|
|
130
132
|
const EXECUTION_ONLY_FIELDS = [
|
|
131
133
|
"successOutput",
|
|
132
134
|
"failureOutput",
|
|
@@ -220,7 +222,7 @@ function parseArgs(command, path, argv, startIndex, stdin) {
|
|
|
220
222
|
setParsedValue(values, field, token);
|
|
221
223
|
positionalIndex += 1;
|
|
222
224
|
}
|
|
223
|
-
if (command.stdinField) values[command.stdinField] = stdin.text;
|
|
225
|
+
if (command.stdinField && values[command.stdinField] === void 0) values[command.stdinField] = stdin.text;
|
|
224
226
|
return {
|
|
225
227
|
path: [...path],
|
|
226
228
|
help: false,
|
|
@@ -261,7 +263,9 @@ async function runRegisteredCommand(root, ctx) {
|
|
|
261
263
|
cwd: ctx.cwd,
|
|
262
264
|
io: parsed.json ? capture : ctx.io,
|
|
263
265
|
storage: ctx.storage,
|
|
264
|
-
host: ctx.host
|
|
266
|
+
host: ctx.host,
|
|
267
|
+
signal: ctx.signal ?? new AbortController().signal,
|
|
268
|
+
stdinStream: ctx.stdinStream ?? emptyStdinStream()
|
|
265
269
|
});
|
|
266
270
|
if (parsed.json && result.exitCode === 0) {
|
|
267
271
|
const raw = capture.stdoutText();
|
|
@@ -695,24 +699,34 @@ var CommandArtifactStore = class {
|
|
|
695
699
|
};
|
|
696
700
|
//#endregion
|
|
697
701
|
//#region src/registered-command-adapter.ts
|
|
698
|
-
|
|
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) {
|
|
699
709
|
return {
|
|
700
710
|
name: command.name,
|
|
701
711
|
consumesStdin: treeConsumesStdin(command),
|
|
702
712
|
execute: async (args, ctx) => {
|
|
703
713
|
const stdin = decodeForkStdin(ctx.stdin);
|
|
704
|
-
const io = createForwardingIO();
|
|
705
714
|
const argv = [command.name, ...args];
|
|
715
|
+
const job = new VirtualForegroundJob(session, command.name, args, ctx.cwd, captureLimitBytes);
|
|
716
|
+
job.install();
|
|
706
717
|
try {
|
|
707
|
-
const result = await runRegisteredCommand(command, {
|
|
718
|
+
const result = await Promise.race([runRegisteredCommand(command, {
|
|
708
719
|
argv,
|
|
709
720
|
stdin,
|
|
710
721
|
env: mapToRecord(ctx.env),
|
|
711
722
|
cwd: ctx.cwd,
|
|
712
|
-
io,
|
|
723
|
+
io: job.io,
|
|
713
724
|
storage,
|
|
714
|
-
host
|
|
715
|
-
|
|
725
|
+
host,
|
|
726
|
+
signal: job.signal,
|
|
727
|
+
stdinStream: job.stdinChunks()
|
|
728
|
+
}), job.killedResult()]);
|
|
729
|
+
if (job.foreground.captureOverflowed) return job.overflowResult();
|
|
716
730
|
session.accumulator.audit.push({
|
|
717
731
|
kind: "registered-command",
|
|
718
732
|
name: command.name,
|
|
@@ -726,12 +740,13 @@ function commandToForkCommand(session, command, storage, host) {
|
|
|
726
740
|
metadata: result.metadata
|
|
727
741
|
});
|
|
728
742
|
return {
|
|
729
|
-
stdout:
|
|
743
|
+
stdout: job.stdoutLatin1(),
|
|
730
744
|
stdoutKind: "bytes",
|
|
731
|
-
stderr:
|
|
745
|
+
stderr: job.stderrText(),
|
|
732
746
|
exitCode: result.exitCode
|
|
733
747
|
};
|
|
734
748
|
} catch (error) {
|
|
749
|
+
if (job.foreground.captureOverflowed) return job.overflowResult();
|
|
735
750
|
const message = error instanceof Error ? error.message : String(error);
|
|
736
751
|
session.accumulator.audit.push({
|
|
737
752
|
kind: "registered-command",
|
|
@@ -740,35 +755,171 @@ function commandToForkCommand(session, command, storage, host) {
|
|
|
740
755
|
exitCode: 1
|
|
741
756
|
});
|
|
742
757
|
return {
|
|
743
|
-
stdout:
|
|
758
|
+
stdout: job.stdoutLatin1(),
|
|
744
759
|
stdoutKind: "bytes",
|
|
745
|
-
stderr: `${
|
|
760
|
+
stderr: `${job.stderrText()}${command.name}: ${message}\n`,
|
|
746
761
|
exitCode: 1
|
|
747
762
|
};
|
|
763
|
+
} finally {
|
|
764
|
+
job.release();
|
|
748
765
|
}
|
|
749
766
|
}
|
|
750
767
|
};
|
|
751
768
|
}
|
|
752
|
-
/**
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
};
|
|
758
848
|
}
|
|
759
|
-
|
|
760
|
-
this.
|
|
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;
|
|
761
869
|
}
|
|
762
870
|
stdoutLatin1() {
|
|
763
|
-
return decodeLatin1(concatBytes(this.
|
|
871
|
+
return decodeLatin1(concatBytes(this.foreground.rawStdoutBytes));
|
|
764
872
|
}
|
|
765
873
|
stderrText() {
|
|
766
|
-
return
|
|
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?.();
|
|
767
917
|
}
|
|
768
918
|
};
|
|
769
|
-
function
|
|
770
|
-
return
|
|
919
|
+
function toBytes(data) {
|
|
920
|
+
return typeof data === "string" ? encodeUtf8(data) : data;
|
|
771
921
|
}
|
|
922
|
+
async function* emptyByteStream() {}
|
|
772
923
|
function treeConsumesStdin(command) {
|
|
773
924
|
if (command.stdinField) return true;
|
|
774
925
|
return command.subcommands?.some(treeConsumesStdin) ?? false;
|
|
@@ -877,7 +1028,7 @@ var BashEnvironment = class {
|
|
|
877
1028
|
if (input.cwd !== void 0) {
|
|
878
1029
|
if (!(await this.host.fs.stat(input.cwd).catch(() => null))?.isDirectory) throw new Error(`Shell exec cwd is not a directory: ${input.cwd}`);
|
|
879
1030
|
}
|
|
880
|
-
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
|
|
1031
|
+
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? await this.createShell(input.agentSessionId, input.cwd) : await this.availableDefaultShell(input.agentSessionId);
|
|
881
1032
|
if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
|
|
882
1033
|
if (session.pendingExec || session.foreground) {
|
|
883
1034
|
const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
|
|
@@ -944,6 +1095,7 @@ var BashEnvironment = class {
|
|
|
944
1095
|
await job.exitPromise.catch(() => {});
|
|
945
1096
|
}
|
|
946
1097
|
session.backgroundJobs.clear();
|
|
1098
|
+
await session.cwdHandle.close().catch(() => {});
|
|
947
1099
|
if (session.abortController) session.abortController.abort();
|
|
948
1100
|
}
|
|
949
1101
|
requireShell(shellId) {
|
|
@@ -968,21 +1120,21 @@ var BashEnvironment = class {
|
|
|
968
1120
|
if (!foreground || foreground.commandId !== commandId) throw new Error(`Command "${commandId}" has no foreground process`);
|
|
969
1121
|
return foreground;
|
|
970
1122
|
}
|
|
971
|
-
defaultShell(agentSessionId) {
|
|
1123
|
+
async defaultShell(agentSessionId) {
|
|
972
1124
|
if (!agentSessionId) return this.createShell(void 0);
|
|
973
1125
|
const existingShellId = this.defaultShellByAgentSessionId.get(agentSessionId);
|
|
974
1126
|
const existing = existingShellId ? this.shells.get(existingShellId) : void 0;
|
|
975
1127
|
if (existing && !existing.exited) return existing;
|
|
976
|
-
const shell = this.createShell(agentSessionId);
|
|
1128
|
+
const shell = await this.createShell(agentSessionId);
|
|
977
1129
|
this.defaultShellByAgentSessionId.set(agentSessionId, shell.id);
|
|
978
1130
|
return shell;
|
|
979
1131
|
}
|
|
980
|
-
availableDefaultShell(agentSessionId) {
|
|
981
|
-
const shell = this.defaultShell(agentSessionId);
|
|
1132
|
+
async availableDefaultShell(agentSessionId) {
|
|
1133
|
+
const shell = await this.defaultShell(agentSessionId);
|
|
982
1134
|
if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
|
|
983
1135
|
return this.createShell(agentSessionId);
|
|
984
1136
|
}
|
|
985
|
-
createShell(agentSessionId, initialCwd) {
|
|
1137
|
+
async createShell(agentSessionId, initialCwd) {
|
|
986
1138
|
const id = this.shellIdFactory();
|
|
987
1139
|
const commandStorageId = agentSessionId ?? id;
|
|
988
1140
|
const cwd = initialCwd ?? this.host.defaultCwd;
|
|
@@ -996,10 +1148,12 @@ var BashEnvironment = class {
|
|
|
996
1148
|
if (!env.has("PS1")) env.set("PS1", "");
|
|
997
1149
|
if (!env.has("PS2")) env.set("PS2", "> ");
|
|
998
1150
|
if (!env.has("SHLVL")) env.set("SHLVL", "1");
|
|
1151
|
+
const cwdHandle = await this.host.process.openCwd(cwd);
|
|
999
1152
|
const exportedVars = /* @__PURE__ */ new Set(["PWD", "DEMI_SHELL_ID"]);
|
|
1000
1153
|
if (agentSessionId) exportedVars.add("DEMI_SESSION_ID");
|
|
1001
1154
|
for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
|
|
1002
1155
|
for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
|
|
1156
|
+
if (!env.has("HOSTNAME")) env.set("HOSTNAME", this.host.identity.hostname);
|
|
1003
1157
|
const state = {
|
|
1004
1158
|
env,
|
|
1005
1159
|
cwd,
|
|
@@ -1015,8 +1169,8 @@ var BashEnvironment = class {
|
|
|
1015
1169
|
lastBackgroundPid: 0,
|
|
1016
1170
|
virtualPid: 1,
|
|
1017
1171
|
virtualPpid: 0,
|
|
1018
|
-
virtualUid:
|
|
1019
|
-
virtualGid:
|
|
1172
|
+
virtualUid: this.host.identity.uid,
|
|
1173
|
+
virtualGid: this.host.identity.gid,
|
|
1020
1174
|
bashPid: 1,
|
|
1021
1175
|
nextVirtualPid: 2,
|
|
1022
1176
|
currentLine: 1,
|
|
@@ -1064,6 +1218,7 @@ var BashEnvironment = class {
|
|
|
1064
1218
|
fs,
|
|
1065
1219
|
interpreter: void 0,
|
|
1066
1220
|
forkCommands,
|
|
1221
|
+
cwdHandle,
|
|
1067
1222
|
accumulator: {
|
|
1068
1223
|
stdout: "",
|
|
1069
1224
|
stderr: "",
|
|
@@ -1077,7 +1232,7 @@ var BashEnvironment = class {
|
|
|
1077
1232
|
};
|
|
1078
1233
|
for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
|
|
1079
1234
|
const storage = new AgentSessionCommandStorage(this.host.store, commandStorageId);
|
|
1080
|
-
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));
|
|
1081
1236
|
session.abortController = new AbortController();
|
|
1082
1237
|
const limits = resolveLimits({
|
|
1083
1238
|
maxOutputSize: this.captureLimitBytes,
|
|
@@ -1096,6 +1251,11 @@ var BashEnvironment = class {
|
|
|
1096
1251
|
exitCode: 0
|
|
1097
1252
|
}),
|
|
1098
1253
|
hostSpawn: (command, args, opts) => this.hostSpawn(session, command, args, opts),
|
|
1254
|
+
hostResolveCommand: (name, env) => this.hostResolveCommand(session, name, env),
|
|
1255
|
+
hostCwd: {
|
|
1256
|
+
enter: (path) => session.cwdHandle.chdir(path),
|
|
1257
|
+
snapshot: () => session.cwdHandle.snapshot()
|
|
1258
|
+
},
|
|
1099
1259
|
rejectTimedPipelines: true,
|
|
1100
1260
|
jobControl: {
|
|
1101
1261
|
startBackground: (statement) => this.startBackgroundJob(session, statement),
|
|
@@ -1177,7 +1337,7 @@ var BashEnvironment = class {
|
|
|
1177
1337
|
const handle = await this.host.process.spawn({
|
|
1178
1338
|
command: backgroundCommand.command,
|
|
1179
1339
|
args: backgroundCommand.args,
|
|
1180
|
-
cwd: session.
|
|
1340
|
+
cwd: session.cwdHandle.spawnPath(),
|
|
1181
1341
|
env: this.exportedEnv(session),
|
|
1182
1342
|
killProcessGroup: true
|
|
1183
1343
|
});
|
|
@@ -1365,10 +1525,11 @@ var BashEnvironment = class {
|
|
|
1365
1525
|
}
|
|
1366
1526
|
async hostSpawn(session, command, args, opts) {
|
|
1367
1527
|
if (session.foreground) throw new Error(`hostSpawn: session "${session.id}" already has a foreground process`);
|
|
1528
|
+
const spawnCwd = session.cwdHandle.spawnPath();
|
|
1368
1529
|
const handle = await this.host.process.spawn({
|
|
1369
1530
|
command,
|
|
1370
1531
|
args,
|
|
1371
|
-
cwd:
|
|
1532
|
+
cwd: spawnCwd,
|
|
1372
1533
|
env: opts.env,
|
|
1373
1534
|
killProcessGroup: true
|
|
1374
1535
|
});
|
|
@@ -1422,8 +1583,13 @@ var BashEnvironment = class {
|
|
|
1422
1583
|
const exit = await foreground.exitPromise;
|
|
1423
1584
|
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1424
1585
|
const stdout = foreground.captureOverflowed ? "" : decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1425
|
-
|
|
1426
|
-
|
|
1586
|
+
let exitCode = foreground.captureOverflowed ? 137 : exit.exitCode ?? 127;
|
|
1587
|
+
let stderr = foreground.captureOverflowed ? `${command}: output exceeded the ${this.captureLimitBytes}-byte capture limit and the process was killed; the shell buffers whole command outputs in memory — narrow the output at the source (filters, head, tighter paths)\n` : foreground.rawStderrBuffer;
|
|
1588
|
+
let spawnError = exit.spawnError;
|
|
1589
|
+
if (!foreground.captureOverflowed && exit.spawnError) {
|
|
1590
|
+
exitCode = spawnErrorExitCode(exit.spawnError.kind);
|
|
1591
|
+
stderr = spawnErrorStderr(command, opts.cwd, opts.env, exit.spawnError.kind);
|
|
1592
|
+
} else if (!foreground.captureOverflowed && exit.exitCode === null && foreground.rawStderrBuffer.length === 0) stderr = `${command}: ${exit.signal ?? "command not found"}\n`;
|
|
1427
1593
|
foreground.audit[0] = {
|
|
1428
1594
|
kind: "system-command",
|
|
1429
1595
|
name: command,
|
|
@@ -1444,9 +1610,39 @@ var BashEnvironment = class {
|
|
|
1444
1610
|
stdout,
|
|
1445
1611
|
stdoutKind: "bytes",
|
|
1446
1612
|
stderr,
|
|
1447
|
-
exitCode
|
|
1613
|
+
exitCode,
|
|
1614
|
+
...spawnError ? { spawnError } : {}
|
|
1448
1615
|
};
|
|
1449
1616
|
}
|
|
1617
|
+
async hostResolveCommand(session, name, env) {
|
|
1618
|
+
if (name.includes("/")) {
|
|
1619
|
+
const resolved = isAbsolutePath(name) ? name : `${session.state.cwd.replace(/\/+$/, "")}/${name}`;
|
|
1620
|
+
try {
|
|
1621
|
+
if (!(await this.host.fs.stat(resolved)).isDirectory) return {
|
|
1622
|
+
kind: "file",
|
|
1623
|
+
value: name
|
|
1624
|
+
};
|
|
1625
|
+
} catch {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
const pathEnv = env.PATH ?? "";
|
|
1631
|
+
for (const dir of pathEnv.split(":")) {
|
|
1632
|
+
if (!dir) continue;
|
|
1633
|
+
const full = isAbsolutePath(dir) ? `${dir.replace(/\/+$/, "")}/${name}` : `${session.state.cwd.replace(/\/+$/, "")}/${dir}/${name}`;
|
|
1634
|
+
try {
|
|
1635
|
+
if ((await this.host.fs.stat(full)).isDirectory) continue;
|
|
1636
|
+
return {
|
|
1637
|
+
kind: "file",
|
|
1638
|
+
value: isAbsolutePath(dir) ? full : `${dir}/${name}`
|
|
1639
|
+
};
|
|
1640
|
+
} catch {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1450
1646
|
collectExited(session, record, resultOrError, foreground, input = {}) {
|
|
1451
1647
|
if (record.status !== "running") return this.commandStatus(record, input);
|
|
1452
1648
|
if (resultOrError instanceof Error) {
|
|
@@ -1629,7 +1825,7 @@ var BashEnvironment = class {
|
|
|
1629
1825
|
function createPortableCommands(session) {
|
|
1630
1826
|
return createLazyCommands([...DEMI_PORTABLE_COMMANDS]).map((command) => ({
|
|
1631
1827
|
...command,
|
|
1632
|
-
preferHostSpawn:
|
|
1828
|
+
preferHostSpawn: shouldPreferHostSpawn(command.name),
|
|
1633
1829
|
execute: async (args, ctx) => {
|
|
1634
1830
|
const result = await command.execute(args, ctx);
|
|
1635
1831
|
session.accumulator.audit.push({
|
|
@@ -1644,6 +1840,20 @@ function createPortableCommands(session) {
|
|
|
1644
1840
|
}));
|
|
1645
1841
|
}
|
|
1646
1842
|
/** True when the string contains a char > 0xFF, i.e. already-decoded Unicode text. */
|
|
1843
|
+
function spawnErrorExitCode(kind) {
|
|
1844
|
+
if (kind === "permission_denied" || kind === "is_directory") return 126;
|
|
1845
|
+
return 127;
|
|
1846
|
+
}
|
|
1847
|
+
function spawnErrorStderr(command, cwd, env, kind) {
|
|
1848
|
+
if (kind === "permission_denied") return `bash: ${command}: Permission denied\n`;
|
|
1849
|
+
if (kind === "is_directory") return `bash: ${command}: Is a directory\n`;
|
|
1850
|
+
if (kind === "cwd_unusable") return `bash: ${cwd}: No such file or directory\n`;
|
|
1851
|
+
if (kind === "executable_not_found") {
|
|
1852
|
+
if (command.includes("/") || !env.PATH) return `bash: ${command}: No such file or directory\n`;
|
|
1853
|
+
return `bash: ${command}: command not found\n`;
|
|
1854
|
+
}
|
|
1855
|
+
return `bash: ${command}: ${kind}\n`;
|
|
1856
|
+
}
|
|
1647
1857
|
function hasWideChar(value) {
|
|
1648
1858
|
for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) > 255) return true;
|
|
1649
1859
|
return false;
|
|
@@ -1765,6 +1975,41 @@ function tailString(value) {
|
|
|
1765
1975
|
return tail(value, 4096);
|
|
1766
1976
|
}
|
|
1767
1977
|
//#endregion
|
|
1978
|
+
//#region src/host.ts
|
|
1979
|
+
/** Path-string cwd for test doubles and Hosts that cannot hold a directory fd. */
|
|
1980
|
+
function createLogicalHostCwd(initialPath) {
|
|
1981
|
+
let path = initialPath;
|
|
1982
|
+
return {
|
|
1983
|
+
get path() {
|
|
1984
|
+
return path;
|
|
1985
|
+
},
|
|
1986
|
+
spawnPath() {
|
|
1987
|
+
return path;
|
|
1988
|
+
},
|
|
1989
|
+
async chdir(next) {
|
|
1990
|
+
if (next === ".") return;
|
|
1991
|
+
path = resolveLogicalCwd(path, next);
|
|
1992
|
+
},
|
|
1993
|
+
async snapshot() {
|
|
1994
|
+
const saved = path;
|
|
1995
|
+
return { restore() {
|
|
1996
|
+
path = saved;
|
|
1997
|
+
} };
|
|
1998
|
+
},
|
|
1999
|
+
async close() {}
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function resolveLogicalCwd(base, next) {
|
|
2003
|
+
if (next.startsWith("/")) return next;
|
|
2004
|
+
const parts = base.split("/").filter(Boolean);
|
|
2005
|
+
for (const part of next.split("/")) {
|
|
2006
|
+
if (!part || part === ".") continue;
|
|
2007
|
+
if (part === "..") parts.pop();
|
|
2008
|
+
else parts.push(part);
|
|
2009
|
+
}
|
|
2010
|
+
return `/${parts.join("/")}`;
|
|
2011
|
+
}
|
|
2012
|
+
//#endregion
|
|
1768
2013
|
//#region src/shell-quote.ts
|
|
1769
2014
|
/** Single-quotes a value for safe use as one shell word. */
|
|
1770
2015
|
function shellQuote(value) {
|
|
@@ -1778,4 +2023,4 @@ function heredocDelimiter(body) {
|
|
|
1778
2023
|
return delimiter;
|
|
1779
2024
|
}
|
|
1780
2025
|
//#endregion
|
|
1781
|
-
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS,
|
|
2026
|
+
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, createLogicalHostCwd, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote, shouldPreferHostSpawn };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as HostStore, t as Host } from "./host-BupAFtK6.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/command.d.ts
|
|
4
4
|
type CommandInputSpec = Record<string, z.ZodType>;
|
|
@@ -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;
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as AgentSessionCommandStorage } from "./storage-
|
|
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.
|
|
4
|
+
"version": "0.19.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@demicodes/just-bash": "^3.1.0-demi.
|
|
23
|
-
"@demicodes/utils": "^0.
|
|
22
|
+
"@demicodes/just-bash": "^3.1.0-demi.4",
|
|
23
|
+
"@demicodes/utils": "^0.19.0",
|
|
24
24
|
"zod": "^4.0.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|