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.
- package/README.md +572 -28
- package/dist/cjs/index.cjs +72 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/cjs/src/errors.cjs +73 -0
- package/dist/cjs/src/errors.cjs.map +10 -0
- package/dist/cjs/src/fs/index.cjs +37 -0
- package/dist/cjs/src/fs/index.cjs.map +10 -0
- package/dist/cjs/src/fs/memfs-adapter.cjs +221 -0
- package/dist/cjs/src/fs/memfs-adapter.cjs.map +10 -0
- package/dist/cjs/src/index.cjs +80 -0
- package/dist/cjs/src/index.cjs.map +10 -0
- package/dist/cjs/src/interpreter/context.cjs +47 -0
- package/dist/cjs/src/interpreter/context.cjs.map +10 -0
- package/dist/cjs/src/interpreter/index.cjs +39 -0
- package/dist/cjs/src/interpreter/index.cjs.map +10 -0
- package/dist/cjs/src/interpreter/interpreter.cjs +381 -0
- package/dist/cjs/src/interpreter/interpreter.cjs.map +10 -0
- package/dist/cjs/src/io/index.cjs +44 -0
- package/dist/cjs/src/io/index.cjs.map +10 -0
- package/dist/cjs/src/io/stdin.cjs +99 -0
- package/dist/cjs/src/io/stdin.cjs.map +10 -0
- package/dist/cjs/src/io/stdout.cjs +203 -0
- package/dist/cjs/src/io/stdout.cjs.map +10 -0
- package/dist/cjs/src/lexer/index.cjs +40 -0
- package/dist/cjs/src/lexer/index.cjs.map +10 -0
- package/dist/cjs/src/lexer/lexer.cjs +335 -0
- package/dist/cjs/src/lexer/lexer.cjs.map +10 -0
- package/dist/cjs/src/lexer/tokens.cjs +66 -0
- package/dist/cjs/src/lexer/tokens.cjs.map +10 -0
- package/dist/cjs/src/parser/ast.cjs +75 -0
- package/dist/cjs/src/parser/ast.cjs.map +10 -0
- package/dist/cjs/src/parser/index.cjs +49 -0
- package/dist/cjs/src/parser/index.cjs.map +10 -0
- package/dist/cjs/src/parser/parser.cjs +216 -0
- package/dist/cjs/src/parser/parser.cjs.map +10 -0
- package/dist/cjs/src/shell-dsl.cjs +187 -0
- package/dist/cjs/src/shell-dsl.cjs.map +10 -0
- package/dist/cjs/src/shell-promise.cjs +159 -0
- package/dist/cjs/src/shell-promise.cjs.map +10 -0
- package/dist/cjs/src/types.cjs +43 -0
- package/dist/cjs/src/types.cjs.map +10 -0
- package/dist/cjs/src/utils/escape.cjs +53 -0
- package/dist/cjs/src/utils/escape.cjs.map +10 -0
- package/dist/cjs/src/utils/index.cjs +38 -0
- package/dist/cjs/src/utils/index.cjs.map +10 -0
- package/dist/mjs/index.mjs +42 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/mjs/src/errors.mjs +42 -0
- package/dist/mjs/src/errors.mjs.map +10 -0
- package/dist/mjs/src/fs/index.mjs +7 -0
- package/dist/mjs/src/fs/index.mjs.map +10 -0
- package/dist/mjs/src/fs/memfs-adapter.mjs +178 -0
- package/dist/mjs/src/fs/memfs-adapter.mjs.map +10 -0
- package/dist/mjs/src/index.mjs +61 -0
- package/dist/mjs/src/index.mjs.map +10 -0
- package/dist/mjs/src/interpreter/context.mjs +17 -0
- package/dist/mjs/src/interpreter/context.mjs.map +10 -0
- package/dist/mjs/src/interpreter/index.mjs +9 -0
- package/dist/mjs/src/interpreter/index.mjs.map +10 -0
- package/dist/mjs/src/interpreter/interpreter.mjs +351 -0
- package/dist/mjs/src/interpreter/interpreter.mjs.map +10 -0
- package/dist/mjs/src/io/index.mjs +14 -0
- package/dist/mjs/src/io/index.mjs.map +10 -0
- package/dist/mjs/src/io/stdin.mjs +68 -0
- package/dist/mjs/src/io/stdin.mjs.map +10 -0
- package/dist/mjs/src/io/stdout.mjs +172 -0
- package/dist/mjs/src/io/stdout.mjs.map +10 -0
- package/dist/mjs/src/lexer/index.mjs +10 -0
- package/dist/mjs/src/lexer/index.mjs.map +10 -0
- package/dist/mjs/src/lexer/lexer.mjs +305 -0
- package/dist/mjs/src/lexer/lexer.mjs.map +10 -0
- package/dist/mjs/src/lexer/tokens.mjs +36 -0
- package/dist/mjs/src/lexer/tokens.mjs.map +10 -0
- package/dist/mjs/src/parser/ast.mjs +45 -0
- package/dist/mjs/src/parser/ast.mjs.map +10 -0
- package/dist/mjs/src/parser/index.mjs +30 -0
- package/dist/mjs/src/parser/index.mjs.map +10 -0
- package/dist/mjs/src/parser/parser.mjs +189 -0
- package/dist/mjs/src/parser/parser.mjs.map +10 -0
- package/dist/mjs/src/shell-dsl.mjs +157 -0
- package/dist/mjs/src/shell-dsl.mjs.map +10 -0
- package/dist/mjs/src/shell-promise.mjs +129 -0
- package/dist/mjs/src/shell-promise.mjs.map +10 -0
- package/dist/mjs/src/types.mjs +13 -0
- package/dist/mjs/src/types.mjs.map +10 -0
- package/dist/mjs/src/utils/escape.mjs +23 -0
- package/dist/mjs/src/utils/escape.mjs.map +10 -0
- package/dist/mjs/src/utils/index.mjs +8 -0
- package/dist/mjs/src/utils/index.mjs.map +10 -0
- package/dist/types/commands/cat.d.ts +2 -0
- package/dist/types/commands/cp.d.ts +2 -0
- package/dist/types/commands/echo.d.ts +2 -0
- package/dist/types/commands/grep.d.ts +2 -0
- package/dist/types/commands/head.d.ts +2 -0
- package/dist/types/commands/index.d.ts +20 -0
- package/dist/types/commands/ls.d.ts +2 -0
- package/dist/types/commands/mkdir.d.ts +2 -0
- package/dist/types/commands/mv.d.ts +2 -0
- package/dist/types/commands/pwd.d.ts +2 -0
- package/dist/types/commands/rm.d.ts +2 -0
- package/dist/types/commands/sort.d.ts +2 -0
- package/dist/types/commands/tail.d.ts +2 -0
- package/dist/types/commands/tee.d.ts +2 -0
- package/dist/types/commands/test.d.ts +3 -0
- package/dist/types/commands/touch.d.ts +2 -0
- package/dist/types/commands/true-false.d.ts +3 -0
- package/dist/types/commands/uniq.d.ts +2 -0
- package/dist/types/commands/wc.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/src/errors.d.ts +16 -0
- package/dist/types/src/fs/index.d.ts +1 -0
- package/dist/types/src/fs/memfs-adapter.d.ts +3 -0
- package/dist/types/src/index.d.ts +15 -0
- package/dist/types/src/interpreter/context.d.ts +11 -0
- package/dist/types/src/interpreter/index.d.ts +2 -0
- package/dist/types/src/interpreter/interpreter.d.ts +32 -0
- package/dist/types/src/io/index.d.ts +2 -0
- package/dist/types/src/io/stdin.d.ts +11 -0
- package/dist/types/src/io/stdout.d.ts +40 -0
- package/dist/types/src/lexer/index.d.ts +3 -0
- package/dist/types/src/lexer/lexer.d.ts +24 -0
- package/dist/types/src/lexer/tokens.d.ts +38 -0
- package/dist/types/src/parser/ast.d.ts +64 -0
- package/dist/types/src/parser/index.d.ts +3 -0
- package/dist/types/src/parser/parser.d.ts +23 -0
- package/dist/types/src/shell-dsl.d.ts +32 -0
- package/dist/types/src/shell-promise.d.ts +39 -0
- package/dist/types/src/types.d.ts +76 -0
- package/dist/types/src/utils/escape.d.ts +2 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/package.json +46 -6
package/README.md
CHANGED
|
@@ -1,45 +1,589 @@
|
|
|
1
1
|
# shell-dsl
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
+
const vol = new Volume();
|
|
11
|
+
vol.fromJSON({ "/data.txt": "foo\nbar\nbaz\n" });
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
const sh = createShellDSL({
|
|
14
|
+
fs: createVirtualFS(createFsFromVolume(vol)),
|
|
15
|
+
cwd: "/",
|
|
16
|
+
env: { USER: "alice" },
|
|
17
|
+
commands: builtinCommands,
|
|
18
|
+
});
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
##
|
|
24
|
+
## Installation
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
```bash
|
|
27
|
+
bun add shell-dsl memfs
|
|
28
|
+
```
|
|
19
29
|
|
|
20
|
-
##
|
|
30
|
+
## Features
|
|
21
31
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
Create a `ShellDSL` instance by providing a virtual filesystem, working directory, environment variables, and a command registry:
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
+
}
|