@demicodes/shell 0.1.0 → 0.2.1
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-BfjoIvF3.d.mts → host-DIg4RxcL.d.mts} +5 -0
- package/dist/host-fs.d.mts +1 -2
- package/dist/host-fs.mjs +4 -4
- package/dist/index.d.mts +50 -32
- package/dist/index.mjs +314 -282
- package/dist/storage-MeB35Df6.d.mts +105 -0
- package/dist/storage.d.mts +1 -1
- package/package.json +3 -3
- package/dist/storage-DJxxqyZA.d.mts +0 -111
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { HostBackedFileSystem, virtualDirectory, virtualFile } from "./host-fs.mjs";
|
|
2
2
|
import { AgentSessionCommandStorage } from "./storage.mjs";
|
|
3
|
-
import { asError, concatBytes, decodeUtf8, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
|
|
4
|
-
import {
|
|
3
|
+
import { asError, base64ToBytes, bytesToBase64, concatBytes, decodeLatin1, decodeUtf8, decodeUtf8Strict, encodeLatin1, encodeUtf8, tail, utf8Bytes, utf8Slice } from "@demicodes/utils";
|
|
4
|
+
import { createLazyCommands, getCommandNames } from "@demicodes/just-bash/commands";
|
|
5
5
|
import { ArithmeticError, BadSubstitutionError, ExecutionLimitError, ExitError, Interpreter } from "@demicodes/just-bash/interpreter";
|
|
6
|
-
import { createLazyCommands } from "@demicodes/just-bash/commands";
|
|
7
6
|
import { decodeBytesToUtf8, unsafeBytesFromLatin1 } from "@demicodes/just-bash/encoding";
|
|
8
7
|
import { parse } from "@demicodes/just-bash/parser";
|
|
9
8
|
import { ParseException } from "@demicodes/just-bash/parser/types";
|
|
@@ -11,63 +10,42 @@ import { LexerError } from "@demicodes/just-bash/parser/lexer";
|
|
|
11
10
|
import { resolveLimits } from "@demicodes/just-bash/limits";
|
|
12
11
|
//#region src/portable-commands.ts
|
|
13
12
|
/**
|
|
13
|
+
* Deliberately routed to real OS processes (Host.process.spawn) even though
|
|
14
|
+
* just-bash ships portable versions. This is a semantics decision, not a
|
|
15
|
+
* missing feature:
|
|
16
|
+
*
|
|
17
|
+
* - `bash`, `sh`: scripts in real repositories expect a real interpreter
|
|
18
|
+
* (full bash semantics, real coreutils, real subprocess behavior). Routing
|
|
19
|
+
* them into just-bash's simulated interpreter would silently downgrade
|
|
20
|
+
* every `bash script.sh` a model runs. (The fork's nested-interpreter
|
|
21
|
+
* stdout also does not forward through this package's wrapping — but even
|
|
22
|
+
* with that fixed, real spawn stays the right routing.)
|
|
23
|
+
* - `sleep`: must be a real, abortable OS process so foreground abort and
|
|
24
|
+
* timeout semantics (`env.abort()` while `running`) genuinely interrupt
|
|
25
|
+
* it; just-bash's version is a plain in-process timer.
|
|
26
|
+
*/
|
|
27
|
+
const REAL_SPAWN_DEPENDENT_COMMANDS = /* @__PURE__ */ new Set([
|
|
28
|
+
"bash",
|
|
29
|
+
"sh",
|
|
30
|
+
"sleep"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
14
33
|
* Fork portable commands BashEnvironment registers in every shell, so
|
|
15
34
|
* cat/ls/grep-class tools work without local coreutils on any Host backend.
|
|
35
|
+
*
|
|
36
|
+
* Sourced directly from just-bash's own registry instead of a hand-maintained
|
|
37
|
+
* copy: `CommandName` is just-bash's own "safe to run anywhere" set — it
|
|
38
|
+
* already excludes the commands that need real process/network access
|
|
39
|
+
* (curl → NetworkCommandName, python3/python → PythonCommandName, js-exec/node
|
|
40
|
+
* → JavaScriptCommandName all live in separate, non-portable type unions). A
|
|
41
|
+
* hand-picked subset of this list drifts silently as just-bash adds commands
|
|
42
|
+
* (this one was missing echo, pwd, printf, date, env, hostname, whoami, gzip,
|
|
43
|
+
* tar, split, sqlite3, xan, and more — all already categorized as safe
|
|
44
|
+
* upstream, just never backported here). Deriving it removes that drift
|
|
45
|
+
* entirely: everything just-bash calls portable (minus the exceptions above),
|
|
46
|
+
* we call portable.
|
|
16
47
|
*/
|
|
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
|
-
];
|
|
48
|
+
const DEMI_PORTABLE_COMMANDS = getCommandNames().filter((name) => !REAL_SPAWN_DEPENDENT_COMMANDS.has(name));
|
|
71
49
|
/** Shell language words and builtins the interpreter itself owns. */
|
|
72
50
|
const SHELL_BUILTIN_NAMES = [
|
|
73
51
|
".",
|
|
@@ -94,7 +72,15 @@ const SHELL_BUILTIN_NAMES = [
|
|
|
94
72
|
"unset",
|
|
95
73
|
"wait"
|
|
96
74
|
];
|
|
97
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Ecosystem tools the model expects to reach through Host.process.spawn.
|
|
77
|
+
*
|
|
78
|
+
* xargs and yq are deliberately absent even though real versions exist: both
|
|
79
|
+
* now have a real (non-simulated-elsewhere) implementation in just-bash's own
|
|
80
|
+
* portable set above, which registers and dispatches before Host.process.spawn
|
|
81
|
+
* is ever consulted — listing them here too would claim they fall through to
|
|
82
|
+
* a real spawn, which no longer happens.
|
|
83
|
+
*/
|
|
98
84
|
const SYSTEM_TOOL_NAMES = [
|
|
99
85
|
"bun",
|
|
100
86
|
"cargo",
|
|
@@ -108,9 +94,7 @@ const SYSTEM_TOOL_NAMES = [
|
|
|
108
94
|
"python3",
|
|
109
95
|
"ruby",
|
|
110
96
|
"rustc",
|
|
111
|
-
"
|
|
112
|
-
"yarn",
|
|
113
|
-
"yq"
|
|
97
|
+
"yarn"
|
|
114
98
|
];
|
|
115
99
|
/**
|
|
116
100
|
* Names registered commands must not shadow, derived from the actual portable
|
|
@@ -124,16 +108,30 @@ const RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
|
124
108
|
]);
|
|
125
109
|
//#endregion
|
|
126
110
|
//#region src/command.ts
|
|
127
|
-
function
|
|
128
|
-
return
|
|
111
|
+
function emptyStdin() {
|
|
112
|
+
return {
|
|
113
|
+
text: "",
|
|
114
|
+
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
115
|
+
};
|
|
129
116
|
}
|
|
117
|
+
const EXECUTION_ONLY_FIELDS = [
|
|
118
|
+
"effects",
|
|
119
|
+
"successOutput",
|
|
120
|
+
"failureOutput",
|
|
121
|
+
"input",
|
|
122
|
+
"positionals",
|
|
123
|
+
"stdinField",
|
|
124
|
+
"output",
|
|
125
|
+
"examples"
|
|
126
|
+
];
|
|
127
|
+
const COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
130
128
|
var CommandRegistry = class {
|
|
131
129
|
commands = /* @__PURE__ */ new Map();
|
|
132
|
-
register(
|
|
133
|
-
if (RESERVED_COMMAND_NAMES.has(
|
|
134
|
-
if (this.commands.has(
|
|
135
|
-
validateCommandTree(
|
|
136
|
-
this.commands.set(
|
|
130
|
+
register(command) {
|
|
131
|
+
if (RESERVED_COMMAND_NAMES.has(command.name)) throw new Error(`CommandRegistry: command "${command.name}" is reserved for shell/system commands`);
|
|
132
|
+
if (this.commands.has(command.name)) throw new Error(`CommandRegistry: command "${command.name}" is already registered`);
|
|
133
|
+
validateCommandTree(command, command.name);
|
|
134
|
+
this.commands.set(command.name, command);
|
|
137
135
|
}
|
|
138
136
|
get(name) {
|
|
139
137
|
return this.commands.get(name) ?? null;
|
|
@@ -141,49 +139,55 @@ var CommandRegistry = class {
|
|
|
141
139
|
list() {
|
|
142
140
|
return [...this.commands.values()];
|
|
143
141
|
}
|
|
144
|
-
|
|
145
|
-
const rendered = this.list().map((
|
|
142
|
+
renderHelp() {
|
|
143
|
+
const rendered = this.list().map((command) => renderCommandHelp(command)).join("\n\n");
|
|
146
144
|
if (!rendered) return rendered;
|
|
147
|
-
return `${
|
|
145
|
+
return `${COMMAND_HELP_DEFAULTS}\n\n${rendered}`;
|
|
148
146
|
}
|
|
149
147
|
};
|
|
150
|
-
const
|
|
151
|
-
function parseCommandInput(
|
|
152
|
-
if (argv[0] !==
|
|
153
|
-
let
|
|
154
|
-
const path = [];
|
|
148
|
+
const COMMAND_HELP_DEFAULTS = "Unless a command states otherwise: success prints raw text on stdout, failure writes an error message to stderr and exits non-zero. Pass --help at any level to print a command's documentation.";
|
|
149
|
+
function parseCommandInput(root, argv, stdin = emptyStdin()) {
|
|
150
|
+
if (argv[0] !== root.name) throw new Error(`Expected command "${root.name}", received "${argv[0] ?? ""}"`);
|
|
151
|
+
let node = root;
|
|
152
|
+
const path = [root.name];
|
|
155
153
|
let index = 1;
|
|
156
154
|
while (true) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
if (index >= argv.length) {
|
|
156
|
+
if (node.run) return parseArgs(node, path, argv, index, stdin);
|
|
157
|
+
throw new Error(`Command "${path.join(" ")}" requires a subcommand`);
|
|
158
|
+
}
|
|
159
|
+
const token = argv[index];
|
|
160
|
+
if (token === "--help") return {
|
|
161
|
+
path: [...path],
|
|
162
|
+
help: true,
|
|
162
163
|
values: {},
|
|
163
164
|
json: false
|
|
164
165
|
};
|
|
165
|
-
const child =
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
].join(" ")}"`);
|
|
171
|
-
path.push(child.name);
|
|
172
|
-
index += 1;
|
|
173
|
-
if (isCommandGroup(child)) {
|
|
174
|
-
group = child;
|
|
166
|
+
const child = node.subcommands?.find((candidate) => candidate.name === token);
|
|
167
|
+
if (child) {
|
|
168
|
+
node = child;
|
|
169
|
+
path.push(child.name);
|
|
170
|
+
index += 1;
|
|
175
171
|
continue;
|
|
176
172
|
}
|
|
177
|
-
|
|
173
|
+
if (!node.run) throw new Error(`Unknown subcommand "${[...path, token].join(" ")}"`);
|
|
174
|
+
return parseArgs(node, path, argv, index, stdin);
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
function
|
|
181
|
-
const
|
|
177
|
+
function parseArgs(command, path, argv, startIndex, stdin) {
|
|
178
|
+
const displayPath = path.join(" ");
|
|
179
|
+
const input = command.input ?? {};
|
|
182
180
|
const values = {};
|
|
183
181
|
let json = false;
|
|
184
182
|
let positionalIndex = 0;
|
|
185
183
|
for (let i = startIndex; i < argv.length; i += 1) {
|
|
186
184
|
const token = argv[i];
|
|
185
|
+
if (token === "--help") return {
|
|
186
|
+
path: [...path],
|
|
187
|
+
help: true,
|
|
188
|
+
values: {},
|
|
189
|
+
json: false
|
|
190
|
+
};
|
|
187
191
|
if (token === "--json") {
|
|
188
192
|
json = true;
|
|
189
193
|
continue;
|
|
@@ -200,43 +204,45 @@ function parseLeafInput(subcommand, displayPath, path, argv, startIndex, stdin)
|
|
|
200
204
|
setParsedValue(values, field, rawValue);
|
|
201
205
|
continue;
|
|
202
206
|
}
|
|
203
|
-
const field =
|
|
207
|
+
const field = command.positionals?.[positionalIndex];
|
|
204
208
|
if (!field) throw new Error(`Unexpected positional argument "${token}"`);
|
|
205
209
|
setParsedValue(values, field, token);
|
|
206
210
|
positionalIndex += 1;
|
|
207
211
|
}
|
|
208
|
-
if (
|
|
212
|
+
if (command.stdinField) values[command.stdinField] = stdin.text;
|
|
209
213
|
return {
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
path: [...path],
|
|
215
|
+
help: false,
|
|
212
216
|
values: validateInput(input, values),
|
|
213
217
|
json
|
|
214
218
|
};
|
|
215
219
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
220
|
+
function resolveCommand(root, path) {
|
|
221
|
+
if (path[0] !== root.name) throw new Error(`Path root "${path[0] ?? ""}" does not match command "${root.name}"`);
|
|
222
|
+
let node = root;
|
|
223
|
+
for (let i = 1; i < path.length; i += 1) {
|
|
224
|
+
const segment = path[i];
|
|
225
|
+
const child = node.subcommands?.find((candidate) => candidate.name === segment);
|
|
226
|
+
if (!child) throw new Error(`Unknown subcommand "${path.slice(0, i + 1).join(" ")}"`);
|
|
222
227
|
node = child;
|
|
223
228
|
}
|
|
224
229
|
return node;
|
|
225
230
|
}
|
|
226
|
-
async function runRegisteredCommand(
|
|
227
|
-
const stdin = ctx.stdin ??
|
|
228
|
-
const parsed = parseCommandInput(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
231
|
+
async function runRegisteredCommand(root, ctx) {
|
|
232
|
+
const stdin = ctx.stdin ?? emptyStdin();
|
|
233
|
+
const parsed = parseCommandInput(root, ctx.argv, stdin);
|
|
234
|
+
const displayPath = parsed.path.join(" ");
|
|
235
|
+
if (parsed.help) {
|
|
236
|
+
const node = resolveCommand(root, parsed.path);
|
|
237
|
+
const parentPath = parsed.path.length > 1 ? parsed.path.slice(0, -1).join(" ") : "";
|
|
238
|
+
await ctx.io.stdout(`${renderCommandHelp(node, parentPath)}\n`);
|
|
233
239
|
return { exitCode: 0 };
|
|
234
240
|
}
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
if (parsed.json && !
|
|
241
|
+
const command = resolveCommand(root, parsed.path);
|
|
242
|
+
if (!command.run) throw new Error(`"${displayPath}" is not runnable`);
|
|
243
|
+
if (parsed.json && !command.output?.json) throw new Error(`Command "${displayPath}" does not define JSON output`);
|
|
238
244
|
const capture = new CapturingIO(ctx.io);
|
|
239
|
-
const result = await
|
|
245
|
+
const result = await command.run({
|
|
240
246
|
argv: ctx.argv,
|
|
241
247
|
parsed,
|
|
242
248
|
stdin,
|
|
@@ -251,60 +257,73 @@ async function runRegisteredCommand(spec, ctx) {
|
|
|
251
257
|
try {
|
|
252
258
|
json = JSON.parse(raw);
|
|
253
259
|
} catch (error) {
|
|
254
|
-
throw new Error(`Invalid JSON output for "${
|
|
260
|
+
throw new Error(`Invalid JSON output for "${displayPath}": ${asError(error).message}`);
|
|
255
261
|
}
|
|
256
|
-
const validation =
|
|
262
|
+
const validation = command.output?.json?.safeParse(json);
|
|
257
263
|
if (!validation?.success) {
|
|
258
264
|
const issue = validation?.error.issues[0];
|
|
259
|
-
throw new Error(`JSON output failed validation for "${
|
|
265
|
+
throw new Error(`JSON output failed validation for "${displayPath}": ${issue?.message}`);
|
|
260
266
|
}
|
|
261
267
|
await ctx.io.stdout(raw);
|
|
262
268
|
}
|
|
263
269
|
return result;
|
|
264
270
|
}
|
|
265
|
-
function
|
|
266
|
-
const path = parentPath ? `${parentPath} ${
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
lines.push("",
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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));
|
|
271
|
+
function renderCommandHelp(command, parentPath = "") {
|
|
272
|
+
const path = parentPath ? `${parentPath} ${command.name}` : command.name;
|
|
273
|
+
const blocks = [];
|
|
274
|
+
const lines = [`${path}: ${command.summary}`];
|
|
275
|
+
if (command.run) {
|
|
276
|
+
lines.push("", "Usage:");
|
|
277
|
+
lines.push("", ` ${path}`);
|
|
278
|
+
if (command.effects) lines.push(` Effects: ${command.effects}`);
|
|
279
|
+
if (command.successOutput) lines.push(` Success output: ${command.successOutput}`);
|
|
280
|
+
else if (command.output?.json) lines.push(" Success output: raw text by default; machine-readable JSON when --json is passed");
|
|
281
|
+
if (command.failureOutput) lines.push(` Failure output: ${command.failureOutput}`);
|
|
282
|
+
const fields = Object.entries(command.input ?? {});
|
|
283
|
+
if (fields.length > 0) {
|
|
284
|
+
lines.push(" Parameters:");
|
|
285
|
+
for (const [field, schema] of fields) {
|
|
286
|
+
const positional = command.positionals?.includes(field) ?? false;
|
|
287
|
+
const stdin = command.stdinField === field;
|
|
288
|
+
lines.push(` ${formatField(field, schema, positional, stdin)}`);
|
|
294
289
|
}
|
|
295
290
|
}
|
|
291
|
+
if (command.stdinField) lines.push(` stdin/heredoc: ${command.stdinField}`);
|
|
292
|
+
if (command.output?.json) lines.push(" --json: emits machine-readable JSON for this command");
|
|
293
|
+
else lines.push(" --json: accepted only when this command defines JSON output");
|
|
294
|
+
const examples = command.examples ?? [];
|
|
295
|
+
if (examples.length > 0) {
|
|
296
|
+
lines.push(" Examples:");
|
|
297
|
+
for (const example of examples) lines.push(indent(example, 6));
|
|
298
|
+
}
|
|
296
299
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
300
|
+
const children = command.subcommands ?? [];
|
|
301
|
+
if (children.length > 0) {
|
|
302
|
+
lines.push("", "Subcommands:");
|
|
303
|
+
for (const child of children) lines.push(` ${path} ${child.name} — ${child.summary}`);
|
|
304
|
+
}
|
|
305
|
+
blocks.push(lines.join("\n"));
|
|
306
|
+
for (const child of children) blocks.push(renderCommandHelp(child, path));
|
|
299
307
|
return blocks.join("\n\n");
|
|
300
308
|
}
|
|
301
|
-
function validateCommandTree(
|
|
309
|
+
function validateCommandTree(command, path) {
|
|
310
|
+
if (!COMMAND_NAME_PATTERN.test(command.name)) throw new Error(`CommandRegistry: "${path}" has invalid name "${command.name}"; use letters, numbers, underscores, and hyphens`);
|
|
311
|
+
const hasRun = typeof command.run === "function";
|
|
312
|
+
const children = command.subcommands ?? [];
|
|
313
|
+
if (!hasRun && children.length === 0) throw new Error(`CommandRegistry: "${path}" must have run() and/or subcommands`);
|
|
314
|
+
if (hasRun) {
|
|
315
|
+
if (!Array.isArray(command.examples)) throw new Error(`CommandRegistry: "${path}" defines run() but is missing examples[]`);
|
|
316
|
+
} else for (const field of EXECUTION_ONLY_FIELDS) if (command[field] !== void 0) throw new Error(`CommandRegistry: "${path}" sets ${field} without run()`);
|
|
317
|
+
if (hasRun) {
|
|
318
|
+
const input = command.input ?? {};
|
|
319
|
+
if (command.stdinField && !(command.stdinField in input)) throw new Error(`CommandRegistry: "${path}" stdinField "${command.stdinField}" is not in input`);
|
|
320
|
+
for (const positional of command.positionals ?? []) if (!(positional in input)) throw new Error(`CommandRegistry: "${path}" positional "${positional}" is not in input`);
|
|
321
|
+
}
|
|
302
322
|
const seen = /* @__PURE__ */ new Set();
|
|
303
|
-
for (const child of
|
|
304
|
-
if (child.name === "prompt") throw new Error(`CommandRegistry: "${path} prompt" is reserved for the help pseudo-subcommand`);
|
|
323
|
+
for (const child of children) {
|
|
305
324
|
if (seen.has(child.name)) throw new Error(`CommandRegistry: duplicate subcommand "${path} ${child.name}"`);
|
|
306
325
|
seen.add(child.name);
|
|
307
|
-
|
|
326
|
+
validateCommandTree(child, `${path} ${child.name}`);
|
|
308
327
|
}
|
|
309
328
|
}
|
|
310
329
|
function setParsedValue(values, field, value) {
|
|
@@ -385,75 +404,11 @@ var CapturingIO = class {
|
|
|
385
404
|
async stderr(data) {
|
|
386
405
|
await this.target.stderr(data);
|
|
387
406
|
}
|
|
388
|
-
async asset(asset) {
|
|
389
|
-
await this.target.asset(asset);
|
|
390
|
-
}
|
|
391
407
|
stdoutText() {
|
|
392
408
|
return decodeUtf8(concatBytes(this.chunks));
|
|
393
409
|
}
|
|
394
410
|
};
|
|
395
411
|
//#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
412
|
//#region src/background-command.ts
|
|
458
413
|
function extractSimpleBackgroundCommand(statementInput) {
|
|
459
414
|
const statement = statementInput;
|
|
@@ -560,8 +515,10 @@ function createOutputSinks(fs, cwd, redirections) {
|
|
|
560
515
|
function recordForegroundChunk(foreground, sourceFd, chunk) {
|
|
561
516
|
const text = decodeUtf8(chunk);
|
|
562
517
|
foreground.lastOutputAt = Date.now();
|
|
563
|
-
if (sourceFd === 1)
|
|
564
|
-
|
|
518
|
+
if (sourceFd === 1) {
|
|
519
|
+
foreground.rawStdoutBuffer += text;
|
|
520
|
+
foreground.rawStdoutBytes.push(chunk);
|
|
521
|
+
} else foreground.rawStderrBuffer += text;
|
|
565
522
|
const sink = foreground.outputSinks[sourceFd];
|
|
566
523
|
if (sink.kind === "file" || sink.kind === "null") {
|
|
567
524
|
sink.bytes.push(chunk);
|
|
@@ -703,18 +660,18 @@ var CommandArtifactStore = class {
|
|
|
703
660
|
};
|
|
704
661
|
//#endregion
|
|
705
662
|
//#region src/registered-command-adapter.ts
|
|
706
|
-
function
|
|
663
|
+
function commandToForkCommand(session, command, storage) {
|
|
707
664
|
return {
|
|
708
|
-
name:
|
|
709
|
-
consumesStdin: treeConsumesStdin(
|
|
665
|
+
name: command.name,
|
|
666
|
+
consumesStdin: treeConsumesStdin(command),
|
|
710
667
|
execute: async (args, ctx) => {
|
|
711
|
-
const
|
|
668
|
+
const stdin = decodeForkStdin(ctx.stdin);
|
|
712
669
|
const io = createForwardingIO();
|
|
713
|
-
const argv = [
|
|
670
|
+
const argv = [command.name, ...args];
|
|
714
671
|
try {
|
|
715
|
-
const result = await runRegisteredCommand(
|
|
672
|
+
const result = await runRegisteredCommand(command, {
|
|
716
673
|
argv,
|
|
717
|
-
stdin
|
|
674
|
+
stdin,
|
|
718
675
|
env: mapToRecord(ctx.env),
|
|
719
676
|
cwd: ctx.cwd,
|
|
720
677
|
io,
|
|
@@ -722,18 +679,19 @@ function commandSpecToForkCommand(session, spec, storage) {
|
|
|
722
679
|
});
|
|
723
680
|
session.accumulator.audit.push({
|
|
724
681
|
kind: "registered-command",
|
|
725
|
-
name:
|
|
682
|
+
name: command.name,
|
|
726
683
|
args,
|
|
727
684
|
exitCode: result.exitCode
|
|
728
685
|
});
|
|
729
686
|
if (result.metadata !== void 0) session.accumulator.commandMetadata.push({
|
|
730
687
|
kind: "registered-command",
|
|
731
|
-
name:
|
|
688
|
+
name: command.name,
|
|
732
689
|
args,
|
|
733
690
|
metadata: result.metadata
|
|
734
691
|
});
|
|
735
692
|
return {
|
|
736
|
-
stdout: io.
|
|
693
|
+
stdout: io.stdoutLatin1(),
|
|
694
|
+
stdoutKind: "bytes",
|
|
737
695
|
stderr: io.stderrText(),
|
|
738
696
|
exitCode: result.exitCode
|
|
739
697
|
};
|
|
@@ -741,75 +699,90 @@ function commandSpecToForkCommand(session, spec, storage) {
|
|
|
741
699
|
const message = error instanceof Error ? error.message : String(error);
|
|
742
700
|
session.accumulator.audit.push({
|
|
743
701
|
kind: "registered-command",
|
|
744
|
-
name:
|
|
702
|
+
name: command.name,
|
|
745
703
|
args,
|
|
746
704
|
exitCode: 1
|
|
747
705
|
});
|
|
748
706
|
return {
|
|
749
|
-
stdout: io.
|
|
750
|
-
|
|
707
|
+
stdout: io.stdoutLatin1(),
|
|
708
|
+
stdoutKind: "bytes",
|
|
709
|
+
stderr: `${io.stderrText()}${command.name}: ${message}\n`,
|
|
751
710
|
exitCode: 1
|
|
752
711
|
};
|
|
753
|
-
} finally {
|
|
754
|
-
if (io.assets().length > 0) session.accumulator.assets.push(...io.assets());
|
|
755
712
|
}
|
|
756
713
|
}
|
|
757
714
|
};
|
|
758
715
|
}
|
|
716
|
+
/** Collects command output as raw bytes; stdout stays byte-clean for the pipe. */
|
|
759
717
|
var ForwardingIO = class {
|
|
760
718
|
stdoutChunks = [];
|
|
761
719
|
stderrChunks = [];
|
|
762
|
-
assetItems = [];
|
|
763
720
|
async stdout(data) {
|
|
764
721
|
this.stdoutChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
765
722
|
}
|
|
766
723
|
async stderr(data) {
|
|
767
724
|
this.stderrChunks.push(typeof data === "string" ? encodeUtf8(data) : data);
|
|
768
725
|
}
|
|
769
|
-
|
|
770
|
-
this.
|
|
771
|
-
}
|
|
772
|
-
stdoutText() {
|
|
773
|
-
return decodeUtf8(concatBytes(this.stdoutChunks));
|
|
726
|
+
stdoutLatin1() {
|
|
727
|
+
return decodeLatin1(concatBytes(this.stdoutChunks));
|
|
774
728
|
}
|
|
775
729
|
stderrText() {
|
|
776
730
|
return decodeUtf8(concatBytes(this.stderrChunks));
|
|
777
731
|
}
|
|
778
|
-
assets() {
|
|
779
|
-
return this.assetItems;
|
|
780
|
-
}
|
|
781
732
|
};
|
|
782
733
|
function createForwardingIO() {
|
|
783
734
|
return new ForwardingIO();
|
|
784
735
|
}
|
|
785
|
-
function treeConsumesStdin(
|
|
786
|
-
|
|
736
|
+
function treeConsumesStdin(command) {
|
|
737
|
+
if (command.stdinField) return true;
|
|
738
|
+
return command.subcommands?.some(treeConsumesStdin) ?? false;
|
|
787
739
|
}
|
|
788
740
|
function mapToRecord(map) {
|
|
789
741
|
const record = Object.create(null);
|
|
790
742
|
for (const [key, value] of map) record[key] = value;
|
|
791
743
|
return record;
|
|
792
744
|
}
|
|
745
|
+
/** Pipes hand stdin over as a latin1-packed byte string; expose bytes and a UTF-8 text view. */
|
|
793
746
|
function decodeForkStdin(stdin) {
|
|
794
|
-
if (!stdin) return
|
|
795
|
-
|
|
747
|
+
if (!stdin) return {
|
|
748
|
+
text: "",
|
|
749
|
+
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
750
|
+
};
|
|
751
|
+
if (stdin instanceof Uint8Array) return {
|
|
752
|
+
text: decodeUtf8(stdin),
|
|
753
|
+
bytes: stdin
|
|
754
|
+
};
|
|
796
755
|
const latin1 = stdin;
|
|
797
|
-
if (!latin1) return
|
|
756
|
+
if (!latin1) return {
|
|
757
|
+
text: "",
|
|
758
|
+
bytes: /* @__PURE__ */ new Uint8Array(0)
|
|
759
|
+
};
|
|
760
|
+
const bytes = encodeLatin1(latin1);
|
|
798
761
|
let hasHighByte = false;
|
|
762
|
+
let hasWideChar = false;
|
|
799
763
|
for (let i = 0; i < latin1.length; i += 1) {
|
|
800
764
|
const code = latin1.charCodeAt(i);
|
|
801
|
-
if (code > 255)
|
|
802
|
-
if (code > 127) hasHighByte = true;
|
|
765
|
+
if (code > 255) hasWideChar = true;
|
|
766
|
+
else if (code > 127) hasHighByte = true;
|
|
803
767
|
}
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
768
|
+
if (hasWideChar) return {
|
|
769
|
+
text: latin1,
|
|
770
|
+
bytes: encodeUtf8(latin1)
|
|
771
|
+
};
|
|
772
|
+
if (!hasHighByte) return {
|
|
773
|
+
text: latin1,
|
|
774
|
+
bytes
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
text: decodeUtf8(bytes),
|
|
778
|
+
bytes
|
|
779
|
+
};
|
|
808
780
|
}
|
|
809
781
|
//#endregion
|
|
810
782
|
//#region src/environment.ts
|
|
811
783
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
812
784
|
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
785
|
+
/** Upper bound for a single exec observation window (also the command-bridge wait ceiling). */
|
|
813
786
|
const MAX_TIMEOUT_MS = 6e5;
|
|
814
787
|
var BashEnvironment = class {
|
|
815
788
|
host;
|
|
@@ -836,19 +809,23 @@ var BashEnvironment = class {
|
|
|
836
809
|
getShell(shellId) {
|
|
837
810
|
return this.shells.get(shellId) ?? null;
|
|
838
811
|
}
|
|
839
|
-
registerCommand(
|
|
840
|
-
if (this.commands.get(
|
|
841
|
-
this.commands.register(
|
|
812
|
+
registerCommand(command) {
|
|
813
|
+
if (this.commands.get(command.name)) return;
|
|
814
|
+
this.commands.register(command);
|
|
842
815
|
}
|
|
843
816
|
registeredCommands() {
|
|
844
817
|
return this.commands.list();
|
|
845
818
|
}
|
|
846
819
|
async exec(input) {
|
|
847
820
|
const timeoutMs = normalizeTimeoutMs(input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
848
|
-
|
|
821
|
+
if (input.shellId && input.ephemeral) throw new Error("ShellExecInput: \"shellId\" and \"ephemeral\" are mutually exclusive");
|
|
822
|
+
if (input.cwd !== void 0 && !input.ephemeral) throw new Error("ShellExecInput: \"cwd\" requires \"ephemeral\"; a persistent shell owns its cwd");
|
|
823
|
+
if (input.cwd !== void 0) {
|
|
824
|
+
if (!(await this.host.fs.stat(input.cwd).catch(() => null))?.isDirectory) throw new Error(`Shell exec cwd is not a directory: ${input.cwd}`);
|
|
825
|
+
}
|
|
826
|
+
const session = input.shellId ? this.requireShell(input.shellId) : input.ephemeral ? this.createShell(input.agentSessionId, input.cwd) : this.availableDefaultShell(input.agentSessionId);
|
|
849
827
|
if (session.exited) throw new Error(`Shell session "${session.id}" has exited`);
|
|
850
828
|
if (input.agentSessionId) {
|
|
851
|
-
session.state.env.set("DEMI_AGENT_SESSION_ID", input.agentSessionId);
|
|
852
829
|
const extraEnv = this.execEnv?.(input.agentSessionId);
|
|
853
830
|
if (extraEnv) {
|
|
854
831
|
const exported = session.state.exportedVars ?? /* @__PURE__ */ new Set();
|
|
@@ -962,10 +939,10 @@ var BashEnvironment = class {
|
|
|
962
939
|
if (!agentSessionId || shell.exited || !shell.pendingExec && !shell.foreground) return shell;
|
|
963
940
|
return this.createShell(agentSessionId);
|
|
964
941
|
}
|
|
965
|
-
createShell(agentSessionId) {
|
|
942
|
+
createShell(agentSessionId, initialCwd) {
|
|
966
943
|
const id = this.shellIdFactory();
|
|
967
944
|
const commandScopeId = agentSessionId ?? id;
|
|
968
|
-
const cwd = this.host.defaultCwd;
|
|
945
|
+
const cwd = initialCwd ?? this.host.defaultCwd;
|
|
969
946
|
const fs = new HostBackedFileSystem(this.host, { lookup: (path) => this.lookupVirtualArtifact(commandScopeId, path) });
|
|
970
947
|
const env = /* @__PURE__ */ new Map();
|
|
971
948
|
for (const [key, value] of Object.entries(this.initialEnv)) env.set(key, value);
|
|
@@ -1050,8 +1027,7 @@ var BashEnvironment = class {
|
|
|
1050
1027
|
stdout: "",
|
|
1051
1028
|
stderr: "",
|
|
1052
1029
|
audit: [],
|
|
1053
|
-
commandMetadata: []
|
|
1054
|
-
assets: []
|
|
1030
|
+
commandMetadata: []
|
|
1055
1031
|
},
|
|
1056
1032
|
foregroundWaiters: /* @__PURE__ */ new Set(),
|
|
1057
1033
|
backgroundJobs: /* @__PURE__ */ new Map(),
|
|
@@ -1060,7 +1036,7 @@ var BashEnvironment = class {
|
|
|
1060
1036
|
};
|
|
1061
1037
|
for (const command of createPortableCommands(session)) forkCommands.set(command.name, command);
|
|
1062
1038
|
const storage = new AgentSessionCommandStorage(this.host.store, commandScopeId);
|
|
1063
|
-
for (const
|
|
1039
|
+
for (const command of this.commands.list()) forkCommands.set(command.name, commandToForkCommand(session, command, storage));
|
|
1064
1040
|
session.abortController = new AbortController();
|
|
1065
1041
|
session.interpreter = new Interpreter({
|
|
1066
1042
|
fs,
|
|
@@ -1090,6 +1066,7 @@ var BashEnvironment = class {
|
|
|
1090
1066
|
}
|
|
1091
1067
|
async runScript(session, script, input) {
|
|
1092
1068
|
const record = this.createCommandRecord(session, script);
|
|
1069
|
+
record.outputLimitBytes = input.maxOutputBytes ?? this.defaultOutputLimitBytes;
|
|
1093
1070
|
let ast;
|
|
1094
1071
|
try {
|
|
1095
1072
|
ast = parse(script);
|
|
@@ -1109,8 +1086,7 @@ var BashEnvironment = class {
|
|
|
1109
1086
|
stdout: "",
|
|
1110
1087
|
stderr: "",
|
|
1111
1088
|
audit: [],
|
|
1112
|
-
commandMetadata: []
|
|
1113
|
-
assets: []
|
|
1089
|
+
commandMetadata: []
|
|
1114
1090
|
};
|
|
1115
1091
|
session.abortController = new AbortController();
|
|
1116
1092
|
session.activeCommandId = record.id;
|
|
@@ -1146,7 +1122,7 @@ var BashEnvironment = class {
|
|
|
1146
1122
|
outputOffset: 0,
|
|
1147
1123
|
audit: [],
|
|
1148
1124
|
commandMetadata: [],
|
|
1149
|
-
|
|
1125
|
+
outputLimitBytes: this.defaultOutputLimitBytes
|
|
1150
1126
|
};
|
|
1151
1127
|
this.commandsById.set(id, record);
|
|
1152
1128
|
return record;
|
|
@@ -1352,6 +1328,7 @@ var BashEnvironment = class {
|
|
|
1352
1328
|
startedAt,
|
|
1353
1329
|
lastOutputAt: startedAt,
|
|
1354
1330
|
rawStdoutBuffer: "",
|
|
1331
|
+
rawStdoutBytes: [],
|
|
1355
1332
|
rawStderrBuffer: "",
|
|
1356
1333
|
stdoutBuffer: "",
|
|
1357
1334
|
stderrBuffer: "",
|
|
@@ -1372,7 +1349,7 @@ var BashEnvironment = class {
|
|
|
1372
1349
|
};
|
|
1373
1350
|
session.foreground = foreground;
|
|
1374
1351
|
notifyForegroundWaiters(session.foregroundWaiters, foreground);
|
|
1375
|
-
if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(
|
|
1352
|
+
if (opts.stdin && opts.stdin.length > 0) await handle.writeStdin(encodeLatin1(opts.stdin));
|
|
1376
1353
|
if (opts.stdinProvided) await handle.closeStdin();
|
|
1377
1354
|
if (handle.output) {
|
|
1378
1355
|
foreground.stdoutPump = pumpOutputStream(handle.output, (chunk) => {
|
|
@@ -1385,7 +1362,7 @@ var BashEnvironment = class {
|
|
|
1385
1362
|
}
|
|
1386
1363
|
const exit = await foreground.exitPromise;
|
|
1387
1364
|
await Promise.allSettled([foreground.stdoutPump, foreground.stderrPump]);
|
|
1388
|
-
const stdout = foreground.
|
|
1365
|
+
const stdout = decodeLatin1(concatBytes(foreground.rawStdoutBytes));
|
|
1389
1366
|
const exitCode = exit.exitCode ?? 127;
|
|
1390
1367
|
const stderr = exit.exitCode === null && foreground.rawStderrBuffer.length === 0 ? `${command}: ${exit.signal ?? "command not found"}\n` : foreground.rawStderrBuffer;
|
|
1391
1368
|
foreground.audit[0] = {
|
|
@@ -1406,6 +1383,7 @@ var BashEnvironment = class {
|
|
|
1406
1383
|
session.foreground = void 0;
|
|
1407
1384
|
return {
|
|
1408
1385
|
stdout,
|
|
1386
|
+
stdoutKind: "bytes",
|
|
1409
1387
|
stderr,
|
|
1410
1388
|
exitCode
|
|
1411
1389
|
};
|
|
@@ -1449,12 +1427,35 @@ var BashEnvironment = class {
|
|
|
1449
1427
|
appendRecordOutput(record, "stderr", text);
|
|
1450
1428
|
return this.finishExited(session, record, 1, input);
|
|
1451
1429
|
}
|
|
1452
|
-
const
|
|
1430
|
+
const raw = resultOrError.stdout;
|
|
1431
|
+
let stdoutText;
|
|
1432
|
+
let binary;
|
|
1433
|
+
if (hasWideChar(raw)) stdoutText = raw;
|
|
1434
|
+
else {
|
|
1435
|
+
const bytes = encodeLatin1(raw);
|
|
1436
|
+
const strict = decodeUtf8Strict(bytes);
|
|
1437
|
+
if (strict !== null) stdoutText = strict;
|
|
1438
|
+
else {
|
|
1439
|
+
const cap = record.outputLimitBytes;
|
|
1440
|
+
const truncated = bytes.length > cap;
|
|
1441
|
+
binary = {
|
|
1442
|
+
data: truncated ? bytes.slice(0, cap) : bytes,
|
|
1443
|
+
truncated,
|
|
1444
|
+
totalBytes: bytes.length
|
|
1445
|
+
};
|
|
1446
|
+
stdoutText = `<binary stdout: ${bytes.length} bytes${truncated ? `, exceeds the ${cap}-byte output limit` : ""}; raw bytes at /@/commands/${record.id}/stdout.bin>\n`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1453
1449
|
const stderrText = foreground ? resultOrError.stderr : decodeBytesToUtf8(unsafeBytesFromLatin1(resultOrError.stderr));
|
|
1454
1450
|
session.accumulator.stdout += stdoutText;
|
|
1455
1451
|
session.accumulator.stderr += stderrText;
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1452
|
+
if (binary) record.binaryStdout = binary;
|
|
1453
|
+
if (foreground && !binary) record.outputChunks = [...foreground.outputChunks];
|
|
1454
|
+
else if (binary) {
|
|
1455
|
+
record.outputChunks = [];
|
|
1456
|
+
appendRecordOutput(record, "stdout", stdoutText);
|
|
1457
|
+
appendRecordOutput(record, "stderr", stderrText);
|
|
1458
|
+
} else if (record.outputChunks.length === 0) {
|
|
1458
1459
|
appendRecordOutput(record, "stdout", stdoutText);
|
|
1459
1460
|
appendRecordOutput(record, "stderr", stderrText);
|
|
1460
1461
|
}
|
|
@@ -1473,7 +1474,6 @@ var BashEnvironment = class {
|
|
|
1473
1474
|
record.exitCode = exitCode;
|
|
1474
1475
|
record.audit = [...session.accumulator.audit];
|
|
1475
1476
|
record.commandMetadata = [...session.accumulator.commandMetadata];
|
|
1476
|
-
record.assets = [...session.accumulator.assets];
|
|
1477
1477
|
session.pendingExec = void 0;
|
|
1478
1478
|
if (session.activeCommandId === record.id) session.activeCommandId = void 0;
|
|
1479
1479
|
return this.snapshotCommand(record, input);
|
|
@@ -1538,7 +1538,7 @@ var BashEnvironment = class {
|
|
|
1538
1538
|
audit: record.audit
|
|
1539
1539
|
};
|
|
1540
1540
|
if (record.commandMetadata.length > 0) result.commandMetadata = record.commandMetadata;
|
|
1541
|
-
if (record.
|
|
1541
|
+
if (record.binaryStdout) result.binaryStdout = record.binaryStdout;
|
|
1542
1542
|
return result;
|
|
1543
1543
|
}
|
|
1544
1544
|
if (record.status === "aborted") return {
|
|
@@ -1555,18 +1555,22 @@ var BashEnvironment = class {
|
|
|
1555
1555
|
if (parts.length === 1 && parts[0] === "@") return virtualDirectory(["commands"]);
|
|
1556
1556
|
if (parts.length === 2 && parts[0] === "@" && parts[1] === "commands") return virtualDirectory(await this.commandArtifactIds(scopeId));
|
|
1557
1557
|
if (parts.length === 3 && parts[0] === "@" && parts[1] === "commands") {
|
|
1558
|
-
|
|
1559
|
-
return
|
|
1558
|
+
const artifact = await this.commandArtifact(scopeId, parts[2]);
|
|
1559
|
+
if (!artifact) return null;
|
|
1560
|
+
const entries = [
|
|
1560
1561
|
"meta.json",
|
|
1561
1562
|
"stderr.txt",
|
|
1562
1563
|
"stdout.txt"
|
|
1563
|
-
]
|
|
1564
|
+
];
|
|
1565
|
+
if (artifact.stdoutBinary) entries.push("stdout.bin");
|
|
1566
|
+
return virtualDirectory(entries);
|
|
1564
1567
|
}
|
|
1565
1568
|
if (parts.length !== 4 || parts[0] !== "@" || parts[1] !== "commands") return null;
|
|
1566
1569
|
const artifact = await this.commandArtifact(scopeId, parts[2]);
|
|
1567
1570
|
if (!artifact) return null;
|
|
1568
1571
|
const fileName = parts[3];
|
|
1569
1572
|
if (fileName === "stdout.txt") return virtualFile(encodeUtf8(artifact.stdout));
|
|
1573
|
+
if (fileName === "stdout.bin" && artifact.stdoutBinary) return virtualFile(base64ToBytes(artifact.stdoutBinary.base64));
|
|
1570
1574
|
if (fileName === "stderr.txt") return virtualFile(encodeUtf8(artifact.stderr));
|
|
1571
1575
|
if (fileName === "meta.json") return virtualFile(encodeUtf8(`${JSON.stringify(commandArtifactMeta(artifact), null, 2)}\n`));
|
|
1572
1576
|
return null;
|
|
@@ -1592,7 +1596,7 @@ var BashEnvironment = class {
|
|
|
1592
1596
|
return isPersistedShellCommandArtifact(value) ? value : null;
|
|
1593
1597
|
}
|
|
1594
1598
|
persistCommandArtifact(record) {
|
|
1595
|
-
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}`;
|
|
1599
|
+
const fingerprint = `${record.status}:${record.exitCode ?? ""}:${record.stdout.length}:${record.stderr.length}:${record.binaryStdout?.totalBytes ?? ""}`;
|
|
1596
1600
|
if (record.persistedFingerprint === fingerprint) return;
|
|
1597
1601
|
record.persistedFingerprint = fingerprint;
|
|
1598
1602
|
this.artifacts.persist(record.commandScopeId, record.id, persistedArtifactFromRecord(record));
|
|
@@ -1623,8 +1627,13 @@ function createPortableCommands(session) {
|
|
|
1623
1627
|
}
|
|
1624
1628
|
}));
|
|
1625
1629
|
}
|
|
1630
|
+
/** True when the string contains a char > 0xFF, i.e. already-decoded Unicode text. */
|
|
1631
|
+
function hasWideChar(value) {
|
|
1632
|
+
for (let i = 0; i < value.length; i += 1) if (value.charCodeAt(i) > 255) return true;
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1626
1635
|
function normalizeTimeoutMs(value) {
|
|
1627
|
-
if (!Number.isFinite(value) || value < 1 || value >
|
|
1636
|
+
if (!Number.isFinite(value) || value < 1 || value > 6e5) throw new Error(`timeoutMs must be between 1 and ${MAX_TIMEOUT_MS}`);
|
|
1628
1637
|
return Math.floor(value);
|
|
1629
1638
|
}
|
|
1630
1639
|
function streamArtifact(record, stream, explicitOffset, maxOutputBytes) {
|
|
@@ -1710,7 +1719,12 @@ function persistedArtifactFromRecord(record) {
|
|
|
1710
1719
|
lastOutputAt: record.lastOutputAt,
|
|
1711
1720
|
exitCode: record.exitCode ?? null,
|
|
1712
1721
|
stdout: record.stdout,
|
|
1713
|
-
stderr: record.stderr
|
|
1722
|
+
stderr: record.stderr,
|
|
1723
|
+
...record.binaryStdout ? { stdoutBinary: {
|
|
1724
|
+
base64: bytesToBase64(record.binaryStdout.data),
|
|
1725
|
+
truncated: record.binaryStdout.truncated,
|
|
1726
|
+
totalBytes: record.binaryStdout.totalBytes
|
|
1727
|
+
} } : {}
|
|
1714
1728
|
};
|
|
1715
1729
|
}
|
|
1716
1730
|
function commandArtifactMeta(artifact) {
|
|
@@ -1732,7 +1746,12 @@ function commandArtifactMeta(artifact) {
|
|
|
1732
1746
|
stderr: {
|
|
1733
1747
|
path: stderrPath,
|
|
1734
1748
|
bytes: utf8Bytes(artifact.stderr)
|
|
1735
|
-
}
|
|
1749
|
+
},
|
|
1750
|
+
...artifact.stdoutBinary ? { stdoutBinary: {
|
|
1751
|
+
path: `/@/commands/${artifact.commandId}/stdout.bin`,
|
|
1752
|
+
bytes: artifact.stdoutBinary.totalBytes,
|
|
1753
|
+
truncated: artifact.stdoutBinary.truncated
|
|
1754
|
+
} } : {}
|
|
1736
1755
|
};
|
|
1737
1756
|
}
|
|
1738
1757
|
function isPersistedShellCommandArtifact(value) {
|
|
@@ -1754,4 +1773,17 @@ function tailString(value) {
|
|
|
1754
1773
|
return tail(value, 4096);
|
|
1755
1774
|
}
|
|
1756
1775
|
//#endregion
|
|
1757
|
-
|
|
1776
|
+
//#region src/shell-quote.ts
|
|
1777
|
+
/** Single-quotes a value for safe use as one shell word. */
|
|
1778
|
+
function shellQuote(value) {
|
|
1779
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1780
|
+
}
|
|
1781
|
+
/** Picks a heredoc delimiter that does not collide with any line already in `body`. */
|
|
1782
|
+
function heredocDelimiter(body) {
|
|
1783
|
+
let delimiter = "DEMI_STDIN";
|
|
1784
|
+
const lines = new Set(body.split("\n"));
|
|
1785
|
+
while (lines.has(delimiter)) delimiter += "_X";
|
|
1786
|
+
return delimiter;
|
|
1787
|
+
}
|
|
1788
|
+
//#endregion
|
|
1789
|
+
export { AgentSessionCommandStorage, BashEnvironment, COMMAND_HELP_DEFAULTS, CommandRegistry, DEMI_PORTABLE_COMMANDS, HostBackedFileSystem, MAX_TIMEOUT_MS, RESERVED_COMMAND_NAMES, emptyStdin, heredocDelimiter, parseCommandInput, renderCommandHelp, runRegisteredCommand, shellQuote, virtualDirectory, virtualFile };
|