@demicodes/shell 0.1.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/LICENSE +201 -0
- package/README.md +14 -0
- package/dist/host-BfjoIvF3.d.mts +120 -0
- package/dist/host-fs.d.mts +52 -0
- package/dist/host-fs.mjs +242 -0
- package/dist/index.d.mts +296 -0
- package/dist/index.mjs +1757 -0
- package/dist/storage-DJxxqyZA.d.mts +111 -0
- package/dist/storage.d.mts +2 -0
- package/dist/storage.mjs +37 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
import { HostBackedFileSystem, virtualDirectory, virtualFile } from "./host-fs.mjs";
|
|
2
|
+
import { AgentSessionCommandStorage } from "./storage.mjs";
|
|
3
|
+
import { asError, concatBytes, decodeUtf8, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ArithmeticError, BadSubstitutionError, ExecutionLimitError, ExitError, Interpreter } from "@demicodes/just-bash/interpreter";
|
|
6
|
+
import { createLazyCommands } from "@demicodes/just-bash/commands";
|
|
7
|
+
import { decodeBytesToUtf8, unsafeBytesFromLatin1 } from "@demicodes/just-bash/encoding";
|
|
8
|
+
import { parse } from "@demicodes/just-bash/parser";
|
|
9
|
+
import { ParseException } from "@demicodes/just-bash/parser/types";
|
|
10
|
+
import { LexerError } from "@demicodes/just-bash/parser/lexer";
|
|
11
|
+
import { resolveLimits } from "@demicodes/just-bash/limits";
|
|
12
|
+
//#region src/portable-commands.ts
|
|
13
|
+
/**
|
|
14
|
+
* Fork portable commands BashEnvironment registers in every shell, so
|
|
15
|
+
* cat/ls/grep-class tools work without local coreutils on any Host backend.
|
|
16
|
+
*/
|
|
17
|
+
const DEMI_PORTABLE_COMMANDS = [
|
|
18
|
+
"cat",
|
|
19
|
+
"ls",
|
|
20
|
+
"mkdir",
|
|
21
|
+
"rmdir",
|
|
22
|
+
"touch",
|
|
23
|
+
"rm",
|
|
24
|
+
"cp",
|
|
25
|
+
"mv",
|
|
26
|
+
"ln",
|
|
27
|
+
"chmod",
|
|
28
|
+
"readlink",
|
|
29
|
+
"head",
|
|
30
|
+
"tail",
|
|
31
|
+
"wc",
|
|
32
|
+
"stat",
|
|
33
|
+
"grep",
|
|
34
|
+
"fgrep",
|
|
35
|
+
"egrep",
|
|
36
|
+
"rg",
|
|
37
|
+
"sed",
|
|
38
|
+
"awk",
|
|
39
|
+
"sort",
|
|
40
|
+
"uniq",
|
|
41
|
+
"comm",
|
|
42
|
+
"cut",
|
|
43
|
+
"paste",
|
|
44
|
+
"tr",
|
|
45
|
+
"rev",
|
|
46
|
+
"nl",
|
|
47
|
+
"fold",
|
|
48
|
+
"expand",
|
|
49
|
+
"unexpand",
|
|
50
|
+
"strings",
|
|
51
|
+
"column",
|
|
52
|
+
"join",
|
|
53
|
+
"tee",
|
|
54
|
+
"find",
|
|
55
|
+
"basename",
|
|
56
|
+
"dirname",
|
|
57
|
+
"tree",
|
|
58
|
+
"du",
|
|
59
|
+
"jq",
|
|
60
|
+
"base64",
|
|
61
|
+
"diff",
|
|
62
|
+
"seq",
|
|
63
|
+
"expr",
|
|
64
|
+
"md5sum",
|
|
65
|
+
"sha1sum",
|
|
66
|
+
"sha256sum",
|
|
67
|
+
"file",
|
|
68
|
+
"tac",
|
|
69
|
+
"od"
|
|
70
|
+
];
|
|
71
|
+
/** Shell language words and builtins the interpreter itself owns. */
|
|
72
|
+
const SHELL_BUILTIN_NAMES = [
|
|
73
|
+
".",
|
|
74
|
+
"bash",
|
|
75
|
+
"break",
|
|
76
|
+
"cd",
|
|
77
|
+
"command",
|
|
78
|
+
"continue",
|
|
79
|
+
"echo",
|
|
80
|
+
"exit",
|
|
81
|
+
"export",
|
|
82
|
+
"jobs",
|
|
83
|
+
"local",
|
|
84
|
+
"popd",
|
|
85
|
+
"printf",
|
|
86
|
+
"pushd",
|
|
87
|
+
"read",
|
|
88
|
+
"return",
|
|
89
|
+
"set",
|
|
90
|
+
"sh",
|
|
91
|
+
"shift",
|
|
92
|
+
"source",
|
|
93
|
+
"test",
|
|
94
|
+
"unset",
|
|
95
|
+
"wait"
|
|
96
|
+
];
|
|
97
|
+
/** Ecosystem tools the model expects to reach through Host.process.spawn. */
|
|
98
|
+
const SYSTEM_TOOL_NAMES = [
|
|
99
|
+
"bun",
|
|
100
|
+
"cargo",
|
|
101
|
+
"docker",
|
|
102
|
+
"git",
|
|
103
|
+
"go",
|
|
104
|
+
"node",
|
|
105
|
+
"npm",
|
|
106
|
+
"pnpm",
|
|
107
|
+
"python",
|
|
108
|
+
"python3",
|
|
109
|
+
"ruby",
|
|
110
|
+
"rustc",
|
|
111
|
+
"xargs",
|
|
112
|
+
"yarn",
|
|
113
|
+
"yq"
|
|
114
|
+
];
|
|
115
|
+
/**
|
|
116
|
+
* Names registered commands must not shadow, derived from the actual portable
|
|
117
|
+
* command set plus interpreter builtins and pass-through system tools — not a
|
|
118
|
+
* hand-maintained parallel list.
|
|
119
|
+
*/
|
|
120
|
+
const RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
121
|
+
...DEMI_PORTABLE_COMMANDS,
|
|
122
|
+
...SHELL_BUILTIN_NAMES,
|
|
123
|
+
...SYSTEM_TOOL_NAMES
|
|
124
|
+
]);
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/command.ts
|
|
127
|
+
function isCommandGroup(node) {
|
|
128
|
+
return "subcommands" in node;
|
|
129
|
+
}
|
|
130
|
+
var CommandRegistry = class {
|
|
131
|
+
commands = /* @__PURE__ */ new Map();
|
|
132
|
+
register(spec) {
|
|
133
|
+
if (RESERVED_COMMAND_NAMES.has(spec.name)) throw new Error(`CommandRegistry: command "${spec.name}" is reserved for shell/system commands`);
|
|
134
|
+
if (this.commands.has(spec.name)) throw new Error(`CommandRegistry: command "${spec.name}" is already registered`);
|
|
135
|
+
validateCommandTree(spec, spec.name);
|
|
136
|
+
this.commands.set(spec.name, spec);
|
|
137
|
+
}
|
|
138
|
+
get(name) {
|
|
139
|
+
return this.commands.get(name) ?? null;
|
|
140
|
+
}
|
|
141
|
+
list() {
|
|
142
|
+
return [...this.commands.values()];
|
|
143
|
+
}
|
|
144
|
+
renderPrompt() {
|
|
145
|
+
const rendered = this.list().map((spec) => renderCommandPrompt(spec)).join("\n\n");
|
|
146
|
+
if (!rendered) return rendered;
|
|
147
|
+
return `${COMMAND_PROMPT_DEFAULTS}\n\n${rendered}`;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const COMMAND_PROMPT_DEFAULTS = "Unless a subcommand states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero.";
|
|
151
|
+
function parseCommandInput(spec, argv, stdin = { text: "" }) {
|
|
152
|
+
if (argv[0] !== spec.name) throw new Error(`Expected command "${spec.name}", received "${argv[0] ?? ""}"`);
|
|
153
|
+
let group = spec;
|
|
154
|
+
const path = [];
|
|
155
|
+
let index = 1;
|
|
156
|
+
while (true) {
|
|
157
|
+
const segment = argv[index];
|
|
158
|
+
if (!segment) throw new Error(`Command "${[spec.name, ...path].join(" ")}" requires a subcommand`);
|
|
159
|
+
if (segment === "prompt") return {
|
|
160
|
+
subcommand: "prompt",
|
|
161
|
+
path,
|
|
162
|
+
values: {},
|
|
163
|
+
json: false
|
|
164
|
+
};
|
|
165
|
+
const child = group.subcommands.find((candidate) => candidate.name === segment);
|
|
166
|
+
if (!child) throw new Error(`Unknown subcommand "${[
|
|
167
|
+
spec.name,
|
|
168
|
+
...path,
|
|
169
|
+
segment
|
|
170
|
+
].join(" ")}"`);
|
|
171
|
+
path.push(child.name);
|
|
172
|
+
index += 1;
|
|
173
|
+
if (isCommandGroup(child)) {
|
|
174
|
+
group = child;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
return parseLeafInput(child, [spec.name, ...path].join(" "), path, argv, index, stdin);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function parseLeafInput(subcommand, displayPath, path, argv, startIndex, stdin) {
|
|
181
|
+
const input = subcommand.input ?? {};
|
|
182
|
+
const values = {};
|
|
183
|
+
let json = false;
|
|
184
|
+
let positionalIndex = 0;
|
|
185
|
+
for (let i = startIndex; i < argv.length; i += 1) {
|
|
186
|
+
const token = argv[i];
|
|
187
|
+
if (token === "--json") {
|
|
188
|
+
json = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (token.startsWith("--")) {
|
|
192
|
+
const field = token.slice(2);
|
|
193
|
+
const schema = input[field];
|
|
194
|
+
if (!schema) throw new Error(`Unknown option "--${field}" for "${displayPath}"`);
|
|
195
|
+
const next = argv[i + 1];
|
|
196
|
+
const takesImplicitBoolean = isBooleanSchema(schema) && (next === void 0 || next.startsWith("--"));
|
|
197
|
+
const rawValue = takesImplicitBoolean ? true : next;
|
|
198
|
+
if (!takesImplicitBoolean) i += 1;
|
|
199
|
+
if (rawValue === void 0) throw new Error(`Missing value for "--${field}"`);
|
|
200
|
+
setParsedValue(values, field, rawValue);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const field = subcommand.positionals?.[positionalIndex];
|
|
204
|
+
if (!field) throw new Error(`Unexpected positional argument "${token}"`);
|
|
205
|
+
setParsedValue(values, field, token);
|
|
206
|
+
positionalIndex += 1;
|
|
207
|
+
}
|
|
208
|
+
if (subcommand.stdinField) values[subcommand.stdinField] = stdin.text;
|
|
209
|
+
return {
|
|
210
|
+
subcommand: subcommand.name,
|
|
211
|
+
path,
|
|
212
|
+
values: validateInput(input, values),
|
|
213
|
+
json
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function resolveCommandNode(spec, path) {
|
|
217
|
+
let node = spec;
|
|
218
|
+
for (const segment of path) {
|
|
219
|
+
if (!isCommandGroup(node)) throw new Error(`"${node.name}" has no subcommand "${segment}"`);
|
|
220
|
+
const child = node.subcommands.find((candidate) => candidate.name === segment);
|
|
221
|
+
if (!child) throw new Error(`Unknown subcommand "${segment}" under "${node.name}"`);
|
|
222
|
+
node = child;
|
|
223
|
+
}
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
async function runRegisteredCommand(spec, ctx) {
|
|
227
|
+
const stdin = ctx.stdin ?? { text: "" };
|
|
228
|
+
const parsed = parseCommandInput(spec, ctx.argv, stdin);
|
|
229
|
+
if (parsed.subcommand === "prompt") {
|
|
230
|
+
const node = resolveCommandNode(spec, parsed.path);
|
|
231
|
+
const parentPath = [spec.name, ...parsed.path.slice(0, -1)].join(" ");
|
|
232
|
+
await ctx.io.stdout(`${renderCommandPrompt(node, parsed.path.length > 0 ? parentPath : "")}\n`);
|
|
233
|
+
return { exitCode: 0 };
|
|
234
|
+
}
|
|
235
|
+
const subcommand = resolveCommandNode(spec, parsed.path);
|
|
236
|
+
if (isCommandGroup(subcommand)) throw new Error(`"${[spec.name, ...parsed.path].join(" ")}" is not runnable`);
|
|
237
|
+
if (parsed.json && !subcommand.output?.json) throw new Error(`Subcommand "${[spec.name, ...parsed.path].join(" ")}" does not define JSON output`);
|
|
238
|
+
const capture = new CapturingIO(ctx.io);
|
|
239
|
+
const result = await subcommand.run({
|
|
240
|
+
argv: ctx.argv,
|
|
241
|
+
parsed,
|
|
242
|
+
stdin,
|
|
243
|
+
env: ctx.env,
|
|
244
|
+
cwd: ctx.cwd,
|
|
245
|
+
io: parsed.json ? capture : ctx.io,
|
|
246
|
+
storage: ctx.storage
|
|
247
|
+
});
|
|
248
|
+
if (parsed.json && result.exitCode === 0) {
|
|
249
|
+
const raw = capture.stdoutText();
|
|
250
|
+
let json;
|
|
251
|
+
try {
|
|
252
|
+
json = JSON.parse(raw);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new Error(`Invalid JSON output for "${[spec.name, ...parsed.path].join(" ")}": ${asError(error).message}`);
|
|
255
|
+
}
|
|
256
|
+
const validation = subcommand.output?.json?.safeParse(json);
|
|
257
|
+
if (!validation?.success) {
|
|
258
|
+
const issue = validation?.error.issues[0];
|
|
259
|
+
throw new Error(`JSON output failed validation for "${[spec.name, ...parsed.path].join(" ")}": ${issue?.message}`);
|
|
260
|
+
}
|
|
261
|
+
await ctx.io.stdout(raw);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function renderCommandPrompt(spec, parentPath = "") {
|
|
266
|
+
const path = parentPath ? `${parentPath} ${spec.name}` : spec.name;
|
|
267
|
+
const leaves = spec.subcommands.filter((node) => !isCommandGroup(node));
|
|
268
|
+
const groups = spec.subcommands.filter(isCommandGroup);
|
|
269
|
+
const lines = [`${path}: ${spec.summary}`];
|
|
270
|
+
if (leaves.length > 0) {
|
|
271
|
+
lines.push("", "Subcommands:");
|
|
272
|
+
for (const subcommand of leaves) {
|
|
273
|
+
lines.push("", ` ${path} ${subcommand.name}`);
|
|
274
|
+
lines.push(` ${subcommand.summary}`);
|
|
275
|
+
if (subcommand.effects) lines.push(` Effects: ${subcommand.effects}`);
|
|
276
|
+
if (subcommand.successOutput) lines.push(` Success output: ${subcommand.successOutput}`);
|
|
277
|
+
else if (subcommand.output?.json) lines.push(" Success output: raw text by default; machine-readable JSON when --json is passed");
|
|
278
|
+
if (subcommand.failureOutput) lines.push(` Failure output: ${subcommand.failureOutput}`);
|
|
279
|
+
const fields = Object.entries(subcommand.input ?? {});
|
|
280
|
+
if (fields.length > 0) {
|
|
281
|
+
lines.push(" Parameters:");
|
|
282
|
+
for (const [field, schema] of fields) {
|
|
283
|
+
const positional = subcommand.positionals?.includes(field) ?? false;
|
|
284
|
+
const stdin = subcommand.stdinField === field;
|
|
285
|
+
lines.push(` ${formatField(field, schema, positional, stdin)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (subcommand.stdinField) lines.push(` stdin/heredoc: ${subcommand.stdinField}`);
|
|
289
|
+
if (subcommand.output?.json) lines.push(" --json: emits machine-readable JSON for this subcommand");
|
|
290
|
+
else lines.push(" --json: accepted only when this subcommand defines JSON output");
|
|
291
|
+
if (subcommand.examples.length > 0) {
|
|
292
|
+
lines.push(" Examples:");
|
|
293
|
+
for (const example of subcommand.examples) lines.push(indent(example, 6));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const blocks = [lines.join("\n")];
|
|
298
|
+
for (const group of groups) blocks.push(renderCommandPrompt(group, path));
|
|
299
|
+
return blocks.join("\n\n");
|
|
300
|
+
}
|
|
301
|
+
function validateCommandTree(node, path) {
|
|
302
|
+
const seen = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const child of node.subcommands) {
|
|
304
|
+
if (child.name === "prompt") throw new Error(`CommandRegistry: "${path} prompt" is reserved for the help pseudo-subcommand`);
|
|
305
|
+
if (seen.has(child.name)) throw new Error(`CommandRegistry: duplicate subcommand "${path} ${child.name}"`);
|
|
306
|
+
seen.add(child.name);
|
|
307
|
+
if (isCommandGroup(child)) validateCommandTree(child, `${path} ${child.name}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function setParsedValue(values, field, value) {
|
|
311
|
+
if (values[field] === void 0) {
|
|
312
|
+
values[field] = value;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(values[field])) {
|
|
316
|
+
values[field].push(value);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
values[field] = [values[field], value];
|
|
320
|
+
}
|
|
321
|
+
function validateInput(input, values) {
|
|
322
|
+
const parsed = {};
|
|
323
|
+
for (const [field, schema] of Object.entries(input)) {
|
|
324
|
+
const candidate = coerceValue(schema, values[field]);
|
|
325
|
+
const result = schema.safeParse(candidate);
|
|
326
|
+
if (!result.success) {
|
|
327
|
+
const issue = result.error.issues[0];
|
|
328
|
+
throw new Error(`Invalid value for "${field}": ${issue?.message ?? "validation failed"}`);
|
|
329
|
+
}
|
|
330
|
+
parsed[field] = result.data;
|
|
331
|
+
}
|
|
332
|
+
return parsed;
|
|
333
|
+
}
|
|
334
|
+
function coerceValue(schema, value) {
|
|
335
|
+
if (value === void 0) return value;
|
|
336
|
+
if (isArraySchema(schema)) return Array.isArray(value) ? value : [value];
|
|
337
|
+
if (isNumberSchema(schema) && typeof value === "string" && value.trim() !== "") return Number(value);
|
|
338
|
+
if (isBooleanSchema(schema) && typeof value === "string") {
|
|
339
|
+
if (value === "true") return true;
|
|
340
|
+
if (value === "false") return false;
|
|
341
|
+
}
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
function formatField(field, schema, positional, stdin) {
|
|
345
|
+
return `${positional ? `<${field}>` : `--${field}`}${stdin ? " (from stdin/heredoc)" : ""}${schema.description ? ` - ${schema.description}` : ""}`;
|
|
346
|
+
}
|
|
347
|
+
function indent(text, spaces) {
|
|
348
|
+
const prefix = " ".repeat(spaces);
|
|
349
|
+
return text.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
350
|
+
}
|
|
351
|
+
function isArraySchema(schema) {
|
|
352
|
+
const unwrapped = unwrapSchema(schema);
|
|
353
|
+
return zodTypeName(unwrapped) === "array" || zodTypeName(unwrapped) === "ZodArray";
|
|
354
|
+
}
|
|
355
|
+
function isBooleanSchema(schema) {
|
|
356
|
+
const unwrapped = unwrapSchema(schema);
|
|
357
|
+
return zodTypeName(unwrapped) === "boolean" || zodTypeName(unwrapped) === "ZodBoolean";
|
|
358
|
+
}
|
|
359
|
+
function isNumberSchema(schema) {
|
|
360
|
+
const unwrapped = unwrapSchema(schema);
|
|
361
|
+
return zodTypeName(unwrapped) === "number" || zodTypeName(unwrapped) === "ZodNumber";
|
|
362
|
+
}
|
|
363
|
+
function zodTypeName(schema) {
|
|
364
|
+
const internals = schema;
|
|
365
|
+
return internals.def?.type ?? internals.def?.typeName ?? internals._def?.type ?? internals._def?.typeName;
|
|
366
|
+
}
|
|
367
|
+
function unwrapSchema(schema) {
|
|
368
|
+
let current = schema;
|
|
369
|
+
while (true) {
|
|
370
|
+
const internals = current;
|
|
371
|
+
const inner = internals.def?.innerType ?? internals._def?.innerType;
|
|
372
|
+
if (!inner) return current;
|
|
373
|
+
current = inner;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
var CapturingIO = class {
|
|
377
|
+
target;
|
|
378
|
+
chunks = [];
|
|
379
|
+
constructor(target) {
|
|
380
|
+
this.target = target;
|
|
381
|
+
}
|
|
382
|
+
async stdout(data) {
|
|
383
|
+
this.chunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
384
|
+
}
|
|
385
|
+
async stderr(data) {
|
|
386
|
+
await this.target.stderr(data);
|
|
387
|
+
}
|
|
388
|
+
async asset(asset) {
|
|
389
|
+
await this.target.asset(asset);
|
|
390
|
+
}
|
|
391
|
+
stdoutText() {
|
|
392
|
+
return decodeUtf8(concatBytes(this.chunks));
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/command-projection.ts
|
|
397
|
+
function listRegisteredCommandOperations(commands) {
|
|
398
|
+
const specs = Array.isArray(commands) ? commands : commands.list();
|
|
399
|
+
const operations = [];
|
|
400
|
+
for (const spec of specs) collectOperations(spec, [spec.name], operations);
|
|
401
|
+
return operations;
|
|
402
|
+
}
|
|
403
|
+
function collectOperations(node, path, operations) {
|
|
404
|
+
for (const child of node.subcommands) {
|
|
405
|
+
if (isCommandGroup(child)) {
|
|
406
|
+
collectOperations(child, [...path, child.name], operations);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
operations.push(operationFromLeaf(child, [...path, child.name]));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function operationFromLeaf(leaf, path) {
|
|
413
|
+
const parts = [leaf.summary.trim()];
|
|
414
|
+
if (leaf.effects) parts.push(`Effects: ${leaf.effects}`);
|
|
415
|
+
return {
|
|
416
|
+
path,
|
|
417
|
+
description: parts.join(" "),
|
|
418
|
+
inputSchema: inputSchemaFromSpec(leaf.input ?? {}),
|
|
419
|
+
renderScript: (values) => renderInvocationScript(leaf, path, values)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function inputSchemaFromSpec(input) {
|
|
423
|
+
const schema = z.toJSONSchema(z.object(input));
|
|
424
|
+
delete schema.$schema;
|
|
425
|
+
return schema;
|
|
426
|
+
}
|
|
427
|
+
function renderInvocationScript(leaf, path, values) {
|
|
428
|
+
const tokens = [...path];
|
|
429
|
+
for (const field of Object.keys(leaf.input ?? {})) {
|
|
430
|
+
if (field === leaf.stdinField) continue;
|
|
431
|
+
const value = values[field];
|
|
432
|
+
if (value === void 0 || value === null) continue;
|
|
433
|
+
for (const item of Array.isArray(value) ? value : [value]) tokens.push(`--${field}`, shellQuote(scalarText(item)));
|
|
434
|
+
}
|
|
435
|
+
const script = tokens.join(" ");
|
|
436
|
+
if (!leaf.stdinField) return script;
|
|
437
|
+
const stdinValue = values[leaf.stdinField];
|
|
438
|
+
const body = stdinValue === void 0 || stdinValue === null ? "" : scalarText(stdinValue);
|
|
439
|
+
const delimiter = heredocDelimiter(body);
|
|
440
|
+
return `${script} <<'${delimiter}'\n${body}\n${delimiter}`;
|
|
441
|
+
}
|
|
442
|
+
function scalarText(value) {
|
|
443
|
+
if (typeof value === "string") return value;
|
|
444
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
445
|
+
return JSON.stringify(value);
|
|
446
|
+
}
|
|
447
|
+
function shellQuote(value) {
|
|
448
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
449
|
+
}
|
|
450
|
+
function heredocDelimiter(body) {
|
|
451
|
+
let delimiter = "DEMI_STDIN";
|
|
452
|
+
const lines = new Set(body.split("\n"));
|
|
453
|
+
while (lines.has(delimiter)) delimiter += "_X";
|
|
454
|
+
return delimiter;
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/background-command.ts
|
|
458
|
+
function extractSimpleBackgroundCommand(statementInput) {
|
|
459
|
+
const statement = statementInput;
|
|
460
|
+
if (!statement.background || (statement.operators?.length ?? 0) > 0 || statement.pipelines?.length !== 1) return null;
|
|
461
|
+
const pipeline = statement.pipelines[0];
|
|
462
|
+
if (pipeline.commands?.length !== 1) return null;
|
|
463
|
+
const commandNode = pipeline.commands[0];
|
|
464
|
+
if (commandNode.type !== "SimpleCommand" || (commandNode.assignments?.length ?? 0) > 0 || (commandNode.redirections?.length ?? 0) > 0) return null;
|
|
465
|
+
const command = wordToBackgroundText(commandNode.name);
|
|
466
|
+
if (!command) return null;
|
|
467
|
+
const args = [];
|
|
468
|
+
for (const arg of commandNode.args ?? []) {
|
|
469
|
+
const value = wordToBackgroundText(arg);
|
|
470
|
+
if (value === null) return null;
|
|
471
|
+
args.push(value);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
command,
|
|
475
|
+
args
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function formatCommandDisplay(command, args) {
|
|
479
|
+
return [command, ...args].join(" ");
|
|
480
|
+
}
|
|
481
|
+
function wordToBackgroundText(word) {
|
|
482
|
+
const node = word;
|
|
483
|
+
if (node?.type !== "Word" || !node.parts) return null;
|
|
484
|
+
let text = "";
|
|
485
|
+
for (const part of node.parts) {
|
|
486
|
+
const value = wordPartToBackgroundText(part);
|
|
487
|
+
if (value === null) return null;
|
|
488
|
+
text += value;
|
|
489
|
+
}
|
|
490
|
+
return text;
|
|
491
|
+
}
|
|
492
|
+
function wordPartToBackgroundText(part) {
|
|
493
|
+
const node = part;
|
|
494
|
+
switch (node.type) {
|
|
495
|
+
case "Literal":
|
|
496
|
+
case "SingleQuoted":
|
|
497
|
+
case "Escaped": return node.value ?? "";
|
|
498
|
+
case "Glob": return node.pattern ?? "";
|
|
499
|
+
case "TildeExpansion": return node.user === null ? "~" : `~${node.user ?? ""}`;
|
|
500
|
+
case "DoubleQuoted": {
|
|
501
|
+
let text = "";
|
|
502
|
+
for (const child of node.parts ?? []) {
|
|
503
|
+
const value = wordPartToBackgroundText(child);
|
|
504
|
+
if (value === null) return null;
|
|
505
|
+
text += value;
|
|
506
|
+
}
|
|
507
|
+
return text;
|
|
508
|
+
}
|
|
509
|
+
default: return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/environment-output.ts
|
|
514
|
+
function createOutputSinks(fs, cwd, redirections) {
|
|
515
|
+
const routes = {
|
|
516
|
+
1: visibleSink(1),
|
|
517
|
+
2: visibleSink(2)
|
|
518
|
+
};
|
|
519
|
+
for (const redirection of redirections ?? []) switch (redirection.operator) {
|
|
520
|
+
case ">":
|
|
521
|
+
case ">|":
|
|
522
|
+
routes[redirection.fd ?? 1] = targetSink(fs, cwd, redirection.target, false);
|
|
523
|
+
break;
|
|
524
|
+
case ">>":
|
|
525
|
+
routes[redirection.fd ?? 1] = targetSink(fs, cwd, redirection.target, true);
|
|
526
|
+
break;
|
|
527
|
+
case "&>": {
|
|
528
|
+
const sink = targetSink(fs, cwd, redirection.target, false);
|
|
529
|
+
routes[1] = sink;
|
|
530
|
+
routes[2] = sink;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "&>>": {
|
|
534
|
+
const sink = targetSink(fs, cwd, redirection.target, true);
|
|
535
|
+
routes[1] = sink;
|
|
536
|
+
routes[2] = sink;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case ">&": {
|
|
540
|
+
const fd = redirection.fd ?? 1;
|
|
541
|
+
if (redirection.target === "-") {
|
|
542
|
+
routes[fd] = nullSink();
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
const sourceFd = Number.parseInt(redirection.target.replace(/^&/, ""), 10);
|
|
546
|
+
if (sourceFd === 1 || sourceFd === 2) {
|
|
547
|
+
routes[fd] = routes[sourceFd];
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
const sink = targetSink(fs, cwd, redirection.target, false);
|
|
551
|
+
if (redirection.fd === null) {
|
|
552
|
+
routes[1] = sink;
|
|
553
|
+
routes[2] = sink;
|
|
554
|
+
} else routes[fd] = sink;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return routes;
|
|
559
|
+
}
|
|
560
|
+
function recordForegroundChunk(foreground, sourceFd, chunk) {
|
|
561
|
+
const text = decodeUtf8(chunk);
|
|
562
|
+
foreground.lastOutputAt = Date.now();
|
|
563
|
+
if (sourceFd === 1) foreground.rawStdoutBuffer += text;
|
|
564
|
+
else foreground.rawStderrBuffer += text;
|
|
565
|
+
const sink = foreground.outputSinks[sourceFd];
|
|
566
|
+
if (sink.kind === "file" || sink.kind === "null") {
|
|
567
|
+
sink.bytes.push(chunk);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
appendVisibleChunk(foreground, sink.fd ?? sourceFd, text, chunk.byteLength);
|
|
571
|
+
}
|
|
572
|
+
async function flushForegroundSinks(session, foreground) {
|
|
573
|
+
const flushed = /* @__PURE__ */ new Set();
|
|
574
|
+
for (const sink of [foreground.outputSinks[1], foreground.outputSinks[2]]) {
|
|
575
|
+
if (sink.kind !== "file" || flushed.has(sink)) continue;
|
|
576
|
+
flushed.add(sink);
|
|
577
|
+
const bytes = concatBytes(sink.bytes);
|
|
578
|
+
if (sink.append) await session.fs.appendFile(sink.path, bytes);
|
|
579
|
+
else await session.fs.writeFile(sink.path, bytes);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function notifyForegroundWaiters(waiters, foreground) {
|
|
583
|
+
const snapshot = [...waiters];
|
|
584
|
+
waiters.clear();
|
|
585
|
+
for (const waiter of snapshot) waiter(foreground);
|
|
586
|
+
}
|
|
587
|
+
async function pumpStream(stream, onChunk) {
|
|
588
|
+
try {
|
|
589
|
+
for await (const chunk of stream) onChunk(chunk);
|
|
590
|
+
} catch {}
|
|
591
|
+
}
|
|
592
|
+
async function pumpOutputStream(stream, onChunk) {
|
|
593
|
+
try {
|
|
594
|
+
for await (const chunk of stream) onChunk(chunk);
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
597
|
+
function buildShellopts(options) {
|
|
598
|
+
const flags = [];
|
|
599
|
+
if (options.errexit) flags.push("errexit");
|
|
600
|
+
if (options.pipefail) flags.push("pipefail");
|
|
601
|
+
if (options.nounset) flags.push("nounset");
|
|
602
|
+
if (options.xtrace) flags.push("xtrace");
|
|
603
|
+
if (options.verbose) flags.push("verbose");
|
|
604
|
+
if (options.posix) flags.push("posix");
|
|
605
|
+
if (options.allexport) flags.push("allexport");
|
|
606
|
+
if (options.noclobber) flags.push("noclobber");
|
|
607
|
+
if (options.noglob) flags.push("noglob");
|
|
608
|
+
if (options.noexec) flags.push("noexec");
|
|
609
|
+
if (options.vi) flags.push("vi");
|
|
610
|
+
if (options.emacs) flags.push("emacs");
|
|
611
|
+
return flags.join(":");
|
|
612
|
+
}
|
|
613
|
+
function buildBashopts(options) {
|
|
614
|
+
const flags = [];
|
|
615
|
+
if (options.extglob) flags.push("extglob");
|
|
616
|
+
if (options.dotglob) flags.push("dotglob");
|
|
617
|
+
if (options.nullglob) flags.push("nullglob");
|
|
618
|
+
if (options.failglob) flags.push("failglob");
|
|
619
|
+
if (options.globstar) flags.push("globstar");
|
|
620
|
+
if (options.globskipdots) flags.push("globskipdots");
|
|
621
|
+
if (options.nocaseglob) flags.push("nocaseglob");
|
|
622
|
+
if (options.nocasematch) flags.push("nocasematch");
|
|
623
|
+
if (options.expand_aliases) flags.push("expand_aliases");
|
|
624
|
+
if (options.lastpipe) flags.push("lastpipe");
|
|
625
|
+
if (options.xpg_echo) flags.push("xpg_echo");
|
|
626
|
+
return flags.join(":");
|
|
627
|
+
}
|
|
628
|
+
function visibleSink(fd) {
|
|
629
|
+
return {
|
|
630
|
+
kind: "visible",
|
|
631
|
+
fd,
|
|
632
|
+
bytes: []
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function nullSink() {
|
|
636
|
+
return {
|
|
637
|
+
kind: "null",
|
|
638
|
+
bytes: []
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function targetSink(fs, cwd, target, append) {
|
|
642
|
+
if (target === "/dev/stdout") return visibleSink(1);
|
|
643
|
+
if (target === "/dev/stderr") return visibleSink(2);
|
|
644
|
+
if (target === "/dev/null") return nullSink();
|
|
645
|
+
return {
|
|
646
|
+
kind: "file",
|
|
647
|
+
path: fs.resolvePath(cwd, target),
|
|
648
|
+
append,
|
|
649
|
+
bytes: []
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function appendVisibleChunk(foreground, targetFd, text, byteLength) {
|
|
653
|
+
foreground.outputChunks.push({
|
|
654
|
+
stream: targetFd === 1 ? "stdout" : "stderr",
|
|
655
|
+
text,
|
|
656
|
+
offset: foreground.outputBytes,
|
|
657
|
+
bytes: byteLength
|
|
658
|
+
});
|
|
659
|
+
foreground.outputBytes += byteLength;
|
|
660
|
+
if (targetFd === 1) foreground.stdoutBuffer += text;
|
|
661
|
+
else foreground.stderrBuffer += text;
|
|
662
|
+
}
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/command-artifact-store.ts
|
|
665
|
+
/**
|
|
666
|
+
* Owns persistence of shell command artifacts: a per-scope storage cache plus the
|
|
667
|
+
* set of released (tombstoned) commands. `BashEnvironment` delegates the storage
|
|
668
|
+
* and release-tracking side of the `/@` virtual filesystem here, keeping only the
|
|
669
|
+
* in-memory record lookups that need live command state.
|
|
670
|
+
*/
|
|
671
|
+
var CommandArtifactStore = class {
|
|
672
|
+
store;
|
|
673
|
+
storageByScope = /* @__PURE__ */ new Map();
|
|
674
|
+
released = /* @__PURE__ */ new Set();
|
|
675
|
+
constructor(store) {
|
|
676
|
+
this.store = store;
|
|
677
|
+
}
|
|
678
|
+
/** The artifact storage for a command scope, created (and cached) on first use. */
|
|
679
|
+
storageFor(scopeId) {
|
|
680
|
+
const existing = this.storageByScope.get(scopeId);
|
|
681
|
+
if (existing) return existing;
|
|
682
|
+
const storage = new AgentSessionCommandStorage(this.store, scopeId);
|
|
683
|
+
this.storageByScope.set(scopeId, storage);
|
|
684
|
+
return storage;
|
|
685
|
+
}
|
|
686
|
+
/** Whether a command's artifact has been released (tombstoned). */
|
|
687
|
+
isReleased(scopeId, commandId) {
|
|
688
|
+
return this.released.has(this.key(scopeId, commandId));
|
|
689
|
+
}
|
|
690
|
+
/** Persists an artifact unless its command has already been released. */
|
|
691
|
+
persist(scopeId, commandId, artifact) {
|
|
692
|
+
if (this.isReleased(scopeId, commandId)) return;
|
|
693
|
+
this.storageFor(scopeId).writeJson(`commands/${commandId}/artifact.json`, artifact).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
/** Tombstones a command and removes its persisted artifact. */
|
|
696
|
+
async release(scopeId, commandId) {
|
|
697
|
+
this.released.add(this.key(scopeId, commandId));
|
|
698
|
+
await this.storageFor(scopeId).delete(`commands/${commandId}/artifact.json`).catch(() => {});
|
|
699
|
+
}
|
|
700
|
+
key(scopeId, commandId) {
|
|
701
|
+
return `${scopeId}\0${commandId}`;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region src/registered-command-adapter.ts
|
|
706
|
+
function commandSpecToForkCommand(session, spec, storage) {
|
|
707
|
+
return {
|
|
708
|
+
name: spec.name,
|
|
709
|
+
consumesStdin: treeConsumesStdin(spec),
|
|
710
|
+
execute: async (args, ctx) => {
|
|
711
|
+
const stdinText = decodeForkStdin(ctx.stdin);
|
|
712
|
+
const io = createForwardingIO();
|
|
713
|
+
const argv = [spec.name, ...args];
|
|
714
|
+
try {
|
|
715
|
+
const result = await runRegisteredCommand(spec, {
|
|
716
|
+
argv,
|
|
717
|
+
stdin: { text: stdinText },
|
|
718
|
+
env: mapToRecord(ctx.env),
|
|
719
|
+
cwd: ctx.cwd,
|
|
720
|
+
io,
|
|
721
|
+
storage
|
|
722
|
+
});
|
|
723
|
+
session.accumulator.audit.push({
|
|
724
|
+
kind: "registered-command",
|
|
725
|
+
name: spec.name,
|
|
726
|
+
args,
|
|
727
|
+
exitCode: result.exitCode
|
|
728
|
+
});
|
|
729
|
+
if (result.metadata !== void 0) session.accumulator.commandMetadata.push({
|
|
730
|
+
kind: "registered-command",
|
|
731
|
+
name: spec.name,
|
|
732
|
+
args,
|
|
733
|
+
metadata: result.metadata
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
stdout: io.stdoutText(),
|
|
737
|
+
stderr: io.stderrText(),
|
|
738
|
+
exitCode: result.exitCode
|
|
739
|
+
};
|
|
740
|
+
} catch (error) {
|
|
741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
742
|
+
session.accumulator.audit.push({
|
|
743
|
+
kind: "registered-command",
|
|
744
|
+
name: spec.name,
|
|
745
|
+
args,
|
|
746
|
+
exitCode: 1
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
stdout: io.stdoutText(),
|
|
750
|
+
stderr: `${io.stderrText()}${spec.name}: ${message}\n`,
|
|
751
|
+
exitCode: 1
|
|
752
|
+
};
|
|
753
|
+
} finally {
|
|
754
|
+
if (io.assets().length > 0) session.accumulator.assets.push(...io.assets());
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
var ForwardingIO = class {
|
|
760
|
+
stdoutChunks = [];
|
|
761
|
+
stderrChunks = [];
|
|
762
|
+
assetItems = [];
|
|
763
|
+
async stdout(data) {
|
|
764
|
+
this.stdoutChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
765
|
+
}
|
|
766
|
+
async stderr(data) {
|
|
767
|
+
this.stderrChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
768
|
+
}
|
|
769
|
+
asset(asset) {
|
|
770
|
+
this.assetItems.push(asset);
|
|
771
|
+
}
|
|
772
|
+
stdoutText() {
|
|
773
|
+
return decodeUtf8(concatBytes(this.stdoutChunks));
|
|
774
|
+
}
|
|
775
|
+
stderrText() {
|
|
776
|
+
return decodeUtf8(concatBytes(this.stderrChunks));
|
|
777
|
+
}
|
|
778
|
+
assets() {
|
|
779
|
+
return this.assetItems;
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
function createForwardingIO() {
|
|
783
|
+
return new ForwardingIO();
|
|
784
|
+
}
|
|
785
|
+
function treeConsumesStdin(spec) {
|
|
786
|
+
return spec.subcommands.some((node) => isCommandGroup(node) ? treeConsumesStdin(node) : Boolean(node.stdinField));
|
|
787
|
+
}
|
|
788
|
+
function mapToRecord(map) {
|
|
789
|
+
const record = Object.create(null);
|
|
790
|
+
for (const [key, value] of map) record[key] = value;
|
|
791
|
+
return record;
|
|
792
|
+
}
|
|
793
|
+
function decodeForkStdin(stdin) {
|
|
794
|
+
if (!stdin) return "";
|
|
795
|
+
if (stdin instanceof Uint8Array) return decodeUtf8(stdin);
|
|
796
|
+
const latin1 = stdin;
|
|
797
|
+
if (!latin1) return "";
|
|
798
|
+
let hasHighByte = false;
|
|
799
|
+
for (let i = 0; i < latin1.length; i += 1) {
|
|
800
|
+
const code = latin1.charCodeAt(i);
|
|
801
|
+
if (code > 255) return latin1;
|
|
802
|
+
if (code > 127) hasHighByte = true;
|
|
803
|
+
}
|
|
804
|
+
if (!hasHighByte) return latin1;
|
|
805
|
+
const bytes = new Uint8Array(latin1.length);
|
|
806
|
+
for (let i = 0; i < latin1.length; i += 1) bytes[i] = latin1.charCodeAt(i);
|
|
807
|
+
return decodeUtf8(bytes);
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/environment.ts
|
|
811
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
812
|
+
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
813
|
+
const MAX_TIMEOUT_MS = 6e5;
|
|
814
|
+
var BashEnvironment = class {
|
|
815
|
+
host;
|
|
816
|
+
commands;
|
|
817
|
+
shellIdFactory;
|
|
818
|
+
commandIdFactory;
|
|
819
|
+
initialEnv;
|
|
820
|
+
execEnv;
|
|
821
|
+
defaultOutputLimitBytes;
|
|
822
|
+
shells = /* @__PURE__ */ new Map();
|
|
823
|
+
defaultShellByAgentSessionId = /* @__PURE__ */ new Map();
|
|
824
|
+
commandsById = /* @__PURE__ */ new Map();
|
|
825
|
+
artifacts;
|
|
826
|
+
constructor(options) {
|
|
827
|
+
this.host = options.host;
|
|
828
|
+
this.artifacts = new CommandArtifactStore(this.host.store);
|
|
829
|
+
this.commands = options.commands ?? new CommandRegistry();
|
|
830
|
+
this.shellIdFactory = options.shellIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
831
|
+
this.commandIdFactory = options.commandIdFactory ?? (() => globalThis.crypto.randomUUID());
|
|
832
|
+
this.initialEnv = options.initialEnv ?? {};
|
|
833
|
+
this.execEnv = options.execEnv;
|
|
834
|
+
this.defaultOutputLimitBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
|
|
835
|
+
}
|
|
836
|
+
getShell(shellId) {
|
|
837
|
+
return this.shells.get(shellId) ?? null;
|
|
838
|
+
}
|
|
839
|
+
registerCommand(spec) {
|
|
840
|
+
if (this.commands.get(spec.name)) return;
|
|
841
|
+
this.commands.register(spec);
|
|
842
|
+
}
|
|
843
|
+
registeredCommands() {
|
|
844
|
+
return this.commands.list();
|
|
845
|
+
}
|
|
846
|
+
async exec(input) {
|
|
847
|
+
const timeoutMs = normalizeTimeoutMs(input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
848
|
+
const session = input.shellId ? this.requireShell(input.shellId) : this.availableDefaultShell(input.agentSessionId);
|
|
849
|
+
if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
|
|
850
|
+
if (input.agentSessionId) {
|
|
851
|
+
session.state.env.set("DEMI_AGENT_SESSION_ID", input.agentSessionId);
|
|
852
|
+
const extraEnv = this.execEnv?.(input.agentSessionId);
|
|
853
|
+
if (extraEnv) {
|
|
854
|
+
const exported = session.state.exportedVars ?? /* @__PURE__ */ new Set();
|
|
855
|
+
session.state.exportedVars = exported;
|
|
856
|
+
for (const [key, value] of Object.entries(extraEnv)) {
|
|
857
|
+
session.state.env.set(key, value);
|
|
858
|
+
exported.add(key);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (session.pendingExec || session.foreground) {
|
|
863
|
+
const commandId = session.activeCommandId ?? session.foreground?.commandId ?? "unknown";
|
|
864
|
+
throw new Error(`Shell session "${session.id}" is already running command "${commandId}"`);
|
|
865
|
+
}
|
|
866
|
+
return this.runScript(session, input.script, {
|
|
867
|
+
...input,
|
|
868
|
+
timeoutMs
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
async status(input) {
|
|
872
|
+
const record = this.requireCommand(input.commandId);
|
|
873
|
+
return this.snapshotCommand(record, input);
|
|
874
|
+
}
|
|
875
|
+
async write(input) {
|
|
876
|
+
const record = this.requireCommand(input.commandId);
|
|
877
|
+
if (record.status !== "running") throw new Error(`Command "${record.id}" is not running`);
|
|
878
|
+
const session = this.requireShell(record.shellId);
|
|
879
|
+
const foreground = this.requireForegroundCommand(session, record.id);
|
|
880
|
+
const data = typeof input.stdin === "string" ? encodeUtf8(input.stdin) : input.stdin;
|
|
881
|
+
if (data.byteLength === 0) throw new Error("shell_write field \"stdin\" must not be empty; use shell_status to poll");
|
|
882
|
+
await foreground.handle.writeStdin(data);
|
|
883
|
+
return this.snapshotCommand(record, input);
|
|
884
|
+
}
|
|
885
|
+
async abort(input) {
|
|
886
|
+
const record = this.requireCommand(input.commandId);
|
|
887
|
+
if (record.status !== "running") return this.snapshotCommand(record, input);
|
|
888
|
+
const session = this.requireShell(record.shellId);
|
|
889
|
+
const foreground = this.requireForegroundCommand(session, record.id);
|
|
890
|
+
foreground.abortController.abort();
|
|
891
|
+
await foreground.handle.kill("SIGTERM");
|
|
892
|
+
session.state.lastExitCode = 130;
|
|
893
|
+
return this.collectAborted(session, record, foreground, input);
|
|
894
|
+
}
|
|
895
|
+
async releaseCommand(commandId) {
|
|
896
|
+
const record = this.commandsById.get(commandId);
|
|
897
|
+
if (!record || record.status === "running") return false;
|
|
898
|
+
this.commandsById.delete(commandId);
|
|
899
|
+
await this.artifacts.release(record.commandScopeId, commandId);
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
async disposeShell(shellId) {
|
|
903
|
+
const session = this.shells.get(shellId);
|
|
904
|
+
if (!session) return false;
|
|
905
|
+
await this.killShell(session);
|
|
906
|
+
this.shells.delete(shellId);
|
|
907
|
+
for (const [agentSessionId, defaultShellId] of this.defaultShellByAgentSessionId) if (defaultShellId === shellId) this.defaultShellByAgentSessionId.delete(agentSessionId);
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
async disposeAllShells() {
|
|
911
|
+
for (const shellId of this.shells.keys()) await this.disposeShell(shellId);
|
|
912
|
+
}
|
|
913
|
+
async killShell(session) {
|
|
914
|
+
const foreground = session.foreground;
|
|
915
|
+
session.foreground = void 0;
|
|
916
|
+
session.pendingExec = void 0;
|
|
917
|
+
if (foreground) {
|
|
918
|
+
foreground.abortController.abort();
|
|
919
|
+
await foreground.handle.kill("SIGKILL").catch(() => {});
|
|
920
|
+
await foreground.exitPromise.catch(() => {});
|
|
921
|
+
}
|
|
922
|
+
for (const job of session.backgroundJobs.values()) {
|
|
923
|
+
await job.handle.kill("SIGKILL").catch(() => {});
|
|
924
|
+
await job.exitPromise.catch(() => {});
|
|
925
|
+
}
|
|
926
|
+
session.backgroundJobs.clear();
|
|
927
|
+
if (session.abortController) session.abortController.abort();
|
|
928
|
+
}
|
|
929
|
+
requireShell(shellId) {
|
|
930
|
+
const session = this.shells.get(shellId);
|
|
931
|
+
if (!session) throw new Error(`Unknown shell session "${shellId}"`);
|
|
932
|
+
return session;
|
|
933
|
+
}
|
|
934
|
+
requireCommand(commandId) {
|
|
935
|
+
const record = this.commandsById.get(commandId);
|
|
936
|
+
if (!record) throw new Error(`Unknown shell command "${commandId}"`);
|
|
937
|
+
const foreground = this.shells.get(record.shellId)?.foreground;
|
|
938
|
+
if (record.status === "running" && foreground?.commandId === record.id) {
|
|
939
|
+
record.stdout = foreground.stdoutBuffer;
|
|
940
|
+
record.stderr = foreground.stderrBuffer;
|
|
941
|
+
record.outputChunks = [...foreground.outputChunks];
|
|
942
|
+
record.lastOutputAt = foreground.lastOutputAt;
|
|
943
|
+
}
|
|
944
|
+
return record;
|
|
945
|
+
}
|
|
946
|
+
requireForegroundCommand(session, commandId) {
|
|
947
|
+
const foreground = session.foreground;
|
|
948
|
+
if (!foreground || foreground.commandId !== commandId) throw new Error(`Command "${commandId}" has no foreground process`);
|
|
949
|
+
return foreground;
|
|
950
|
+
}
|
|
951
|
+
defaultShell(agentSessionId) {
|
|
952
|
+
if (!agentSessionId) return this.createShell(void 0);
|
|
953
|
+
const existingShellId = this.defaultShellByAgentSessionId.get(agentSessionId);
|
|
954
|
+
const existing = existingShellId ? this.shells.get(existingShellId) : void 0;
|
|
955
|
+
if (existing && !existing.exited) return existing;
|
|
956
|
+
const shell = this.createShell(agentSessionId);
|
|
957
|
+
this.defaultShellByAgentSessionId.set(agentSessionId, shell.id);
|
|
958
|
+
return shell;
|
|
959
|
+
}
|
|
960
|
+
availableDefaultShell(agentSessionId) {
|
|
961
|
+
const shell = this.defaultShell(agentSessionId);
|
|
962
|
+
if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
|
|
963
|
+
return this.createShell(agentSessionId);
|
|
964
|
+
}
|
|
965
|
+
createShell(agentSessionId) {
|
|
966
|
+
const id = this.shellIdFactory();
|
|
967
|
+
const commandScopeId = agentSessionId ?? id;
|
|
968
|
+
const cwd = this.host.defaultCwd;
|
|
969
|
+
const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandScopeId, path) });
|
|
970
|
+
const env = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
|
|
972
|
+
env.set("PWD", cwd);
|
|
973
|
+
env.set("DEMI_SESSION_ID", commandScopeId);
|
|
974
|
+
env.set("DEMI_SHELL_ID", id);
|
|
975
|
+
if (!env.has("IFS")) env.set("IFS", " \n");
|
|
976
|
+
if (!env.has("PS1")) env.set("PS1", "");
|
|
977
|
+
if (!env.has("PS2")) env.set("PS2", "> ");
|
|
978
|
+
if (!env.has("SHLVL")) env.set("SHLVL", "1");
|
|
979
|
+
const exportedVars = /* @__PURE__ */ new Set([
|
|
980
|
+
"PWD",
|
|
981
|
+
"DEMI_SESSION_ID",
|
|
982
|
+
"DEMI_SHELL_ID"
|
|
983
|
+
]);
|
|
984
|
+
for (const key of env.keys()) if (key !== key.toLowerCase()) exportedVars.add(key);
|
|
985
|
+
for (const key of Object.keys(this.initialEnv)) exportedVars.add(key);
|
|
986
|
+
const state = {
|
|
987
|
+
env,
|
|
988
|
+
cwd,
|
|
989
|
+
previousDir: cwd,
|
|
990
|
+
functions: /* @__PURE__ */ new Map(),
|
|
991
|
+
localScopes: [],
|
|
992
|
+
callDepth: 0,
|
|
993
|
+
sourceDepth: 0,
|
|
994
|
+
commandCount: 0,
|
|
995
|
+
lastExitCode: 0,
|
|
996
|
+
lastArg: "",
|
|
997
|
+
startTime: Date.now(),
|
|
998
|
+
lastBackgroundPid: 0,
|
|
999
|
+
virtualPid: 1,
|
|
1000
|
+
virtualPpid: 0,
|
|
1001
|
+
virtualUid: 1e3,
|
|
1002
|
+
virtualGid: 1e3,
|
|
1003
|
+
bashPid: 1,
|
|
1004
|
+
nextVirtualPid: 2,
|
|
1005
|
+
currentLine: 1,
|
|
1006
|
+
options: {
|
|
1007
|
+
errexit: false,
|
|
1008
|
+
pipefail: false,
|
|
1009
|
+
nounset: false,
|
|
1010
|
+
xtrace: false,
|
|
1011
|
+
verbose: false,
|
|
1012
|
+
posix: false,
|
|
1013
|
+
allexport: false,
|
|
1014
|
+
noclobber: false,
|
|
1015
|
+
noglob: false,
|
|
1016
|
+
noexec: false,
|
|
1017
|
+
vi: false,
|
|
1018
|
+
emacs: false
|
|
1019
|
+
},
|
|
1020
|
+
shoptOptions: {
|
|
1021
|
+
extglob: false,
|
|
1022
|
+
dotglob: false,
|
|
1023
|
+
nullglob: false,
|
|
1024
|
+
failglob: false,
|
|
1025
|
+
globstar: false,
|
|
1026
|
+
globskipdots: true,
|
|
1027
|
+
nocaseglob: false,
|
|
1028
|
+
nocasematch: false,
|
|
1029
|
+
expand_aliases: false,
|
|
1030
|
+
lastpipe: false,
|
|
1031
|
+
xpg_echo: false
|
|
1032
|
+
},
|
|
1033
|
+
inCondition: false,
|
|
1034
|
+
loopDepth: 0,
|
|
1035
|
+
exportedVars,
|
|
1036
|
+
readonlyVars: /* @__PURE__ */ new Set(["SHELLOPTS", "BASHOPTS"]),
|
|
1037
|
+
hashTable: /* @__PURE__ */ new Map()
|
|
1038
|
+
};
|
|
1039
|
+
state.env.set("SHELLOPTS", buildShellopts(state.options));
|
|
1040
|
+
state.env.set("BASHOPTS", buildBashopts(state.shoptOptions));
|
|
1041
|
+
const forkCommands = /* @__PURE__ */ new Map();
|
|
1042
|
+
const session = {
|
|
1043
|
+
id,
|
|
1044
|
+
commandScopeId,
|
|
1045
|
+
state,
|
|
1046
|
+
fs,
|
|
1047
|
+
interpreter: void 0,
|
|
1048
|
+
forkCommands,
|
|
1049
|
+
accumulator: {
|
|
1050
|
+
stdout: "",
|
|
1051
|
+
stderr: "",
|
|
1052
|
+
audit: [],
|
|
1053
|
+
commandMetadata: [],
|
|
1054
|
+
assets: []
|
|
1055
|
+
},
|
|
1056
|
+
foregroundWaiters: /* @__PURE__ */ new Set(),
|
|
1057
|
+
backgroundJobs: /* @__PURE__ */ new Map(),
|
|
1058
|
+
nextBackgroundJobId: 1,
|
|
1059
|
+
exited: false
|
|
1060
|
+
};
|
|
1061
|
+
for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
|
|
1062
|
+
const storage = new AgentSessionCommandStorage(this.host.store, commandScopeId);
|
|
1063
|
+
for (const spec of this.commands.list()) forkCommands.set(spec.name, commandSpecToForkCommand(session, spec, storage));
|
|
1064
|
+
session.abortController = new AbortController();
|
|
1065
|
+
session.interpreter = new Interpreter({
|
|
1066
|
+
fs,
|
|
1067
|
+
commands: forkCommands,
|
|
1068
|
+
limits: resolveLimits({
|
|
1069
|
+
maxOutputSize: 1024 * 1024 * 1024,
|
|
1070
|
+
maxCommandCount: 1e6,
|
|
1071
|
+
maxLoopIterations: 1e6,
|
|
1072
|
+
maxCallDepth: 1e3,
|
|
1073
|
+
maxGlobOperations: 1e6
|
|
1074
|
+
}),
|
|
1075
|
+
exec: async () => ({
|
|
1076
|
+
stdout: "",
|
|
1077
|
+
stderr: "",
|
|
1078
|
+
exitCode: 0
|
|
1079
|
+
}),
|
|
1080
|
+
hostSpawn: (command, args, opts) => this.hostSpawn(session, command, args, opts),
|
|
1081
|
+
rejectTimedPipelines: true,
|
|
1082
|
+
jobControl: {
|
|
1083
|
+
startBackground: (statement) => this.startBackgroundJob(session, statement),
|
|
1084
|
+
jobs: (args) => this.listBackgroundJobs(session, args),
|
|
1085
|
+
wait: (args) => this.waitForBackgroundJob(session, args)
|
|
1086
|
+
}
|
|
1087
|
+
}, state);
|
|
1088
|
+
this.shells.set(id, session);
|
|
1089
|
+
return session;
|
|
1090
|
+
}
|
|
1091
|
+
async runScript(session, script, input) {
|
|
1092
|
+
const record = this.createCommandRecord(session, script);
|
|
1093
|
+
let ast;
|
|
1094
|
+
try {
|
|
1095
|
+
ast = parse(script);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (error instanceof ParseException || error instanceof LexerError) {
|
|
1098
|
+
record.stderr = `bash: ${error.message}\n`;
|
|
1099
|
+
appendRecordOutput(record, "stderr", record.stderr);
|
|
1100
|
+
record.status = "exited";
|
|
1101
|
+
record.exitCode = 2;
|
|
1102
|
+
session.state.lastExitCode = 2;
|
|
1103
|
+
session.activeCommandId = void 0;
|
|
1104
|
+
return this.snapshotCommand(record, input);
|
|
1105
|
+
}
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
session.accumulator = {
|
|
1109
|
+
stdout: "",
|
|
1110
|
+
stderr: "",
|
|
1111
|
+
audit: [],
|
|
1112
|
+
commandMetadata: [],
|
|
1113
|
+
assets: []
|
|
1114
|
+
};
|
|
1115
|
+
session.abortController = new AbortController();
|
|
1116
|
+
session.activeCommandId = record.id;
|
|
1117
|
+
const execPromise = session.interpreter.executeScript(ast).then((result) => result, (error) => error);
|
|
1118
|
+
session.pendingExec = execPromise;
|
|
1119
|
+
execPromise.then((result) => {
|
|
1120
|
+
try {
|
|
1121
|
+
if (record.status === "running" && session.pendingExec === execPromise) this.collectExited(session, record, result, session.foreground, {
|
|
1122
|
+
stdoutOffset: record.stdoutOffset,
|
|
1123
|
+
stderrOffset: record.stderrOffset,
|
|
1124
|
+
outputOffset: record.outputOffset
|
|
1125
|
+
});
|
|
1126
|
+
} catch {}
|
|
1127
|
+
}, () => {});
|
|
1128
|
+
return this.raceForeground(session, record, void 0, execPromise, input);
|
|
1129
|
+
}
|
|
1130
|
+
createCommandRecord(session, script) {
|
|
1131
|
+
const id = this.commandIdFactory();
|
|
1132
|
+
const now = Date.now();
|
|
1133
|
+
const record = {
|
|
1134
|
+
id,
|
|
1135
|
+
shellId: session.id,
|
|
1136
|
+
commandScopeId: session.commandScopeId,
|
|
1137
|
+
script,
|
|
1138
|
+
startedAt: now,
|
|
1139
|
+
lastOutputAt: now,
|
|
1140
|
+
status: "running",
|
|
1141
|
+
stdout: "",
|
|
1142
|
+
stderr: "",
|
|
1143
|
+
stdoutOffset: 0,
|
|
1144
|
+
stderrOffset: 0,
|
|
1145
|
+
outputChunks: [],
|
|
1146
|
+
outputOffset: 0,
|
|
1147
|
+
audit: [],
|
|
1148
|
+
commandMetadata: [],
|
|
1149
|
+
assets: []
|
|
1150
|
+
};
|
|
1151
|
+
this.commandsById.set(id, record);
|
|
1152
|
+
return record;
|
|
1153
|
+
}
|
|
1154
|
+
async startBackgroundJob(session, statement) {
|
|
1155
|
+
const backgroundCommand = extractSimpleBackgroundCommand(statement);
|
|
1156
|
+
if (!backgroundCommand) return null;
|
|
1157
|
+
const id = session.nextBackgroundJobId++;
|
|
1158
|
+
const handle = await this.host.process.spawn({
|
|
1159
|
+
command: backgroundCommand.command,
|
|
1160
|
+
args: backgroundCommand.args,
|
|
1161
|
+
cwd: session.state.cwd,
|
|
1162
|
+
env: this.exportedEnv(session),
|
|
1163
|
+
killProcessGroup: true
|
|
1164
|
+
});
|
|
1165
|
+
await handle.closeStdin().catch(() => {});
|
|
1166
|
+
const job = {
|
|
1167
|
+
id,
|
|
1168
|
+
command: backgroundCommand.command,
|
|
1169
|
+
args: backgroundCommand.args,
|
|
1170
|
+
display: formatCommandDisplay(backgroundCommand.command, backgroundCommand.args),
|
|
1171
|
+
cwd: session.state.cwd,
|
|
1172
|
+
handle,
|
|
1173
|
+
stdoutBuffer: "",
|
|
1174
|
+
stderrBuffer: "",
|
|
1175
|
+
stdoutPump: Promise.resolve(),
|
|
1176
|
+
stderrPump: Promise.resolve(),
|
|
1177
|
+
exitPromise: handle.wait()
|
|
1178
|
+
};
|
|
1179
|
+
job.stdoutPump = pumpStream(handle.stdout, (chunk) => {
|
|
1180
|
+
job.stdoutBuffer += decodeUtf8(chunk);
|
|
1181
|
+
});
|
|
1182
|
+
job.stderrPump = pumpStream(handle.stderr, (chunk) => {
|
|
1183
|
+
job.stderrBuffer += decodeUtf8(chunk);
|
|
1184
|
+
});
|
|
1185
|
+
session.backgroundJobs.set(id, job);
|
|
1186
|
+
session.state.lastBackgroundPid = id;
|
|
1187
|
+
session.state.env.set("!", String(id));
|
|
1188
|
+
return {
|
|
1189
|
+
stdout: `[${id}] ${job.display}\n`,
|
|
1190
|
+
stderr: "",
|
|
1191
|
+
exitCode: 0
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
async listBackgroundJobs(session, args) {
|
|
1195
|
+
if (args.length > 0) return {
|
|
1196
|
+
stdout: "",
|
|
1197
|
+
stderr: `bash: jobs: unsupported option or argument: ${args.join(" ")}\n`,
|
|
1198
|
+
exitCode: 2
|
|
1199
|
+
};
|
|
1200
|
+
let stdout = "";
|
|
1201
|
+
for (const job of session.backgroundJobs.values()) stdout += `[${job.id}] Running ${job.display}\n`;
|
|
1202
|
+
return {
|
|
1203
|
+
stdout,
|
|
1204
|
+
stderr: "",
|
|
1205
|
+
exitCode: 0
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
async waitForBackgroundJob(session, args) {
|
|
1209
|
+
if (args.length !== 1) return {
|
|
1210
|
+
stdout: "",
|
|
1211
|
+
stderr: "bash: wait: expected a single job spec\n",
|
|
1212
|
+
exitCode: 2
|
|
1213
|
+
};
|
|
1214
|
+
const match = args[0].match(/^%(\d+)$/);
|
|
1215
|
+
if (!match) return {
|
|
1216
|
+
stdout: "",
|
|
1217
|
+
stderr: `bash: wait: ${args[0]}: unsupported job spec\n`,
|
|
1218
|
+
exitCode: 2
|
|
1219
|
+
};
|
|
1220
|
+
const id = Number.parseInt(match[1], 10);
|
|
1221
|
+
const job = session.backgroundJobs.get(id);
|
|
1222
|
+
if (!job) return {
|
|
1223
|
+
stdout: "",
|
|
1224
|
+
stderr: `bash: wait: %${id}: no such job\n`,
|
|
1225
|
+
exitCode: 127
|
|
1226
|
+
};
|
|
1227
|
+
const exit = await job.exitPromise;
|
|
1228
|
+
await Promise.allSettled([job.stdoutPump, job.stderrPump]);
|
|
1229
|
+
session.backgroundJobs.delete(id);
|
|
1230
|
+
const exitCode = exit.exitCode ?? 127;
|
|
1231
|
+
const stderr = exit.exitCode === null && job.stderrBuffer.length === 0 ? `${job.command}: ${exit.signal ?? "command not found"}\n` : job.stderrBuffer;
|
|
1232
|
+
session.accumulator.audit.push({
|
|
1233
|
+
kind: "system-command",
|
|
1234
|
+
name: job.command,
|
|
1235
|
+
args: job.args,
|
|
1236
|
+
cwd: job.cwd,
|
|
1237
|
+
exitCode
|
|
1238
|
+
});
|
|
1239
|
+
return {
|
|
1240
|
+
stdout: job.stdoutBuffer,
|
|
1241
|
+
stderr,
|
|
1242
|
+
exitCode
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
exportedEnv(session) {
|
|
1246
|
+
const env = {};
|
|
1247
|
+
for (const name of session.state.exportedVars ?? []) {
|
|
1248
|
+
const value = session.state.env.get(name);
|
|
1249
|
+
if (value !== void 0) env[name] = value;
|
|
1250
|
+
}
|
|
1251
|
+
return env;
|
|
1252
|
+
}
|
|
1253
|
+
async raceForeground(session, record, foreground, execPromise, input) {
|
|
1254
|
+
const timeoutMs = normalizeTimeoutMs(input.timeoutMs);
|
|
1255
|
+
const operationStartedAt = Date.now();
|
|
1256
|
+
while (true) {
|
|
1257
|
+
const foregroundNow = session.foreground;
|
|
1258
|
+
if (foregroundNow && foregroundNow !== foreground) foreground = foregroundNow;
|
|
1259
|
+
const boundary = this.waitForBoundary(session, foreground, operationStartedAt, timeoutMs, input.signal);
|
|
1260
|
+
const outcome = await Promise.race([execPromise.then((r) => ({
|
|
1261
|
+
kind: "done",
|
|
1262
|
+
result: r
|
|
1263
|
+
})), boundary.promise]);
|
|
1264
|
+
boundary.cancel();
|
|
1265
|
+
if (outcome.kind === "done") return this.collectExited(session, record, outcome.result, foreground, input);
|
|
1266
|
+
if (outcome.kind === "foreground_appeared") {
|
|
1267
|
+
foreground = outcome.foreground;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (outcome.kind === "timeout") return this.snapshotCommand(record, input);
|
|
1271
|
+
if (outcome.kind === "aborted") {
|
|
1272
|
+
const activeForeground = foreground ?? session.foreground;
|
|
1273
|
+
if (!activeForeground) return this.collectAbortedWithoutForeground(session, record, input);
|
|
1274
|
+
return this.collectAborted(session, record, activeForeground, input);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
waitForBoundary(session, foreground, operationStartedAt, timeoutMs, externalSignal) {
|
|
1279
|
+
const timers = [];
|
|
1280
|
+
const listeners = [];
|
|
1281
|
+
const cancel = () => {
|
|
1282
|
+
for (const clear of timers) clear();
|
|
1283
|
+
for (const remove of listeners) remove();
|
|
1284
|
+
};
|
|
1285
|
+
if (!foreground) if (session.foreground) foreground = session.foreground;
|
|
1286
|
+
else return {
|
|
1287
|
+
promise: new Promise((resolve) => {
|
|
1288
|
+
if (externalSignal?.aborted) {
|
|
1289
|
+
resolve({ kind: "aborted" });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const timeoutIn = Math.max(0, timeoutMs - (Date.now() - operationStartedAt));
|
|
1293
|
+
const t = setTimeout(() => resolve({ kind: "timeout" }), timeoutIn);
|
|
1294
|
+
timers.push(() => clearTimeout(t));
|
|
1295
|
+
const onForeground = (nextForeground) => {
|
|
1296
|
+
resolve({
|
|
1297
|
+
kind: "foreground_appeared",
|
|
1298
|
+
foreground: nextForeground
|
|
1299
|
+
});
|
|
1300
|
+
};
|
|
1301
|
+
session.foregroundWaiters.add(onForeground);
|
|
1302
|
+
listeners.push(() => session.foregroundWaiters.delete(onForeground));
|
|
1303
|
+
if (externalSignal) {
|
|
1304
|
+
const onExternal = () => resolve({ kind: "aborted" });
|
|
1305
|
+
externalSignal.addEventListener("abort", onExternal, { once: true });
|
|
1306
|
+
listeners.push(() => externalSignal.removeEventListener("abort", onExternal));
|
|
1307
|
+
}
|
|
1308
|
+
}),
|
|
1309
|
+
cancel
|
|
1310
|
+
};
|
|
1311
|
+
const fg = foreground;
|
|
1312
|
+
return {
|
|
1313
|
+
promise: new Promise((resolve) => {
|
|
1314
|
+
if (fg.abortController.signal.aborted || externalSignal?.aborted) {
|
|
1315
|
+
resolve({ kind: "aborted" });
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const timeoutIn = Math.max(0, timeoutMs - (Date.now() - operationStartedAt));
|
|
1319
|
+
const t = setTimeout(() => resolve({ kind: "timeout" }), timeoutIn);
|
|
1320
|
+
timers.push(() => clearTimeout(t));
|
|
1321
|
+
const onAbort = () => resolve({ kind: "aborted" });
|
|
1322
|
+
fg.abortController.signal.addEventListener("abort", onAbort, { once: true });
|
|
1323
|
+
listeners.push(() => fg.abortController.signal.removeEventListener("abort", onAbort));
|
|
1324
|
+
if (externalSignal) {
|
|
1325
|
+
const onExternal = () => resolve({ kind: "aborted" });
|
|
1326
|
+
externalSignal.addEventListener("abort", onExternal, { once: true });
|
|
1327
|
+
listeners.push(() => externalSignal.removeEventListener("abort", onExternal));
|
|
1328
|
+
}
|
|
1329
|
+
}),
|
|
1330
|
+
cancel
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
async hostSpawn(session, command, args, opts) {
|
|
1334
|
+
if (session.foreground) throw new Error(`hostSpawn: session "${session.id}" already has a foreground process`);
|
|
1335
|
+
const handle = await this.host.process.spawn({
|
|
1336
|
+
command,
|
|
1337
|
+
args,
|
|
1338
|
+
cwd: opts.cwd,
|
|
1339
|
+
env: opts.env,
|
|
1340
|
+
killProcessGroup: true
|
|
1341
|
+
});
|
|
1342
|
+
const startedAt = Date.now();
|
|
1343
|
+
const abortController = new AbortController();
|
|
1344
|
+
const commandId = session.activeCommandId;
|
|
1345
|
+
if (!commandId) throw new Error(`hostSpawn: session "${session.id}" has no active command`);
|
|
1346
|
+
const foreground = {
|
|
1347
|
+
commandId,
|
|
1348
|
+
command,
|
|
1349
|
+
args,
|
|
1350
|
+
cwd: opts.cwd,
|
|
1351
|
+
handle,
|
|
1352
|
+
startedAt,
|
|
1353
|
+
lastOutputAt: startedAt,
|
|
1354
|
+
rawStdoutBuffer: "",
|
|
1355
|
+
rawStderrBuffer: "",
|
|
1356
|
+
stdoutBuffer: "",
|
|
1357
|
+
stderrBuffer: "",
|
|
1358
|
+
outputChunks: [],
|
|
1359
|
+
outputBytes: 0,
|
|
1360
|
+
audit: [{
|
|
1361
|
+
kind: "system-command",
|
|
1362
|
+
name: command,
|
|
1363
|
+
args,
|
|
1364
|
+
cwd: opts.cwd,
|
|
1365
|
+
exitCode: 0
|
|
1366
|
+
}],
|
|
1367
|
+
stdoutPump: Promise.resolve(),
|
|
1368
|
+
stderrPump: Promise.resolve(),
|
|
1369
|
+
exitPromise: handle.wait(),
|
|
1370
|
+
outputSinks: createOutputSinks(session.fs, opts.cwd, opts.redirections),
|
|
1371
|
+
abortController
|
|
1372
|
+
};
|
|
1373
|
+
session.foreground = foreground;
|
|
1374
|
+
notifyForegroundWaiters(session.foregroundWaiters, foreground);
|
|
1375
|
+
if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(encodeUtf8(opts.stdin));
|
|
1376
|
+
if (opts.stdinProvided) await handle.closeStdin();
|
|
1377
|
+
if (handle.output) {
|
|
1378
|
+
foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
|
|
1379
|
+
recordForegroundChunk(foreground, chunk.stream === "stdout" ? 1 : 2, chunk.chunk);
|
|
1380
|
+
});
|
|
1381
|
+
foreground.stderrPump = Promise.resolve();
|
|
1382
|
+
} else {
|
|
1383
|
+
foreground.stdoutPump = pumpStream(handle.stdout, (chunk) => recordForegroundChunk(foreground, 1, chunk));
|
|
1384
|
+
foreground.stderrPump = pumpStream(handle.stderr, (chunk) => recordForegroundChunk(foreground, 2, chunk));
|
|
1385
|
+
}
|
|
1386
|
+
const exit = await foreground.exitPromise;
|
|
1387
|
+
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1388
|
+
const stdout = foreground.rawStdoutBuffer;
|
|
1389
|
+
const exitCode = exit.exitCode ?? 127;
|
|
1390
|
+
const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
|
|
1391
|
+
foreground.audit[0] = {
|
|
1392
|
+
kind: "system-command",
|
|
1393
|
+
name: command,
|
|
1394
|
+
args,
|
|
1395
|
+
cwd: opts.cwd,
|
|
1396
|
+
exitCode
|
|
1397
|
+
};
|
|
1398
|
+
session.accumulator.audit.push(...foreground.audit);
|
|
1399
|
+
const record = this.commandsById.get(commandId);
|
|
1400
|
+
if (record) {
|
|
1401
|
+
record.stdout = foreground.stdoutBuffer;
|
|
1402
|
+
record.stderr = foreground.stderrBuffer;
|
|
1403
|
+
record.outputChunks = [...foreground.outputChunks];
|
|
1404
|
+
record.lastOutputAt = foreground.lastOutputAt;
|
|
1405
|
+
}
|
|
1406
|
+
session.foreground = void 0;
|
|
1407
|
+
return {
|
|
1408
|
+
stdout,
|
|
1409
|
+
stderr,
|
|
1410
|
+
exitCode
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
collectExited(session, record, resultOrError, foreground, input = {}) {
|
|
1414
|
+
if (record.status !== "running") return this.snapshotCommand(record, input);
|
|
1415
|
+
if (resultOrError instanceof Error) {
|
|
1416
|
+
if (resultOrError instanceof ExitError) {
|
|
1417
|
+
session.exited = true;
|
|
1418
|
+
const err = resultOrError;
|
|
1419
|
+
const outText = decodeBytesToUtf8(unsafeBytesFromLatin1(err.stdout));
|
|
1420
|
+
const errText = decodeBytesToUtf8(unsafeBytesFromLatin1(err.stderr));
|
|
1421
|
+
session.accumulator.stdout += outText;
|
|
1422
|
+
session.accumulator.stderr += errText;
|
|
1423
|
+
appendRecordOutput(record, "stdout", outText);
|
|
1424
|
+
appendRecordOutput(record, "stderr", errText);
|
|
1425
|
+
return this.finishExited(session, record, err.exitCode, input);
|
|
1426
|
+
}
|
|
1427
|
+
if (resultOrError instanceof ExecutionLimitError) {
|
|
1428
|
+
const text = `bash: execution limit exceeded: ${resultOrError.message}\n`;
|
|
1429
|
+
session.accumulator.stderr += text;
|
|
1430
|
+
appendRecordOutput(record, "stderr", text);
|
|
1431
|
+
return this.finishExited(session, record, ExecutionLimitError.EXIT_CODE, input);
|
|
1432
|
+
}
|
|
1433
|
+
if (resultOrError instanceof ParseException || resultOrError instanceof LexerError) {
|
|
1434
|
+
const text = `bash: ${resultOrError.message}\n`;
|
|
1435
|
+
session.accumulator.stderr += text;
|
|
1436
|
+
appendRecordOutput(record, "stderr", text);
|
|
1437
|
+
return this.finishExited(session, record, 2, input);
|
|
1438
|
+
}
|
|
1439
|
+
if (resultOrError.message.startsWith("Unsupported shell syntax:")) {
|
|
1440
|
+
session.pendingExec = void 0;
|
|
1441
|
+
throw resultOrError;
|
|
1442
|
+
}
|
|
1443
|
+
if (resultOrError instanceof ArithmeticError || resultOrError instanceof BadSubstitutionError) {
|
|
1444
|
+
session.pendingExec = void 0;
|
|
1445
|
+
throw resultOrError;
|
|
1446
|
+
}
|
|
1447
|
+
const text = `bash: ${resultOrError.message}\n`;
|
|
1448
|
+
session.accumulator.stderr += text;
|
|
1449
|
+
appendRecordOutput(record, "stderr", text);
|
|
1450
|
+
return this.finishExited(session, record, 1, input);
|
|
1451
|
+
}
|
|
1452
|
+
const stdoutText = foreground ? resultOrError.stdout : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stdout));
|
|
1453
|
+
const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
|
|
1454
|
+
session.accumulator.stdout += stdoutText;
|
|
1455
|
+
session.accumulator.stderr += stderrText;
|
|
1456
|
+
if (foreground) record.outputChunks = [...foreground.outputChunks];
|
|
1457
|
+
else if (record.outputChunks.length === 0) {
|
|
1458
|
+
appendRecordOutput(record, "stdout", stdoutText);
|
|
1459
|
+
appendRecordOutput(record, "stderr", stderrText);
|
|
1460
|
+
}
|
|
1461
|
+
return this.finishExited(session, record, resultOrError.exitCode, input);
|
|
1462
|
+
}
|
|
1463
|
+
finishExited(session, record, exitCode, input) {
|
|
1464
|
+
record.stdout = session.accumulator.stdout;
|
|
1465
|
+
record.stderr = session.accumulator.stderr;
|
|
1466
|
+
if (record.outputChunks.length === 0) {
|
|
1467
|
+
appendRecordOutput(record, "stdout", record.stdout);
|
|
1468
|
+
appendRecordOutput(record, "stderr", record.stderr);
|
|
1469
|
+
}
|
|
1470
|
+
ensureRecordOutputCoverage(record);
|
|
1471
|
+
record.lastOutputAt = Date.now();
|
|
1472
|
+
record.status = "exited";
|
|
1473
|
+
record.exitCode = exitCode;
|
|
1474
|
+
record.audit = [...session.accumulator.audit];
|
|
1475
|
+
record.commandMetadata = [...session.accumulator.commandMetadata];
|
|
1476
|
+
record.assets = [...session.accumulator.assets];
|
|
1477
|
+
session.pendingExec = void 0;
|
|
1478
|
+
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1479
|
+
return this.snapshotCommand(record, input);
|
|
1480
|
+
}
|
|
1481
|
+
async collectAborted(session, record, foreground, input = {}) {
|
|
1482
|
+
if (record.status !== "running") return this.snapshotCommand(record, input);
|
|
1483
|
+
foreground.abortController.abort();
|
|
1484
|
+
foreground.handle.kill("SIGTERM").catch(() => {});
|
|
1485
|
+
await flushForegroundSinks(session, foreground);
|
|
1486
|
+
record.stdout = foreground.stdoutBuffer;
|
|
1487
|
+
record.stderr = foreground.stderrBuffer;
|
|
1488
|
+
record.outputChunks = [...foreground.outputChunks];
|
|
1489
|
+
record.lastOutputAt = Date.now();
|
|
1490
|
+
record.status = "aborted";
|
|
1491
|
+
session.foreground = void 0;
|
|
1492
|
+
session.pendingExec = void 0;
|
|
1493
|
+
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1494
|
+
return this.snapshotCommand(record, input);
|
|
1495
|
+
}
|
|
1496
|
+
collectAbortedWithoutForeground(session, record, input = {}) {
|
|
1497
|
+
if (record.status !== "running") return this.snapshotCommand(record, input);
|
|
1498
|
+
session.abortController?.abort();
|
|
1499
|
+
session.pendingExec = void 0;
|
|
1500
|
+
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1501
|
+
record.stdout = session.accumulator.stdout;
|
|
1502
|
+
record.stderr = session.accumulator.stderr;
|
|
1503
|
+
if (record.outputChunks.length === 0) {
|
|
1504
|
+
appendRecordOutput(record, "stdout", record.stdout);
|
|
1505
|
+
appendRecordOutput(record, "stderr", record.stderr);
|
|
1506
|
+
}
|
|
1507
|
+
record.lastOutputAt = Date.now();
|
|
1508
|
+
record.status = "aborted";
|
|
1509
|
+
return this.snapshotCommand(record, input);
|
|
1510
|
+
}
|
|
1511
|
+
snapshotCommand(record, input = {}) {
|
|
1512
|
+
const foreground = this.shells.get(record.shellId)?.foreground;
|
|
1513
|
+
if (record.status === "running" && foreground?.commandId === record.id) {
|
|
1514
|
+
record.stdout = foreground.stdoutBuffer;
|
|
1515
|
+
record.stderr = foreground.stderrBuffer;
|
|
1516
|
+
record.outputChunks = [...foreground.outputChunks];
|
|
1517
|
+
record.lastOutputAt = foreground.lastOutputAt;
|
|
1518
|
+
}
|
|
1519
|
+
const maxOutputBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
|
|
1520
|
+
const stdout = streamArtifact(record, "stdout", input.stdoutOffset, maxOutputBytes);
|
|
1521
|
+
const stderr = streamArtifact(record, "stderr", input.stderrOffset, maxOutputBytes);
|
|
1522
|
+
const output = streamOutputArtifact(record, input.outputOffset, maxOutputBytes);
|
|
1523
|
+
const base = {
|
|
1524
|
+
shellId: record.shellId,
|
|
1525
|
+
commandId: record.id,
|
|
1526
|
+
stdout,
|
|
1527
|
+
stderr,
|
|
1528
|
+
output,
|
|
1529
|
+
runningMs: Date.now() - record.startedAt,
|
|
1530
|
+
idleMs: Date.now() - record.lastOutputAt
|
|
1531
|
+
};
|
|
1532
|
+
this.persistCommandArtifact(record);
|
|
1533
|
+
if (record.status === "exited") {
|
|
1534
|
+
const result = {
|
|
1535
|
+
...base,
|
|
1536
|
+
status: "exited",
|
|
1537
|
+
exitCode: record.exitCode ?? 0,
|
|
1538
|
+
audit: record.audit
|
|
1539
|
+
};
|
|
1540
|
+
if (record.commandMetadata.length > 0) result.commandMetadata = record.commandMetadata;
|
|
1541
|
+
if (record.assets.length > 0) result.assets = record.assets;
|
|
1542
|
+
return result;
|
|
1543
|
+
}
|
|
1544
|
+
if (record.status === "aborted") return {
|
|
1545
|
+
...base,
|
|
1546
|
+
status: "aborted"
|
|
1547
|
+
};
|
|
1548
|
+
return {
|
|
1549
|
+
...base,
|
|
1550
|
+
status: "running"
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
async lookupVirtualArtifact(scopeId, path) {
|
|
1554
|
+
const parts = path.split("/").filter(Boolean);
|
|
1555
|
+
if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
|
|
1556
|
+
if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(scopeId));
|
|
1557
|
+
if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
|
|
1558
|
+
if (!await this.commandArtifact(scopeId, parts[2])) return null;
|
|
1559
|
+
return virtualDirectory([
|
|
1560
|
+
"meta.json",
|
|
1561
|
+
"stderr.txt",
|
|
1562
|
+
"stdout.txt"
|
|
1563
|
+
]);
|
|
1564
|
+
}
|
|
1565
|
+
if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
|
|
1566
|
+
const artifact = await this.commandArtifact(scopeId, parts[2]);
|
|
1567
|
+
if (!artifact) return null;
|
|
1568
|
+
const fileName = parts[3];
|
|
1569
|
+
if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
|
|
1570
|
+
if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
|
|
1571
|
+
if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
async commandArtifactIds(scopeId) {
|
|
1575
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1576
|
+
for (const record of this.commandsById.values()) if (record.commandScopeId === scopeId && !this.artifacts.isReleased(scopeId, record.id)) ids.add(record.id);
|
|
1577
|
+
const keys = await this.artifacts.storageFor(scopeId).list("commands").catch(() => []);
|
|
1578
|
+
for (const key of keys) {
|
|
1579
|
+
const match = /^commands\/([^/]+)\/artifact\.json$/.exec(key);
|
|
1580
|
+
if (match && !this.artifacts.isReleased(scopeId, match[1])) ids.add(match[1]);
|
|
1581
|
+
}
|
|
1582
|
+
return [...ids];
|
|
1583
|
+
}
|
|
1584
|
+
async commandArtifact(scopeId, commandId) {
|
|
1585
|
+
if (this.artifacts.isReleased(scopeId, commandId)) return null;
|
|
1586
|
+
const record = this.commandsById.get(commandId);
|
|
1587
|
+
if (record?.commandScopeId === scopeId) {
|
|
1588
|
+
this.syncRunningRecord(record);
|
|
1589
|
+
return persistedArtifactFromRecord(record);
|
|
1590
|
+
}
|
|
1591
|
+
const value = await this.artifacts.storageFor(scopeId).readJson(`commands/${commandId}/artifact.json`).catch(() => null);
|
|
1592
|
+
return isPersistedShellCommandArtifact(value) ? value : null;
|
|
1593
|
+
}
|
|
1594
|
+
persistCommandArtifact(record) {
|
|
1595
|
+
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}`;
|
|
1596
|
+
if (record.persistedFingerprint === fingerprint) return;
|
|
1597
|
+
record.persistedFingerprint = fingerprint;
|
|
1598
|
+
this.artifacts.persist(record.commandScopeId, record.id, persistedArtifactFromRecord(record));
|
|
1599
|
+
}
|
|
1600
|
+
syncRunningRecord(record) {
|
|
1601
|
+
const foreground = this.shells.get(record.shellId)?.foreground;
|
|
1602
|
+
if (record.status === "running" && foreground?.commandId === record.id) {
|
|
1603
|
+
record.stdout = foreground.stdoutBuffer;
|
|
1604
|
+
record.stderr = foreground.stderrBuffer;
|
|
1605
|
+
record.outputChunks = [...foreground.outputChunks];
|
|
1606
|
+
record.lastOutputAt = foreground.lastOutputAt;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
function createPortableCommands(session) {
|
|
1611
|
+
return createLazyCommands([...DEMI_PORTABLE_COMMANDS]).map((command) => ({
|
|
1612
|
+
...command,
|
|
1613
|
+
execute: async (args, ctx) => {
|
|
1614
|
+
const result = await command.execute(args, ctx);
|
|
1615
|
+
session.accumulator.audit.push({
|
|
1616
|
+
kind: "portable-command",
|
|
1617
|
+
name: command.name,
|
|
1618
|
+
args,
|
|
1619
|
+
cwd: ctx.cwd,
|
|
1620
|
+
exitCode: result.exitCode
|
|
1621
|
+
});
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
}));
|
|
1625
|
+
}
|
|
1626
|
+
function normalizeTimeoutMs(value) {
|
|
1627
|
+
if (!Number.isFinite(value) || value < 1 || value > MAX_TIMEOUT_MS) throw new Error(`timeoutMs must be between 1 and ${MAX_TIMEOUT_MS}`);
|
|
1628
|
+
return Math.floor(value);
|
|
1629
|
+
}
|
|
1630
|
+
function streamArtifact(record, stream, explicitOffset, maxOutputBytes) {
|
|
1631
|
+
const text = stream === "stdout" ? record.stdout : record.stderr;
|
|
1632
|
+
const totalBytes = utf8Bytes(text);
|
|
1633
|
+
const boundedOffset = clampOffset(explicitOffset ?? (stream === "stdout" ? record.stdoutOffset : record.stderrOffset), totalBytes);
|
|
1634
|
+
const available = Math.max(0, totalBytes - boundedOffset);
|
|
1635
|
+
const byteLimit = Math.max(0, Math.floor(maxOutputBytes));
|
|
1636
|
+
const delta = utf8Slice(text, boundedOffset, boundedOffset + (byteLimit === 0 ? available : Math.min(available, byteLimit)));
|
|
1637
|
+
const nextOffset = boundedOffset + utf8Bytes(delta);
|
|
1638
|
+
const truncated = nextOffset < totalBytes;
|
|
1639
|
+
if (explicitOffset === void 0) if (stream === "stdout") record.stdoutOffset = nextOffset;
|
|
1640
|
+
else record.stderrOffset = nextOffset;
|
|
1641
|
+
return {
|
|
1642
|
+
path: `/@/commands/${record.id}/${stream}.txt`,
|
|
1643
|
+
offset: nextOffset,
|
|
1644
|
+
delta,
|
|
1645
|
+
tail: tailString(text),
|
|
1646
|
+
bytes: totalBytes,
|
|
1647
|
+
truncated
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function streamOutputArtifact(record, explicitOffset, maxOutputBytes) {
|
|
1651
|
+
const totalBytes = record.outputChunks.reduce((total, chunk) => total + chunk.bytes, 0);
|
|
1652
|
+
const offset = clampOffset(explicitOffset ?? record.outputOffset, totalBytes);
|
|
1653
|
+
const byteLimit = Math.max(0, Math.floor(maxOutputBytes));
|
|
1654
|
+
const available = Math.max(0, totalBytes - offset);
|
|
1655
|
+
let remaining = byteLimit === 0 ? available : Math.min(available, byteLimit);
|
|
1656
|
+
const chunks = [];
|
|
1657
|
+
for (const chunk of record.outputChunks) {
|
|
1658
|
+
if (remaining <= 0) break;
|
|
1659
|
+
const chunkStart = chunk.offset;
|
|
1660
|
+
if (chunk.offset + chunk.bytes <= offset) continue;
|
|
1661
|
+
const start = Math.max(0, offset - chunkStart);
|
|
1662
|
+
const take = Math.min(chunk.bytes - start, remaining);
|
|
1663
|
+
const text = utf8Slice(chunk.text, start, start + take);
|
|
1664
|
+
if (text.length > 0) {
|
|
1665
|
+
chunks.push({
|
|
1666
|
+
stream: chunk.stream,
|
|
1667
|
+
text
|
|
1668
|
+
});
|
|
1669
|
+
remaining -= utf8Bytes(text);
|
|
1670
|
+
} else remaining -= take;
|
|
1671
|
+
}
|
|
1672
|
+
const text = chunks.map((chunk) => chunk.text).join("");
|
|
1673
|
+
const nextOffset = offset + utf8Bytes(text);
|
|
1674
|
+
const truncated = nextOffset < totalBytes;
|
|
1675
|
+
if (explicitOffset === void 0) record.outputOffset = nextOffset;
|
|
1676
|
+
return {
|
|
1677
|
+
path: `demi://shell/${record.shellId}/commands/${record.id}/output`,
|
|
1678
|
+
offset: nextOffset,
|
|
1679
|
+
text,
|
|
1680
|
+
tail: tailOutputText(record.outputChunks),
|
|
1681
|
+
chunks,
|
|
1682
|
+
bytes: totalBytes,
|
|
1683
|
+
truncated
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function appendRecordOutput(record, stream, text) {
|
|
1687
|
+
if (text.length === 0) return;
|
|
1688
|
+
const offset = record.outputChunks.reduce((total, chunk) => total + chunk.bytes, 0);
|
|
1689
|
+
record.outputChunks.push({
|
|
1690
|
+
stream,
|
|
1691
|
+
text,
|
|
1692
|
+
offset,
|
|
1693
|
+
bytes: utf8Bytes(text)
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
function ensureRecordOutputCoverage(record) {
|
|
1697
|
+
const stdoutBytes = record.outputChunks.filter((chunk) => chunk.stream === "stdout").reduce((total, chunk) => total + chunk.bytes, 0);
|
|
1698
|
+
const stderrBytes = record.outputChunks.filter((chunk) => chunk.stream === "stderr").reduce((total, chunk) => total + chunk.bytes, 0);
|
|
1699
|
+
if (stdoutBytes === utf8Bytes(record.stdout) && stderrBytes === utf8Bytes(record.stderr)) return;
|
|
1700
|
+
record.outputChunks = [];
|
|
1701
|
+
appendRecordOutput(record, "stdout", record.stdout);
|
|
1702
|
+
appendRecordOutput(record, "stderr", record.stderr);
|
|
1703
|
+
}
|
|
1704
|
+
function persistedArtifactFromRecord(record) {
|
|
1705
|
+
return {
|
|
1706
|
+
status: record.status,
|
|
1707
|
+
shellId: record.shellId,
|
|
1708
|
+
commandId: record.id,
|
|
1709
|
+
startedAt: record.startedAt,
|
|
1710
|
+
lastOutputAt: record.lastOutputAt,
|
|
1711
|
+
exitCode: record.exitCode ?? null,
|
|
1712
|
+
stdout: record.stdout,
|
|
1713
|
+
stderr: record.stderr
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function commandArtifactMeta(artifact) {
|
|
1717
|
+
const stdoutPath = `/@/commands/${artifact.commandId}/stdout.txt`;
|
|
1718
|
+
const stderrPath = `/@/commands/${artifact.commandId}/stderr.txt`;
|
|
1719
|
+
return {
|
|
1720
|
+
status: artifact.status,
|
|
1721
|
+
shellId: artifact.shellId,
|
|
1722
|
+
commandId: artifact.commandId,
|
|
1723
|
+
startedAt: artifact.startedAt,
|
|
1724
|
+
lastOutputAt: artifact.lastOutputAt,
|
|
1725
|
+
runningMs: Date.now() - artifact.startedAt,
|
|
1726
|
+
idleMs: Date.now() - artifact.lastOutputAt,
|
|
1727
|
+
exitCode: artifact.exitCode,
|
|
1728
|
+
stdout: {
|
|
1729
|
+
path: stdoutPath,
|
|
1730
|
+
bytes: utf8Bytes(artifact.stdout)
|
|
1731
|
+
},
|
|
1732
|
+
stderr: {
|
|
1733
|
+
path: stderrPath,
|
|
1734
|
+
bytes: utf8Bytes(artifact.stderr)
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function isPersistedShellCommandArtifact(value) {
|
|
1739
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1740
|
+
const record = value;
|
|
1741
|
+
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";
|
|
1742
|
+
}
|
|
1743
|
+
function tailOutputText(chunks) {
|
|
1744
|
+
const maxChars = 4096;
|
|
1745
|
+
let text = "";
|
|
1746
|
+
for (let i = chunks.length - 1; i >= 0 && text.length < maxChars; i -= 1) text = `${chunks[i].text}${text}`;
|
|
1747
|
+
return tailString(text);
|
|
1748
|
+
}
|
|
1749
|
+
function clampOffset(value, max) {
|
|
1750
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
1751
|
+
return Math.min(Math.floor(value), max);
|
|
1752
|
+
}
|
|
1753
|
+
function tailString(value) {
|
|
1754
|
+
return tail(value, 4096);
|
|
1755
|
+
}
|
|
1756
|
+
//#endregion
|
|
1757
|
+
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_PROMPT_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, RESERVED_COMMAND_NAMES, isCommandGroup, listRegisteredCommandOperations, parseCommandInput, renderCommandPrompt, runRegisteredCommand, virtualDirectory, virtualFile };
|