shell-dsl 0.0.41 → 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.
package/README.md CHANGED
@@ -393,6 +393,29 @@ const session = createShellSession({
393
393
  });
394
394
  ```
395
395
 
396
+ Custom commands can provide argument completions by registering a `CommandCompleter` under the same command name:
397
+
398
+ ```ts
399
+ import type { CommandCompleter } from "shell-dsl";
400
+
401
+ const completions: Record<string, CommandCompleter> = {
402
+ deploy: async (ctx) => ({
403
+ replacement: ctx.word,
404
+ matches: ["--dry-run ", "--prod ", "--staging "].filter((match) =>
405
+ match.startsWith(ctx.word)
406
+ ),
407
+ }),
408
+ };
409
+
410
+ const session = createShellSession({
411
+ fs,
412
+ cwd: "/",
413
+ env: {},
414
+ commands: { ...builtinCommands, deploy },
415
+ completions,
416
+ });
417
+ ```
418
+
396
419
  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
420
 
398
421
  ## Terminal Demo
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "type": "commonjs"
5
5
  }
@@ -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\";\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"
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 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.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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACuD,IAAvD;AACuD,IAAvD;AAC2E,IAA3E;AA8B2B,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
8
  "debugId": "CCA17BE2B94A373E64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -50,14 +50,22 @@ var import_stdout = require("./io/stdout.cjs");
50
50
  var import_async_queue = require("./io/async-queue.cjs");
51
51
 
52
52
  class ShellSession {
53
+ fs;
54
+ commands;
55
+ completions;
56
+ terminal;
53
57
  interpreter;
54
58
  constructor(options) {
59
+ this.fs = options.fs;
60
+ this.commands = options.commands;
61
+ this.completions = options.completions ?? {};
62
+ this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
55
63
  this.interpreter = new import_interpreter.Interpreter({
56
64
  fs: options.fs,
57
65
  cwd: options.cwd,
58
66
  env: options.env,
59
67
  commands: options.commands,
60
- terminal: options.terminal ?? { isTTY: options.isTTY ?? false },
68
+ terminal: this.terminal,
61
69
  externalCommand: options.externalCommand
62
70
  });
63
71
  }
@@ -87,7 +95,63 @@ class ShellSession {
87
95
  getLastExitCode() {
88
96
  return this.interpreter.getLastExitCode();
89
97
  }
98
+ async complete(source, cursor = source.length) {
99
+ const boundedCursor = Math.max(0, Math.min(cursor, source.length));
100
+ const prefix = source.slice(0, boundedCursor);
101
+ const { word, start } = getCurrentWord(prefix);
102
+ if (isCommandPosition(prefix, start) && !looksLikePath(word)) {
103
+ return {
104
+ replacement: word,
105
+ matches: Object.keys(this.commands).filter((name) => name.startsWith(word)).sort().map((name) => `${name} `)
106
+ };
107
+ }
108
+ const commandLine = getCurrentCommandLine(prefix, start);
109
+ const [command = "", ...args] = splitCompletionWords(commandLine);
110
+ const completer = command === "" ? undefined : this.completions[command];
111
+ if (completer) {
112
+ return completer({
113
+ source,
114
+ cursor: boundedCursor,
115
+ command,
116
+ args,
117
+ word,
118
+ wordStart: start,
119
+ wordEnd: boundedCursor,
120
+ cwd: this.getCwd(),
121
+ env: this.getEnv(),
122
+ fs: this.fs,
123
+ terminal: this.terminal
124
+ });
125
+ }
126
+ return this.completePath(word);
127
+ }
90
128
  async dispose() {}
129
+ async completePath(word) {
130
+ const slash = word.lastIndexOf("/");
131
+ const dirPart = slash === -1 ? "" : word.slice(0, slash + 1);
132
+ const namePrefix = slash === -1 ? word : word.slice(slash + 1);
133
+ const basePath = dirPart === "" ? this.getCwd() : dirPart.startsWith("/") ? dirPart : this.fs.resolve(this.getCwd(), dirPart);
134
+ let entries;
135
+ try {
136
+ entries = await this.fs.readdir(basePath);
137
+ } catch {
138
+ return { replacement: word, matches: [] };
139
+ }
140
+ const matches = await Promise.all(entries.filter((entry) => entry.startsWith(namePrefix)).sort().map(async (entry) => {
141
+ const candidate = `${dirPart}${escapeCompletionSegment(entry)}`;
142
+ const path = this.fs.resolve(basePath, entry);
143
+ try {
144
+ const stat = await this.fs.stat(path);
145
+ return stat.isDirectory() ? `${candidate}/` : candidate;
146
+ } catch {
147
+ return candidate;
148
+ }
149
+ }));
150
+ if (matches.length === 1 && !matches[0].endsWith("/")) {
151
+ matches[0] = `${matches[0]} `;
152
+ }
153
+ return { replacement: word, matches };
154
+ }
91
155
  createImmediateExecution(exitCode, stdoutText, stderrText) {
92
156
  const stdout = import_stdout.createStdout();
93
157
  const stderr = import_stdout.createStderr();
@@ -124,5 +188,35 @@ class ShellSession {
124
188
  function createShellSession(config) {
125
189
  return new ShellSession(config);
126
190
  }
191
+ function getCurrentWord(prefix) {
192
+ let start = prefix.length;
193
+ while (start > 0 && !/\s/.test(prefix[start - 1])) {
194
+ start--;
195
+ }
196
+ return { word: prefix.slice(start), start };
197
+ }
198
+ function isCommandPosition(prefix, wordStart) {
199
+ const beforeWord = prefix.slice(0, wordStart);
200
+ const segmentStart = Math.max(beforeWord.lastIndexOf(";"), beforeWord.lastIndexOf("|"), beforeWord.lastIndexOf("&"));
201
+ const segment = beforeWord.slice(segmentStart + 1).trim();
202
+ if (segment === "") {
203
+ return true;
204
+ }
205
+ return segment.split(/\s+/).every((part) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(part));
206
+ }
207
+ function getCurrentCommandLine(prefix, wordStart) {
208
+ const beforeWord = prefix.slice(0, wordStart);
209
+ const segmentStart = Math.max(beforeWord.lastIndexOf(";"), beforeWord.lastIndexOf("|"), beforeWord.lastIndexOf("&"));
210
+ return prefix.slice(segmentStart + 1).trimStart();
211
+ }
212
+ function splitCompletionWords(source) {
213
+ return source.trim().split(/\s+/).filter(Boolean);
214
+ }
215
+ function looksLikePath(word) {
216
+ return word.startsWith("/") || word.startsWith(".") || word.includes("/");
217
+ }
218
+ function escapeCompletionSegment(segment) {
219
+ return segment.replace(/([\s\\'"$`!#&;|<>()[\]{}*?])/g, "\\$1");
220
+ }
127
221
 
128
- //# debugId=636F3B47CF304B5264756E2164756E21
222
+ //# debugId=C807EA9CC9D2A2E164756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/shell-session.ts"],
4
4
  "sourcesContent": [
5
- "import type {\n Command,\n ExecResult,\n ShellCommandFallback,\n ShellConfig,\n ShellExecution,\n ShellExecutionOptions,\n TerminalInfo,\n VirtualFS,\n} from \"./types.cjs\";\nimport type { Program } from \"./shell-dsl.cjs\";\nimport { Lexer } from \"./lexer/lexer.cjs\";\nimport { Parser } from \"./parser/parser.cjs\";\nimport { Interpreter } from \"./interpreter/interpreter.cjs\";\nimport { createStderr, createStdout } from \"./io/stdout.cjs\";\nimport { AsyncQueue } from \"./io/async-queue.cjs\";\nimport type { ShellOutputEvent } from \"./types.cjs\";\n\nexport interface ShellSessionOptions {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\nexport class ShellSession {\n private interpreter: Interpreter;\n\n constructor(options: ShellSessionOptions) {\n this.interpreter = new Interpreter({\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n commands: options.commands,\n terminal: options.terminal ?? { isTTY: options.isTTY ?? false },\n externalCommand: options.externalCommand,\n });\n }\n\n run(source: string, options: ShellExecutionOptions = {}): ShellExecution {\n try {\n const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return this.createImmediateExecution(0, \"\", \"\");\n }\n const ast = new Parser(tokens).parse();\n return this.interpreter.executeStreaming(ast, options);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return this.createImmediateExecution(2, \"\", `sh: ${message}\\n`);\n }\n }\n\n runProgram(program: Program, options: ShellExecutionOptions = {}): ShellExecution {\n return this.interpreter.executeStreaming(program.ast, options);\n }\n\n getCwd(): string {\n return this.interpreter.getCwd();\n }\n\n getEnv(): Record<string, string> {\n return this.interpreter.getEnv();\n }\n\n getLastExitCode(): number {\n return this.interpreter.getLastExitCode();\n }\n\n async dispose(): Promise<void> {\n // Reserved for future resources owned by a session.\n }\n\n private createImmediateExecution(exitCode: number, stdoutText: string, stderrText: string): ShellExecution {\n const stdout = createStdout();\n const stderr = createStderr();\n const output = new AsyncQueue<ShellOutputEvent>();\n\n const exit = (async (): Promise<ExecResult> => {\n if (stdoutText.length > 0) {\n const chunk = new TextEncoder().encode(stdoutText);\n output.push({ fd: 1, chunk });\n await stdout.write(chunk);\n }\n if (stderrText.length > 0) {\n const chunk = new TextEncoder().encode(stderrText);\n output.push({ fd: 2, chunk });\n await stderr.write(chunk);\n }\n stdout.close();\n stderr.close();\n output.close();\n return {\n stdout: await stdout.collect(),\n stderr: await stderr.collect(),\n exitCode,\n };\n })();\n\n return {\n stdout: stdout.getReadableStream(),\n stderr: stderr.getReadableStream(),\n output,\n exit,\n kill: () => {},\n };\n }\n}\n\nexport function createShellSession(config: ShellConfig): ShellSession {\n return new ShellSession(config);\n}\n"
5
+ "import type {\n Command,\n CommandCompleter,\n CompletionResult,\n ExecResult,\n ShellCommandFallback,\n ShellConfig,\n ShellExecution,\n ShellExecutionOptions,\n TerminalInfo,\n VirtualFS,\n} from \"./types.cjs\";\nimport type { Program } from \"./shell-dsl.cjs\";\nimport { Lexer } from \"./lexer/lexer.cjs\";\nimport { Parser } from \"./parser/parser.cjs\";\nimport { Interpreter } from \"./interpreter/interpreter.cjs\";\nimport { createStderr, createStdout } from \"./io/stdout.cjs\";\nimport { AsyncQueue } from \"./io/async-queue.cjs\";\nimport type { ShellOutputEvent } from \"./types.cjs\";\n\nexport interface ShellSessionOptions {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n completions?: Record<string, CommandCompleter>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\nexport class ShellSession {\n private fs: VirtualFS;\n private commands: Record<string, Command>;\n private completions: Record<string, CommandCompleter>;\n private terminal: TerminalInfo;\n private interpreter: Interpreter;\n\n constructor(options: ShellSessionOptions) {\n this.fs = options.fs;\n this.commands = options.commands;\n this.completions = options.completions ?? {};\n this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };\n this.interpreter = new Interpreter({\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n commands: options.commands,\n terminal: this.terminal,\n externalCommand: options.externalCommand,\n });\n }\n\n run(source: string, options: ShellExecutionOptions = {}): ShellExecution {\n try {\n const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return this.createImmediateExecution(0, \"\", \"\");\n }\n const ast = new Parser(tokens).parse();\n return this.interpreter.executeStreaming(ast, options);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return this.createImmediateExecution(2, \"\", `sh: ${message}\\n`);\n }\n }\n\n runProgram(program: Program, options: ShellExecutionOptions = {}): ShellExecution {\n return this.interpreter.executeStreaming(program.ast, options);\n }\n\n getCwd(): string {\n return this.interpreter.getCwd();\n }\n\n getEnv(): Record<string, string> {\n return this.interpreter.getEnv();\n }\n\n getLastExitCode(): number {\n return this.interpreter.getLastExitCode();\n }\n\n async complete(source: string, cursor: number = source.length): Promise<CompletionResult> {\n const boundedCursor = Math.max(0, Math.min(cursor, source.length));\n const prefix = source.slice(0, boundedCursor);\n const { word, start } = getCurrentWord(prefix);\n\n if (isCommandPosition(prefix, start) && !looksLikePath(word)) {\n return {\n replacement: word,\n matches: Object.keys(this.commands)\n .filter((name) => name.startsWith(word))\n .sort()\n .map((name) => `${name} `),\n };\n }\n\n const commandLine = getCurrentCommandLine(prefix, start);\n const [command = \"\", ...args] = splitCompletionWords(commandLine);\n const completer = command === \"\" ? undefined : this.completions[command];\n if (completer) {\n return completer({\n source,\n cursor: boundedCursor,\n command,\n args,\n word,\n wordStart: start,\n wordEnd: boundedCursor,\n cwd: this.getCwd(),\n env: this.getEnv(),\n fs: this.fs,\n terminal: this.terminal,\n });\n }\n\n return this.completePath(word);\n }\n\n async dispose(): Promise<void> {\n // Reserved for future resources owned by a session.\n }\n\n private async completePath(word: string): Promise<CompletionResult> {\n const slash = word.lastIndexOf(\"/\");\n const dirPart = slash === -1 ? \"\" : word.slice(0, slash + 1);\n const namePrefix = slash === -1 ? word : word.slice(slash + 1);\n const basePath = dirPart === \"\"\n ? this.getCwd()\n : dirPart.startsWith(\"/\")\n ? dirPart\n : this.fs.resolve(this.getCwd(), dirPart);\n\n let entries: string[];\n try {\n entries = await this.fs.readdir(basePath);\n } catch {\n return { replacement: word, matches: [] };\n }\n\n const matches = await Promise.all(\n entries\n .filter((entry) => entry.startsWith(namePrefix))\n .sort()\n .map(async (entry) => {\n const candidate = `${dirPart}${escapeCompletionSegment(entry)}`;\n const path = this.fs.resolve(basePath, entry);\n try {\n const stat = await this.fs.stat(path);\n return stat.isDirectory() ? `${candidate}/` : candidate;\n } catch {\n return candidate;\n }\n })\n );\n\n if (matches.length === 1 && !matches[0]!.endsWith(\"/\")) {\n matches[0] = `${matches[0]} `;\n }\n\n return { replacement: word, matches };\n }\n\n private createImmediateExecution(exitCode: number, stdoutText: string, stderrText: string): ShellExecution {\n const stdout = createStdout();\n const stderr = createStderr();\n const output = new AsyncQueue<ShellOutputEvent>();\n\n const exit = (async (): Promise<ExecResult> => {\n if (stdoutText.length > 0) {\n const chunk = new TextEncoder().encode(stdoutText);\n output.push({ fd: 1, chunk });\n await stdout.write(chunk);\n }\n if (stderrText.length > 0) {\n const chunk = new TextEncoder().encode(stderrText);\n output.push({ fd: 2, chunk });\n await stderr.write(chunk);\n }\n stdout.close();\n stderr.close();\n output.close();\n return {\n stdout: await stdout.collect(),\n stderr: await stderr.collect(),\n exitCode,\n };\n })();\n\n return {\n stdout: stdout.getReadableStream(),\n stderr: stderr.getReadableStream(),\n output,\n exit,\n kill: () => {},\n };\n }\n}\n\nexport function createShellSession(config: ShellConfig): ShellSession {\n return new ShellSession(config);\n}\n\nfunction getCurrentWord(prefix: string): { word: string; start: number } {\n let start = prefix.length;\n while (start > 0 && !/\\s/.test(prefix[start - 1]!)) {\n start--;\n }\n return { word: prefix.slice(start), start };\n}\n\nfunction isCommandPosition(prefix: string, wordStart: number): boolean {\n const beforeWord = prefix.slice(0, wordStart);\n const segmentStart = Math.max(\n beforeWord.lastIndexOf(\";\"),\n beforeWord.lastIndexOf(\"|\"),\n beforeWord.lastIndexOf(\"&\")\n );\n const segment = beforeWord.slice(segmentStart + 1).trim();\n if (segment === \"\") {\n return true;\n }\n return segment.split(/\\s+/).every((part) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(part));\n}\n\nfunction getCurrentCommandLine(prefix: string, wordStart: number): string {\n const beforeWord = prefix.slice(0, wordStart);\n const segmentStart = Math.max(\n beforeWord.lastIndexOf(\";\"),\n beforeWord.lastIndexOf(\"|\"),\n beforeWord.lastIndexOf(\"&\")\n );\n return prefix.slice(segmentStart + 1).trimStart();\n}\n\nfunction splitCompletionWords(source: string): string[] {\n return source.trim().split(/\\s+/).filter(Boolean);\n}\n\nfunction looksLikePath(word: string): boolean {\n return word.startsWith(\"/\") || word.startsWith(\".\") || word.includes(\"/\");\n}\n\nfunction escapeCompletionSegment(segment: string): string {\n return segment.replace(/([\\s\\\\'\"$`!#&;|<>()[\\]{}*?])/g, \"\\\\$1\");\n}\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWsB,IAAtB;AACuB,IAAvB;AAC4B,IAA5B;AAC2C,IAA3C;AAC2B,IAA3B;AAAA;AAaO,MAAM,aAAa;AAAA,EAChB;AAAA,EAER,WAAW,CAAC,SAA8B;AAAA,IACxC,KAAK,cAAc,IAAI,+BAAY;AAAA,MACjC,IAAI,QAAQ;AAAA,MACZ,KAAK,QAAQ;AAAA,MACb,KAAK,QAAQ;AAAA,MACb,UAAU,QAAQ;AAAA,MAClB,UAAU,QAAQ,YAAY,EAAE,OAAO,QAAQ,SAAS,MAAM;AAAA,MAC9D,iBAAiB,QAAQ;AAAA,IAC3B,CAAC;AAAA;AAAA,EAGH,GAAG,CAAC,QAAgB,UAAiC,CAAC,GAAmB;AAAA,IACvE,IAAI;AAAA,MACF,MAAM,SAAS,IAAI,mBAAM,QAAQ,EAAE,kBAAkB,KAAK,CAAC,EAAE,SAAS;AAAA,MACtE,IAAI,OAAO,MAAM,CAAC,UAAU,MAAM,SAAS,aAAa,MAAM,SAAS,KAAK,GAAG;AAAA,QAC7E,OAAO,KAAK,yBAAyB,GAAG,IAAI,EAAE;AAAA,MAChD;AAAA,MACA,MAAM,MAAM,IAAI,qBAAO,MAAM,EAAE,MAAM;AAAA,MACrC,OAAO,KAAK,YAAY,iBAAiB,KAAK,OAAO;AAAA,MACrD,OAAO,KAAK;AAAA,MACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC/D,OAAO,KAAK,yBAAyB,GAAG,IAAI,OAAO;AAAA,CAAW;AAAA;AAAA;AAAA,EAIlE,UAAU,CAAC,SAAkB,UAAiC,CAAC,GAAmB;AAAA,IAChF,OAAO,KAAK,YAAY,iBAAiB,QAAQ,KAAK,OAAO;AAAA;AAAA,EAG/D,MAAM,GAAW;AAAA,IACf,OAAO,KAAK,YAAY,OAAO;AAAA;AAAA,EAGjC,MAAM,GAA2B;AAAA,IAC/B,OAAO,KAAK,YAAY,OAAO;AAAA;AAAA,EAGjC,eAAe,GAAW;AAAA,IACxB,OAAO,KAAK,YAAY,gBAAgB;AAAA;AAAA,OAGpC,QAAO,GAAkB;AAAA,EAIvB,wBAAwB,CAAC,UAAkB,YAAoB,YAAoC;AAAA,IACzG,MAAM,SAAS,2BAAa;AAAA,IAC5B,MAAM,SAAS,2BAAa;AAAA,IAC5B,MAAM,SAAS,IAAI;AAAA,IAEnB,MAAM,QAAQ,YAAiC;AAAA,MAC7C,IAAI,WAAW,SAAS,GAAG;AAAA,QACzB,MAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,QACjD,OAAO,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC;AAAA,QAC5B,MAAM,OAAO,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA,IAAI,WAAW,SAAS,GAAG;AAAA,QACzB,MAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,QACjD,OAAO,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC;AAAA,QAC5B,MAAM,OAAO,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,OAAO;AAAA,QACL,QAAQ,MAAM,OAAO,QAAQ;AAAA,QAC7B,QAAQ,MAAM,OAAO,QAAQ;AAAA,QAC7B;AAAA,MACF;AAAA,OACC;AAAA,IAEH,OAAO;AAAA,MACL,QAAQ,OAAO,kBAAkB;AAAA,MACjC,QAAQ,OAAO,kBAAkB;AAAA,MACjC;AAAA,MACA;AAAA,MACA,MAAM,MAAM;AAAA,IACd;AAAA;AAEJ;AAEO,SAAS,kBAAkB,CAAC,QAAmC;AAAA,EACpE,OAAO,IAAI,aAAa,MAAM;AAAA;",
8
- "debugId": "636F3B47CF304B5264756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "C807EA9CC9D2A2E164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/types.ts"],
4
4
  "sourcesContent": [
5
- "// Virtual Filesystem Interface\nexport interface VirtualFSWritable {\n write(chunk: Uint8Array): Promise<void>;\n close(): Promise<void>;\n abort?(reason?: unknown): Promise<void>;\n}\n\nexport interface VirtualFS {\n readFile(path: string): Promise<Buffer>;\n readFile(path: string, encoding: BufferEncoding): Promise<string>;\n readStream(path: string): AsyncIterable<Uint8Array>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<FileStat>;\n exists(path: string): Promise<boolean>;\n\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n writeStream(path: string, opts?: { append?: boolean }): Promise<VirtualFSWritable>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;\n\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n\n resolve(...paths: string[]): string;\n dirname(path: string): string;\n basename(path: string): string;\n glob(pattern: string, opts?: { cwd?: string }): Promise<string[]>;\n}\n\nexport interface FileStat {\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n mtimeMs: number;\n}\n\n// Command Interfaces\nexport type Command = (ctx: CommandContext) => Promise<number>;\n\nexport interface ShellRunOptions {\n argv0?: string;\n args?: string[];\n}\n\nexport interface ShellCommandApi {\n eval(source: string): Promise<number>;\n source(path: string, args?: string[]): Promise<number>;\n runScript(path: string, args?: string[]): Promise<number>;\n runShell(source: string, options?: ShellRunOptions): Promise<number>;\n getLastExitCode(): number;\n exit(exitCode?: number): never;\n}\n\nexport interface TerminalInfo {\n isTTY: boolean;\n columns?: number;\n rows?: number;\n colorDepth?: number;\n}\n\nexport interface CommandContext {\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 interface ExternalCommandContext extends CommandContext {\n name: string;\n}\n\nexport type ShellCommandFallback = (ctx: ExternalCommandContext) => Promise<number>;\n\nexport interface Stdin {\n stream(): AsyncIterable<Uint8Array>;\n buffer(): Promise<Buffer>;\n text(): Promise<string>;\n lines(): AsyncIterable<string>;\n}\n\nexport interface ShellInputController extends AsyncIterable<Uint8Array> {\n write(chunk: Uint8Array | string): Promise<void>;\n close(): void;\n abort(reason?: unknown): void;\n}\n\nexport interface Stdout {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface Stderr {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface OutputCollector extends Stdout {\n close(): void;\n collect(): Promise<Buffer>;\n getReadableStream(): AsyncIterable<Uint8Array>;\n}\n\n// Execution Result\nexport interface ExecResult {\n stdout: Buffer;\n stderr: Buffer;\n exitCode: number;\n}\n\nexport interface ShellOutputEvent {\n fd: 1 | 2;\n chunk: Uint8Array;\n}\n\nexport type ShellInputSource = AsyncIterable<Uint8Array> | Buffer | string | null;\n\nexport interface ShellExecutionOptions {\n stdin?: ShellInputSource;\n stdout?: Stdout;\n stderr?: Stderr;\n terminal?: TerminalInfo;\n signal?: AbortSignal;\n outputMode?: \"separate\" | \"merged\";\n}\n\nexport interface ShellExecution {\n stdout: AsyncIterable<Uint8Array>;\n stderr: AsyncIterable<Uint8Array>;\n output: AsyncIterable<ShellOutputEvent>;\n exit: Promise<ExecResult>;\n kill(reason?: unknown): void;\n}\n\n// Shell Configuration\nexport interface ShellConfig {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\n// Raw escape hatch type\nexport interface RawValue {\n raw: string;\n}\n\nexport function isRawValue(value: unknown): value is RawValue {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"raw\" in value &&\n typeof (value as RawValue).raw === \"string\"\n );\n}\n\n// JS Object Redirection types\nexport type RedirectObject = Buffer | Blob | Response | string;\n\nexport interface RedirectObjectMap {\n [marker: string]: RedirectObject;\n}\n\nexport function isRedirectObject(value: unknown): value is RedirectObject {\n return (\n Buffer.isBuffer(value) ||\n value instanceof Blob ||\n value instanceof Response ||\n typeof value === \"string\"\n );\n}\n"
5
+ "// Virtual Filesystem Interface\nexport interface VirtualFSWritable {\n write(chunk: Uint8Array): Promise<void>;\n close(): Promise<void>;\n abort?(reason?: unknown): Promise<void>;\n}\n\nexport interface VirtualFS {\n readFile(path: string): Promise<Buffer>;\n readFile(path: string, encoding: BufferEncoding): Promise<string>;\n readStream(path: string): AsyncIterable<Uint8Array>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<FileStat>;\n exists(path: string): Promise<boolean>;\n\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n writeStream(path: string, opts?: { append?: boolean }): Promise<VirtualFSWritable>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;\n\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n\n resolve(...paths: string[]): string;\n dirname(path: string): string;\n basename(path: string): string;\n glob(pattern: string, opts?: { cwd?: string }): Promise<string[]>;\n}\n\nexport interface FileStat {\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n mtimeMs: number;\n}\n\n// Command Interfaces\nexport type Command = (ctx: CommandContext) => Promise<number>;\n\nexport interface ShellRunOptions {\n argv0?: string;\n args?: string[];\n}\n\nexport interface ShellCommandApi {\n eval(source: string): Promise<number>;\n source(path: string, args?: string[]): Promise<number>;\n runScript(path: string, args?: string[]): Promise<number>;\n runShell(source: string, options?: ShellRunOptions): Promise<number>;\n getLastExitCode(): number;\n exit(exitCode?: number): never;\n}\n\nexport interface TerminalInfo {\n isTTY: boolean;\n columns?: number;\n rows?: number;\n colorDepth?: number;\n}\n\nexport interface CompletionContext {\n source: string;\n cursor: number;\n command: string;\n args: string[];\n word: string;\n wordStart: number;\n wordEnd: number;\n cwd: string;\n env: Record<string, string>;\n fs: VirtualFS;\n terminal: TerminalInfo;\n}\n\nexport interface CompletionResult {\n replacement: string;\n matches: string[];\n}\n\nexport type CommandCompleter = (ctx: CompletionContext) => CompletionResult | Promise<CompletionResult>;\n\nexport interface CommandContext {\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 interface ExternalCommandContext extends CommandContext {\n name: string;\n}\n\nexport type ShellCommandFallback = (ctx: ExternalCommandContext) => Promise<number>;\n\nexport interface Stdin {\n stream(): AsyncIterable<Uint8Array>;\n buffer(): Promise<Buffer>;\n text(): Promise<string>;\n lines(): AsyncIterable<string>;\n}\n\nexport interface ShellInputController extends AsyncIterable<Uint8Array> {\n write(chunk: Uint8Array | string): Promise<void>;\n close(): void;\n abort(reason?: unknown): void;\n}\n\nexport interface Stdout {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface Stderr {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface OutputCollector extends Stdout {\n close(): void;\n collect(): Promise<Buffer>;\n getReadableStream(): AsyncIterable<Uint8Array>;\n}\n\n// Execution Result\nexport interface ExecResult {\n stdout: Buffer;\n stderr: Buffer;\n exitCode: number;\n}\n\nexport interface ShellOutputEvent {\n fd: 1 | 2;\n chunk: Uint8Array;\n}\n\nexport type ShellInputSource = AsyncIterable<Uint8Array> | Buffer | string | null;\n\nexport interface ShellExecutionOptions {\n stdin?: ShellInputSource;\n stdout?: Stdout;\n stderr?: Stderr;\n terminal?: TerminalInfo;\n signal?: AbortSignal;\n outputMode?: \"separate\" | \"merged\";\n}\n\nexport interface ShellExecution {\n stdout: AsyncIterable<Uint8Array>;\n stderr: AsyncIterable<Uint8Array>;\n output: AsyncIterable<ShellOutputEvent>;\n exit: Promise<ExecResult>;\n kill(reason?: unknown): void;\n}\n\n// Shell Configuration\nexport interface ShellConfig {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n completions?: Record<string, CommandCompleter>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\n// Raw escape hatch type\nexport interface RawValue {\n raw: string;\n}\n\nexport function isRawValue(value: unknown): value is RawValue {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"raw\" in value &&\n typeof (value as RawValue).raw === \"string\"\n );\n}\n\n// JS Object Redirection types\nexport type RedirectObject = Buffer | Blob | Response | string;\n\nexport interface RedirectObjectMap {\n [marker: string]: RedirectObject;\n}\n\nexport function isRedirectObject(value: unknown): value is RedirectObject {\n return (\n Buffer.isBuffer(value) ||\n value instanceof Blob ||\n value instanceof Response ||\n typeof value === \"string\"\n );\n}\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+JO,SAAS,UAAU,CAAC,OAAmC;AAAA,EAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,OAAQ,MAAmB,QAAQ;AAAA;AAWhC,SAAS,gBAAgB,CAAC,OAAyC;AAAA,EACxE,OACE,OAAO,SAAS,KAAK,KACrB,iBAAiB,QACjB,iBAAiB,YACjB,OAAO,UAAU;AAAA;",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqLO,SAAS,UAAU,CAAC,OAAmC;AAAA,EAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,OAAQ,MAAmB,QAAQ;AAAA;AAWhC,SAAS,gBAAgB,CAAC,OAAyC;AAAA,EACxE,OACE,OAAO,SAAS,KAAK,KACrB,iBAAiB,QACjB,iBAAiB,YACjB,OAAO,UAAU;AAAA;",
8
8
  "debugId": "324A9AF65A2908F664756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "type": "module"
5
5
  }
@@ -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\";\nexport { ShellSession, createShellSession, type ShellSessionOptions } from \"./shell-session.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 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"
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;AACA;AA2BA;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;",
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
8
  "debugId": "A98B7667567C2E6F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -6,14 +6,22 @@ import { createStderr, createStdout } from "./io/stdout.mjs";
6
6
  import { AsyncQueue } from "./io/async-queue.mjs";
7
7
 
8
8
  class ShellSession {
9
+ fs;
10
+ commands;
11
+ completions;
12
+ terminal;
9
13
  interpreter;
10
14
  constructor(options) {
15
+ this.fs = options.fs;
16
+ this.commands = options.commands;
17
+ this.completions = options.completions ?? {};
18
+ this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
11
19
  this.interpreter = new Interpreter({
12
20
  fs: options.fs,
13
21
  cwd: options.cwd,
14
22
  env: options.env,
15
23
  commands: options.commands,
16
- terminal: options.terminal ?? { isTTY: options.isTTY ?? false },
24
+ terminal: this.terminal,
17
25
  externalCommand: options.externalCommand
18
26
  });
19
27
  }
@@ -43,7 +51,63 @@ class ShellSession {
43
51
  getLastExitCode() {
44
52
  return this.interpreter.getLastExitCode();
45
53
  }
54
+ async complete(source, cursor = source.length) {
55
+ const boundedCursor = Math.max(0, Math.min(cursor, source.length));
56
+ const prefix = source.slice(0, boundedCursor);
57
+ const { word, start } = getCurrentWord(prefix);
58
+ if (isCommandPosition(prefix, start) && !looksLikePath(word)) {
59
+ return {
60
+ replacement: word,
61
+ matches: Object.keys(this.commands).filter((name) => name.startsWith(word)).sort().map((name) => `${name} `)
62
+ };
63
+ }
64
+ const commandLine = getCurrentCommandLine(prefix, start);
65
+ const [command = "", ...args] = splitCompletionWords(commandLine);
66
+ const completer = command === "" ? undefined : this.completions[command];
67
+ if (completer) {
68
+ return completer({
69
+ source,
70
+ cursor: boundedCursor,
71
+ command,
72
+ args,
73
+ word,
74
+ wordStart: start,
75
+ wordEnd: boundedCursor,
76
+ cwd: this.getCwd(),
77
+ env: this.getEnv(),
78
+ fs: this.fs,
79
+ terminal: this.terminal
80
+ });
81
+ }
82
+ return this.completePath(word);
83
+ }
46
84
  async dispose() {}
85
+ async completePath(word) {
86
+ const slash = word.lastIndexOf("/");
87
+ const dirPart = slash === -1 ? "" : word.slice(0, slash + 1);
88
+ const namePrefix = slash === -1 ? word : word.slice(slash + 1);
89
+ const basePath = dirPart === "" ? this.getCwd() : dirPart.startsWith("/") ? dirPart : this.fs.resolve(this.getCwd(), dirPart);
90
+ let entries;
91
+ try {
92
+ entries = await this.fs.readdir(basePath);
93
+ } catch {
94
+ return { replacement: word, matches: [] };
95
+ }
96
+ const matches = await Promise.all(entries.filter((entry) => entry.startsWith(namePrefix)).sort().map(async (entry) => {
97
+ const candidate = `${dirPart}${escapeCompletionSegment(entry)}`;
98
+ const path = this.fs.resolve(basePath, entry);
99
+ try {
100
+ const stat = await this.fs.stat(path);
101
+ return stat.isDirectory() ? `${candidate}/` : candidate;
102
+ } catch {
103
+ return candidate;
104
+ }
105
+ }));
106
+ if (matches.length === 1 && !matches[0].endsWith("/")) {
107
+ matches[0] = `${matches[0]} `;
108
+ }
109
+ return { replacement: word, matches };
110
+ }
47
111
  createImmediateExecution(exitCode, stdoutText, stderrText) {
48
112
  const stdout = createStdout();
49
113
  const stderr = createStderr();
@@ -80,9 +144,39 @@ class ShellSession {
80
144
  function createShellSession(config) {
81
145
  return new ShellSession(config);
82
146
  }
147
+ function getCurrentWord(prefix) {
148
+ let start = prefix.length;
149
+ while (start > 0 && !/\s/.test(prefix[start - 1])) {
150
+ start--;
151
+ }
152
+ return { word: prefix.slice(start), start };
153
+ }
154
+ function isCommandPosition(prefix, wordStart) {
155
+ const beforeWord = prefix.slice(0, wordStart);
156
+ const segmentStart = Math.max(beforeWord.lastIndexOf(";"), beforeWord.lastIndexOf("|"), beforeWord.lastIndexOf("&"));
157
+ const segment = beforeWord.slice(segmentStart + 1).trim();
158
+ if (segment === "") {
159
+ return true;
160
+ }
161
+ return segment.split(/\s+/).every((part) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(part));
162
+ }
163
+ function getCurrentCommandLine(prefix, wordStart) {
164
+ const beforeWord = prefix.slice(0, wordStart);
165
+ const segmentStart = Math.max(beforeWord.lastIndexOf(";"), beforeWord.lastIndexOf("|"), beforeWord.lastIndexOf("&"));
166
+ return prefix.slice(segmentStart + 1).trimStart();
167
+ }
168
+ function splitCompletionWords(source) {
169
+ return source.trim().split(/\s+/).filter(Boolean);
170
+ }
171
+ function looksLikePath(word) {
172
+ return word.startsWith("/") || word.startsWith(".") || word.includes("/");
173
+ }
174
+ function escapeCompletionSegment(segment) {
175
+ return segment.replace(/([\s\\'"$`!#&;|<>()[\]{}*?])/g, "\\$1");
176
+ }
83
177
  export {
84
178
  createShellSession,
85
179
  ShellSession
86
180
  };
87
181
 
88
- //# debugId=C1F9D01ECC6BB46864756E2164756E21
182
+ //# debugId=EDCA97BB05872ACD64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/shell-session.ts"],
4
4
  "sourcesContent": [
5
- "import type {\n Command,\n ExecResult,\n ShellCommandFallback,\n ShellConfig,\n ShellExecution,\n ShellExecutionOptions,\n TerminalInfo,\n VirtualFS,\n} from \"./types.mjs\";\nimport type { Program } from \"./shell-dsl.mjs\";\nimport { Lexer } from \"./lexer/lexer.mjs\";\nimport { Parser } from \"./parser/parser.mjs\";\nimport { Interpreter } from \"./interpreter/interpreter.mjs\";\nimport { createStderr, createStdout } from \"./io/stdout.mjs\";\nimport { AsyncQueue } from \"./io/async-queue.mjs\";\nimport type { ShellOutputEvent } from \"./types.mjs\";\n\nexport interface ShellSessionOptions {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\nexport class ShellSession {\n private interpreter: Interpreter;\n\n constructor(options: ShellSessionOptions) {\n this.interpreter = new Interpreter({\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n commands: options.commands,\n terminal: options.terminal ?? { isTTY: options.isTTY ?? false },\n externalCommand: options.externalCommand,\n });\n }\n\n run(source: string, options: ShellExecutionOptions = {}): ShellExecution {\n try {\n const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return this.createImmediateExecution(0, \"\", \"\");\n }\n const ast = new Parser(tokens).parse();\n return this.interpreter.executeStreaming(ast, options);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return this.createImmediateExecution(2, \"\", `sh: ${message}\\n`);\n }\n }\n\n runProgram(program: Program, options: ShellExecutionOptions = {}): ShellExecution {\n return this.interpreter.executeStreaming(program.ast, options);\n }\n\n getCwd(): string {\n return this.interpreter.getCwd();\n }\n\n getEnv(): Record<string, string> {\n return this.interpreter.getEnv();\n }\n\n getLastExitCode(): number {\n return this.interpreter.getLastExitCode();\n }\n\n async dispose(): Promise<void> {\n // Reserved for future resources owned by a session.\n }\n\n private createImmediateExecution(exitCode: number, stdoutText: string, stderrText: string): ShellExecution {\n const stdout = createStdout();\n const stderr = createStderr();\n const output = new AsyncQueue<ShellOutputEvent>();\n\n const exit = (async (): Promise<ExecResult> => {\n if (stdoutText.length > 0) {\n const chunk = new TextEncoder().encode(stdoutText);\n output.push({ fd: 1, chunk });\n await stdout.write(chunk);\n }\n if (stderrText.length > 0) {\n const chunk = new TextEncoder().encode(stderrText);\n output.push({ fd: 2, chunk });\n await stderr.write(chunk);\n }\n stdout.close();\n stderr.close();\n output.close();\n return {\n stdout: await stdout.collect(),\n stderr: await stderr.collect(),\n exitCode,\n };\n })();\n\n return {\n stdout: stdout.getReadableStream(),\n stderr: stderr.getReadableStream(),\n output,\n exit,\n kill: () => {},\n };\n }\n}\n\nexport function createShellSession(config: ShellConfig): ShellSession {\n return new ShellSession(config);\n}\n"
5
+ "import type {\n Command,\n CommandCompleter,\n CompletionResult,\n ExecResult,\n ShellCommandFallback,\n ShellConfig,\n ShellExecution,\n ShellExecutionOptions,\n TerminalInfo,\n VirtualFS,\n} from \"./types.mjs\";\nimport type { Program } from \"./shell-dsl.mjs\";\nimport { Lexer } from \"./lexer/lexer.mjs\";\nimport { Parser } from \"./parser/parser.mjs\";\nimport { Interpreter } from \"./interpreter/interpreter.mjs\";\nimport { createStderr, createStdout } from \"./io/stdout.mjs\";\nimport { AsyncQueue } from \"./io/async-queue.mjs\";\nimport type { ShellOutputEvent } from \"./types.mjs\";\n\nexport interface ShellSessionOptions {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n completions?: Record<string, CommandCompleter>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\nexport class ShellSession {\n private fs: VirtualFS;\n private commands: Record<string, Command>;\n private completions: Record<string, CommandCompleter>;\n private terminal: TerminalInfo;\n private interpreter: Interpreter;\n\n constructor(options: ShellSessionOptions) {\n this.fs = options.fs;\n this.commands = options.commands;\n this.completions = options.completions ?? {};\n this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };\n this.interpreter = new Interpreter({\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n commands: options.commands,\n terminal: this.terminal,\n externalCommand: options.externalCommand,\n });\n }\n\n run(source: string, options: ShellExecutionOptions = {}): ShellExecution {\n try {\n const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return this.createImmediateExecution(0, \"\", \"\");\n }\n const ast = new Parser(tokens).parse();\n return this.interpreter.executeStreaming(ast, options);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return this.createImmediateExecution(2, \"\", `sh: ${message}\\n`);\n }\n }\n\n runProgram(program: Program, options: ShellExecutionOptions = {}): ShellExecution {\n return this.interpreter.executeStreaming(program.ast, options);\n }\n\n getCwd(): string {\n return this.interpreter.getCwd();\n }\n\n getEnv(): Record<string, string> {\n return this.interpreter.getEnv();\n }\n\n getLastExitCode(): number {\n return this.interpreter.getLastExitCode();\n }\n\n async complete(source: string, cursor: number = source.length): Promise<CompletionResult> {\n const boundedCursor = Math.max(0, Math.min(cursor, source.length));\n const prefix = source.slice(0, boundedCursor);\n const { word, start } = getCurrentWord(prefix);\n\n if (isCommandPosition(prefix, start) && !looksLikePath(word)) {\n return {\n replacement: word,\n matches: Object.keys(this.commands)\n .filter((name) => name.startsWith(word))\n .sort()\n .map((name) => `${name} `),\n };\n }\n\n const commandLine = getCurrentCommandLine(prefix, start);\n const [command = \"\", ...args] = splitCompletionWords(commandLine);\n const completer = command === \"\" ? undefined : this.completions[command];\n if (completer) {\n return completer({\n source,\n cursor: boundedCursor,\n command,\n args,\n word,\n wordStart: start,\n wordEnd: boundedCursor,\n cwd: this.getCwd(),\n env: this.getEnv(),\n fs: this.fs,\n terminal: this.terminal,\n });\n }\n\n return this.completePath(word);\n }\n\n async dispose(): Promise<void> {\n // Reserved for future resources owned by a session.\n }\n\n private async completePath(word: string): Promise<CompletionResult> {\n const slash = word.lastIndexOf(\"/\");\n const dirPart = slash === -1 ? \"\" : word.slice(0, slash + 1);\n const namePrefix = slash === -1 ? word : word.slice(slash + 1);\n const basePath = dirPart === \"\"\n ? this.getCwd()\n : dirPart.startsWith(\"/\")\n ? dirPart\n : this.fs.resolve(this.getCwd(), dirPart);\n\n let entries: string[];\n try {\n entries = await this.fs.readdir(basePath);\n } catch {\n return { replacement: word, matches: [] };\n }\n\n const matches = await Promise.all(\n entries\n .filter((entry) => entry.startsWith(namePrefix))\n .sort()\n .map(async (entry) => {\n const candidate = `${dirPart}${escapeCompletionSegment(entry)}`;\n const path = this.fs.resolve(basePath, entry);\n try {\n const stat = await this.fs.stat(path);\n return stat.isDirectory() ? `${candidate}/` : candidate;\n } catch {\n return candidate;\n }\n })\n );\n\n if (matches.length === 1 && !matches[0]!.endsWith(\"/\")) {\n matches[0] = `${matches[0]} `;\n }\n\n return { replacement: word, matches };\n }\n\n private createImmediateExecution(exitCode: number, stdoutText: string, stderrText: string): ShellExecution {\n const stdout = createStdout();\n const stderr = createStderr();\n const output = new AsyncQueue<ShellOutputEvent>();\n\n const exit = (async (): Promise<ExecResult> => {\n if (stdoutText.length > 0) {\n const chunk = new TextEncoder().encode(stdoutText);\n output.push({ fd: 1, chunk });\n await stdout.write(chunk);\n }\n if (stderrText.length > 0) {\n const chunk = new TextEncoder().encode(stderrText);\n output.push({ fd: 2, chunk });\n await stderr.write(chunk);\n }\n stdout.close();\n stderr.close();\n output.close();\n return {\n stdout: await stdout.collect(),\n stderr: await stderr.collect(),\n exitCode,\n };\n })();\n\n return {\n stdout: stdout.getReadableStream(),\n stderr: stderr.getReadableStream(),\n output,\n exit,\n kill: () => {},\n };\n }\n}\n\nexport function createShellSession(config: ShellConfig): ShellSession {\n return new ShellSession(config);\n}\n\nfunction getCurrentWord(prefix: string): { word: string; start: number } {\n let start = prefix.length;\n while (start > 0 && !/\\s/.test(prefix[start - 1]!)) {\n start--;\n }\n return { word: prefix.slice(start), start };\n}\n\nfunction isCommandPosition(prefix: string, wordStart: number): boolean {\n const beforeWord = prefix.slice(0, wordStart);\n const segmentStart = Math.max(\n beforeWord.lastIndexOf(\";\"),\n beforeWord.lastIndexOf(\"|\"),\n beforeWord.lastIndexOf(\"&\")\n );\n const segment = beforeWord.slice(segmentStart + 1).trim();\n if (segment === \"\") {\n return true;\n }\n return segment.split(/\\s+/).every((part) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(part));\n}\n\nfunction getCurrentCommandLine(prefix: string, wordStart: number): string {\n const beforeWord = prefix.slice(0, wordStart);\n const segmentStart = Math.max(\n beforeWord.lastIndexOf(\";\"),\n beforeWord.lastIndexOf(\"|\"),\n beforeWord.lastIndexOf(\"&\")\n );\n return prefix.slice(segmentStart + 1).trimStart();\n}\n\nfunction splitCompletionWords(source: string): string[] {\n return source.trim().split(/\\s+/).filter(Boolean);\n}\n\nfunction looksLikePath(word: string): boolean {\n return word.startsWith(\"/\") || word.startsWith(\".\") || word.includes(\"/\");\n}\n\nfunction escapeCompletionSegment(segment: string): string {\n return segment.replace(/([\\s\\\\'\"$`!#&;|<>()[\\]{}*?])/g, \"\\\\$1\");\n}\n"
6
6
  ],
7
- "mappings": ";AAWA;AACA;AACA;AACA;AACA;AAAA;AAaO,MAAM,aAAa;AAAA,EAChB;AAAA,EAER,WAAW,CAAC,SAA8B;AAAA,IACxC,KAAK,cAAc,IAAI,YAAY;AAAA,MACjC,IAAI,QAAQ;AAAA,MACZ,KAAK,QAAQ;AAAA,MACb,KAAK,QAAQ;AAAA,MACb,UAAU,QAAQ;AAAA,MAClB,UAAU,QAAQ,YAAY,EAAE,OAAO,QAAQ,SAAS,MAAM;AAAA,MAC9D,iBAAiB,QAAQ;AAAA,IAC3B,CAAC;AAAA;AAAA,EAGH,GAAG,CAAC,QAAgB,UAAiC,CAAC,GAAmB;AAAA,IACvE,IAAI;AAAA,MACF,MAAM,SAAS,IAAI,MAAM,QAAQ,EAAE,kBAAkB,KAAK,CAAC,EAAE,SAAS;AAAA,MACtE,IAAI,OAAO,MAAM,CAAC,UAAU,MAAM,SAAS,aAAa,MAAM,SAAS,KAAK,GAAG;AAAA,QAC7E,OAAO,KAAK,yBAAyB,GAAG,IAAI,EAAE;AAAA,MAChD;AAAA,MACA,MAAM,MAAM,IAAI,OAAO,MAAM,EAAE,MAAM;AAAA,MACrC,OAAO,KAAK,YAAY,iBAAiB,KAAK,OAAO;AAAA,MACrD,OAAO,KAAK;AAAA,MACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC/D,OAAO,KAAK,yBAAyB,GAAG,IAAI,OAAO;AAAA,CAAW;AAAA;AAAA;AAAA,EAIlE,UAAU,CAAC,SAAkB,UAAiC,CAAC,GAAmB;AAAA,IAChF,OAAO,KAAK,YAAY,iBAAiB,QAAQ,KAAK,OAAO;AAAA;AAAA,EAG/D,MAAM,GAAW;AAAA,IACf,OAAO,KAAK,YAAY,OAAO;AAAA;AAAA,EAGjC,MAAM,GAA2B;AAAA,IAC/B,OAAO,KAAK,YAAY,OAAO;AAAA;AAAA,EAGjC,eAAe,GAAW;AAAA,IACxB,OAAO,KAAK,YAAY,gBAAgB;AAAA;AAAA,OAGpC,QAAO,GAAkB;AAAA,EAIvB,wBAAwB,CAAC,UAAkB,YAAoB,YAAoC;AAAA,IACzG,MAAM,SAAS,aAAa;AAAA,IAC5B,MAAM,SAAS,aAAa;AAAA,IAC5B,MAAM,SAAS,IAAI;AAAA,IAEnB,MAAM,QAAQ,YAAiC;AAAA,MAC7C,IAAI,WAAW,SAAS,GAAG;AAAA,QACzB,MAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,QACjD,OAAO,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC;AAAA,QAC5B,MAAM,OAAO,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA,IAAI,WAAW,SAAS,GAAG;AAAA,QACzB,MAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,QACjD,OAAO,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC;AAAA,QAC5B,MAAM,OAAO,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,OAAO;AAAA,QACL,QAAQ,MAAM,OAAO,QAAQ;AAAA,QAC7B,QAAQ,MAAM,OAAO,QAAQ;AAAA,QAC7B;AAAA,MACF;AAAA,OACC;AAAA,IAEH,OAAO;AAAA,MACL,QAAQ,OAAO,kBAAkB;AAAA,MACjC,QAAQ,OAAO,kBAAkB;AAAA,MACjC;AAAA,MACA;AAAA,MACA,MAAM,MAAM;AAAA,IACd;AAAA;AAEJ;AAEO,SAAS,kBAAkB,CAAC,QAAmC;AAAA,EACpE,OAAO,IAAI,aAAa,MAAM;AAAA;",
8
- "debugId": "C1F9D01ECC6BB46864756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "EDCA97BB05872ACD64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/types.ts"],
4
4
  "sourcesContent": [
5
- "// Virtual Filesystem Interface\nexport interface VirtualFSWritable {\n write(chunk: Uint8Array): Promise<void>;\n close(): Promise<void>;\n abort?(reason?: unknown): Promise<void>;\n}\n\nexport interface VirtualFS {\n readFile(path: string): Promise<Buffer>;\n readFile(path: string, encoding: BufferEncoding): Promise<string>;\n readStream(path: string): AsyncIterable<Uint8Array>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<FileStat>;\n exists(path: string): Promise<boolean>;\n\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n writeStream(path: string, opts?: { append?: boolean }): Promise<VirtualFSWritable>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;\n\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n\n resolve(...paths: string[]): string;\n dirname(path: string): string;\n basename(path: string): string;\n glob(pattern: string, opts?: { cwd?: string }): Promise<string[]>;\n}\n\nexport interface FileStat {\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n mtimeMs: number;\n}\n\n// Command Interfaces\nexport type Command = (ctx: CommandContext) => Promise<number>;\n\nexport interface ShellRunOptions {\n argv0?: string;\n args?: string[];\n}\n\nexport interface ShellCommandApi {\n eval(source: string): Promise<number>;\n source(path: string, args?: string[]): Promise<number>;\n runScript(path: string, args?: string[]): Promise<number>;\n runShell(source: string, options?: ShellRunOptions): Promise<number>;\n getLastExitCode(): number;\n exit(exitCode?: number): never;\n}\n\nexport interface TerminalInfo {\n isTTY: boolean;\n columns?: number;\n rows?: number;\n colorDepth?: number;\n}\n\nexport interface CommandContext {\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 interface ExternalCommandContext extends CommandContext {\n name: string;\n}\n\nexport type ShellCommandFallback = (ctx: ExternalCommandContext) => Promise<number>;\n\nexport interface Stdin {\n stream(): AsyncIterable<Uint8Array>;\n buffer(): Promise<Buffer>;\n text(): Promise<string>;\n lines(): AsyncIterable<string>;\n}\n\nexport interface ShellInputController extends AsyncIterable<Uint8Array> {\n write(chunk: Uint8Array | string): Promise<void>;\n close(): void;\n abort(reason?: unknown): void;\n}\n\nexport interface Stdout {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface Stderr {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface OutputCollector extends Stdout {\n close(): void;\n collect(): Promise<Buffer>;\n getReadableStream(): AsyncIterable<Uint8Array>;\n}\n\n// Execution Result\nexport interface ExecResult {\n stdout: Buffer;\n stderr: Buffer;\n exitCode: number;\n}\n\nexport interface ShellOutputEvent {\n fd: 1 | 2;\n chunk: Uint8Array;\n}\n\nexport type ShellInputSource = AsyncIterable<Uint8Array> | Buffer | string | null;\n\nexport interface ShellExecutionOptions {\n stdin?: ShellInputSource;\n stdout?: Stdout;\n stderr?: Stderr;\n terminal?: TerminalInfo;\n signal?: AbortSignal;\n outputMode?: \"separate\" | \"merged\";\n}\n\nexport interface ShellExecution {\n stdout: AsyncIterable<Uint8Array>;\n stderr: AsyncIterable<Uint8Array>;\n output: AsyncIterable<ShellOutputEvent>;\n exit: Promise<ExecResult>;\n kill(reason?: unknown): void;\n}\n\n// Shell Configuration\nexport interface ShellConfig {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\n// Raw escape hatch type\nexport interface RawValue {\n raw: string;\n}\n\nexport function isRawValue(value: unknown): value is RawValue {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"raw\" in value &&\n typeof (value as RawValue).raw === \"string\"\n );\n}\n\n// JS Object Redirection types\nexport type RedirectObject = Buffer | Blob | Response | string;\n\nexport interface RedirectObjectMap {\n [marker: string]: RedirectObject;\n}\n\nexport function isRedirectObject(value: unknown): value is RedirectObject {\n return (\n Buffer.isBuffer(value) ||\n value instanceof Blob ||\n value instanceof Response ||\n typeof value === \"string\"\n );\n}\n"
5
+ "// Virtual Filesystem Interface\nexport interface VirtualFSWritable {\n write(chunk: Uint8Array): Promise<void>;\n close(): Promise<void>;\n abort?(reason?: unknown): Promise<void>;\n}\n\nexport interface VirtualFS {\n readFile(path: string): Promise<Buffer>;\n readFile(path: string, encoding: BufferEncoding): Promise<string>;\n readStream(path: string): AsyncIterable<Uint8Array>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<FileStat>;\n exists(path: string): Promise<boolean>;\n\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n writeStream(path: string, opts?: { append?: boolean }): Promise<VirtualFSWritable>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;\n\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n\n resolve(...paths: string[]): string;\n dirname(path: string): string;\n basename(path: string): string;\n glob(pattern: string, opts?: { cwd?: string }): Promise<string[]>;\n}\n\nexport interface FileStat {\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n mtimeMs: number;\n}\n\n// Command Interfaces\nexport type Command = (ctx: CommandContext) => Promise<number>;\n\nexport interface ShellRunOptions {\n argv0?: string;\n args?: string[];\n}\n\nexport interface ShellCommandApi {\n eval(source: string): Promise<number>;\n source(path: string, args?: string[]): Promise<number>;\n runScript(path: string, args?: string[]): Promise<number>;\n runShell(source: string, options?: ShellRunOptions): Promise<number>;\n getLastExitCode(): number;\n exit(exitCode?: number): never;\n}\n\nexport interface TerminalInfo {\n isTTY: boolean;\n columns?: number;\n rows?: number;\n colorDepth?: number;\n}\n\nexport interface CompletionContext {\n source: string;\n cursor: number;\n command: string;\n args: string[];\n word: string;\n wordStart: number;\n wordEnd: number;\n cwd: string;\n env: Record<string, string>;\n fs: VirtualFS;\n terminal: TerminalInfo;\n}\n\nexport interface CompletionResult {\n replacement: string;\n matches: string[];\n}\n\nexport type CommandCompleter = (ctx: CompletionContext) => CompletionResult | Promise<CompletionResult>;\n\nexport interface CommandContext {\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 interface ExternalCommandContext extends CommandContext {\n name: string;\n}\n\nexport type ShellCommandFallback = (ctx: ExternalCommandContext) => Promise<number>;\n\nexport interface Stdin {\n stream(): AsyncIterable<Uint8Array>;\n buffer(): Promise<Buffer>;\n text(): Promise<string>;\n lines(): AsyncIterable<string>;\n}\n\nexport interface ShellInputController extends AsyncIterable<Uint8Array> {\n write(chunk: Uint8Array | string): Promise<void>;\n close(): void;\n abort(reason?: unknown): void;\n}\n\nexport interface Stdout {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface Stderr {\n write(chunk: Uint8Array): Promise<void>;\n writeText(str: string): Promise<void>;\n isTTY: boolean;\n}\n\nexport interface OutputCollector extends Stdout {\n close(): void;\n collect(): Promise<Buffer>;\n getReadableStream(): AsyncIterable<Uint8Array>;\n}\n\n// Execution Result\nexport interface ExecResult {\n stdout: Buffer;\n stderr: Buffer;\n exitCode: number;\n}\n\nexport interface ShellOutputEvent {\n fd: 1 | 2;\n chunk: Uint8Array;\n}\n\nexport type ShellInputSource = AsyncIterable<Uint8Array> | Buffer | string | null;\n\nexport interface ShellExecutionOptions {\n stdin?: ShellInputSource;\n stdout?: Stdout;\n stderr?: Stderr;\n terminal?: TerminalInfo;\n signal?: AbortSignal;\n outputMode?: \"separate\" | \"merged\";\n}\n\nexport interface ShellExecution {\n stdout: AsyncIterable<Uint8Array>;\n stderr: AsyncIterable<Uint8Array>;\n output: AsyncIterable<ShellOutputEvent>;\n exit: Promise<ExecResult>;\n kill(reason?: unknown): void;\n}\n\n// Shell Configuration\nexport interface ShellConfig {\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n commands: Record<string, Command>;\n completions?: Record<string, CommandCompleter>;\n isTTY?: boolean;\n terminal?: TerminalInfo;\n externalCommand?: ShellCommandFallback;\n}\n\n// Raw escape hatch type\nexport interface RawValue {\n raw: string;\n}\n\nexport function isRawValue(value: unknown): value is RawValue {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"raw\" in value &&\n typeof (value as RawValue).raw === \"string\"\n );\n}\n\n// JS Object Redirection types\nexport type RedirectObject = Buffer | Blob | Response | string;\n\nexport interface RedirectObjectMap {\n [marker: string]: RedirectObject;\n}\n\nexport function isRedirectObject(value: unknown): value is RedirectObject {\n return (\n Buffer.isBuffer(value) ||\n value instanceof Blob ||\n value instanceof Response ||\n typeof value === \"string\"\n );\n}\n"
6
6
  ],
7
- "mappings": ";AA+JO,SAAS,UAAU,CAAC,OAAmC;AAAA,EAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,OAAQ,MAAmB,QAAQ;AAAA;AAWhC,SAAS,gBAAgB,CAAC,OAAyC;AAAA,EACxE,OACE,OAAO,SAAS,KAAK,KACrB,iBAAiB,QACjB,iBAAiB,YACjB,OAAO,UAAU;AAAA;",
7
+ "mappings": ";AAqLO,SAAS,UAAU,CAAC,OAAmC;AAAA,EAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,OAAQ,MAAmB,QAAQ;AAAA;AAWhC,SAAS,gBAAgB,CAAC,OAAyC;AAAA,EACxE,OACE,OAAO,SAAS,KAAK,KACrB,iBAAiB,QACjB,iBAAiB,YACjB,OAAO,UAAU;AAAA;",
8
8
  "debugId": "9C8E04A91CC7802764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,7 +1,7 @@
1
1
  export { ShellDSL, createShellDSL, type Program } from "./shell-dsl.ts";
2
2
  export { ShellPromise, type ShellPromiseOptions } from "./shell-promise.ts";
3
3
  export { ShellSession, createShellSession, type ShellSessionOptions } from "./shell-session.ts";
4
- export type { VirtualFS, VirtualFSWritable, FileStat, Command, CommandContext, Stdin, Stdout, Stderr, OutputCollector, ExecResult, ShellConfig, ShellCommandApi, ShellCommandFallback, ExternalCommandContext, ShellRunOptions, TerminalInfo, ShellInputController, ShellInputSource, ShellExecutionOptions, ShellExecution, ShellOutputEvent, RawValue, } from "./types.ts";
4
+ export type { VirtualFS, VirtualFSWritable, FileStat, Command, CommandCompleter, CommandContext, CompletionContext, CompletionResult, Stdin, Stdout, Stderr, OutputCollector, ExecResult, ShellConfig, ShellCommandApi, ShellCommandFallback, ExternalCommandContext, ShellRunOptions, TerminalInfo, ShellInputController, ShellInputSource, ShellExecutionOptions, ShellExecution, ShellOutputEvent, RawValue, } from "./types.ts";
5
5
  export { isRawValue } from "./types.ts";
6
6
  export { ShellError, LexError, ParseError } from "./errors.ts";
7
7
  export { Lexer, lex, tokenToString } from "./lexer/index.ts";
@@ -1,15 +1,20 @@
1
- import type { Command, ShellCommandFallback, ShellConfig, ShellExecution, ShellExecutionOptions, TerminalInfo, VirtualFS } from "./types.ts";
1
+ import type { Command, CommandCompleter, CompletionResult, ShellCommandFallback, ShellConfig, ShellExecution, ShellExecutionOptions, TerminalInfo, VirtualFS } from "./types.ts";
2
2
  import type { Program } from "./shell-dsl.ts";
3
3
  export interface ShellSessionOptions {
4
4
  fs: VirtualFS;
5
5
  cwd: string;
6
6
  env: Record<string, string>;
7
7
  commands: Record<string, Command>;
8
+ completions?: Record<string, CommandCompleter>;
8
9
  isTTY?: boolean;
9
10
  terminal?: TerminalInfo;
10
11
  externalCommand?: ShellCommandFallback;
11
12
  }
12
13
  export declare class ShellSession {
14
+ private fs;
15
+ private commands;
16
+ private completions;
17
+ private terminal;
13
18
  private interpreter;
14
19
  constructor(options: ShellSessionOptions);
15
20
  run(source: string, options?: ShellExecutionOptions): ShellExecution;
@@ -17,7 +22,9 @@ export declare class ShellSession {
17
22
  getCwd(): string;
18
23
  getEnv(): Record<string, string>;
19
24
  getLastExitCode(): number;
25
+ complete(source: string, cursor?: number): Promise<CompletionResult>;
20
26
  dispose(): Promise<void>;
27
+ private completePath;
21
28
  private createImmediateExecution;
22
29
  }
23
30
  export declare function createShellSession(config: ShellConfig): ShellSession;
@@ -55,6 +55,24 @@ export interface TerminalInfo {
55
55
  rows?: number;
56
56
  colorDepth?: number;
57
57
  }
58
+ export interface CompletionContext {
59
+ source: string;
60
+ cursor: number;
61
+ command: string;
62
+ args: string[];
63
+ word: string;
64
+ wordStart: number;
65
+ wordEnd: number;
66
+ cwd: string;
67
+ env: Record<string, string>;
68
+ fs: VirtualFS;
69
+ terminal: TerminalInfo;
70
+ }
71
+ export interface CompletionResult {
72
+ replacement: string;
73
+ matches: string[];
74
+ }
75
+ export type CommandCompleter = (ctx: CompletionContext) => CompletionResult | Promise<CompletionResult>;
58
76
  export interface CommandContext {
59
77
  args: string[];
60
78
  stdin: Stdin;
@@ -129,6 +147,7 @@ export interface ShellConfig {
129
147
  cwd: string;
130
148
  env: Record<string, string>;
131
149
  commands: Record<string, Command>;
150
+ completions?: Record<string, CommandCompleter>;
132
151
  isTTY?: boolean;
133
152
  terminal?: TerminalInfo;
134
153
  externalCommand?: ShellCommandFallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "A sandboxed shell-style DSL for running scriptable command pipelines in-process without host OS access",
5
5
  "author": "ricsam <oss@ricsam.dev>",
6
6
  "license": "MIT",