@demicodes/shell 0.14.1 → 0.15.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/{host-DIg4RxcL.d.mts → host-B-oKG7x5.d.mts} +9 -0
- package/dist/host-fs.d.mts +14 -23
- package/dist/host-fs.mjs +11 -104
- package/dist/index.d.mts +38 -8
- package/dist/index.mjs +133 -133
- package/dist/{storage-CNMBL9_j.d.mts → storage-DYvEwSc9.d.mts} +1 -1
- package/dist/storage.d.mts +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
//#region src/host.d.ts
|
|
2
2
|
interface Host {
|
|
3
3
|
defaultCwd: string;
|
|
4
|
+
/**
|
|
5
|
+
* Directory where command artifacts (stdout.txt / stderr.txt / stdout.bin /
|
|
6
|
+
* meta.json) are written as plain files, laid out as
|
|
7
|
+
* `<dir>/<storageId>/<commandId>/`. Contract: the path is reachable through
|
|
8
|
+
* `fs` AND visible to processes started via `process.spawn` — one shared
|
|
9
|
+
* filesystem namespace, so any tool (portable or real) can read and search
|
|
10
|
+
* artifacts with ordinary file operations.
|
|
11
|
+
*/
|
|
12
|
+
commandArtifactsDir: string;
|
|
4
13
|
fs: HostFileSystem;
|
|
5
14
|
process: HostProcess;
|
|
6
15
|
store: HostStore;
|
package/dist/host-fs.d.mts
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import { t as Host } from "./host-
|
|
1
|
+
import { t as Host } from "./host-B-oKG7x5.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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
interface VirtualFileSystemProvider {
|
|
16
|
-
lookup(path: string): Promise<VirtualFileSystemNode | null> | VirtualFileSystemNode | null;
|
|
4
|
+
interface HostBackedFileSystemOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Ceiling for a single file read through this filesystem. Portable commands
|
|
7
|
+
* (cat/grep/rg/head/...) run in-process and load files whole, so one read of
|
|
8
|
+
* an oversized file becomes resident memory in the embedding host — a broad
|
|
9
|
+
* scan over a large tree can balloon the process by gigabytes. Oversized
|
|
10
|
+
* files fail loudly instead; real-process routes (`bash -c`) stay available
|
|
11
|
+
* for genuinely large files.
|
|
12
|
+
*/
|
|
13
|
+
maxFileReadBytes?: number;
|
|
17
14
|
}
|
|
18
|
-
/** Builds a virtual directory node; entry names containing a dot are treated as files. */
|
|
19
|
-
declare function virtualDirectory(names: string[]): VirtualFileSystemNode;
|
|
20
|
-
/** Builds a virtual file node from raw bytes. */
|
|
21
|
-
declare function virtualFile(content: Uint8Array): VirtualFileSystemNode;
|
|
22
15
|
declare class HostBackedFileSystem implements IFileSystem {
|
|
23
16
|
private readonly host;
|
|
24
|
-
private readonly
|
|
25
|
-
constructor(host: Host,
|
|
17
|
+
private readonly maxFileReadBytes;
|
|
18
|
+
constructor(host: Host, options?: HostBackedFileSystemOptions);
|
|
26
19
|
readFile(path: string, options?: ReadFileOptions | BufferEncoding): Promise<string>;
|
|
27
20
|
readFileBuffer(path: string): Promise<Uint8Array>;
|
|
28
21
|
writeFile(path: string, content: FileContent, options?: WriteFileOptions | BufferEncoding): Promise<void>;
|
|
@@ -44,8 +37,6 @@ declare class HostBackedFileSystem implements IFileSystem {
|
|
|
44
37
|
readlink(path: string): Promise<string>;
|
|
45
38
|
realpath(path: string): Promise<string>;
|
|
46
39
|
utimes(path: string, atime: Date, mtime: Date): Promise<void>;
|
|
47
|
-
private lookupVirtual;
|
|
48
|
-
private assertWritablePath;
|
|
49
40
|
}
|
|
50
41
|
//#endregion
|
|
51
|
-
export { HostBackedFileSystem,
|
|
42
|
+
export { HostBackedFileSystem, HostBackedFileSystemOptions };
|
package/dist/host-fs.mjs
CHANGED
|
@@ -1,78 +1,40 @@
|
|
|
1
1
|
import { decodeUtf8, encodeUtf8, isAbsolutePath, normalizePath } from "@demicodes/utils";
|
|
2
2
|
//#region src/host-fs.ts
|
|
3
|
-
|
|
4
|
-
function virtualDirectory(names) {
|
|
5
|
-
return {
|
|
6
|
-
kind: "directory",
|
|
7
|
-
entries: names.sort().map((name) => ({
|
|
8
|
-
name,
|
|
9
|
-
isFile: name.includes("."),
|
|
10
|
-
isDirectory: !name.includes("."),
|
|
11
|
-
isSymbolicLink: false
|
|
12
|
-
}))
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/** Builds a virtual file node from raw bytes. */
|
|
16
|
-
function virtualFile(content) {
|
|
17
|
-
return {
|
|
18
|
-
kind: "file",
|
|
19
|
-
content
|
|
20
|
-
};
|
|
21
|
-
}
|
|
3
|
+
const DEFAULT_FILE_READ_LIMIT_BYTES = 67108864;
|
|
22
4
|
var HostBackedFileSystem = class {
|
|
23
5
|
host;
|
|
24
|
-
|
|
25
|
-
constructor(host,
|
|
6
|
+
maxFileReadBytes;
|
|
7
|
+
constructor(host, options = {}) {
|
|
26
8
|
this.host = host;
|
|
27
|
-
this.
|
|
9
|
+
this.maxFileReadBytes = options.maxFileReadBytes ?? DEFAULT_FILE_READ_LIMIT_BYTES;
|
|
28
10
|
}
|
|
29
11
|
async readFile(path, options) {
|
|
30
12
|
return decodeBytes(await this.readFileBuffer(path), encodingFrom(options));
|
|
31
13
|
}
|
|
32
14
|
async readFileBuffer(path) {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
if (virtual.kind !== "file") throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
|
|
36
|
-
return virtual.content;
|
|
37
|
-
}
|
|
15
|
+
const stat = await this.host.fs.stat(path, { cwd: this.host.defaultCwd });
|
|
16
|
+
if (stat.isFile && stat.size > this.maxFileReadBytes) throw new Error(`EFBIG: file exceeds the ${this.maxFileReadBytes}-byte in-shell read limit (${stat.size} bytes), read '${path}'; read a slice instead, or process it with a real process via 'bash -c'`);
|
|
38
17
|
return this.host.fs.readFile(path, { cwd: this.host.defaultCwd });
|
|
39
18
|
}
|
|
40
19
|
async writeFile(path, content, options) {
|
|
41
|
-
this.assertWritablePath(path);
|
|
42
20
|
await this.host.fs.writeFile(path, encodeContent(content, encodingFrom(options)), { cwd: this.host.defaultCwd });
|
|
43
21
|
}
|
|
44
22
|
async appendFile(path, content, options) {
|
|
45
|
-
this.assertWritablePath(path);
|
|
46
23
|
await this.host.fs.appendFile(path, encodeContent(content, encodingFrom(options)), { cwd: this.host.defaultCwd });
|
|
47
24
|
}
|
|
48
25
|
async exists(path) {
|
|
49
|
-
if (isVirtualPath(path)) return await this.lookupVirtual(path) !== null;
|
|
50
26
|
return this.host.fs.exists(path, { cwd: this.host.defaultCwd });
|
|
51
27
|
}
|
|
52
28
|
async stat(path) {
|
|
53
|
-
const virtual = await this.lookupVirtual(path);
|
|
54
|
-
if (virtual) return virtualStat(virtual);
|
|
55
29
|
return toFsStat(await this.host.fs.stat(path, { cwd: this.host.defaultCwd }));
|
|
56
30
|
}
|
|
57
31
|
async lstat(path) {
|
|
58
|
-
const virtual = await this.lookupVirtual(path);
|
|
59
|
-
if (virtual) return virtualStat(virtual);
|
|
60
32
|
return toFsStat(await this.host.fs.lstat(path, { cwd: this.host.defaultCwd }));
|
|
61
33
|
}
|
|
62
34
|
async readdir(path) {
|
|
63
|
-
const virtual = await this.lookupVirtual(path);
|
|
64
|
-
if (virtual) {
|
|
65
|
-
if (virtual.kind !== "directory") throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
|
|
66
|
-
return virtual.entries.map((entry) => entry.name);
|
|
67
|
-
}
|
|
68
35
|
return this.host.fs.readdir(path, { cwd: this.host.defaultCwd });
|
|
69
36
|
}
|
|
70
37
|
async readdirWithFileTypes(path) {
|
|
71
|
-
const virtual = await this.lookupVirtual(path);
|
|
72
|
-
if (virtual) {
|
|
73
|
-
if (virtual.kind !== "directory") throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
|
|
74
|
-
return virtual.entries;
|
|
75
|
-
}
|
|
76
38
|
return (await this.host.fs.readdir(path, {
|
|
77
39
|
cwd: this.host.defaultCwd,
|
|
78
40
|
withFileTypes: true
|
|
@@ -86,14 +48,12 @@ var HostBackedFileSystem = class {
|
|
|
86
48
|
return [];
|
|
87
49
|
}
|
|
88
50
|
async mkdir(path, options) {
|
|
89
|
-
this.assertWritablePath(path);
|
|
90
51
|
await this.host.fs.mkdir(path, {
|
|
91
52
|
cwd: this.host.defaultCwd,
|
|
92
53
|
recursive: options?.recursive
|
|
93
54
|
});
|
|
94
55
|
}
|
|
95
56
|
async rm(path, options) {
|
|
96
|
-
this.assertWritablePath(path);
|
|
97
57
|
await this.host.fs.rm(path, {
|
|
98
58
|
cwd: this.host.defaultCwd,
|
|
99
59
|
recursive: options?.recursive,
|
|
@@ -101,62 +61,32 @@ var HostBackedFileSystem = class {
|
|
|
101
61
|
});
|
|
102
62
|
}
|
|
103
63
|
async cp(src, dest, options) {
|
|
104
|
-
this.assertWritablePath(dest);
|
|
105
|
-
if (isVirtualPath(src)) {
|
|
106
|
-
const virtual = await this.lookupVirtual(src);
|
|
107
|
-
if (!virtual) throw new Error(`ENOENT: no such file or directory, copyfile '${src}'`);
|
|
108
|
-
if (virtual.kind !== "file") throw new Error(`EISDIR: illegal operation on a directory, copyfile '${src}'`);
|
|
109
|
-
await this.host.fs.writeFile(dest, virtual.content, {
|
|
110
|
-
cwd: this.host.defaultCwd,
|
|
111
|
-
createParents: options?.recursive
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
64
|
await this.host.fs.cp(src, dest, {
|
|
116
65
|
cwd: this.host.defaultCwd,
|
|
117
66
|
recursive: options?.recursive
|
|
118
67
|
});
|
|
119
68
|
}
|
|
120
69
|
async mv(src, dest) {
|
|
121
|
-
this.assertWritablePath(src);
|
|
122
|
-
this.assertWritablePath(dest);
|
|
123
70
|
await this.host.fs.mv(src, dest, { cwd: this.host.defaultCwd });
|
|
124
71
|
}
|
|
125
72
|
async chmod(path, mode) {
|
|
126
|
-
this.assertWritablePath(path);
|
|
127
73
|
await this.host.fs.chmod(path, mode, { cwd: this.host.defaultCwd });
|
|
128
74
|
}
|
|
129
75
|
async symlink(target, linkPath) {
|
|
130
|
-
this.assertWritablePath(linkPath);
|
|
131
76
|
await this.host.fs.symlink(target, linkPath, { cwd: this.host.defaultCwd });
|
|
132
77
|
}
|
|
133
78
|
async link(existingPath, newPath) {
|
|
134
|
-
this.assertWritablePath(existingPath);
|
|
135
|
-
this.assertWritablePath(newPath);
|
|
136
79
|
await this.host.fs.link(existingPath, newPath, { cwd: this.host.defaultCwd });
|
|
137
80
|
}
|
|
138
81
|
async readlink(path) {
|
|
139
|
-
if (isVirtualPath(path)) throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
|
|
140
82
|
return this.host.fs.readlink(path, { cwd: this.host.defaultCwd });
|
|
141
83
|
}
|
|
142
84
|
async realpath(path) {
|
|
143
|
-
if (isVirtualPath(path)) {
|
|
144
|
-
if (!await this.lookupVirtual(path)) throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
|
|
145
|
-
return normalizePath(path);
|
|
146
|
-
}
|
|
147
85
|
return this.host.fs.realpath(path, { cwd: this.host.defaultCwd });
|
|
148
86
|
}
|
|
149
87
|
async utimes(path, atime, mtime) {
|
|
150
|
-
this.assertWritablePath(path);
|
|
151
88
|
await this.host.fs.utimes(path, atime, mtime, { cwd: this.host.defaultCwd });
|
|
152
89
|
}
|
|
153
|
-
async lookupVirtual(path) {
|
|
154
|
-
if (!this.virtualProvider || !isVirtualPath(path)) return null;
|
|
155
|
-
return await this.virtualProvider.lookup(normalizePath(path)) ?? null;
|
|
156
|
-
}
|
|
157
|
-
assertWritablePath(path) {
|
|
158
|
-
if (isVirtualPath(path)) throw new Error(`EROFS: read-only virtual filesystem, '${path}'`);
|
|
159
|
-
}
|
|
160
90
|
};
|
|
161
91
|
function encodingFrom(options) {
|
|
162
92
|
if (!options) return void 0;
|
|
@@ -166,13 +96,13 @@ function encodingFrom(options) {
|
|
|
166
96
|
function encodeContent(content, encoding) {
|
|
167
97
|
if (content instanceof Uint8Array) return content;
|
|
168
98
|
if (encoding === "binary" || encoding === "latin1") return latin1ToBytes(content);
|
|
169
|
-
if (encoding === "base64") return base64ToBytes
|
|
99
|
+
if (encoding === "base64") return base64ToBytes(content);
|
|
170
100
|
if (encoding === "hex") return hexToBytes(content);
|
|
171
101
|
return encodeUtf8(content);
|
|
172
102
|
}
|
|
173
103
|
function decodeBytes(bytes, encoding) {
|
|
174
104
|
if (encoding === "binary" || encoding === "latin1") return bytesToLatin1(bytes);
|
|
175
|
-
if (encoding === "base64") return bytesToBase64
|
|
105
|
+
if (encoding === "base64") return bytesToBase64(bytes);
|
|
176
106
|
if (encoding === "hex") return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
177
107
|
return decodeUtf8(bytes);
|
|
178
108
|
}
|
|
@@ -186,10 +116,10 @@ function bytesToLatin1(bytes) {
|
|
|
186
116
|
for (let index = 0; index < bytes.length; index += 8192) result += String.fromCharCode(...bytes.subarray(index, index + 8192));
|
|
187
117
|
return result;
|
|
188
118
|
}
|
|
189
|
-
function base64ToBytes
|
|
119
|
+
function base64ToBytes(content) {
|
|
190
120
|
return latin1ToBytes(atob(content));
|
|
191
121
|
}
|
|
192
|
-
function bytesToBase64
|
|
122
|
+
function bytesToBase64(bytes) {
|
|
193
123
|
return btoa(bytesToLatin1(bytes));
|
|
194
124
|
}
|
|
195
125
|
function hexToBytes(content) {
|
|
@@ -215,28 +145,5 @@ function toDirentEntry(value) {
|
|
|
215
145
|
isSymbolicLink: value.isSymbolicLink
|
|
216
146
|
};
|
|
217
147
|
}
|
|
218
|
-
function virtualStat(node) {
|
|
219
|
-
const now = /* @__PURE__ */ new Date(0);
|
|
220
|
-
if (node.kind === "directory") return {
|
|
221
|
-
isFile: false,
|
|
222
|
-
isDirectory: true,
|
|
223
|
-
isSymbolicLink: false,
|
|
224
|
-
mode: node.mode ?? 365,
|
|
225
|
-
size: 0,
|
|
226
|
-
mtime: node.mtime ?? now
|
|
227
|
-
};
|
|
228
|
-
return {
|
|
229
|
-
isFile: true,
|
|
230
|
-
isDirectory: false,
|
|
231
|
-
isSymbolicLink: false,
|
|
232
|
-
mode: node.mode ?? 292,
|
|
233
|
-
size: node.content.byteLength,
|
|
234
|
-
mtime: node.mtime ?? now
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function isVirtualPath(path) {
|
|
238
|
-
const normalized = normalizePath(path);
|
|
239
|
-
return normalized === "/@" || normalized.startsWith("/@/");
|
|
240
|
-
}
|
|
241
148
|
//#endregion
|
|
242
|
-
export { HostBackedFileSystem
|
|
149
|
+
export { HostBackedFileSystem };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-
|
|
2
|
-
import { HostBackedFileSystem,
|
|
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-
|
|
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-B-oKG7x5.mjs";
|
|
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-DYvEwSc9.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";
|
|
@@ -38,6 +38,9 @@ interface BackgroundJob {
|
|
|
38
38
|
handle: HostSpawnHandle;
|
|
39
39
|
stdoutBuffer: string;
|
|
40
40
|
stderrBuffer: string;
|
|
41
|
+
/** Chars discarded from the head of each buffer once it outgrew the capture limit. */
|
|
42
|
+
droppedStdoutChars: number;
|
|
43
|
+
droppedStderrChars: number;
|
|
41
44
|
stdoutPump: Promise<void>;
|
|
42
45
|
stderrPump: Promise<void>;
|
|
43
46
|
exitPromise: Promise<{
|
|
@@ -66,6 +69,10 @@ interface ForegroundProcess {
|
|
|
66
69
|
/** Interleaved visible chunks with running byte offsets, for merged replay. */
|
|
67
70
|
outputChunks: ShellOutputRecordChunk[];
|
|
68
71
|
outputBytes: number;
|
|
72
|
+
/** Total bytes ingested across both streams; the capture limit judges this. */
|
|
73
|
+
capturedBytes: number;
|
|
74
|
+
/** Set when the capture limit was breached: the process is killed and further chunks dropped. */
|
|
75
|
+
captureOverflowed: boolean;
|
|
69
76
|
audit: BashAuditEvent[];
|
|
70
77
|
stdoutPump: Promise<void>;
|
|
71
78
|
stderrPump: Promise<void>;
|
|
@@ -106,6 +113,19 @@ interface BashEnvironmentOptions {
|
|
|
106
113
|
* above, where models are known.
|
|
107
114
|
*/
|
|
108
115
|
maxBinaryBytes?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Ceiling for the bytes a single command's output capture may ingest.
|
|
118
|
+
*
|
|
119
|
+
* Distinct from `maxOutputBytes`, which sizes the rendered view: capture is
|
|
120
|
+
* what the environment holds in memory while a command runs, and it holds
|
|
121
|
+
* several copies (raw bytes for the pipe, text renders, replay chunks). A
|
|
122
|
+
* foreground process that produces more than this is SIGKILLed and the
|
|
123
|
+
* command fails with an explicit error; a background job keeps only the most
|
|
124
|
+
* recent bytes within the ceiling. Without this ceiling one stray
|
|
125
|
+
* `rg`/`cat` over a large tree balloons the embedding process by tens of
|
|
126
|
+
* gigabytes until the kernel OOM-kills it.
|
|
127
|
+
*/
|
|
128
|
+
maxCaptureBytes?: number;
|
|
109
129
|
}
|
|
110
130
|
interface ShellExecInput {
|
|
111
131
|
script: string;
|
|
@@ -207,6 +227,8 @@ type ShellCommandStatus = {
|
|
|
207
227
|
status: 'exited';
|
|
208
228
|
shellId: string;
|
|
209
229
|
commandId: string;
|
|
230
|
+
/** Real directory holding this command's artifact files. */
|
|
231
|
+
artifactDir: string;
|
|
210
232
|
exitCode: number;
|
|
211
233
|
stdout: ShellStreamView;
|
|
212
234
|
stderr: ShellStreamView;
|
|
@@ -221,6 +243,7 @@ type ShellCommandStatus = {
|
|
|
221
243
|
status: 'running';
|
|
222
244
|
shellId: string;
|
|
223
245
|
commandId: string;
|
|
246
|
+
artifactDir: string;
|
|
224
247
|
stdout: ShellStreamView;
|
|
225
248
|
stderr: ShellStreamView;
|
|
226
249
|
output: ShellOutputView;
|
|
@@ -230,6 +253,7 @@ type ShellCommandStatus = {
|
|
|
230
253
|
status: 'aborted';
|
|
231
254
|
shellId: string;
|
|
232
255
|
commandId: string;
|
|
256
|
+
artifactDir: string;
|
|
233
257
|
stdout: ShellStreamView;
|
|
234
258
|
stderr: ShellStreamView;
|
|
235
259
|
output: ShellOutputView;
|
|
@@ -246,6 +270,7 @@ declare class BashEnvironment {
|
|
|
246
270
|
private readonly initialEnv;
|
|
247
271
|
private readonly defaultOutputLimitBytes;
|
|
248
272
|
private readonly defaultBinaryLimitBytes;
|
|
273
|
+
private readonly captureLimitBytes;
|
|
249
274
|
private readonly shells;
|
|
250
275
|
private readonly defaultShellByAgentSessionId;
|
|
251
276
|
private readonly commandsById;
|
|
@@ -283,11 +308,7 @@ declare class BashEnvironment {
|
|
|
283
308
|
private collectAborted;
|
|
284
309
|
private collectAbortedWithoutForeground;
|
|
285
310
|
private commandStatus;
|
|
286
|
-
private lookupVirtualArtifact;
|
|
287
|
-
private commandArtifactIds;
|
|
288
|
-
private commandArtifact;
|
|
289
311
|
private persistCommandArtifact;
|
|
290
|
-
private syncRunningRecord;
|
|
291
312
|
}
|
|
292
313
|
//#endregion
|
|
293
314
|
//#region src/portable-commands.d.ts
|
|
@@ -308,6 +329,15 @@ declare class BashEnvironment {
|
|
|
308
329
|
* we call portable.
|
|
309
330
|
*/
|
|
310
331
|
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>;
|
|
311
341
|
/**
|
|
312
342
|
* Names registered commands must not shadow, derived from the actual portable
|
|
313
343
|
* command set plus interpreter builtins and pass-through system tools — not a
|
|
@@ -321,4 +351,4 @@ declare function shellQuote(value: string): string;
|
|
|
321
351
|
/** Picks a heredoc delimiter that does not collide with any line already in `body`. */
|
|
322
352
|
declare function heredocDelimiter(body: string): string;
|
|
323
353
|
//#endregion
|
|
324
|
-
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, HostDirent, HostFileStat, HostFileSystem, HostProcess, HostProcessOutputChunk, HostSpawnExit, HostSpawnHandle, HostSpawnParams, HostStore, MAX_TIMEOUT_MS, ParsedCommandInput, RESERVED_COMMAND_NAMES, ShellAbortInput, ShellCommandStatus, ShellExecInput, ShellOutputChunk, ShellOutputRecordChunk, ShellOutputView, ShellStatusInput, ShellStreamView, ShellWriteInput,
|
|
354
|
+
export { AgentSessionCommandStorage, BashAuditEvent, BashEnvironment, BashEnvironmentOptions, BinaryStdout, COMMAND_HELP_DEFAULTS, Command, CommandExecutionContext, CommandIO, CommandInputSpec, CommandMetadataRecord, CommandOutputSpec, CommandRegistry, CommandRunContext, CommandRunResult, CommandStdin, CommandStorage, DEMI_PORTABLE_COMMANDS, HOST_PREFERRED_SCAN_COMMANDS, Host, HostBackedFileSystem, HostBackedFileSystemOptions, HostDirent, HostFileStat, HostFileSystem, HostProcess, HostProcessOutputChunk, HostSpawnExit, HostSpawnHandle, HostSpawnParams, HostStore, MAX_TIMEOUT_MS, ParsedCommandInput, RESERVED_COMMAND_NAMES, ShellAbortInput, ShellCommandStatus, ShellExecInput, ShellOutputChunk, ShellOutputRecordChunk, ShellOutputView, ShellStatusInput, ShellStreamView, ShellWriteInput, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { HostBackedFileSystem
|
|
1
|
+
import { HostBackedFileSystem } from "./host-fs.mjs";
|
|
2
2
|
import { AgentSessionCommandStorage } from "./storage.mjs";
|
|
3
|
-
import { asError,
|
|
3
|
+
import { asError, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, 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";
|
|
@@ -46,6 +46,19 @@ const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
46
46
|
* we call portable.
|
|
47
47
|
*/
|
|
48
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.
|
|
56
|
+
*/
|
|
57
|
+
const HOST_PREFERRED_SCAN_COMMANDS = /* @__PURE__ */ new Set([
|
|
58
|
+
"rg",
|
|
59
|
+
"grep",
|
|
60
|
+
"find"
|
|
61
|
+
]);
|
|
49
62
|
/** Shell language words and builtins the interpreter itself owns. */
|
|
50
63
|
const SHELL_BUILTIN_NAMES = [
|
|
51
64
|
".",
|
|
@@ -501,7 +514,16 @@ function createOutputSinks(fs, cwd, redirections) {
|
|
|
501
514
|
}
|
|
502
515
|
return routes;
|
|
503
516
|
}
|
|
504
|
-
function recordForegroundChunk(foreground, sourceFd, chunk) {
|
|
517
|
+
function recordForegroundChunk(foreground, sourceFd, chunk, captureLimitBytes) {
|
|
518
|
+
if (foreground.captureOverflowed) return;
|
|
519
|
+
if (foreground.capturedBytes + chunk.byteLength > captureLimitBytes) {
|
|
520
|
+
foreground.captureOverflowed = true;
|
|
521
|
+
foreground.rawStdoutBuffer = "";
|
|
522
|
+
foreground.rawStdoutBytes = [];
|
|
523
|
+
foreground.handle.kill("SIGKILL").catch(() => {});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
foreground.capturedBytes += chunk.byteLength;
|
|
505
527
|
const text = decodeUtf8(chunk);
|
|
506
528
|
foreground.lastOutputAt = Date.now();
|
|
507
529
|
if (sourceFd === 1) {
|
|
@@ -609,39 +631,63 @@ function appendVisibleChunk(foreground, targetFd, text, byteLength) {
|
|
|
609
631
|
//#endregion
|
|
610
632
|
//#region src/command-artifact-store.ts
|
|
611
633
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
634
|
+
* Writes shell command artifacts as plain files under
|
|
635
|
+
* `host.commandArtifactsDir/<storageId>/<commandId>/` — one shared filesystem
|
|
636
|
+
* namespace with the processes the shell spawns, so any tool (portable or
|
|
637
|
+
* real) reads and searches artifacts with ordinary file operations.
|
|
616
638
|
*/
|
|
617
639
|
var CommandArtifactStore = class {
|
|
618
|
-
|
|
619
|
-
storageById = /* @__PURE__ */ new Map();
|
|
640
|
+
host;
|
|
620
641
|
released = /* @__PURE__ */ new Set();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
this.storageById.set(commandStorageId, storage);
|
|
630
|
-
return storage;
|
|
631
|
-
}
|
|
632
|
-
/** Whether a command's artifact has been released (tombstoned). */
|
|
642
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
643
|
+
constructor(host) {
|
|
644
|
+
this.host = host;
|
|
645
|
+
}
|
|
646
|
+
dirFor(commandStorageId, commandId) {
|
|
647
|
+
return `${this.host.commandArtifactsDir}/${commandStorageId}/${commandId}`;
|
|
648
|
+
}
|
|
649
|
+
/** Whether a command's artifact has been released (removed from disk). */
|
|
633
650
|
isReleased(commandStorageId, commandId) {
|
|
634
651
|
return this.released.has(this.key(commandStorageId, commandId));
|
|
635
652
|
}
|
|
636
|
-
/** Persists
|
|
637
|
-
persist(commandStorageId, commandId,
|
|
638
|
-
|
|
639
|
-
this.
|
|
653
|
+
/** Persists the artifact files unless the command has already been released. */
|
|
654
|
+
persist(commandStorageId, commandId, files) {
|
|
655
|
+
const key = this.key(commandStorageId, commandId);
|
|
656
|
+
if (this.released.has(key)) return;
|
|
657
|
+
const dir = this.dirFor(commandStorageId, commandId);
|
|
658
|
+
this.chain(key, async () => {
|
|
659
|
+
if (this.released.has(key)) return;
|
|
660
|
+
const writeOptions = {
|
|
661
|
+
cwd: this.host.defaultCwd,
|
|
662
|
+
createParents: true
|
|
663
|
+
};
|
|
664
|
+
await this.host.fs.writeFile(`${dir}/meta.json`, encodeUtf8(files.meta), writeOptions);
|
|
665
|
+
await this.host.fs.writeFile(`${dir}/stdout.txt`, encodeUtf8(files.stdout), writeOptions);
|
|
666
|
+
await this.host.fs.writeFile(`${dir}/stderr.txt`, encodeUtf8(files.stderr), writeOptions);
|
|
667
|
+
if (files.stdoutBin) await this.host.fs.writeFile(`${dir}/stdout.bin`, files.stdoutBin, writeOptions);
|
|
668
|
+
});
|
|
640
669
|
}
|
|
641
|
-
/** Tombstones a command and removes its
|
|
670
|
+
/** Tombstones a command and removes its artifact directory. */
|
|
642
671
|
async release(commandStorageId, commandId) {
|
|
643
|
-
this.
|
|
644
|
-
|
|
672
|
+
const key = this.key(commandStorageId, commandId);
|
|
673
|
+
this.released.add(key);
|
|
674
|
+
await new Promise((resolve) => {
|
|
675
|
+
this.chain(key, async () => {
|
|
676
|
+
await this.host.fs.rm(this.dirFor(commandStorageId, commandId), {
|
|
677
|
+
cwd: this.host.defaultCwd,
|
|
678
|
+
recursive: true,
|
|
679
|
+
force: true
|
|
680
|
+
}).catch(() => {});
|
|
681
|
+
resolve();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
chain(key, work) {
|
|
686
|
+
const next = (this.writeChains.get(key) ?? Promise.resolve()).then(work).catch(() => {});
|
|
687
|
+
this.writeChains.set(key, next);
|
|
688
|
+
next.finally(() => {
|
|
689
|
+
if (this.writeChains.get(key) === next) this.writeChains.delete(key);
|
|
690
|
+
});
|
|
645
691
|
}
|
|
646
692
|
key(commandStorageId, commandId) {
|
|
647
693
|
return `${commandStorageId}\0${commandId}`;
|
|
@@ -778,6 +824,13 @@ const DEFAULT_OUTPUT_LIMIT_BYTES = 1048576;
|
|
|
778
824
|
* while still bounding a runaway producer.
|
|
779
825
|
*/
|
|
780
826
|
const DEFAULT_BINARY_LIMIT_BYTES = 16777216;
|
|
827
|
+
/**
|
|
828
|
+
* Ceiling for a single command's in-memory output capture. Sized to the same
|
|
829
|
+
* order as the in-shell file read limit: the execution model buffers whole
|
|
830
|
+
* command outputs (in several copies), so this is a hard memory-safety bound
|
|
831
|
+
* on the embedding process, not a view budget.
|
|
832
|
+
*/
|
|
833
|
+
const DEFAULT_CAPTURE_LIMIT_BYTES = 67108864;
|
|
781
834
|
/** Upper bound for a single exec observation window (also the command-bridge wait ceiling). */
|
|
782
835
|
const MAX_TIMEOUT_MS = 6e5;
|
|
783
836
|
var BashEnvironment = class {
|
|
@@ -788,19 +841,21 @@ var BashEnvironment = class {
|
|
|
788
841
|
initialEnv;
|
|
789
842
|
defaultOutputLimitBytes;
|
|
790
843
|
defaultBinaryLimitBytes;
|
|
844
|
+
captureLimitBytes;
|
|
791
845
|
shells = /* @__PURE__ */ new Map();
|
|
792
846
|
defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
|
|
793
847
|
commandsById = /* @__PURE__ */ new Map();
|
|
794
848
|
artifacts;
|
|
795
849
|
constructor(options) {
|
|
796
850
|
this.host = options.host;
|
|
797
|
-
this.artifacts = new CommandArtifactStore(this.host
|
|
851
|
+
this.artifacts = new CommandArtifactStore(this.host);
|
|
798
852
|
this.commands = options.commands ?? new CommandRegistry();
|
|
799
853
|
this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
800
854
|
this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
801
855
|
this.initialEnv = options.initialEnv ?? {};
|
|
802
856
|
this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
|
|
803
857
|
this.defaultBinaryLimitBytes = options.maxBinaryBytes ?? DEFAULT_BINARY_LIMIT_BYTES;
|
|
858
|
+
this.captureLimitBytes = options.maxCaptureBytes ?? DEFAULT_CAPTURE_LIMIT_BYTES;
|
|
804
859
|
}
|
|
805
860
|
getShell(shellId) {
|
|
806
861
|
return this.shells.get(shellId) ?? null;
|
|
@@ -931,7 +986,7 @@ var BashEnvironment = class {
|
|
|
931
986
|
const id = this.shellIdFactory();
|
|
932
987
|
const commandStorageId = agentSessionId ?? id;
|
|
933
988
|
const cwd = initialCwd ?? this.host.defaultCwd;
|
|
934
|
-
const fs = new HostBackedFileSystem(this.host
|
|
989
|
+
const fs = new HostBackedFileSystem(this.host);
|
|
935
990
|
const env = /* @__PURE__ */ new Map();
|
|
936
991
|
for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
|
|
937
992
|
env.set("PWD", cwd);
|
|
@@ -1025,7 +1080,7 @@ var BashEnvironment = class {
|
|
|
1025
1080
|
for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage, this.host));
|
|
1026
1081
|
session.abortController = new AbortController();
|
|
1027
1082
|
const limits = resolveLimits({
|
|
1028
|
-
maxOutputSize:
|
|
1083
|
+
maxOutputSize: this.captureLimitBytes,
|
|
1029
1084
|
maxCommandCount: 1e6,
|
|
1030
1085
|
maxLoopIterations: 1e6,
|
|
1031
1086
|
maxCallDepth: 1e3,
|
|
@@ -1097,6 +1152,7 @@ var BashEnvironment = class {
|
|
|
1097
1152
|
id,
|
|
1098
1153
|
shellId: session.id,
|
|
1099
1154
|
commandStorageId: session.commandStorageId,
|
|
1155
|
+
artifactDir: this.artifacts.dirFor(session.commandStorageId, id),
|
|
1100
1156
|
script,
|
|
1101
1157
|
startedAt: now,
|
|
1102
1158
|
lastOutputAt: now,
|
|
@@ -1135,15 +1191,26 @@ var BashEnvironment = class {
|
|
|
1135
1191
|
handle,
|
|
1136
1192
|
stdoutBuffer: "",
|
|
1137
1193
|
stderrBuffer: "",
|
|
1194
|
+
droppedStdoutChars: 0,
|
|
1195
|
+
droppedStderrChars: 0,
|
|
1138
1196
|
stdoutPump: Promise.resolve(),
|
|
1139
1197
|
stderrPump: Promise.resolve(),
|
|
1140
1198
|
exitPromise: handle.wait()
|
|
1141
1199
|
};
|
|
1200
|
+
const retainLimit = this.defaultOutputLimitBytes;
|
|
1142
1201
|
job.stdoutPump = pumpStream(handle.stdout, (chunk) => {
|
|
1143
1202
|
job.stdoutBuffer += decodeUtf8(chunk);
|
|
1203
|
+
if (job.stdoutBuffer.length > retainLimit) {
|
|
1204
|
+
job.droppedStdoutChars += job.stdoutBuffer.length - retainLimit;
|
|
1205
|
+
job.stdoutBuffer = job.stdoutBuffer.slice(-retainLimit);
|
|
1206
|
+
}
|
|
1144
1207
|
});
|
|
1145
1208
|
job.stderrPump = pumpStream(handle.stderr, (chunk) => {
|
|
1146
1209
|
job.stderrBuffer += decodeUtf8(chunk);
|
|
1210
|
+
if (job.stderrBuffer.length > retainLimit) {
|
|
1211
|
+
job.droppedStderrChars += job.stderrBuffer.length - retainLimit;
|
|
1212
|
+
job.stderrBuffer = job.stderrBuffer.slice(-retainLimit);
|
|
1213
|
+
}
|
|
1147
1214
|
});
|
|
1148
1215
|
session.backgroundJobs.set(id, job);
|
|
1149
1216
|
session.state.lastBackgroundPid = id;
|
|
@@ -1191,7 +1258,8 @@ var BashEnvironment = class {
|
|
|
1191
1258
|
await Promise.allSettled([job.stdoutPump, job.stderrPump]);
|
|
1192
1259
|
session.backgroundJobs.delete(id);
|
|
1193
1260
|
const exitCode = exit.exitCode ?? 127;
|
|
1194
|
-
const
|
|
1261
|
+
const stdout = job.droppedStdoutChars > 0 ? `[... dropped ${job.droppedStdoutChars} chars of earlier stdout over the capture limit ...]\n${job.stdoutBuffer}` : job.stdoutBuffer;
|
|
1262
|
+
const stderr = exit.exitCode === null && job.stderrBuffer.length === 0 ? `${job.command}: ${exit.signal ?? "command not found"}\n` : job.droppedStderrChars > 0 ? `[... dropped ${job.droppedStderrChars} chars of earlier stderr over the capture limit ...]\n${job.stderrBuffer}` : job.stderrBuffer;
|
|
1195
1263
|
session.accumulator.audit.push({
|
|
1196
1264
|
kind: "system-command",
|
|
1197
1265
|
name: job.command,
|
|
@@ -1200,7 +1268,7 @@ var BashEnvironment = class {
|
|
|
1200
1268
|
exitCode
|
|
1201
1269
|
});
|
|
1202
1270
|
return {
|
|
1203
|
-
stdout
|
|
1271
|
+
stdout,
|
|
1204
1272
|
stderr,
|
|
1205
1273
|
exitCode
|
|
1206
1274
|
};
|
|
@@ -1321,6 +1389,8 @@ var BashEnvironment = class {
|
|
|
1321
1389
|
stderrBuffer: "",
|
|
1322
1390
|
outputChunks: [],
|
|
1323
1391
|
outputBytes: 0,
|
|
1392
|
+
capturedBytes: 0,
|
|
1393
|
+
captureOverflowed: false,
|
|
1324
1394
|
audit: [{
|
|
1325
1395
|
kind: "system-command",
|
|
1326
1396
|
name: command,
|
|
@@ -1340,18 +1410,18 @@ var BashEnvironment = class {
|
|
|
1340
1410
|
if (opts.stdinProvided) await handle.closeStdin();
|
|
1341
1411
|
if (handle.output) {
|
|
1342
1412
|
foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
|
|
1343
|
-
recordForegroundChunk(foreground, chunk.stream === "stdout" ? 1 : 2, chunk.chunk);
|
|
1413
|
+
recordForegroundChunk(foreground, chunk.stream === "stdout" ? 1 : 2, chunk.chunk, this.captureLimitBytes);
|
|
1344
1414
|
});
|
|
1345
1415
|
foreground.stderrPump = Promise.resolve();
|
|
1346
1416
|
} else {
|
|
1347
|
-
foreground.stdoutPump = pumpStream(handle.stdout, (chunk) => recordForegroundChunk(foreground, 1, chunk));
|
|
1348
|
-
foreground.stderrPump = pumpStream(handle.stderr, (chunk) => recordForegroundChunk(foreground, 2, chunk));
|
|
1417
|
+
foreground.stdoutPump = pumpStream(handle.stdout, (chunk) => recordForegroundChunk(foreground, 1, chunk, this.captureLimitBytes));
|
|
1418
|
+
foreground.stderrPump = pumpStream(handle.stderr, (chunk) => recordForegroundChunk(foreground, 2, chunk, this.captureLimitBytes));
|
|
1349
1419
|
}
|
|
1350
1420
|
const exit = await foreground.exitPromise;
|
|
1351
1421
|
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1352
|
-
const stdout = decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1353
|
-
const exitCode = exit.exitCode ?? 127;
|
|
1354
|
-
const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
|
|
1422
|
+
const stdout = foreground.captureOverflowed ? "" : decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1423
|
+
const exitCode = foreground.captureOverflowed ? 137 : exit.exitCode ?? 127;
|
|
1424
|
+
const 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` : exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
|
|
1355
1425
|
foreground.audit[0] = {
|
|
1356
1426
|
kind: "system-command",
|
|
1357
1427
|
name: command,
|
|
@@ -1431,7 +1501,8 @@ var BashEnvironment = class {
|
|
|
1431
1501
|
totalBytes: bytes.length,
|
|
1432
1502
|
limitBytes: cap
|
|
1433
1503
|
};
|
|
1434
|
-
stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at
|
|
1504
|
+
stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte binary limit` : ""}; raw bytes at ${record.artifactDir}/stdout.bin>\n`;
|
|
1505
|
+
record.pendingBinaryArtifact = bytes;
|
|
1435
1506
|
}
|
|
1436
1507
|
}
|
|
1437
1508
|
const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
|
|
@@ -1511,6 +1582,7 @@ var BashEnvironment = class {
|
|
|
1511
1582
|
const base = {
|
|
1512
1583
|
shellId: record.shellId,
|
|
1513
1584
|
commandId: record.id,
|
|
1585
|
+
artifactDir: record.artifactDir,
|
|
1514
1586
|
stdout,
|
|
1515
1587
|
stderr,
|
|
1516
1588
|
output,
|
|
@@ -1538,70 +1610,24 @@ var BashEnvironment = class {
|
|
|
1538
1610
|
status: "running"
|
|
1539
1611
|
};
|
|
1540
1612
|
}
|
|
1541
|
-
async lookupVirtualArtifact(commandStorageId, path) {
|
|
1542
|
-
const parts = path.split("/").filter(Boolean);
|
|
1543
|
-
if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
|
|
1544
|
-
if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(commandStorageId));
|
|
1545
|
-
if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
|
|
1546
|
-
const artifact = await this.commandArtifact(commandStorageId, parts[2]);
|
|
1547
|
-
if (!artifact) return null;
|
|
1548
|
-
const entries = [
|
|
1549
|
-
"meta.json",
|
|
1550
|
-
"stderr.txt",
|
|
1551
|
-
"stdout.txt"
|
|
1552
|
-
];
|
|
1553
|
-
if (artifact.stdoutBinary) entries.push("stdout.bin");
|
|
1554
|
-
return virtualDirectory(entries);
|
|
1555
|
-
}
|
|
1556
|
-
if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
|
|
1557
|
-
const artifact = await this.commandArtifact(commandStorageId, parts[2]);
|
|
1558
|
-
if (!artifact) return null;
|
|
1559
|
-
const fileName = parts[3];
|
|
1560
|
-
if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
|
|
1561
|
-
if (fileName === "stdout.bin" && artifact.stdoutBinary) return virtualFile(base64ToBytes(artifact.stdoutBinary.base64));
|
|
1562
|
-
if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
|
|
1563
|
-
if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
|
|
1564
|
-
return null;
|
|
1565
|
-
}
|
|
1566
|
-
async commandArtifactIds(commandStorageId) {
|
|
1567
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1568
|
-
for (const record of this.commandsById.values()) if (record.commandStorageId === commandStorageId && !this.artifacts.isReleased(commandStorageId, record.id)) ids.add(record.id);
|
|
1569
|
-
const keys = await this.artifacts.storageFor(commandStorageId).list("commands").catch(() => []);
|
|
1570
|
-
for (const key of keys) {
|
|
1571
|
-
const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
|
|
1572
|
-
if (match && !this.artifacts.isReleased(commandStorageId, match[1])) ids.add(match[1]);
|
|
1573
|
-
}
|
|
1574
|
-
return [...ids];
|
|
1575
|
-
}
|
|
1576
|
-
async commandArtifact(commandStorageId, commandId) {
|
|
1577
|
-
if (this.artifacts.isReleased(commandStorageId, commandId)) return null;
|
|
1578
|
-
const record = this.commandsById.get(commandId);
|
|
1579
|
-
if (record?.commandStorageId === commandStorageId) {
|
|
1580
|
-
this.syncRunningRecord(record);
|
|
1581
|
-
return commandArtifactFromRecord(record);
|
|
1582
|
-
}
|
|
1583
|
-
const value = await this.artifacts.storageFor(commandStorageId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
|
|
1584
|
-
return isCommandArtifact(value) ? value : null;
|
|
1585
|
-
}
|
|
1586
1613
|
persistCommandArtifact(record) {
|
|
1587
1614
|
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
|
|
1588
1615
|
if (record.persistedFingerprint === fingerprint) return;
|
|
1589
1616
|
record.persistedFingerprint = fingerprint;
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
record.
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
record.lastOutputAt = foreground.lastOutputAt;
|
|
1599
|
-
}
|
|
1617
|
+
const stdoutBin = record.pendingBinaryArtifact;
|
|
1618
|
+
record.pendingBinaryArtifact = void 0;
|
|
1619
|
+
this.artifacts.persist(record.commandStorageId, record.id, {
|
|
1620
|
+
meta: `${JSON.stringify(commandArtifactMeta(record), null, 2)}\n`,
|
|
1621
|
+
stdout: record.stdout,
|
|
1622
|
+
stderr: record.stderr,
|
|
1623
|
+
...stdoutBin ? { stdoutBin } : {}
|
|
1624
|
+
});
|
|
1600
1625
|
}
|
|
1601
1626
|
};
|
|
1602
1627
|
function createPortableCommands(session) {
|
|
1603
1628
|
return createLazyCommands([...DEMI_PORTABLE_COMMANDS]).map((command) => ({
|
|
1604
1629
|
...command,
|
|
1630
|
+
preferHostSpawn: HOST_PREFERRED_SCAN_COMMANDS.has(command.name),
|
|
1605
1631
|
execute: async (args, ctx) => {
|
|
1606
1632
|
const result = await command.execute(args, ctx);
|
|
1607
1633
|
session.accumulator.audit.push({
|
|
@@ -1636,7 +1662,7 @@ function streamView(record, stream, explicitOffset, maxOutputBytes) {
|
|
|
1636
1662
|
if (explicitOffset === void 0) if (stream === "stdout") record.stdoutOffset = nextOffset;
|
|
1637
1663
|
else record.stderrOffset = nextOffset;
|
|
1638
1664
|
return {
|
|
1639
|
-
path:
|
|
1665
|
+
path: `${record.artifactDir}/${stream}.txt`,
|
|
1640
1666
|
offset: nextOffset,
|
|
1641
1667
|
delta,
|
|
1642
1668
|
tail: tailString(text),
|
|
@@ -1671,7 +1697,7 @@ function mergedOutputView(record, explicitOffset, maxOutputBytes) {
|
|
|
1671
1697
|
const truncated = nextOffset < totalBytes;
|
|
1672
1698
|
if (explicitOffset === void 0) record.outputOffset = nextOffset;
|
|
1673
1699
|
return {
|
|
1674
|
-
path:
|
|
1700
|
+
path: record.artifactDir,
|
|
1675
1701
|
offset: nextOffset,
|
|
1676
1702
|
text,
|
|
1677
1703
|
tail: tailOutputText(record.outputChunks),
|
|
@@ -1698,55 +1724,29 @@ function ensureRecordOutputCoverage(record) {
|
|
|
1698
1724
|
appendRecordOutput(record, "stdout", record.stdout);
|
|
1699
1725
|
appendRecordOutput(record, "stderr", record.stderr);
|
|
1700
1726
|
}
|
|
1701
|
-
function
|
|
1727
|
+
function commandArtifactMeta(record) {
|
|
1702
1728
|
return {
|
|
1703
1729
|
status: record.status,
|
|
1704
1730
|
shellId: record.shellId,
|
|
1705
1731
|
commandId: record.id,
|
|
1732
|
+
script: record.script,
|
|
1706
1733
|
startedAt: record.startedAt,
|
|
1707
1734
|
lastOutputAt: record.lastOutputAt,
|
|
1708
1735
|
exitCode: record.exitCode ?? null,
|
|
1709
|
-
stdout: record.stdout,
|
|
1710
|
-
stderr: record.stderr,
|
|
1711
|
-
...record.binaryStdout ? { stdoutBinary: {
|
|
1712
|
-
base64: bytesToBase64(record.binaryStdout.data),
|
|
1713
|
-
truncated: record.binaryStdout.truncated,
|
|
1714
|
-
totalBytes: record.binaryStdout.totalBytes
|
|
1715
|
-
} } : {}
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
function commandArtifactMeta(artifact) {
|
|
1719
|
-
const stdoutPath = `/@/commands/${artifact.commandId}/stdout.txt`;
|
|
1720
|
-
const stderrPath = `/@/commands/${artifact.commandId}/stderr.txt`;
|
|
1721
|
-
return {
|
|
1722
|
-
status: artifact.status,
|
|
1723
|
-
shellId: artifact.shellId,
|
|
1724
|
-
commandId: artifact.commandId,
|
|
1725
|
-
startedAt: artifact.startedAt,
|
|
1726
|
-
lastOutputAt: artifact.lastOutputAt,
|
|
1727
|
-
runningMs: Date.now() - artifact.startedAt,
|
|
1728
|
-
idleMs: Date.now() - artifact.lastOutputAt,
|
|
1729
|
-
exitCode: artifact.exitCode,
|
|
1730
1736
|
stdout: {
|
|
1731
|
-
path:
|
|
1732
|
-
bytes: utf8Bytes(
|
|
1737
|
+
path: `${record.artifactDir}/stdout.txt`,
|
|
1738
|
+
bytes: utf8Bytes(record.stdout)
|
|
1733
1739
|
},
|
|
1734
1740
|
stderr: {
|
|
1735
|
-
path:
|
|
1736
|
-
bytes: utf8Bytes(
|
|
1741
|
+
path: `${record.artifactDir}/stderr.txt`,
|
|
1742
|
+
bytes: utf8Bytes(record.stderr)
|
|
1737
1743
|
},
|
|
1738
|
-
...
|
|
1739
|
-
path:
|
|
1740
|
-
bytes:
|
|
1741
|
-
truncated: artifact.stdoutBinary.truncated
|
|
1744
|
+
...record.binaryStdout ? { stdoutBinary: {
|
|
1745
|
+
path: `${record.artifactDir}/stdout.bin`,
|
|
1746
|
+
bytes: record.binaryStdout.totalBytes
|
|
1742
1747
|
} } : {}
|
|
1743
1748
|
};
|
|
1744
1749
|
}
|
|
1745
|
-
function isCommandArtifact(value) {
|
|
1746
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1747
|
-
const record = value;
|
|
1748
|
-
return (record.status === "running" || record.status === "exited" || record.status === "aborted") && typeof record.shellId === "string" && typeof record.commandId === "string" && typeof record.startedAt === "number" && typeof record.lastOutputAt === "number" && (typeof record.exitCode === "number" || record.exitCode === null) && typeof record.stdout === "string" && typeof record.stderr === "string";
|
|
1749
|
-
}
|
|
1750
1750
|
function tailOutputText(chunks) {
|
|
1751
1751
|
const maxChars = 4096;
|
|
1752
1752
|
let text = "";
|
|
@@ -1774,4 +1774,4 @@ function heredocDelimiter(body) {
|
|
|
1774
1774
|
return delimiter;
|
|
1775
1775
|
}
|
|
1776
1776
|
//#endregion
|
|
1777
|
-
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote
|
|
1777
|
+
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HOST_PREFERRED_SCAN_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote };
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as AgentSessionCommandStorage } from "./storage-
|
|
1
|
+
import { t as AgentSessionCommandStorage } from "./storage-DYvEwSc9.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.15.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.0
|
|
23
|
-
"@demicodes/utils": "^0.
|
|
22
|
+
"@demicodes/just-bash": "^3.1.0-demi.3",
|
|
23
|
+
"@demicodes/utils": "^0.15.0",
|
|
24
24
|
"zod": "^4.0.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|