shell-dsl 0.0.1 → 0.0.3

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 (133) hide show
  1. package/README.md +572 -28
  2. package/dist/cjs/index.cjs +72 -0
  3. package/dist/cjs/index.cjs.map +10 -0
  4. package/dist/cjs/package.json +5 -0
  5. package/dist/cjs/src/errors.cjs +73 -0
  6. package/dist/cjs/src/errors.cjs.map +10 -0
  7. package/dist/cjs/src/fs/index.cjs +37 -0
  8. package/dist/cjs/src/fs/index.cjs.map +10 -0
  9. package/dist/cjs/src/fs/memfs-adapter.cjs +221 -0
  10. package/dist/cjs/src/fs/memfs-adapter.cjs.map +10 -0
  11. package/dist/cjs/src/index.cjs +80 -0
  12. package/dist/cjs/src/index.cjs.map +10 -0
  13. package/dist/cjs/src/interpreter/context.cjs +47 -0
  14. package/dist/cjs/src/interpreter/context.cjs.map +10 -0
  15. package/dist/cjs/src/interpreter/index.cjs +39 -0
  16. package/dist/cjs/src/interpreter/index.cjs.map +10 -0
  17. package/dist/cjs/src/interpreter/interpreter.cjs +381 -0
  18. package/dist/cjs/src/interpreter/interpreter.cjs.map +10 -0
  19. package/dist/cjs/src/io/index.cjs +44 -0
  20. package/dist/cjs/src/io/index.cjs.map +10 -0
  21. package/dist/cjs/src/io/stdin.cjs +99 -0
  22. package/dist/cjs/src/io/stdin.cjs.map +10 -0
  23. package/dist/cjs/src/io/stdout.cjs +203 -0
  24. package/dist/cjs/src/io/stdout.cjs.map +10 -0
  25. package/dist/cjs/src/lexer/index.cjs +40 -0
  26. package/dist/cjs/src/lexer/index.cjs.map +10 -0
  27. package/dist/cjs/src/lexer/lexer.cjs +335 -0
  28. package/dist/cjs/src/lexer/lexer.cjs.map +10 -0
  29. package/dist/cjs/src/lexer/tokens.cjs +66 -0
  30. package/dist/cjs/src/lexer/tokens.cjs.map +10 -0
  31. package/dist/cjs/src/parser/ast.cjs +75 -0
  32. package/dist/cjs/src/parser/ast.cjs.map +10 -0
  33. package/dist/cjs/src/parser/index.cjs +49 -0
  34. package/dist/cjs/src/parser/index.cjs.map +10 -0
  35. package/dist/cjs/src/parser/parser.cjs +216 -0
  36. package/dist/cjs/src/parser/parser.cjs.map +10 -0
  37. package/dist/cjs/src/shell-dsl.cjs +187 -0
  38. package/dist/cjs/src/shell-dsl.cjs.map +10 -0
  39. package/dist/cjs/src/shell-promise.cjs +159 -0
  40. package/dist/cjs/src/shell-promise.cjs.map +10 -0
  41. package/dist/cjs/src/types.cjs +43 -0
  42. package/dist/cjs/src/types.cjs.map +10 -0
  43. package/dist/cjs/src/utils/escape.cjs +53 -0
  44. package/dist/cjs/src/utils/escape.cjs.map +10 -0
  45. package/dist/cjs/src/utils/index.cjs +38 -0
  46. package/dist/cjs/src/utils/index.cjs.map +10 -0
  47. package/dist/mjs/index.mjs +42 -0
  48. package/dist/mjs/index.mjs.map +10 -0
  49. package/dist/mjs/package.json +5 -0
  50. package/dist/mjs/src/errors.mjs +42 -0
  51. package/dist/mjs/src/errors.mjs.map +10 -0
  52. package/dist/mjs/src/fs/index.mjs +7 -0
  53. package/dist/mjs/src/fs/index.mjs.map +10 -0
  54. package/dist/mjs/src/fs/memfs-adapter.mjs +178 -0
  55. package/dist/mjs/src/fs/memfs-adapter.mjs.map +10 -0
  56. package/dist/mjs/src/index.mjs +61 -0
  57. package/dist/mjs/src/index.mjs.map +10 -0
  58. package/dist/mjs/src/interpreter/context.mjs +17 -0
  59. package/dist/mjs/src/interpreter/context.mjs.map +10 -0
  60. package/dist/mjs/src/interpreter/index.mjs +9 -0
  61. package/dist/mjs/src/interpreter/index.mjs.map +10 -0
  62. package/dist/mjs/src/interpreter/interpreter.mjs +351 -0
  63. package/dist/mjs/src/interpreter/interpreter.mjs.map +10 -0
  64. package/dist/mjs/src/io/index.mjs +14 -0
  65. package/dist/mjs/src/io/index.mjs.map +10 -0
  66. package/dist/mjs/src/io/stdin.mjs +68 -0
  67. package/dist/mjs/src/io/stdin.mjs.map +10 -0
  68. package/dist/mjs/src/io/stdout.mjs +172 -0
  69. package/dist/mjs/src/io/stdout.mjs.map +10 -0
  70. package/dist/mjs/src/lexer/index.mjs +10 -0
  71. package/dist/mjs/src/lexer/index.mjs.map +10 -0
  72. package/dist/mjs/src/lexer/lexer.mjs +305 -0
  73. package/dist/mjs/src/lexer/lexer.mjs.map +10 -0
  74. package/dist/mjs/src/lexer/tokens.mjs +36 -0
  75. package/dist/mjs/src/lexer/tokens.mjs.map +10 -0
  76. package/dist/mjs/src/parser/ast.mjs +45 -0
  77. package/dist/mjs/src/parser/ast.mjs.map +10 -0
  78. package/dist/mjs/src/parser/index.mjs +30 -0
  79. package/dist/mjs/src/parser/index.mjs.map +10 -0
  80. package/dist/mjs/src/parser/parser.mjs +189 -0
  81. package/dist/mjs/src/parser/parser.mjs.map +10 -0
  82. package/dist/mjs/src/shell-dsl.mjs +157 -0
  83. package/dist/mjs/src/shell-dsl.mjs.map +10 -0
  84. package/dist/mjs/src/shell-promise.mjs +129 -0
  85. package/dist/mjs/src/shell-promise.mjs.map +10 -0
  86. package/dist/mjs/src/types.mjs +13 -0
  87. package/dist/mjs/src/types.mjs.map +10 -0
  88. package/dist/mjs/src/utils/escape.mjs +23 -0
  89. package/dist/mjs/src/utils/escape.mjs.map +10 -0
  90. package/dist/mjs/src/utils/index.mjs +8 -0
  91. package/dist/mjs/src/utils/index.mjs.map +10 -0
  92. package/dist/types/commands/cat.d.ts +2 -0
  93. package/dist/types/commands/cp.d.ts +2 -0
  94. package/dist/types/commands/echo.d.ts +2 -0
  95. package/dist/types/commands/grep.d.ts +2 -0
  96. package/dist/types/commands/head.d.ts +2 -0
  97. package/dist/types/commands/index.d.ts +20 -0
  98. package/dist/types/commands/ls.d.ts +2 -0
  99. package/dist/types/commands/mkdir.d.ts +2 -0
  100. package/dist/types/commands/mv.d.ts +2 -0
  101. package/dist/types/commands/pwd.d.ts +2 -0
  102. package/dist/types/commands/rm.d.ts +2 -0
  103. package/dist/types/commands/sort.d.ts +2 -0
  104. package/dist/types/commands/tail.d.ts +2 -0
  105. package/dist/types/commands/tee.d.ts +2 -0
  106. package/dist/types/commands/test.d.ts +3 -0
  107. package/dist/types/commands/touch.d.ts +2 -0
  108. package/dist/types/commands/true-false.d.ts +3 -0
  109. package/dist/types/commands/uniq.d.ts +2 -0
  110. package/dist/types/commands/wc.d.ts +2 -0
  111. package/dist/types/index.d.ts +3 -0
  112. package/dist/types/src/errors.d.ts +16 -0
  113. package/dist/types/src/fs/index.d.ts +1 -0
  114. package/dist/types/src/fs/memfs-adapter.d.ts +3 -0
  115. package/dist/types/src/index.d.ts +15 -0
  116. package/dist/types/src/interpreter/context.d.ts +11 -0
  117. package/dist/types/src/interpreter/index.d.ts +2 -0
  118. package/dist/types/src/interpreter/interpreter.d.ts +32 -0
  119. package/dist/types/src/io/index.d.ts +2 -0
  120. package/dist/types/src/io/stdin.d.ts +11 -0
  121. package/dist/types/src/io/stdout.d.ts +40 -0
  122. package/dist/types/src/lexer/index.d.ts +3 -0
  123. package/dist/types/src/lexer/lexer.d.ts +24 -0
  124. package/dist/types/src/lexer/tokens.d.ts +38 -0
  125. package/dist/types/src/parser/ast.d.ts +64 -0
  126. package/dist/types/src/parser/index.d.ts +3 -0
  127. package/dist/types/src/parser/parser.d.ts +23 -0
  128. package/dist/types/src/shell-dsl.d.ts +32 -0
  129. package/dist/types/src/shell-promise.d.ts +39 -0
  130. package/dist/types/src/types.d.ts +76 -0
  131. package/dist/types/src/utils/escape.d.ts +2 -0
  132. package/dist/types/src/utils/index.d.ts +1 -0
  133. package/package.json +46 -6
package/README.md CHANGED
@@ -1,45 +1,589 @@
1
1
  # shell-dsl
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ A sandboxed shell-style DSL for running scriptable command pipelines where all commands are explicitly registered and executed in-process, without access to the host OS.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ```ts
6
+ import { createShellDSL, createVirtualFS } from "shell-dsl";
7
+ import { createFsFromVolume, Volume } from "memfs";
8
+ import { builtinCommands } from "shell-dsl/commands";
6
9
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
10
+ const vol = new Volume();
11
+ vol.fromJSON({ "/data.txt": "foo\nbar\nbaz\n" });
8
12
 
9
- ## Purpose
13
+ const sh = createShellDSL({
14
+ fs: createVirtualFS(createFsFromVolume(vol)),
15
+ cwd: "/",
16
+ env: { USER: "alice" },
17
+ commands: builtinCommands,
18
+ });
10
19
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `shell-dsl`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
20
+ const count = await sh`cat /data.txt | grep foo | wc -l`.text();
21
+ console.log(count.trim()); // "1"
22
+ ```
15
23
 
16
- ## What is OIDC Trusted Publishing?
24
+ ## Installation
17
25
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
26
+ ```bash
27
+ bun add shell-dsl memfs
28
+ ```
19
29
 
20
- ## Setup Instructions
30
+ ## Features
21
31
 
22
- To properly configure OIDC trusted publishing for this package:
32
+ - **Sandboxed execution** No host OS access; all commands run in-process
33
+ - **Virtual filesystem** — Uses memfs for complete isolation from the real filesystem
34
+ - **Explicit command registry** — Only registered commands can execute
35
+ - **Automatic escaping** — Interpolated values are escaped by default for safety
36
+ - **POSIX-inspired syntax** — Pipes, redirects, control flow operators, and more
37
+ - **Streaming pipelines** — Commands communicate via async iteration
38
+ - **TypeScript-first** — Full type definitions included
23
39
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
40
+ ## Getting Started
28
41
 
29
- ## DO NOT USE THIS PACKAGE
42
+ Create a `ShellDSL` instance by providing a virtual filesystem, working directory, environment variables, and a command registry:
30
43
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
44
+ ```ts
45
+ import { createShellDSL, createVirtualFS } from "shell-dsl";
46
+ import { createFsFromVolume, Volume } from "memfs";
47
+ import { builtinCommands } from "shell-dsl/commands";
36
48
 
37
- ## More Information
49
+ const vol = new Volume();
50
+ const sh = createShellDSL({
51
+ fs: createVirtualFS(createFsFromVolume(vol)),
52
+ cwd: "/",
53
+ env: { USER: "alice", HOME: "/home/alice" },
54
+ commands: builtinCommands,
55
+ });
38
56
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
57
+ const greeting = await sh`echo "Hello, $USER"`.text();
58
+ console.log(greeting); // "Hello, alice\n"
59
+ ```
42
60
 
43
- ---
61
+ ## Output Methods
44
62
 
45
- **Maintained for OIDC setup purposes only**
63
+ Every shell command returns a `ShellPromise` that can be consumed in different formats:
64
+
65
+ ```ts
66
+ // String output
67
+ await sh`echo hello`.text(); // "hello\n"
68
+
69
+ // Parsed JSON
70
+ await sh`cat config.json`.json(); // { key: "value" }
71
+
72
+ // Async line iterator
73
+ for await (const line of sh`cat data.txt`.lines()) {
74
+ console.log(line);
75
+ }
76
+
77
+ // Raw Buffer
78
+ await sh`cat binary.dat`.buffer(); // Buffer
79
+
80
+ // Blob
81
+ await sh`cat image.png`.blob(); // Blob
82
+ ```
83
+
84
+ ## Error Handling
85
+
86
+ By default, commands with non-zero exit codes throw a `ShellError`:
87
+
88
+ ```ts
89
+ import { ShellError } from "shell-dsl";
90
+
91
+ try {
92
+ await sh`cat /nonexistent`;
93
+ } catch (err) {
94
+ if (err instanceof ShellError) {
95
+ console.log(err.exitCode); // 1
96
+ console.log(err.stderr.toString()); // "cat: /nonexistent: ..."
97
+ console.log(err.stdout.toString()); // ""
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Disabling Throws
103
+
104
+ Use `.nothrow()` to suppress throwing for a single command:
105
+
106
+ ```ts
107
+ const result = await sh`cat /nonexistent`.nothrow();
108
+ console.log(result.exitCode); // 1
109
+ ```
110
+
111
+ Use `.throws(boolean)` for explicit control:
112
+
113
+ ```ts
114
+ const result = await sh`cat /nonexistent`.throws(false);
115
+ ```
116
+
117
+ ### Global Throw Setting
118
+
119
+ Disable throwing globally with `sh.throws(false)`:
120
+
121
+ ```ts
122
+ sh.throws(false);
123
+ const result = await sh`cat /nonexistent`;
124
+ console.log(result.exitCode); // 1
125
+
126
+ // Per-command override still works
127
+ await sh`cat /nonexistent`.throws(true); // This throws
128
+ ```
129
+
130
+ ## Piping
131
+
132
+ Use `|` to connect commands. Data flows between commands via async streams:
133
+
134
+ ```ts
135
+ const result = await sh`cat /data.txt | grep pattern | wc -l`.text();
136
+ ```
137
+
138
+ Each command in the pipeline receives the previous command's stdout as its stdin.
139
+
140
+ ## Control Flow Operators
141
+
142
+ ### Sequential Execution (`;`)
143
+
144
+ Run commands one after another, regardless of exit codes:
145
+
146
+ ```ts
147
+ await sh`echo one; echo two; echo three`.text();
148
+ // "one\ntwo\nthree\n"
149
+ ```
150
+
151
+ ### AND Operator (`&&`)
152
+
153
+ Run the next command only if the previous one succeeds (exit code 0):
154
+
155
+ ```ts
156
+ await sh`test -f /config.json && cat /config.json`;
157
+ ```
158
+
159
+ ### OR Operator (`||`)
160
+
161
+ Run the next command only if the previous one fails (non-zero exit code):
162
+
163
+ ```ts
164
+ await sh`cat /config.json || echo "default config"`;
165
+ ```
166
+
167
+ ### Combined Operators
168
+
169
+ ```ts
170
+ await sh`mkdir -p /out && echo "created" || echo "failed"`;
171
+ ```
172
+
173
+ ## Redirection
174
+
175
+ ### Input Redirection (`<`)
176
+
177
+ Read stdin from a file:
178
+
179
+ ```ts
180
+ await sh`cat < /input.txt`.text();
181
+ ```
182
+
183
+ ### Output Redirection (`>`, `>>`)
184
+
185
+ Write stdout to a file:
186
+
187
+ ```ts
188
+ // Overwrite
189
+ await sh`echo "content" > /output.txt`;
190
+
191
+ // Append
192
+ await sh`echo "more" >> /output.txt`;
193
+ ```
194
+
195
+ ### Stderr Redirection (`2>`, `2>>`)
196
+
197
+ ```ts
198
+ await sh`cmd 2> /errors.txt`; // stderr to file
199
+ await sh`cmd 2>> /errors.txt`; // append stderr
200
+ ```
201
+
202
+ ### File Descriptor Redirects
203
+
204
+ | Redirect | Effect |
205
+ |----------|--------|
206
+ | `2>&1` | Redirect stderr to stdout |
207
+ | `1>&2` | Redirect stdout to stderr |
208
+ | `&>` | Redirect both stdout and stderr to file |
209
+ | `&>>` | Append both stdout and stderr to file |
210
+
211
+ ```ts
212
+ // Capture both stdout and stderr
213
+ const result = await sh`cmd 2>&1`.text();
214
+
215
+ // Write both to file
216
+ await sh`cmd &> /all-output.txt`;
217
+ ```
218
+
219
+ ## Environment Variables
220
+
221
+ ### Variable Expansion
222
+
223
+ Variables are expanded with `$VAR` or `${VAR}` syntax:
224
+
225
+ ```ts
226
+ const sh = createShellDSL({
227
+ // ...
228
+ env: { USER: "alice", HOME: "/home/alice" },
229
+ });
230
+
231
+ await sh`echo $USER`.text(); // "alice\n"
232
+ await sh`echo "Home: $HOME"`.text(); // "Home: /home/alice\n"
233
+ ```
234
+
235
+ ### Quoting Semantics
236
+
237
+ | Quote | Behavior |
238
+ |-------|----------|
239
+ | `"..."` | Variables expanded, special chars preserved |
240
+ | `'...'` | Literal string, no expansion |
241
+
242
+ ```ts
243
+ await sh`echo "Hello $USER"`.text(); // "Hello alice\n"
244
+ await sh`echo 'Hello $USER'`.text(); // "Hello $USER\n"
245
+ ```
246
+
247
+ ### Inline Assignment
248
+
249
+ Assign variables for subsequent commands:
250
+
251
+ ```ts
252
+ await sh`FOO=bar && echo $FOO`.text(); // "bar\n"
253
+ ```
254
+
255
+ Assign variables for a single command (scoped):
256
+
257
+ ```ts
258
+ await sh`FOO=bar echo $FOO`.text(); // "bar\n"
259
+ // FOO is not set after this command
260
+ ```
261
+
262
+ ### Per-Command Environment
263
+
264
+ Override environment for a single command:
265
+
266
+ ```ts
267
+ await sh`echo $CUSTOM`.env({ CUSTOM: "value" }).text();
268
+ ```
269
+
270
+ ### Global Environment
271
+
272
+ Set environment variables globally:
273
+
274
+ ```ts
275
+ sh.env({ API_KEY: "secret" });
276
+ await sh`echo $API_KEY`.text(); // "secret\n"
277
+
278
+ sh.resetEnv(); // Restore initial environment
279
+ ```
280
+
281
+ ## Glob Expansion
282
+
283
+ Globs are expanded by the interpreter before command execution:
284
+
285
+ ```ts
286
+ await sh`ls *.txt`; // Matches: a.txt, b.txt, ...
287
+ await sh`cat src/**/*.ts`; // Recursive glob
288
+ await sh`echo file[123].txt`; // Character classes
289
+ await sh`echo {a,b,c}.txt`; // Brace expansion: a.txt b.txt c.txt
290
+ ```
291
+
292
+ ## Command Substitution
293
+
294
+ Use `$(command)` to capture command output:
295
+
296
+ ```ts
297
+ await sh`echo "Current dir: $(pwd)"`.text();
298
+ ```
299
+
300
+ Nested substitution is supported:
301
+
302
+ ```ts
303
+ await sh`echo "Files: $(ls $(pwd))"`.text();
304
+ ```
305
+
306
+ ## Defining Custom Commands
307
+
308
+ Commands are async functions that receive a `CommandContext` and return an exit code (0 = success):
309
+
310
+ ```ts
311
+ import type { Command } from "shell-dsl";
312
+
313
+ const hello: Command = async (ctx) => {
314
+ const name = ctx.args[0] ?? "World";
315
+ await ctx.stdout.writeText(`Hello, ${name}!\n`);
316
+ return 0;
317
+ };
318
+
319
+ const sh = createShellDSL({
320
+ // ...
321
+ commands: { ...builtinCommands, hello },
322
+ });
323
+
324
+ await sh`hello Alice`.text(); // "Hello, Alice!\n"
325
+ ```
326
+
327
+ ### CommandContext Interface
328
+
329
+ ```ts
330
+ interface CommandContext {
331
+ args: string[]; // Command arguments
332
+ stdin: Stdin; // Input stream
333
+ stdout: Stdout; // Output stream
334
+ stderr: Stderr; // Error stream
335
+ fs: VirtualFS; // Virtual filesystem
336
+ cwd: string; // Current working directory
337
+ env: Record<string, string>; // Environment variables
338
+ }
339
+ ```
340
+
341
+ ### Stdin Interface
342
+
343
+ ```ts
344
+ interface Stdin {
345
+ stream(): AsyncIterable<Uint8Array>; // Raw byte stream
346
+ buffer(): Promise<Buffer>; // All input as Buffer
347
+ text(): Promise<string>; // All input as string
348
+ lines(): AsyncIterable<string>; // Line-by-line iterator
349
+ }
350
+ ```
351
+
352
+ ### Stdout/Stderr Interface
353
+
354
+ ```ts
355
+ interface Stdout {
356
+ write(chunk: Uint8Array): Promise<void>; // Write bytes
357
+ writeText(str: string): Promise<void>; // Write UTF-8 string
358
+ }
359
+ ```
360
+
361
+ ### Example: echo
362
+
363
+ ```ts
364
+ const echo: Command = async (ctx) => {
365
+ await ctx.stdout.writeText(ctx.args.join(" ") + "\n");
366
+ return 0;
367
+ };
368
+ ```
369
+
370
+ ### Example: cat
371
+
372
+ Read from stdin or files:
373
+
374
+ ```ts
375
+ const cat: Command = async (ctx) => {
376
+ if (ctx.args.length === 0) {
377
+ // Read from stdin
378
+ for await (const chunk of ctx.stdin.stream()) {
379
+ await ctx.stdout.write(chunk);
380
+ }
381
+ } else {
382
+ // Read from files
383
+ for (const file of ctx.args) {
384
+ const path = ctx.fs.resolve(ctx.cwd, file);
385
+ const content = await ctx.fs.readFile(path);
386
+ await ctx.stdout.write(new Uint8Array(content));
387
+ }
388
+ }
389
+ return 0;
390
+ };
391
+ ```
392
+
393
+ ### Example: grep
394
+
395
+ Pattern matching with stdin:
396
+
397
+ ```ts
398
+ const grep: Command = async (ctx) => {
399
+ const pattern = ctx.args[0];
400
+ if (!pattern) {
401
+ await ctx.stderr.writeText("grep: missing pattern\n");
402
+ return 1;
403
+ }
404
+
405
+ const regex = new RegExp(pattern);
406
+ let found = false;
407
+
408
+ for await (const line of ctx.stdin.lines()) {
409
+ if (regex.test(line)) {
410
+ await ctx.stdout.writeText(line + "\n");
411
+ found = true;
412
+ }
413
+ }
414
+
415
+ return found ? 0 : 1;
416
+ };
417
+ ```
418
+
419
+ ### Example: Custom uppercase command
420
+
421
+ ```ts
422
+ const upper: Command = async (ctx) => {
423
+ const text = await ctx.stdin.text();
424
+ await ctx.stdout.writeText(text.toUpperCase());
425
+ return 0;
426
+ };
427
+
428
+ // Usage
429
+ await sh`echo "hello" | upper`.text(); // "HELLO\n"
430
+ ```
431
+
432
+ ## Built-in Commands
433
+
434
+ Import all built-in commands:
435
+
436
+ ```ts
437
+ import { builtinCommands } from "shell-dsl/commands";
438
+ ```
439
+
440
+ Or import individually:
441
+
442
+ ```ts
443
+ import { echo, cat, grep, wc, cp, mv, touch, tee } from "shell-dsl/commands";
444
+ ```
445
+
446
+ | Command | Description |
447
+ |---------|-------------|
448
+ | `echo` | Print arguments to stdout |
449
+ | `cat` | Concatenate files or stdin to stdout |
450
+ | `grep` | Linux-compatible pattern search |
451
+ | `wc` | Count lines, words, or characters (`-l`, `-w`, `-c`) |
452
+ | `head` | Output first lines (`-n`) |
453
+ | `tail` | Output last lines (`-n`) |
454
+ | `sort` | Sort lines (`-r` reverse, `-n` numeric) |
455
+ | `uniq` | Remove duplicate adjacent lines (`-c` count) |
456
+ | `pwd` | Print working directory |
457
+ | `ls` | List directory contents |
458
+ | `mkdir` | Create directories (`-p` parents) |
459
+ | `rm` | Remove files/directories (`-r` recursive, `-f` force) |
460
+ | `cp` | Copy files/directories (`-r` recursive, `-n` no-clobber) |
461
+ | `mv` | Move/rename files/directories (`-n` no-clobber) |
462
+ | `touch` | Create empty files or update timestamps (`-c` no-create) |
463
+ | `tee` | Duplicate stdin to stdout and files (`-a` append) |
464
+ | `test` / `[` | File and string tests (`-f`, `-d`, `-e`, `-z`, `-n`, `=`, `!=`) |
465
+ | `true` | Exit with code 0 |
466
+ | `false` | Exit with code 1 |
467
+
468
+ ## Virtual Filesystem
469
+
470
+ The `VirtualFS` interface wraps memfs for sandboxed file operations:
471
+
472
+ ```ts
473
+ import { createVirtualFS } from "shell-dsl";
474
+ import { createFsFromVolume, Volume } from "memfs";
475
+
476
+ const vol = new Volume();
477
+ vol.fromJSON({
478
+ "/data.txt": "file content",
479
+ "/config.json": '{"key": "value"}',
480
+ });
481
+
482
+ const fs = createVirtualFS(createFsFromVolume(vol));
483
+ ```
484
+
485
+ ### VirtualFS Interface
486
+
487
+ ```ts
488
+ interface VirtualFS {
489
+ // Reading
490
+ readFile(path: string): Promise<Buffer>;
491
+ readdir(path: string): Promise<string[]>;
492
+ stat(path: string): Promise<FileStat>;
493
+ exists(path: string): Promise<boolean>;
494
+
495
+ // Writing
496
+ writeFile(path: string, data: Buffer | string): Promise<void>;
497
+ appendFile(path: string, data: Buffer | string): Promise<void>;
498
+ mkdir(path: string, opts?: { recursive?: boolean }): Promise<void>;
499
+
500
+ // Deletion
501
+ rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;
502
+
503
+ // Utilities
504
+ resolve(...paths: string[]): string;
505
+ dirname(path: string): string;
506
+ basename(path: string): string;
507
+ glob(pattern: string, opts?: { cwd?: string }): Promise<string[]>;
508
+ }
509
+ ```
510
+
511
+ ## Low-Level API
512
+
513
+ For advanced use cases (custom tooling, AST inspection):
514
+
515
+ ```ts
516
+ // Tokenize shell source
517
+ const tokens = sh.lex("cat foo | grep bar");
518
+
519
+ // Parse tokens into AST
520
+ const ast = sh.parse(tokens);
521
+
522
+ // Compile AST to executable program
523
+ const program = sh.compile(ast);
524
+
525
+ // Execute a compiled program
526
+ const result = await sh.run(program);
527
+ ```
528
+
529
+ ### Manual Escaping
530
+
531
+ ```ts
532
+ sh.escape("hello world"); // "'hello world'"
533
+ sh.escape("$(rm -rf /)"); // "'$(rm -rf /)'"
534
+ sh.escape("safe"); // "safe"
535
+ ```
536
+
537
+ ### Raw Escape Hatch
538
+
539
+ Bypass escaping for trusted input:
540
+
541
+ ```ts
542
+ await sh`echo ${{ raw: "$(date)" }}`.text();
543
+ ```
544
+
545
+ **Warning:** Use `{ raw: ... }` with extreme caution when handling untrusted input.
546
+
547
+ ## Safety & Security
548
+
549
+ 1. **No host access** — All commands run in-process against a virtual filesystem
550
+ 2. **Automatic escaping** — Interpolated values are escaped by default
551
+ 3. **Explicit command registry** — Only registered commands can execute
552
+ 4. **No shell spawning** — Never invokes `/bin/sh` or similar
553
+
554
+ The `{ raw: ... }` escape hatch exists for advanced use cases but should be used with extreme caution.
555
+
556
+ ## TypeScript Types
557
+
558
+ Key exported types:
559
+
560
+ ```ts
561
+ import type {
562
+ Command,
563
+ CommandContext,
564
+ Stdin,
565
+ Stdout,
566
+ Stderr,
567
+ VirtualFS,
568
+ FileStat,
569
+ ExecResult,
570
+ ShellConfig,
571
+ RawValue,
572
+ } from "shell-dsl";
573
+ ```
574
+
575
+ ## Running Tests
576
+
577
+ ```bash
578
+ bun test
579
+ ```
580
+
581
+ ## Typecheck
582
+
583
+ ```bash
584
+ bun run typecheck
585
+ ```
586
+
587
+ ## License
588
+
589
+ MIT
@@ -0,0 +1,72 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __reExport = (target, mod, secondTarget) => {
6
+ for (let key of __getOwnPropNames(mod))
7
+ if (!__hasOwnProp.call(target, key) && key !== "default")
8
+ __defProp(target, key, {
9
+ get: () => mod[key],
10
+ enumerable: true
11
+ });
12
+ if (secondTarget) {
13
+ for (let key of __getOwnPropNames(mod))
14
+ if (!__hasOwnProp.call(secondTarget, key) && key !== "default")
15
+ __defProp(secondTarget, key, {
16
+ get: () => mod[key],
17
+ enumerable: true
18
+ });
19
+ return secondTarget;
20
+ }
21
+ };
22
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
23
+ var __toCommonJS = (from) => {
24
+ var entry = __moduleCache.get(from), desc;
25
+ if (entry)
26
+ return entry;
27
+ entry = __defProp({}, "__esModule", { value: true });
28
+ if (from && typeof from === "object" || typeof from === "function")
29
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
30
+ get: () => from[key],
31
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
32
+ }));
33
+ __moduleCache.set(from, entry);
34
+ return entry;
35
+ };
36
+ var __export = (target, all) => {
37
+ for (var name in all)
38
+ __defProp(target, name, {
39
+ get: all[name],
40
+ enumerable: true,
41
+ configurable: true,
42
+ set: (newValue) => all[name] = () => newValue
43
+ });
44
+ };
45
+
46
+ // index.ts
47
+ var exports_shell_dsl = {};
48
+ __export(exports_shell_dsl, {
49
+ wc: () => import_commands2.wc,
50
+ uniq: () => import_commands2.uniq,
51
+ trueCmd: () => import_commands2.trueCmd,
52
+ test: () => import_commands2.test,
53
+ tail: () => import_commands2.tail,
54
+ sort: () => import_commands2.sort,
55
+ rm: () => import_commands2.rm,
56
+ pwd: () => import_commands2.pwd,
57
+ mkdir: () => import_commands2.mkdir,
58
+ ls: () => import_commands2.ls,
59
+ head: () => import_commands2.head,
60
+ grep: () => import_commands2.grep,
61
+ falseCmd: () => import_commands2.falseCmd,
62
+ echo: () => import_commands2.echo,
63
+ cat: () => import_commands2.cat,
64
+ builtinCommands: () => import_commands.builtinCommands,
65
+ bracket: () => import_commands2.bracket
66
+ });
67
+ module.exports = __toCommonJS(exports_shell_dsl);
68
+ __reExport(exports_shell_dsl, require("./src/index.cjs"), module.exports);
69
+ var import_commands = require("./commands/index.cjs");
70
+ var import_commands2 = require("./commands/index.cjs");
71
+
72
+ //# debugId=01F96ADDEEC727E964756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../index.ts"],
4
+ "sourcesContent": [
5
+ "// Re-export everything from src\nexport * from \"./src/index.cjs\";\n\n// Re-export built-in commands\nexport { builtinCommands } from \"./commands/index.cjs\";\nexport {\n echo,\n cat,\n grep,\n wc,\n head,\n tail,\n sort,\n uniq,\n pwd,\n ls,\n mkdir,\n rm,\n test,\n bracket,\n trueCmd,\n falseCmd,\n} from \"./commands/index.cjs\";\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA;AAGgC,IAAhC;AAkBO,IAjBP;",
8
+ "debugId": "01F96ADDEEC727E964756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "shell-dsl",
3
+ "version": "0.0.3",
4
+ "type": "commonjs"
5
+ }