@demicodes/shell 0.17.3 → 0.18.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 +137 -42
- package/dist/{storage-DYvEwSc9.d.mts → storage-CAc1N6MN.d.mts} +1 -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-CAc1N6MN.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.
|
|
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.
|
|
47
58
|
*/
|
|
48
|
-
const DEMI_PORTABLE_COMMANDS = getCommandNames().filter((name) => !REAL_SPAWN_DEPENDENT_COMMANDS.has(name));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
56
|
-
*/
|
|
57
|
-
const HOST_PREFERRED_SCAN_COMMANDS = /* @__PURE__ */ new Set([
|
|
58
|
-
"rg",
|
|
59
|
-
"grep",
|
|
60
|
-
"find"
|
|
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",
|
|
@@ -877,7 +878,7 @@ var BashEnvironment = class {
|
|
|
877
878
|
if (input.cwd !== void 0) {
|
|
878
879
|
if (!(await this.host.fs.stat(input.cwd).catch(() => null))?.isDirectory) throw new Error(`Shell exec cwd is not a directory: ${input.cwd}`);
|
|
879
880
|
}
|
|
880
|
-
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
|
|
881
|
+
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? await this.createShell(input.agentSessionId, input.cwd) : await this.availableDefaultShell(input.agentSessionId);
|
|
881
882
|
if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
|
|
882
883
|
if (session.pendingExec || session.foreground) {
|
|
883
884
|
const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
|
|
@@ -944,6 +945,7 @@ var BashEnvironment = class {
|
|
|
944
945
|
await job.exitPromise.catch(() => {});
|
|
945
946
|
}
|
|
946
947
|
session.backgroundJobs.clear();
|
|
948
|
+
await session.cwdHandle.close().catch(() => {});
|
|
947
949
|
if (session.abortController) session.abortController.abort();
|
|
948
950
|
}
|
|
949
951
|
requireShell(shellId) {
|
|
@@ -968,21 +970,21 @@ var BashEnvironment = class {
|
|
|
968
970
|
if (!foreground || foreground.commandId !== commandId) throw new Error(`Command "${commandId}" has no foreground process`);
|
|
969
971
|
return foreground;
|
|
970
972
|
}
|
|
971
|
-
defaultShell(agentSessionId) {
|
|
973
|
+
async defaultShell(agentSessionId) {
|
|
972
974
|
if (!agentSessionId) return this.createShell(void 0);
|
|
973
975
|
const existingShellId = this.defaultShellByAgentSessionId.get(agentSessionId);
|
|
974
976
|
const existing = existingShellId ? this.shells.get(existingShellId) : void 0;
|
|
975
977
|
if (existing && !existing.exited) return existing;
|
|
976
|
-
const shell = this.createShell(agentSessionId);
|
|
978
|
+
const shell = await this.createShell(agentSessionId);
|
|
977
979
|
this.defaultShellByAgentSessionId.set(agentSessionId, shell.id);
|
|
978
980
|
return shell;
|
|
979
981
|
}
|
|
980
|
-
availableDefaultShell(agentSessionId) {
|
|
981
|
-
const shell = this.defaultShell(agentSessionId);
|
|
982
|
+
async availableDefaultShell(agentSessionId) {
|
|
983
|
+
const shell = await this.defaultShell(agentSessionId);
|
|
982
984
|
if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
|
|
983
985
|
return this.createShell(agentSessionId);
|
|
984
986
|
}
|
|
985
|
-
createShell(agentSessionId, initialCwd) {
|
|
987
|
+
async createShell(agentSessionId, initialCwd) {
|
|
986
988
|
const id = this.shellIdFactory();
|
|
987
989
|
const commandStorageId = agentSessionId ?? id;
|
|
988
990
|
const cwd = initialCwd ?? this.host.defaultCwd;
|
|
@@ -996,10 +998,12 @@ var BashEnvironment = class {
|
|
|
996
998
|
if (!env.has("PS1")) env.set("PS1", "");
|
|
997
999
|
if (!env.has("PS2")) env.set("PS2", "> ");
|
|
998
1000
|
if (!env.has("SHLVL")) env.set("SHLVL", "1");
|
|
1001
|
+
const cwdHandle = await this.host.process.openCwd(cwd);
|
|
999
1002
|
const exportedVars = /* @__PURE__ */ new Set(["PWD", "DEMI_SHELL_ID"]);
|
|
1000
1003
|
if (agentSessionId) exportedVars.add("DEMI_SESSION_ID");
|
|
1001
1004
|
for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
|
|
1002
1005
|
for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
|
|
1006
|
+
if (!env.has("HOSTNAME")) env.set("HOSTNAME", this.host.identity.hostname);
|
|
1003
1007
|
const state = {
|
|
1004
1008
|
env,
|
|
1005
1009
|
cwd,
|
|
@@ -1015,8 +1019,8 @@ var BashEnvironment = class {
|
|
|
1015
1019
|
lastBackgroundPid: 0,
|
|
1016
1020
|
virtualPid: 1,
|
|
1017
1021
|
virtualPpid: 0,
|
|
1018
|
-
virtualUid:
|
|
1019
|
-
virtualGid:
|
|
1022
|
+
virtualUid: this.host.identity.uid,
|
|
1023
|
+
virtualGid: this.host.identity.gid,
|
|
1020
1024
|
bashPid: 1,
|
|
1021
1025
|
nextVirtualPid: 2,
|
|
1022
1026
|
currentLine: 1,
|
|
@@ -1064,6 +1068,7 @@ var BashEnvironment = class {
|
|
|
1064
1068
|
fs,
|
|
1065
1069
|
interpreter: void 0,
|
|
1066
1070
|
forkCommands,
|
|
1071
|
+
cwdHandle,
|
|
1067
1072
|
accumulator: {
|
|
1068
1073
|
stdout: "",
|
|
1069
1074
|
stderr: "",
|
|
@@ -1096,6 +1101,11 @@ var BashEnvironment = class {
|
|
|
1096
1101
|
exitCode: 0
|
|
1097
1102
|
}),
|
|
1098
1103
|
hostSpawn: (command, args, opts) => this.hostSpawn(session, command, args, opts),
|
|
1104
|
+
hostResolveCommand: (name, env) => this.hostResolveCommand(session, name, env),
|
|
1105
|
+
hostCwd: {
|
|
1106
|
+
enter: (path) => session.cwdHandle.chdir(path),
|
|
1107
|
+
snapshot: () => session.cwdHandle.snapshot()
|
|
1108
|
+
},
|
|
1099
1109
|
rejectTimedPipelines: true,
|
|
1100
1110
|
jobControl: {
|
|
1101
1111
|
startBackground: (statement) => this.startBackgroundJob(session, statement),
|
|
@@ -1177,7 +1187,7 @@ var BashEnvironment = class {
|
|
|
1177
1187
|
const handle = await this.host.process.spawn({
|
|
1178
1188
|
command: backgroundCommand.command,
|
|
1179
1189
|
args: backgroundCommand.args,
|
|
1180
|
-
cwd: session.
|
|
1190
|
+
cwd: session.cwdHandle.spawnPath(),
|
|
1181
1191
|
env: this.exportedEnv(session),
|
|
1182
1192
|
killProcessGroup: true
|
|
1183
1193
|
});
|
|
@@ -1365,10 +1375,11 @@ var BashEnvironment = class {
|
|
|
1365
1375
|
}
|
|
1366
1376
|
async hostSpawn(session, command, args, opts) {
|
|
1367
1377
|
if (session.foreground) throw new Error(`hostSpawn: session "${session.id}" already has a foreground process`);
|
|
1378
|
+
const spawnCwd = session.cwdHandle.spawnPath();
|
|
1368
1379
|
const handle = await this.host.process.spawn({
|
|
1369
1380
|
command,
|
|
1370
1381
|
args,
|
|
1371
|
-
cwd:
|
|
1382
|
+
cwd: spawnCwd,
|
|
1372
1383
|
env: opts.env,
|
|
1373
1384
|
killProcessGroup: true
|
|
1374
1385
|
});
|
|
@@ -1422,8 +1433,13 @@ var BashEnvironment = class {
|
|
|
1422
1433
|
const exit = await foreground.exitPromise;
|
|
1423
1434
|
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1424
1435
|
const stdout = foreground.captureOverflowed ? "" : decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1425
|
-
|
|
1426
|
-
|
|
1436
|
+
let exitCode = foreground.captureOverflowed ? 137 : exit.exitCode ?? 127;
|
|
1437
|
+
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;
|
|
1438
|
+
let spawnError = exit.spawnError;
|
|
1439
|
+
if (!foreground.captureOverflowed && exit.spawnError) {
|
|
1440
|
+
exitCode = spawnErrorExitCode(exit.spawnError.kind);
|
|
1441
|
+
stderr = spawnErrorStderr(command, opts.cwd, opts.env, exit.spawnError.kind);
|
|
1442
|
+
} else if (!foreground.captureOverflowed && exit.exitCode === null && foreground.rawStderrBuffer.length === 0) stderr = `${command}: ${exit.signal ?? "command not found"}\n`;
|
|
1427
1443
|
foreground.audit[0] = {
|
|
1428
1444
|
kind: "system-command",
|
|
1429
1445
|
name: command,
|
|
@@ -1444,9 +1460,39 @@ var BashEnvironment = class {
|
|
|
1444
1460
|
stdout,
|
|
1445
1461
|
stdoutKind: "bytes",
|
|
1446
1462
|
stderr,
|
|
1447
|
-
exitCode
|
|
1463
|
+
exitCode,
|
|
1464
|
+
...spawnError ? { spawnError } : {}
|
|
1448
1465
|
};
|
|
1449
1466
|
}
|
|
1467
|
+
async hostResolveCommand(session, name, env) {
|
|
1468
|
+
if (name.includes("/")) {
|
|
1469
|
+
const resolved = isAbsolutePath(name) ? name : `${session.state.cwd.replace(/\/+$/, "")}/${name}`;
|
|
1470
|
+
try {
|
|
1471
|
+
if (!(await this.host.fs.stat(resolved)).isDirectory) return {
|
|
1472
|
+
kind: "file",
|
|
1473
|
+
value: name
|
|
1474
|
+
};
|
|
1475
|
+
} catch {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
const pathEnv = env.PATH ?? "";
|
|
1481
|
+
for (const dir of pathEnv.split(":")) {
|
|
1482
|
+
if (!dir) continue;
|
|
1483
|
+
const full = isAbsolutePath(dir) ? `${dir.replace(/\/+$/, "")}/${name}` : `${session.state.cwd.replace(/\/+$/, "")}/${dir}/${name}`;
|
|
1484
|
+
try {
|
|
1485
|
+
if ((await this.host.fs.stat(full)).isDirectory) continue;
|
|
1486
|
+
return {
|
|
1487
|
+
kind: "file",
|
|
1488
|
+
value: isAbsolutePath(dir) ? full : `${dir}/${name}`
|
|
1489
|
+
};
|
|
1490
|
+
} catch {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1450
1496
|
collectExited(session, record, resultOrError, foreground, input = {}) {
|
|
1451
1497
|
if (record.status !== "running") return this.commandStatus(record, input);
|
|
1452
1498
|
if (resultOrError instanceof Error) {
|
|
@@ -1629,7 +1675,7 @@ var BashEnvironment = class {
|
|
|
1629
1675
|
function createPortableCommands(session) {
|
|
1630
1676
|
return createLazyCommands([...DEMI_PORTABLE_COMMANDS]).map((command) => ({
|
|
1631
1677
|
...command,
|
|
1632
|
-
preferHostSpawn:
|
|
1678
|
+
preferHostSpawn: shouldPreferHostSpawn(command.name),
|
|
1633
1679
|
execute: async (args, ctx) => {
|
|
1634
1680
|
const result = await command.execute(args, ctx);
|
|
1635
1681
|
session.accumulator.audit.push({
|
|
@@ -1644,6 +1690,20 @@ function createPortableCommands(session) {
|
|
|
1644
1690
|
}));
|
|
1645
1691
|
}
|
|
1646
1692
|
/** True when the string contains a char > 0xFF, i.e. already-decoded Unicode text. */
|
|
1693
|
+
function spawnErrorExitCode(kind) {
|
|
1694
|
+
if (kind === "permission_denied" || kind === "is_directory") return 126;
|
|
1695
|
+
return 127;
|
|
1696
|
+
}
|
|
1697
|
+
function spawnErrorStderr(command, cwd, env, kind) {
|
|
1698
|
+
if (kind === "permission_denied") return `bash: ${command}: Permission denied\n`;
|
|
1699
|
+
if (kind === "is_directory") return `bash: ${command}: Is a directory\n`;
|
|
1700
|
+
if (kind === "cwd_unusable") return `bash: ${cwd}: No such file or directory\n`;
|
|
1701
|
+
if (kind === "executable_not_found") {
|
|
1702
|
+
if (command.includes("/") || !env.PATH) return `bash: ${command}: No such file or directory\n`;
|
|
1703
|
+
return `bash: ${command}: command not found\n`;
|
|
1704
|
+
}
|
|
1705
|
+
return `bash: ${command}: ${kind}\n`;
|
|
1706
|
+
}
|
|
1647
1707
|
function hasWideChar(value) {
|
|
1648
1708
|
for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) > 255) return true;
|
|
1649
1709
|
return false;
|
|
@@ -1765,6 +1825,41 @@ function tailString(value) {
|
|
|
1765
1825
|
return tail(value, 4096);
|
|
1766
1826
|
}
|
|
1767
1827
|
//#endregion
|
|
1828
|
+
//#region src/host.ts
|
|
1829
|
+
/** Path-string cwd for test doubles and Hosts that cannot hold a directory fd. */
|
|
1830
|
+
function createLogicalHostCwd(initialPath) {
|
|
1831
|
+
let path = initialPath;
|
|
1832
|
+
return {
|
|
1833
|
+
get path() {
|
|
1834
|
+
return path;
|
|
1835
|
+
},
|
|
1836
|
+
spawnPath() {
|
|
1837
|
+
return path;
|
|
1838
|
+
},
|
|
1839
|
+
async chdir(next) {
|
|
1840
|
+
if (next === ".") return;
|
|
1841
|
+
path = resolveLogicalCwd(path, next);
|
|
1842
|
+
},
|
|
1843
|
+
async snapshot() {
|
|
1844
|
+
const saved = path;
|
|
1845
|
+
return { restore() {
|
|
1846
|
+
path = saved;
|
|
1847
|
+
} };
|
|
1848
|
+
},
|
|
1849
|
+
async close() {}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function resolveLogicalCwd(base, next) {
|
|
1853
|
+
if (next.startsWith("/")) return next;
|
|
1854
|
+
const parts = base.split("/").filter(Boolean);
|
|
1855
|
+
for (const part of next.split("/")) {
|
|
1856
|
+
if (!part || part === ".") continue;
|
|
1857
|
+
if (part === "..") parts.pop();
|
|
1858
|
+
else parts.push(part);
|
|
1859
|
+
}
|
|
1860
|
+
return `/${parts.join("/")}`;
|
|
1861
|
+
}
|
|
1862
|
+
//#endregion
|
|
1768
1863
|
//#region src/shell-quote.ts
|
|
1769
1864
|
/** Single-quotes a value for safe use as one shell word. */
|
|
1770
1865
|
function shellQuote(value) {
|
|
@@ -1778,4 +1873,4 @@ function heredocDelimiter(body) {
|
|
|
1778
1873
|
return delimiter;
|
|
1779
1874
|
}
|
|
1780
1875
|
//#endregion
|
|
1781
|
-
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS,
|
|
1876
|
+
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 };
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as AgentSessionCommandStorage } from "./storage-
|
|
1
|
+
import { t as AgentSessionCommandStorage } from "./storage-CAc1N6MN.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.18.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.18.0",
|
|
24
24
|
"zod": "^4.0.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|