shell-dsl 0.0.40 → 0.0.42

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.
Files changed (57) hide show
  1. package/README.md +89 -0
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/src/index.cjs +8 -1
  4. package/dist/cjs/src/index.cjs.map +3 -3
  5. package/dist/cjs/src/input-analysis.cjs +154 -0
  6. package/dist/cjs/src/input-analysis.cjs.map +10 -0
  7. package/dist/cjs/src/interpreter/context.cjs +3 -1
  8. package/dist/cjs/src/interpreter/context.cjs.map +3 -3
  9. package/dist/cjs/src/interpreter/interpreter.cjs +146 -19
  10. package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
  11. package/dist/cjs/src/io/async-queue.cjs +105 -0
  12. package/dist/cjs/src/io/async-queue.cjs.map +10 -0
  13. package/dist/cjs/src/io/index.cjs +4 -1
  14. package/dist/cjs/src/io/index.cjs.map +3 -3
  15. package/dist/cjs/src/io/input-controller.cjs +113 -0
  16. package/dist/cjs/src/io/input-controller.cjs.map +10 -0
  17. package/dist/cjs/src/io/stdout.cjs +9 -6
  18. package/dist/cjs/src/io/stdout.cjs.map +3 -3
  19. package/dist/cjs/src/shell-dsl.cjs +13 -5
  20. package/dist/cjs/src/shell-dsl.cjs.map +3 -3
  21. package/dist/cjs/src/shell-session.cjs +222 -0
  22. package/dist/cjs/src/shell-session.cjs.map +10 -0
  23. package/dist/cjs/src/types.cjs.map +2 -2
  24. package/dist/mjs/package.json +1 -1
  25. package/dist/mjs/src/index.mjs +17 -2
  26. package/dist/mjs/src/index.mjs.map +3 -3
  27. package/dist/mjs/src/input-analysis.mjs +114 -0
  28. package/dist/mjs/src/input-analysis.mjs.map +10 -0
  29. package/dist/mjs/src/interpreter/context.mjs +3 -1
  30. package/dist/mjs/src/interpreter/context.mjs.map +3 -3
  31. package/dist/mjs/src/interpreter/interpreter.mjs +146 -19
  32. package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
  33. package/dist/mjs/src/io/async-queue.mjs +64 -0
  34. package/dist/mjs/src/io/async-queue.mjs.map +10 -0
  35. package/dist/mjs/src/io/index.mjs +4 -1
  36. package/dist/mjs/src/io/index.mjs.map +3 -3
  37. package/dist/mjs/src/io/input-controller.mjs +72 -0
  38. package/dist/mjs/src/io/input-controller.mjs.map +10 -0
  39. package/dist/mjs/src/io/stdout.mjs +9 -6
  40. package/dist/mjs/src/io/stdout.mjs.map +3 -3
  41. package/dist/mjs/src/shell-dsl.mjs +13 -5
  42. package/dist/mjs/src/shell-dsl.mjs.map +3 -3
  43. package/dist/mjs/src/shell-session.mjs +182 -0
  44. package/dist/mjs/src/shell-session.mjs.map +10 -0
  45. package/dist/mjs/src/types.mjs.map +2 -2
  46. package/dist/types/src/index.d.ts +5 -2
  47. package/dist/types/src/input-analysis.d.ts +14 -0
  48. package/dist/types/src/interpreter/context.d.ts +3 -1
  49. package/dist/types/src/interpreter/interpreter.d.ts +12 -1
  50. package/dist/types/src/io/async-queue.d.ts +12 -0
  51. package/dist/types/src/io/index.d.ts +1 -0
  52. package/dist/types/src/io/input-controller.d.ts +15 -0
  53. package/dist/types/src/io/stdout.d.ts +4 -3
  54. package/dist/types/src/shell-dsl.d.ts +2 -0
  55. package/dist/types/src/shell-session.d.ts +30 -0
  56. package/dist/types/src/types.d.ts +58 -0
  57. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  // src/index.ts
2
2
  import { ShellDSL, createShellDSL } from "./shell-dsl.mjs";
3
3
  import { ShellPromise } from "./shell-promise.mjs";
4
+ import { ShellSession, createShellSession } from "./shell-session.mjs";
4
5
  import { isRawValue } from "./types.mjs";
5
6
  import { ShellError, LexError, ParseError } from "./errors.mjs";
6
7
  import { Lexer, lex, tokenToString } from "./lexer/index.mjs";
@@ -27,7 +28,16 @@ import {
27
28
  createWebUnderlyingFS
28
29
  } from "./fs/index.mjs";
29
30
  import { createStdin, StdinImpl } from "./io/index.mjs";
30
- import { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.mjs";
31
+ import {
32
+ createStdout,
33
+ createStderr,
34
+ createPipe,
35
+ createShellInput,
36
+ OutputCollectorImpl,
37
+ PipeBuffer,
38
+ ShellInputControllerImpl
39
+ } from "./io/index.mjs";
40
+ import { analyzeInput } from "./input-analysis.mjs";
31
41
  import { escape, escapeForInterpolation, globVirtualFS } from "./utils/index.mjs";
32
42
  import { VersionControlSystem } from "./vcs/index.mjs";
33
43
  export {
@@ -54,12 +64,17 @@ export {
54
64
  createStdout,
55
65
  createStdin,
56
66
  createStderr,
67
+ createShellSession,
68
+ createShellInput,
57
69
  createShellDSL,
58
70
  createPipe,
71
+ analyzeInput,
59
72
  WebFileSystem,
60
73
  VersionControlSystem,
61
74
  StdinImpl,
75
+ ShellSession,
62
76
  ShellPromise,
77
+ ShellInputControllerImpl,
63
78
  ShellError,
64
79
  ShellDSL,
65
80
  ReadOnlyFileSystem,
@@ -76,4 +91,4 @@ export {
76
91
  BreakException
77
92
  };
78
93
 
79
- //# debugId=D29830CA96A8F15764756E2164756E21
94
+ //# debugId=A98B7667567C2E6F64756E2164756E21
@@ -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.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\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.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\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.mjs\";\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.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException, ExitException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\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.mjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.mjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.mjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation, globVirtualFS } from \"./utils/index.mjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.mjs\";\n\n// Version Control\nexport { VersionControlSystem } from \"./vcs/index.mjs\";\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.mjs\";\n"
5
+ "// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\nexport { ShellSession, createShellSession, type ShellSessionOptions } from \"./shell-session.mjs\";\n\n// Types\nexport type {\n VirtualFS,\n VirtualFSWritable,\n FileStat,\n Command,\n CommandCompleter,\n CommandContext,\n CompletionContext,\n CompletionResult,\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.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\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.mjs\";\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.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException, ExitException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\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.mjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.mjs\";\nexport {\n createStdout,\n createStderr,\n createPipe,\n createShellInput,\n OutputCollectorImpl,\n PipeBuffer,\n ShellInputControllerImpl,\n} from \"./io/index.mjs\";\n\n// Interactive input analysis\nexport { analyzeInput } from \"./input-analysis.mjs\";\nexport type { InputAnalysis, InputIncompleteReason } from \"./input-analysis.mjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation, globVirtualFS } from \"./utils/index.mjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.mjs\";\n\n// Version Control\nexport { VersionControlSystem } from \"./vcs/index.mjs\";\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.mjs\";\n"
6
6
  ],
7
- "mappings": ";AACA;AACA;AAmBA;AAGA;AAGA;AAIA;AAsBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA;AACA;AAGA;AAIA;",
8
- "debugId": "D29830CA96A8F15764756E2164756E21",
7
+ "mappings": ";AACA;AACA;AACA;AA8BA;AAGA;AAGA;AAIA;AAsBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA;AAIA;AAIA;",
8
+ "debugId": "A98B7667567C2E6F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,114 @@
1
+ // src/input-analysis.ts
2
+ import { Lexer } from "./lexer/lexer.mjs";
3
+ import { Parser } from "./parser/parser.mjs";
4
+ import { LexError, ParseError } from "./errors.mjs";
5
+ function analyzeInput(source) {
6
+ const heredoc = findIncompleteHeredoc(source);
7
+ if (heredoc) {
8
+ return { kind: "incomplete", reason: "heredoc" };
9
+ }
10
+ let tokens;
11
+ try {
12
+ tokens = new Lexer(source, { preserveNewlines: true }).tokenize();
13
+ } catch (err) {
14
+ if (err instanceof LexError && err.message.toLowerCase().includes("unterminated")) {
15
+ return { kind: "incomplete", reason: "quote" };
16
+ }
17
+ return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
18
+ }
19
+ if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
20
+ return { kind: "complete", ast: emptyCommandAst() };
21
+ }
22
+ if (hasTrailingPipeline(tokens)) {
23
+ return { kind: "incomplete", reason: "pipeline" };
24
+ }
25
+ try {
26
+ const ast = new Parser(tokens).parse();
27
+ return { kind: "complete", ast };
28
+ } catch (err) {
29
+ if (hasOpenCompound(tokens)) {
30
+ return { kind: "incomplete", reason: "compound" };
31
+ }
32
+ if (err instanceof ParseError && isIncompleteParseMessage(err.message)) {
33
+ return { kind: "incomplete", reason: "compound" };
34
+ }
35
+ return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
36
+ }
37
+ }
38
+ function emptyCommandAst() {
39
+ return {
40
+ type: "command",
41
+ name: { type: "word", parts: [] },
42
+ args: [],
43
+ redirects: [],
44
+ assignments: []
45
+ };
46
+ }
47
+ function hasTrailingPipeline(tokens) {
48
+ let i = tokens.length - 1;
49
+ while (i >= 0 && (tokens[i].type === "eof" || tokens[i].type === "newline")) {
50
+ i--;
51
+ }
52
+ return i >= 0 && tokens[i].type === "pipe";
53
+ }
54
+ function hasOpenCompound(tokens) {
55
+ const stack = [];
56
+ for (const token of tokens) {
57
+ if (token.type !== "keyword") {
58
+ continue;
59
+ }
60
+ switch (token.value) {
61
+ case "if":
62
+ stack.push("fi");
63
+ break;
64
+ case "for":
65
+ case "while":
66
+ case "until":
67
+ stack.push("done");
68
+ break;
69
+ case "case":
70
+ stack.push("esac");
71
+ break;
72
+ case "fi":
73
+ case "done":
74
+ case "esac":
75
+ if (stack[stack.length - 1] === token.value) {
76
+ stack.pop();
77
+ }
78
+ break;
79
+ }
80
+ }
81
+ return stack.length > 0;
82
+ }
83
+ function isIncompleteParseMessage(message) {
84
+ return message.includes("Expected 'fi'") || message.includes("Expected 'done'") || message.includes("Expected 'esac'") || message.includes("Expected 'then'") || message.includes("Expected 'do'");
85
+ }
86
+ function findIncompleteHeredoc(source) {
87
+ const lines = source.split(/\n/);
88
+ for (let i = 0;i < lines.length; i++) {
89
+ const match = lines[i].match(/<<-?\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*|\S+))/);
90
+ if (!match) {
91
+ continue;
92
+ }
93
+ const delimiter = match[1] ?? match[2] ?? match[3];
94
+ if (!delimiter) {
95
+ continue;
96
+ }
97
+ let found = false;
98
+ for (let j = i + 1;j < lines.length; j++) {
99
+ if (lines[j] === delimiter || lines[j].replace(/^\t+/, "") === delimiter) {
100
+ found = true;
101
+ break;
102
+ }
103
+ }
104
+ if (!found) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+ export {
111
+ analyzeInput
112
+ };
113
+
114
+ //# debugId=840AA9CEDC5E108D64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/input-analysis.ts"],
4
+ "sourcesContent": [
5
+ "import type { ASTNode } from \"./parser/ast.mjs\";\nimport type { Token, KeywordValue } from \"./lexer/tokens.mjs\";\nimport { Lexer } from \"./lexer/lexer.mjs\";\nimport { Parser } from \"./parser/parser.mjs\";\nimport { LexError, ParseError } from \"./errors.mjs\";\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": "840AA9CEDC5E108D64756E2164756E21",
9
+ "names": []
10
+ }
@@ -8,6 +8,8 @@ function createCommandContext(options) {
8
8
  fs: options.fs,
9
9
  cwd: options.cwd,
10
10
  env: options.env,
11
+ terminal: options.terminal,
12
+ signal: options.signal,
11
13
  setCwd: options.setCwd
12
14
  };
13
15
  if (options.exec) {
@@ -22,4 +24,4 @@ export {
22
24
  createCommandContext
23
25
  };
24
26
 
25
- //# debugId=114011848381283E64756E2164756E21
27
+ //# debugId=648BE3914FE77F1D64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/interpreter/context.ts"],
4
4
  "sourcesContent": [
5
- "import type { CommandContext, VirtualFS, Stdin, Stdout, Stderr, ExecResult, ShellCommandApi } from \"../types.mjs\";\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 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 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"
5
+ "import type {\n CommandContext,\n VirtualFS,\n Stdin,\n Stdout,\n Stderr,\n ExecResult,\n ShellCommandApi,\n TerminalInfo,\n} from \"../types.mjs\";\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": ";AAeO,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,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": "114011848381283E64756E2164756E21",
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": "648BE3914FE77F1D64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -4,6 +4,7 @@ import { Lexer } from "../lexer/lexer.mjs";
4
4
  import { Parser } from "../parser/parser.mjs";
5
5
  import { createStdin } from "../io/stdin.mjs";
6
6
  import { createStdout, createStderr, createPipe, createBufferTargetCollector } from "../io/stdout.mjs";
7
+ import { AsyncQueue } from "../io/async-queue.mjs";
7
8
  import { isDevNullPath } from "../fs/special-files.mjs";
8
9
  var DEFAULT_IFS = `
9
10
  `;
@@ -41,6 +42,9 @@ class Interpreter {
41
42
  redirectObjects;
42
43
  loopDepth = 0;
43
44
  isTTY;
45
+ terminal;
46
+ activeSignal;
47
+ externalCommand;
44
48
  argv0;
45
49
  positionalParameters;
46
50
  lastExitCode;
@@ -50,7 +54,10 @@ class Interpreter {
50
54
  this.env = { ...options.env };
51
55
  this.commands = options.commands;
52
56
  this.redirectObjects = options.redirectObjects ?? {};
53
- this.isTTY = options.isTTY ?? false;
57
+ this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
58
+ this.isTTY = this.terminal.isTTY;
59
+ this.activeSignal = options.signal ?? new AbortController().signal;
60
+ this.externalCommand = options.externalCommand;
54
61
  this.argv0 = options.argv0 ?? "sh";
55
62
  this.positionalParameters = [...options.positionalParameters ?? []];
56
63
  this.lastExitCode = options.lastExitCode ?? 0;
@@ -59,27 +66,88 @@ class Interpreter {
59
66
  return this.loopDepth;
60
67
  }
61
68
  async execute(ast) {
62
- const stdout = createStdout(this.isTTY);
63
- const stderr = createStderr(this.isTTY);
64
- let exitCode;
65
- try {
66
- exitCode = await this.executeNode(ast, null, stdout, stderr);
67
- } catch (err) {
68
- if (!(err instanceof ExitException)) {
69
- throw err;
69
+ return this.executeStreaming(ast).exit;
70
+ }
71
+ executeStreaming(ast, options = {}) {
72
+ const terminal = options.terminal ?? this.terminal;
73
+ const controller = new AbortController;
74
+ const eventQueue = new AsyncQueue;
75
+ if (options.signal) {
76
+ if (options.signal.aborted) {
77
+ controller.abort(options.signal.reason);
78
+ } else {
79
+ options.signal.addEventListener("abort", () => controller.abort(options.signal?.reason), { once: true });
70
80
  }
71
- exitCode = err.exitCode;
72
- this.lastExitCode = exitCode;
73
81
  }
74
- stdout.close();
75
- stderr.close();
82
+ const stdout = createStdout(terminal.isTTY, async (chunk) => {
83
+ eventQueue.push({ fd: 1, chunk });
84
+ await options.stdout?.write(chunk);
85
+ });
86
+ const stderr = createStderr(terminal.isTTY, async (chunk) => {
87
+ eventQueue.push({ fd: 2, chunk });
88
+ await options.stderr?.write(chunk);
89
+ });
90
+ const previousSignal = this.activeSignal;
91
+ const previousTerminal = this.terminal;
92
+ const previousIsTTY = this.isTTY;
93
+ const stdinSource = this.normalizeInputSource(options.stdin);
94
+ const exit = (async () => {
95
+ this.activeSignal = controller.signal;
96
+ this.terminal = terminal;
97
+ this.isTTY = terminal.isTTY;
98
+ let exitCode;
99
+ try {
100
+ this.throwIfAborted();
101
+ exitCode = await this.executeNode(ast, stdinSource, stdout, stderr);
102
+ } catch (err) {
103
+ if (err instanceof ExitException) {
104
+ exitCode = err.exitCode;
105
+ } else if (controller.signal.aborted) {
106
+ exitCode = 130;
107
+ } else {
108
+ throw err;
109
+ }
110
+ } finally {
111
+ stdout.close();
112
+ stderr.close();
113
+ eventQueue.close();
114
+ this.activeSignal = previousSignal;
115
+ this.terminal = previousTerminal;
116
+ this.isTTY = previousIsTTY;
117
+ }
118
+ this.lastExitCode = exitCode;
119
+ return {
120
+ stdout: await stdout.collect(),
121
+ stderr: await stderr.collect(),
122
+ exitCode
123
+ };
124
+ })();
76
125
  return {
77
- stdout: await stdout.collect(),
78
- stderr: await stderr.collect(),
79
- exitCode
126
+ stdout: stdout.getReadableStream(),
127
+ stderr: stderr.getReadableStream(),
128
+ output: eventQueue,
129
+ exit,
130
+ kill: (reason) => controller.abort(reason ?? new Error("Shell execution killed"))
80
131
  };
81
132
  }
133
+ normalizeInputSource(source) {
134
+ if (source === undefined || source === null) {
135
+ return null;
136
+ }
137
+ if (typeof source === "string") {
138
+ return async function* () {
139
+ yield new TextEncoder().encode(source);
140
+ }();
141
+ }
142
+ if (Buffer.isBuffer(source)) {
143
+ return async function* () {
144
+ yield new Uint8Array(source);
145
+ }();
146
+ }
147
+ return source;
148
+ }
82
149
  async executeNode(node, stdinSource, stdout, stderr) {
150
+ this.throwIfAborted();
83
151
  let exitCode;
84
152
  switch (node.type) {
85
153
  case "command":
@@ -118,6 +186,11 @@ class Interpreter {
118
186
  this.lastExitCode = exitCode;
119
187
  return exitCode;
120
188
  }
189
+ throwIfAborted() {
190
+ if (this.activeSignal.aborted) {
191
+ throw this.activeSignal.reason ?? new Error("Shell execution aborted");
192
+ }
193
+ }
121
194
  async executeCommand(node, stdinSource, stdout, stderr) {
122
195
  const assignmentEnv = { ...this.env };
123
196
  for (const assignment of node.assignments) {
@@ -189,12 +262,55 @@ class Interpreter {
189
262
  return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
190
263
  }
191
264
  if (name.includes("/")) {
265
+ const resolved = this.fs.resolve(this.cwd, name);
266
+ if (await this.fs.exists(resolved)) {
267
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
268
+ }
269
+ if (this.externalCommand) {
270
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
271
+ }
192
272
  return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
193
273
  }
274
+ if (this.externalCommand) {
275
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
276
+ }
194
277
  await stderr.writeText(`${name}: command not found
195
278
  `);
196
279
  return 127;
197
280
  }
281
+ async invokeExternalCommand(name, args, stdinSource, stdout, stderr, env) {
282
+ if (!this.externalCommand) {
283
+ return 127;
284
+ }
285
+ const ctx = createCommandContext({
286
+ args,
287
+ stdin: createStdin(stdinSource),
288
+ stdout,
289
+ stderr,
290
+ fs: this.fs,
291
+ cwd: this.cwd,
292
+ env,
293
+ terminal: this.terminal,
294
+ signal: this.activeSignal,
295
+ setCwd: (path) => this.setCwd(path),
296
+ exec: this.createExec(env),
297
+ shell: this.createShellApi(stdinSource, stdout, stderr, env)
298
+ });
299
+ try {
300
+ return await this.externalCommand({ ...ctx, name });
301
+ } catch (err) {
302
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
303
+ throw err;
304
+ }
305
+ if (this.activeSignal.aborted) {
306
+ throw err;
307
+ }
308
+ const message = err instanceof Error ? err.message : String(err);
309
+ await stderr.writeText(`${name}: ${message}
310
+ `);
311
+ return 1;
312
+ }
313
+ }
198
314
  async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
199
315
  const exec = this.createExec(env);
200
316
  const shell = this.createShellApi(stdinSource, stdout, stderr, env);
@@ -206,6 +322,8 @@ class Interpreter {
206
322
  fs: this.fs,
207
323
  cwd: this.cwd,
208
324
  env,
325
+ terminal: this.terminal,
326
+ signal: this.activeSignal,
209
327
  setCwd: (path) => this.setCwd(path),
210
328
  exec,
211
329
  shell
@@ -269,7 +387,9 @@ class Interpreter {
269
387
  env: { ...env },
270
388
  commands: this.commands,
271
389
  redirectObjects: this.redirectObjects,
272
- isTTY: this.isTTY,
390
+ terminal: this.terminal,
391
+ externalCommand: this.externalCommand,
392
+ signal: this.activeSignal,
273
393
  argv0: shebang.command,
274
394
  positionalParameters: []
275
395
  });
@@ -289,7 +409,9 @@ class Interpreter {
289
409
  env: { ...env },
290
410
  commands: this.commands,
291
411
  redirectObjects: this.redirectObjects,
292
- isTTY: this.isTTY,
412
+ terminal: this.terminal,
413
+ externalCommand: this.externalCommand,
414
+ signal: this.activeSignal,
293
415
  argv0,
294
416
  positionalParameters: args
295
417
  });
@@ -882,6 +1004,8 @@ class Interpreter {
882
1004
  commands: this.commands,
883
1005
  redirectObjects: this.redirectObjects,
884
1006
  isTTY: false,
1007
+ externalCommand: this.externalCommand,
1008
+ signal: this.activeSignal,
885
1009
  argv0: this.argv0,
886
1010
  positionalParameters: this.positionalParameters,
887
1011
  lastExitCode: this.lastExitCode
@@ -1162,6 +1286,9 @@ class Interpreter {
1162
1286
  getEnv() {
1163
1287
  return { ...this.env };
1164
1288
  }
1289
+ getLastExitCode() {
1290
+ return this.lastExitCode;
1291
+ }
1165
1292
  }
1166
1293
  export {
1167
1294
  Interpreter,
@@ -1170,4 +1297,4 @@ export {
1170
1297
  BreakException
1171
1298
  };
1172
1299
 
1173
- //# debugId=37348867B8905B5F64756E2164756E21
1300
+ //# debugId=1C8DFE25269FD4A764756E2164756E21