shell-dsl 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/index.cjs +8 -1
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/input-analysis.cjs +154 -0
- package/dist/cjs/src/input-analysis.cjs.map +10 -0
- package/dist/cjs/src/interpreter/context.cjs +3 -1
- package/dist/cjs/src/interpreter/context.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +146 -19
- package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
- package/dist/cjs/src/io/async-queue.cjs +105 -0
- package/dist/cjs/src/io/async-queue.cjs.map +10 -0
- package/dist/cjs/src/io/index.cjs +4 -1
- package/dist/cjs/src/io/index.cjs.map +3 -3
- package/dist/cjs/src/io/input-controller.cjs +113 -0
- package/dist/cjs/src/io/input-controller.cjs.map +10 -0
- package/dist/cjs/src/io/stdout.cjs +9 -6
- package/dist/cjs/src/io/stdout.cjs.map +3 -3
- package/dist/cjs/src/shell-dsl.cjs +13 -5
- package/dist/cjs/src/shell-dsl.cjs.map +3 -3
- package/dist/cjs/src/shell-session.cjs +128 -0
- package/dist/cjs/src/shell-session.cjs.map +10 -0
- package/dist/cjs/src/types.cjs.map +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/index.mjs +17 -2
- package/dist/mjs/src/index.mjs.map +3 -3
- package/dist/mjs/src/input-analysis.mjs +114 -0
- package/dist/mjs/src/input-analysis.mjs.map +10 -0
- package/dist/mjs/src/interpreter/context.mjs +3 -1
- package/dist/mjs/src/interpreter/context.mjs.map +3 -3
- package/dist/mjs/src/interpreter/interpreter.mjs +146 -19
- package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
- package/dist/mjs/src/io/async-queue.mjs +64 -0
- package/dist/mjs/src/io/async-queue.mjs.map +10 -0
- package/dist/mjs/src/io/index.mjs +4 -1
- package/dist/mjs/src/io/index.mjs.map +3 -3
- package/dist/mjs/src/io/input-controller.mjs +72 -0
- package/dist/mjs/src/io/input-controller.mjs.map +10 -0
- package/dist/mjs/src/io/stdout.mjs +9 -6
- package/dist/mjs/src/io/stdout.mjs.map +3 -3
- package/dist/mjs/src/shell-dsl.mjs +13 -5
- package/dist/mjs/src/shell-dsl.mjs.map +3 -3
- package/dist/mjs/src/shell-session.mjs +88 -0
- package/dist/mjs/src/shell-session.mjs.map +10 -0
- package/dist/mjs/src/types.mjs.map +2 -2
- package/dist/types/src/index.d.ts +5 -2
- package/dist/types/src/input-analysis.d.ts +14 -0
- package/dist/types/src/interpreter/context.d.ts +3 -1
- package/dist/types/src/interpreter/interpreter.d.ts +12 -1
- package/dist/types/src/io/async-queue.d.ts +12 -0
- package/dist/types/src/io/index.d.ts +1 -0
- package/dist/types/src/io/input-controller.d.ts +15 -0
- package/dist/types/src/io/stdout.d.ts +4 -3
- package/dist/types/src/shell-dsl.d.ts +2 -0
- package/dist/types/src/shell-session.d.ts +23 -0
- package/dist/types/src/types.d.ts +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -339,6 +339,72 @@ const myls: Command = async (ctx) => {
|
|
|
339
339
|
};
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
+
## Streaming Sessions
|
|
343
|
+
|
|
344
|
+
Use `createShellSession()` when you want a stateful, streaming shell runtime for a terminal, agent console, or other UI. A session preserves shell state across runs, including `cwd`, environment changes, and `$?`:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { createShellInput, createShellSession, createVirtualFS } from "shell-dsl";
|
|
348
|
+
import { builtinCommands } from "shell-dsl/commands";
|
|
349
|
+
|
|
350
|
+
const session = createShellSession({
|
|
351
|
+
fs,
|
|
352
|
+
cwd: "/",
|
|
353
|
+
env: {},
|
|
354
|
+
commands: builtinCommands,
|
|
355
|
+
terminal: { isTTY: true, columns: 100, rows: 30 },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await session.run("cd /work").exit;
|
|
359
|
+
|
|
360
|
+
const execution = session.run("pwd; echo hello");
|
|
361
|
+
for await (const event of execution.output) {
|
|
362
|
+
const stream = event.fd === 1 ? process.stdout : process.stderr;
|
|
363
|
+
stream.write(event.chunk);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const result = await execution.exit;
|
|
367
|
+
console.log(result.exitCode);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For interactive input, create a writable async stdin source:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
const stdin = createShellInput();
|
|
374
|
+
const execution = session.run("cat", { stdin });
|
|
375
|
+
|
|
376
|
+
await stdin.write("hello\n");
|
|
377
|
+
stdin.close();
|
|
378
|
+
await execution.exit;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Unknown commands can be delegated to a runtime-specific adapter without making shell-dsl spawn processes itself:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const session = createShellSession({
|
|
385
|
+
fs,
|
|
386
|
+
cwd: "/",
|
|
387
|
+
env: {},
|
|
388
|
+
commands: builtinCommands,
|
|
389
|
+
externalCommand: async (ctx) => {
|
|
390
|
+
await ctx.stderr.writeText(`${ctx.name}: not implemented by this host\n`);
|
|
391
|
+
return 127;
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Use `analyzeInput(source)` before running user-entered text to distinguish complete commands from multiline input such as unclosed quotes, heredocs, trailing pipes, and compound statements.
|
|
397
|
+
|
|
398
|
+
## Terminal Demo
|
|
399
|
+
|
|
400
|
+
This repository includes a Bun-powered terminal demo:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
bun examples/terminal/shell-cli.ts
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The CLI process uses `node:readline/promises`, so normal prompt editing such as Ctrl+A, Ctrl+E, arrow-key movement, and history are handled by your terminal/readline layer. Tab completion is served by the executor process and includes registered shell-dsl command names plus paths in the virtual filesystem. The demo spawns `shell-executor.ts` with Bun and exchanges JSON-lines messages over stdio. Full raw PTY behavior for interactive child programs is intentionally left to demo or host runtime code rather than shell-dsl core.
|
|
407
|
+
|
|
342
408
|
## Field Splitting
|
|
343
409
|
|
|
344
410
|
Unquoted parameter, command, and arithmetic expansions follow shell-style word expansion:
|
package/dist/cjs/package.json
CHANGED
package/dist/cjs/src/index.cjs
CHANGED
|
@@ -62,12 +62,17 @@ __export(exports_src, {
|
|
|
62
62
|
createStdout: () => import_io2.createStdout,
|
|
63
63
|
createStdin: () => import_io.createStdin,
|
|
64
64
|
createStderr: () => import_io2.createStderr,
|
|
65
|
+
createShellSession: () => import_shell_session.createShellSession,
|
|
66
|
+
createShellInput: () => import_io2.createShellInput,
|
|
65
67
|
createShellDSL: () => import_shell_dsl.createShellDSL,
|
|
66
68
|
createPipe: () => import_io2.createPipe,
|
|
69
|
+
analyzeInput: () => import_input_analysis.analyzeInput,
|
|
67
70
|
WebFileSystem: () => import_fs2.WebFileSystem,
|
|
68
71
|
VersionControlSystem: () => import_vcs.VersionControlSystem,
|
|
69
72
|
StdinImpl: () => import_io.StdinImpl,
|
|
73
|
+
ShellSession: () => import_shell_session.ShellSession,
|
|
70
74
|
ShellPromise: () => import_shell_promise.ShellPromise,
|
|
75
|
+
ShellInputControllerImpl: () => import_io2.ShellInputControllerImpl,
|
|
71
76
|
ShellError: () => import_errors.ShellError,
|
|
72
77
|
ShellDSL: () => import_shell_dsl.ShellDSL,
|
|
73
78
|
ReadOnlyFileSystem: () => import_fs2.ReadOnlyFileSystem,
|
|
@@ -86,6 +91,7 @@ __export(exports_src, {
|
|
|
86
91
|
module.exports = __toCommonJS(exports_src);
|
|
87
92
|
var import_shell_dsl = require("./shell-dsl.cjs");
|
|
88
93
|
var import_shell_promise = require("./shell-promise.cjs");
|
|
94
|
+
var import_shell_session = require("./shell-session.cjs");
|
|
89
95
|
var import_types = require("./types.cjs");
|
|
90
96
|
var import_errors = require("./errors.cjs");
|
|
91
97
|
var import_lexer = require("./lexer/index.cjs");
|
|
@@ -96,7 +102,8 @@ var import_fs = require("./fs/index.cjs");
|
|
|
96
102
|
var import_fs2 = require("./fs/index.cjs");
|
|
97
103
|
var import_io = require("./io/index.cjs");
|
|
98
104
|
var import_io2 = require("./io/index.cjs");
|
|
105
|
+
var import_input_analysis = require("./input-analysis.cjs");
|
|
99
106
|
var import_utils = require("./utils/index.cjs");
|
|
100
107
|
var import_vcs = require("./vcs/index.cjs");
|
|
101
108
|
|
|
102
|
-
//# debugId=
|
|
109
|
+
//# debugId=CCA17BE2B94A373E64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n VirtualFSWritable,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n ShellCommandApi,\n ShellRunOptions,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n WordNode,\n WordPart,\n TextPart,\n VariablePart,\n SubstitutionPart,\n ArithmeticPart,\n IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n} from \"./parser/index.cjs\";\nexport {\n isWordNode,\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n} from \"./parser/index.cjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException, ExitException } from \"./interpreter/index.cjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.cjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.cjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.cjs\";\nexport {
|
|
5
|
+
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\nexport { ShellSession, createShellSession, type ShellSessionOptions } from \"./shell-session.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n VirtualFSWritable,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n ShellCommandApi,\n ShellCommandFallback,\n ExternalCommandContext,\n ShellRunOptions,\n TerminalInfo,\n ShellInputController,\n ShellInputSource,\n ShellExecutionOptions,\n ShellExecution,\n ShellOutputEvent,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n WordNode,\n WordPart,\n TextPart,\n VariablePart,\n SubstitutionPart,\n ArithmeticPart,\n IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n} from \"./parser/index.cjs\";\nexport {\n isWordNode,\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n} from \"./parser/index.cjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException, ExitException } from \"./interpreter/index.cjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.cjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.cjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.cjs\";\nexport {\n createStdout,\n createStderr,\n createPipe,\n createShellInput,\n OutputCollectorImpl,\n PipeBuffer,\n ShellInputControllerImpl,\n} from \"./io/index.cjs\";\n\n// Interactive input analysis\nexport { analyzeInput } from \"./input-analysis.cjs\";\nexport type { InputAnalysis, InputIncompleteReason } from \"./input-analysis.cjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation, globVirtualFS } from \"./utils/index.cjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.cjs\";\n\n// Version Control\nexport { VersionControlSystem } from \"./vcs/index.cjs\";\nexport type {\n VCSConfig,\n VCSAttributeRule,\n VCSResolvedAttributes,\n VCSDiffMode,\n VCSPatchSuppressionReason,\n Revision,\n DiffEntry,\n TreeManifest,\n TreeEntry,\n FileEntry,\n DirectoryEntry,\n VCSIndexEntry,\n VCSIndexFile,\n CommitOptions,\n CheckoutOptions,\n LogOptions,\n LogEntry,\n BranchInfo,\n} from \"./vcs/index.cjs\";\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACuD,IAAvD;AACuD,IAAvD;AAC2E,IAA3E;AA2B2B,IAA3B;AAGiD,IAAjD;AAG0C,IAA1C;AAI8B,IAA9B;AAkCO,IAZP;AAeuG,IAAvG;AAGgC,IAAhC;AAUO,IATP;AAYuC,IAAvC;AASO,IARP;AAW6B,IAA7B;AAI8D,IAA9D;AAIqC,IAArC;",
|
|
8
|
+
"debugId": "CCA17BE2B94A373E64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/input-analysis.ts
|
|
40
|
+
var exports_input_analysis = {};
|
|
41
|
+
__export(exports_input_analysis, {
|
|
42
|
+
analyzeInput: () => analyzeInput
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(exports_input_analysis);
|
|
45
|
+
var import_lexer = require("./lexer/lexer.cjs");
|
|
46
|
+
var import_parser = require("./parser/parser.cjs");
|
|
47
|
+
var import_errors = require("./errors.cjs");
|
|
48
|
+
function analyzeInput(source) {
|
|
49
|
+
const heredoc = findIncompleteHeredoc(source);
|
|
50
|
+
if (heredoc) {
|
|
51
|
+
return { kind: "incomplete", reason: "heredoc" };
|
|
52
|
+
}
|
|
53
|
+
let tokens;
|
|
54
|
+
try {
|
|
55
|
+
tokens = new import_lexer.Lexer(source, { preserveNewlines: true }).tokenize();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof import_errors.LexError && err.message.toLowerCase().includes("unterminated")) {
|
|
58
|
+
return { kind: "incomplete", reason: "quote" };
|
|
59
|
+
}
|
|
60
|
+
return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
|
|
61
|
+
}
|
|
62
|
+
if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
|
|
63
|
+
return { kind: "complete", ast: emptyCommandAst() };
|
|
64
|
+
}
|
|
65
|
+
if (hasTrailingPipeline(tokens)) {
|
|
66
|
+
return { kind: "incomplete", reason: "pipeline" };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const ast = new import_parser.Parser(tokens).parse();
|
|
70
|
+
return { kind: "complete", ast };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (hasOpenCompound(tokens)) {
|
|
73
|
+
return { kind: "incomplete", reason: "compound" };
|
|
74
|
+
}
|
|
75
|
+
if (err instanceof import_errors.ParseError && isIncompleteParseMessage(err.message)) {
|
|
76
|
+
return { kind: "incomplete", reason: "compound" };
|
|
77
|
+
}
|
|
78
|
+
return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function emptyCommandAst() {
|
|
82
|
+
return {
|
|
83
|
+
type: "command",
|
|
84
|
+
name: { type: "word", parts: [] },
|
|
85
|
+
args: [],
|
|
86
|
+
redirects: [],
|
|
87
|
+
assignments: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function hasTrailingPipeline(tokens) {
|
|
91
|
+
let i = tokens.length - 1;
|
|
92
|
+
while (i >= 0 && (tokens[i].type === "eof" || tokens[i].type === "newline")) {
|
|
93
|
+
i--;
|
|
94
|
+
}
|
|
95
|
+
return i >= 0 && tokens[i].type === "pipe";
|
|
96
|
+
}
|
|
97
|
+
function hasOpenCompound(tokens) {
|
|
98
|
+
const stack = [];
|
|
99
|
+
for (const token of tokens) {
|
|
100
|
+
if (token.type !== "keyword") {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
switch (token.value) {
|
|
104
|
+
case "if":
|
|
105
|
+
stack.push("fi");
|
|
106
|
+
break;
|
|
107
|
+
case "for":
|
|
108
|
+
case "while":
|
|
109
|
+
case "until":
|
|
110
|
+
stack.push("done");
|
|
111
|
+
break;
|
|
112
|
+
case "case":
|
|
113
|
+
stack.push("esac");
|
|
114
|
+
break;
|
|
115
|
+
case "fi":
|
|
116
|
+
case "done":
|
|
117
|
+
case "esac":
|
|
118
|
+
if (stack[stack.length - 1] === token.value) {
|
|
119
|
+
stack.pop();
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return stack.length > 0;
|
|
125
|
+
}
|
|
126
|
+
function isIncompleteParseMessage(message) {
|
|
127
|
+
return message.includes("Expected 'fi'") || message.includes("Expected 'done'") || message.includes("Expected 'esac'") || message.includes("Expected 'then'") || message.includes("Expected 'do'");
|
|
128
|
+
}
|
|
129
|
+
function findIncompleteHeredoc(source) {
|
|
130
|
+
const lines = source.split(/\n/);
|
|
131
|
+
for (let i = 0;i < lines.length; i++) {
|
|
132
|
+
const match = lines[i].match(/<<-?\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*|\S+))/);
|
|
133
|
+
if (!match) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const delimiter = match[1] ?? match[2] ?? match[3];
|
|
137
|
+
if (!delimiter) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
let found = false;
|
|
141
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
142
|
+
if (lines[j] === delimiter || lines[j].replace(/^\t+/, "") === delimiter) {
|
|
143
|
+
found = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!found) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//# debugId=32A2D9A6396B91EF64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/input-analysis.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { ASTNode } from \"./parser/ast.cjs\";\nimport type { Token, KeywordValue } from \"./lexer/tokens.cjs\";\nimport { Lexer } from \"./lexer/lexer.cjs\";\nimport { Parser } from \"./parser/parser.cjs\";\nimport { LexError, ParseError } from \"./errors.cjs\";\n\nexport type InputIncompleteReason = \"quote\" | \"heredoc\" | \"compound\" | \"pipeline\";\n\nexport type InputAnalysis =\n | { kind: \"complete\"; ast: ASTNode }\n | { kind: \"incomplete\"; reason: InputIncompleteReason }\n | { kind: \"invalid\"; error: LexError | ParseError | Error };\n\nexport function analyzeInput(source: string): InputAnalysis {\n const heredoc = findIncompleteHeredoc(source);\n if (heredoc) {\n return { kind: \"incomplete\", reason: \"heredoc\" };\n }\n\n let tokens: Token[];\n try {\n tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n } catch (err) {\n if (err instanceof LexError && err.message.toLowerCase().includes(\"unterminated\")) {\n return { kind: \"incomplete\", reason: \"quote\" };\n }\n return { kind: \"invalid\", error: err instanceof Error ? err : new Error(String(err)) };\n }\n\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return { kind: \"complete\", ast: emptyCommandAst() };\n }\n\n if (hasTrailingPipeline(tokens)) {\n return { kind: \"incomplete\", reason: \"pipeline\" };\n }\n\n try {\n const ast = new Parser(tokens).parse();\n return { kind: \"complete\", ast };\n } catch (err) {\n if (hasOpenCompound(tokens)) {\n return { kind: \"incomplete\", reason: \"compound\" };\n }\n if (err instanceof ParseError && isIncompleteParseMessage(err.message)) {\n return { kind: \"incomplete\", reason: \"compound\" };\n }\n return { kind: \"invalid\", error: err instanceof Error ? err : new Error(String(err)) };\n }\n}\n\nfunction emptyCommandAst(): ASTNode {\n return {\n type: \"command\",\n name: { type: \"word\", parts: [] },\n args: [],\n redirects: [],\n assignments: [],\n };\n}\n\nfunction hasTrailingPipeline(tokens: Token[]): boolean {\n let i = tokens.length - 1;\n while (i >= 0 && (tokens[i]!.type === \"eof\" || tokens[i]!.type === \"newline\")) {\n i--;\n }\n return i >= 0 && tokens[i]!.type === \"pipe\";\n}\n\nfunction hasOpenCompound(tokens: Token[]): boolean {\n const stack: KeywordValue[] = [];\n\n for (const token of tokens) {\n if (token.type !== \"keyword\") {\n continue;\n }\n\n switch (token.value) {\n case \"if\":\n stack.push(\"fi\");\n break;\n case \"for\":\n case \"while\":\n case \"until\":\n stack.push(\"done\");\n break;\n case \"case\":\n stack.push(\"esac\");\n break;\n case \"fi\":\n case \"done\":\n case \"esac\":\n if (stack[stack.length - 1] === token.value) {\n stack.pop();\n }\n break;\n }\n }\n\n return stack.length > 0;\n}\n\nfunction isIncompleteParseMessage(message: string): boolean {\n return (\n message.includes(\"Expected 'fi'\") ||\n message.includes(\"Expected 'done'\") ||\n message.includes(\"Expected 'esac'\") ||\n message.includes(\"Expected 'then'\") ||\n message.includes(\"Expected 'do'\")\n );\n}\n\nfunction findIncompleteHeredoc(source: string): boolean {\n const lines = source.split(/\\n/);\n\n for (let i = 0; i < lines.length; i++) {\n const match = lines[i]!.match(/<<-?\\s*(?:\"([^\"]+)\"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*|\\S+))/);\n if (!match) {\n continue;\n }\n\n const delimiter = match[1] ?? match[2] ?? match[3];\n if (!delimiter) {\n continue;\n }\n\n let found = false;\n for (let j = i + 1; j < lines.length; j++) {\n if (lines[j] === delimiter || lines[j]!.replace(/^\\t+/, \"\") === delimiter) {\n found = true;\n break;\n }\n }\n if (!found) {\n return true;\n }\n }\n\n return false;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "32A2D9A6396B91EF64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -51,6 +51,8 @@ function createCommandContext(options) {
|
|
|
51
51
|
fs: options.fs,
|
|
52
52
|
cwd: options.cwd,
|
|
53
53
|
env: options.env,
|
|
54
|
+
terminal: options.terminal,
|
|
55
|
+
signal: options.signal,
|
|
54
56
|
setCwd: options.setCwd
|
|
55
57
|
};
|
|
56
58
|
if (options.exec) {
|
|
@@ -62,4 +64,4 @@ function createCommandContext(options) {
|
|
|
62
64
|
return ctx;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
//# debugId=
|
|
67
|
+
//# debugId=EF2EDEF001ADFBB864756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/interpreter/context.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type {
|
|
5
|
+
"import type {\n CommandContext,\n VirtualFS,\n Stdin,\n Stdout,\n Stderr,\n ExecResult,\n ShellCommandApi,\n TerminalInfo,\n} from \"../types.cjs\";\n\nexport interface ContextOptions {\n args: string[];\n stdin: Stdin;\n stdout: Stdout;\n stderr: Stderr;\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n terminal: TerminalInfo;\n signal: AbortSignal;\n setCwd: (path: string) => void;\n exec?: (name: string, args: string[]) => Promise<ExecResult>;\n shell?: ShellCommandApi;\n}\n\nexport function createCommandContext(options: ContextOptions): CommandContext {\n const ctx: CommandContext = {\n args: options.args,\n stdin: options.stdin,\n stdout: options.stdout,\n stderr: options.stderr,\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n terminal: options.terminal,\n signal: options.signal,\n setCwd: options.setCwd,\n };\n if (options.exec) {\n ctx.exec = options.exec;\n }\n if (options.shell) {\n ctx.shell = options.shell;\n }\n return ctx;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BO,SAAS,oBAAoB,CAAC,SAAyC;AAAA,EAC5E,MAAM,MAAsB;AAAA,IAC1B,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,KAAK,QAAQ;AAAA,IACb,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,EAClB;AAAA,EACA,IAAI,QAAQ,MAAM;AAAA,IAChB,IAAI,OAAO,QAAQ;AAAA,EACrB;AAAA,EACA,IAAI,QAAQ,OAAO;AAAA,IACjB,IAAI,QAAQ,QAAQ;AAAA,EACtB;AAAA,EACA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "EF2EDEF001ADFBB864756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -50,6 +50,7 @@ var import_lexer = require("../lexer/lexer.cjs");
|
|
|
50
50
|
var import_parser = require("../parser/parser.cjs");
|
|
51
51
|
var import_stdin = require("../io/stdin.cjs");
|
|
52
52
|
var import_stdout = require("../io/stdout.cjs");
|
|
53
|
+
var import_async_queue = require("../io/async-queue.cjs");
|
|
53
54
|
var import_special_files = require("../fs/special-files.cjs");
|
|
54
55
|
var DEFAULT_IFS = `
|
|
55
56
|
`;
|
|
@@ -87,6 +88,9 @@ class Interpreter {
|
|
|
87
88
|
redirectObjects;
|
|
88
89
|
loopDepth = 0;
|
|
89
90
|
isTTY;
|
|
91
|
+
terminal;
|
|
92
|
+
activeSignal;
|
|
93
|
+
externalCommand;
|
|
90
94
|
argv0;
|
|
91
95
|
positionalParameters;
|
|
92
96
|
lastExitCode;
|
|
@@ -96,7 +100,10 @@ class Interpreter {
|
|
|
96
100
|
this.env = { ...options.env };
|
|
97
101
|
this.commands = options.commands;
|
|
98
102
|
this.redirectObjects = options.redirectObjects ?? {};
|
|
99
|
-
this.
|
|
103
|
+
this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
|
|
104
|
+
this.isTTY = this.terminal.isTTY;
|
|
105
|
+
this.activeSignal = options.signal ?? new AbortController().signal;
|
|
106
|
+
this.externalCommand = options.externalCommand;
|
|
100
107
|
this.argv0 = options.argv0 ?? "sh";
|
|
101
108
|
this.positionalParameters = [...options.positionalParameters ?? []];
|
|
102
109
|
this.lastExitCode = options.lastExitCode ?? 0;
|
|
@@ -105,27 +112,88 @@ class Interpreter {
|
|
|
105
112
|
return this.loopDepth;
|
|
106
113
|
}
|
|
107
114
|
async execute(ast) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
return this.executeStreaming(ast).exit;
|
|
116
|
+
}
|
|
117
|
+
executeStreaming(ast, options = {}) {
|
|
118
|
+
const terminal = options.terminal ?? this.terminal;
|
|
119
|
+
const controller = new AbortController;
|
|
120
|
+
const eventQueue = new import_async_queue.AsyncQueue;
|
|
121
|
+
if (options.signal) {
|
|
122
|
+
if (options.signal.aborted) {
|
|
123
|
+
controller.abort(options.signal.reason);
|
|
124
|
+
} else {
|
|
125
|
+
options.signal.addEventListener("abort", () => controller.abort(options.signal?.reason), { once: true });
|
|
116
126
|
}
|
|
117
|
-
exitCode = err.exitCode;
|
|
118
|
-
this.lastExitCode = exitCode;
|
|
119
127
|
}
|
|
120
|
-
stdout.
|
|
121
|
-
|
|
128
|
+
const stdout = import_stdout.createStdout(terminal.isTTY, async (chunk) => {
|
|
129
|
+
eventQueue.push({ fd: 1, chunk });
|
|
130
|
+
await options.stdout?.write(chunk);
|
|
131
|
+
});
|
|
132
|
+
const stderr = import_stdout.createStderr(terminal.isTTY, async (chunk) => {
|
|
133
|
+
eventQueue.push({ fd: 2, chunk });
|
|
134
|
+
await options.stderr?.write(chunk);
|
|
135
|
+
});
|
|
136
|
+
const previousSignal = this.activeSignal;
|
|
137
|
+
const previousTerminal = this.terminal;
|
|
138
|
+
const previousIsTTY = this.isTTY;
|
|
139
|
+
const stdinSource = this.normalizeInputSource(options.stdin);
|
|
140
|
+
const exit = (async () => {
|
|
141
|
+
this.activeSignal = controller.signal;
|
|
142
|
+
this.terminal = terminal;
|
|
143
|
+
this.isTTY = terminal.isTTY;
|
|
144
|
+
let exitCode;
|
|
145
|
+
try {
|
|
146
|
+
this.throwIfAborted();
|
|
147
|
+
exitCode = await this.executeNode(ast, stdinSource, stdout, stderr);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof ExitException) {
|
|
150
|
+
exitCode = err.exitCode;
|
|
151
|
+
} else if (controller.signal.aborted) {
|
|
152
|
+
exitCode = 130;
|
|
153
|
+
} else {
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
stdout.close();
|
|
158
|
+
stderr.close();
|
|
159
|
+
eventQueue.close();
|
|
160
|
+
this.activeSignal = previousSignal;
|
|
161
|
+
this.terminal = previousTerminal;
|
|
162
|
+
this.isTTY = previousIsTTY;
|
|
163
|
+
}
|
|
164
|
+
this.lastExitCode = exitCode;
|
|
165
|
+
return {
|
|
166
|
+
stdout: await stdout.collect(),
|
|
167
|
+
stderr: await stderr.collect(),
|
|
168
|
+
exitCode
|
|
169
|
+
};
|
|
170
|
+
})();
|
|
122
171
|
return {
|
|
123
|
-
stdout:
|
|
124
|
-
stderr:
|
|
125
|
-
|
|
172
|
+
stdout: stdout.getReadableStream(),
|
|
173
|
+
stderr: stderr.getReadableStream(),
|
|
174
|
+
output: eventQueue,
|
|
175
|
+
exit,
|
|
176
|
+
kill: (reason) => controller.abort(reason ?? new Error("Shell execution killed"))
|
|
126
177
|
};
|
|
127
178
|
}
|
|
179
|
+
normalizeInputSource(source) {
|
|
180
|
+
if (source === undefined || source === null) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (typeof source === "string") {
|
|
184
|
+
return async function* () {
|
|
185
|
+
yield new TextEncoder().encode(source);
|
|
186
|
+
}();
|
|
187
|
+
}
|
|
188
|
+
if (Buffer.isBuffer(source)) {
|
|
189
|
+
return async function* () {
|
|
190
|
+
yield new Uint8Array(source);
|
|
191
|
+
}();
|
|
192
|
+
}
|
|
193
|
+
return source;
|
|
194
|
+
}
|
|
128
195
|
async executeNode(node, stdinSource, stdout, stderr) {
|
|
196
|
+
this.throwIfAborted();
|
|
129
197
|
let exitCode;
|
|
130
198
|
switch (node.type) {
|
|
131
199
|
case "command":
|
|
@@ -164,6 +232,11 @@ class Interpreter {
|
|
|
164
232
|
this.lastExitCode = exitCode;
|
|
165
233
|
return exitCode;
|
|
166
234
|
}
|
|
235
|
+
throwIfAborted() {
|
|
236
|
+
if (this.activeSignal.aborted) {
|
|
237
|
+
throw this.activeSignal.reason ?? new Error("Shell execution aborted");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
167
240
|
async executeCommand(node, stdinSource, stdout, stderr) {
|
|
168
241
|
const assignmentEnv = { ...this.env };
|
|
169
242
|
for (const assignment of node.assignments) {
|
|
@@ -235,12 +308,55 @@ class Interpreter {
|
|
|
235
308
|
return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
|
|
236
309
|
}
|
|
237
310
|
if (name.includes("/")) {
|
|
311
|
+
const resolved = this.fs.resolve(this.cwd, name);
|
|
312
|
+
if (await this.fs.exists(resolved)) {
|
|
313
|
+
return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
|
|
314
|
+
}
|
|
315
|
+
if (this.externalCommand) {
|
|
316
|
+
return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
|
|
317
|
+
}
|
|
238
318
|
return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
|
|
239
319
|
}
|
|
320
|
+
if (this.externalCommand) {
|
|
321
|
+
return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
|
|
322
|
+
}
|
|
240
323
|
await stderr.writeText(`${name}: command not found
|
|
241
324
|
`);
|
|
242
325
|
return 127;
|
|
243
326
|
}
|
|
327
|
+
async invokeExternalCommand(name, args, stdinSource, stdout, stderr, env) {
|
|
328
|
+
if (!this.externalCommand) {
|
|
329
|
+
return 127;
|
|
330
|
+
}
|
|
331
|
+
const ctx = import_context.createCommandContext({
|
|
332
|
+
args,
|
|
333
|
+
stdin: import_stdin.createStdin(stdinSource),
|
|
334
|
+
stdout,
|
|
335
|
+
stderr,
|
|
336
|
+
fs: this.fs,
|
|
337
|
+
cwd: this.cwd,
|
|
338
|
+
env,
|
|
339
|
+
terminal: this.terminal,
|
|
340
|
+
signal: this.activeSignal,
|
|
341
|
+
setCwd: (path) => this.setCwd(path),
|
|
342
|
+
exec: this.createExec(env),
|
|
343
|
+
shell: this.createShellApi(stdinSource, stdout, stderr, env)
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
return await this.externalCommand({ ...ctx, name });
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
if (this.activeSignal.aborted) {
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
355
|
+
await stderr.writeText(`${name}: ${message}
|
|
356
|
+
`);
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
244
360
|
async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
|
|
245
361
|
const exec = this.createExec(env);
|
|
246
362
|
const shell = this.createShellApi(stdinSource, stdout, stderr, env);
|
|
@@ -252,6 +368,8 @@ class Interpreter {
|
|
|
252
368
|
fs: this.fs,
|
|
253
369
|
cwd: this.cwd,
|
|
254
370
|
env,
|
|
371
|
+
terminal: this.terminal,
|
|
372
|
+
signal: this.activeSignal,
|
|
255
373
|
setCwd: (path) => this.setCwd(path),
|
|
256
374
|
exec,
|
|
257
375
|
shell
|
|
@@ -315,7 +433,9 @@ class Interpreter {
|
|
|
315
433
|
env: { ...env },
|
|
316
434
|
commands: this.commands,
|
|
317
435
|
redirectObjects: this.redirectObjects,
|
|
318
|
-
|
|
436
|
+
terminal: this.terminal,
|
|
437
|
+
externalCommand: this.externalCommand,
|
|
438
|
+
signal: this.activeSignal,
|
|
319
439
|
argv0: shebang.command,
|
|
320
440
|
positionalParameters: []
|
|
321
441
|
});
|
|
@@ -335,7 +455,9 @@ class Interpreter {
|
|
|
335
455
|
env: { ...env },
|
|
336
456
|
commands: this.commands,
|
|
337
457
|
redirectObjects: this.redirectObjects,
|
|
338
|
-
|
|
458
|
+
terminal: this.terminal,
|
|
459
|
+
externalCommand: this.externalCommand,
|
|
460
|
+
signal: this.activeSignal,
|
|
339
461
|
argv0,
|
|
340
462
|
positionalParameters: args
|
|
341
463
|
});
|
|
@@ -928,6 +1050,8 @@ class Interpreter {
|
|
|
928
1050
|
commands: this.commands,
|
|
929
1051
|
redirectObjects: this.redirectObjects,
|
|
930
1052
|
isTTY: false,
|
|
1053
|
+
externalCommand: this.externalCommand,
|
|
1054
|
+
signal: this.activeSignal,
|
|
931
1055
|
argv0: this.argv0,
|
|
932
1056
|
positionalParameters: this.positionalParameters,
|
|
933
1057
|
lastExitCode: this.lastExitCode
|
|
@@ -1208,6 +1332,9 @@ class Interpreter {
|
|
|
1208
1332
|
getEnv() {
|
|
1209
1333
|
return { ...this.env };
|
|
1210
1334
|
}
|
|
1335
|
+
getLastExitCode() {
|
|
1336
|
+
return this.lastExitCode;
|
|
1337
|
+
}
|
|
1211
1338
|
}
|
|
1212
1339
|
|
|
1213
|
-
//# debugId=
|
|
1340
|
+
//# debugId=F9FD7440AC6CB03664756E2164756E21
|