@taliesinb/mash 0.0.0-stage → 0.1.0

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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/bin/mash.ts +253 -0
  4. package/dist/bin/mash.d.ts +39 -0
  5. package/dist/bin/mash.d.ts.map +1 -0
  6. package/dist/bin/mash.js +281 -0
  7. package/dist/bin/mash.js.map +1 -0
  8. package/dist/src/analysis/analyze.d.ts +97 -0
  9. package/dist/src/analysis/analyze.d.ts.map +1 -0
  10. package/dist/src/analysis/analyze.js +1406 -0
  11. package/dist/src/analysis/analyze.js.map +1 -0
  12. package/dist/src/analysis/fs.d.ts +39 -0
  13. package/dist/src/analysis/fs.d.ts.map +1 -0
  14. package/dist/src/analysis/fs.js +117 -0
  15. package/dist/src/analysis/fs.js.map +1 -0
  16. package/dist/src/analysis/outputs.d.ts +16 -0
  17. package/dist/src/analysis/outputs.d.ts.map +1 -0
  18. package/dist/src/analysis/outputs.js +277 -0
  19. package/dist/src/analysis/outputs.js.map +1 -0
  20. package/dist/src/analysis/value.d.ts +76 -0
  21. package/dist/src/analysis/value.d.ts.map +1 -0
  22. package/dist/src/analysis/value.js +248 -0
  23. package/dist/src/analysis/value.js.map +1 -0
  24. package/dist/src/behaviors/defs/core.d.ts +15 -0
  25. package/dist/src/behaviors/defs/core.d.ts.map +1 -0
  26. package/dist/src/behaviors/defs/core.js +1084 -0
  27. package/dist/src/behaviors/defs/core.js.map +1 -0
  28. package/dist/src/behaviors/defs/devtools.d.ts +9 -0
  29. package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
  30. package/dist/src/behaviors/defs/devtools.js +432 -0
  31. package/dist/src/behaviors/defs/devtools.js.map +1 -0
  32. package/dist/src/behaviors/defs/macos.d.ts +12 -0
  33. package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
  34. package/dist/src/behaviors/defs/macos.js +454 -0
  35. package/dist/src/behaviors/defs/macos.js.map +1 -0
  36. package/dist/src/behaviors/defs/tools.d.ts +9 -0
  37. package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
  38. package/dist/src/behaviors/defs/tools.js +639 -0
  39. package/dist/src/behaviors/defs/tools.js.map +1 -0
  40. package/dist/src/behaviors/dsl.d.ts +95 -0
  41. package/dist/src/behaviors/dsl.d.ts.map +1 -0
  42. package/dist/src/behaviors/dsl.js +267 -0
  43. package/dist/src/behaviors/dsl.js.map +1 -0
  44. package/dist/src/behaviors/index.d.ts +12 -0
  45. package/dist/src/behaviors/index.d.ts.map +1 -0
  46. package/dist/src/behaviors/index.js +18 -0
  47. package/dist/src/behaviors/index.js.map +1 -0
  48. package/dist/src/behaviors/registry.d.ts +45 -0
  49. package/dist/src/behaviors/registry.d.ts.map +1 -0
  50. package/dist/src/behaviors/registry.js +266 -0
  51. package/dist/src/behaviors/registry.js.map +1 -0
  52. package/dist/src/behaviors/roles.d.ts +29 -0
  53. package/dist/src/behaviors/roles.d.ts.map +1 -0
  54. package/dist/src/behaviors/roles.js +198 -0
  55. package/dist/src/behaviors/roles.js.map +1 -0
  56. package/dist/src/behaviors/types.d.ts +120 -0
  57. package/dist/src/behaviors/types.d.ts.map +1 -0
  58. package/dist/src/behaviors/types.js +12 -0
  59. package/dist/src/behaviors/types.js.map +1 -0
  60. package/dist/src/index.d.ts +27 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +24 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/integrations/deepsea.d.ts +121 -0
  65. package/dist/src/integrations/deepsea.d.ts.map +1 -0
  66. package/dist/src/integrations/deepsea.js +199 -0
  67. package/dist/src/integrations/deepsea.js.map +1 -0
  68. package/dist/src/machine.d.ts +139 -0
  69. package/dist/src/machine.d.ts.map +1 -0
  70. package/dist/src/machine.js +400 -0
  71. package/dist/src/machine.js.map +1 -0
  72. package/dist/src/parser/ast.d.ts +369 -0
  73. package/dist/src/parser/ast.d.ts.map +1 -0
  74. package/dist/src/parser/ast.js +17 -0
  75. package/dist/src/parser/ast.js.map +1 -0
  76. package/dist/src/parser/parser.d.ts +48 -0
  77. package/dist/src/parser/parser.d.ts.map +1 -0
  78. package/dist/src/parser/parser.js +1950 -0
  79. package/dist/src/parser/parser.js.map +1 -0
  80. package/dist/src/policy/effects.d.ts +86 -0
  81. package/dist/src/policy/effects.d.ts.map +1 -0
  82. package/dist/src/policy/effects.js +53 -0
  83. package/dist/src/policy/effects.js.map +1 -0
  84. package/dist/src/policy/policy.d.ts +130 -0
  85. package/dist/src/policy/policy.d.ts.map +1 -0
  86. package/dist/src/policy/policy.js +532 -0
  87. package/dist/src/policy/policy.js.map +1 -0
  88. package/dist/src/printer/doc.d.ts +151 -0
  89. package/dist/src/printer/doc.d.ts.map +1 -0
  90. package/dist/src/printer/doc.js +409 -0
  91. package/dist/src/printer/doc.js.map +1 -0
  92. package/dist/src/printer/highlight.d.ts +53 -0
  93. package/dist/src/printer/highlight.d.ts.map +1 -0
  94. package/dist/src/printer/highlight.js +339 -0
  95. package/dist/src/printer/highlight.js.map +1 -0
  96. package/dist/src/printer/html.d.ts +72 -0
  97. package/dist/src/printer/html.d.ts.map +1 -0
  98. package/dist/src/printer/html.js +423 -0
  99. package/dist/src/printer/html.js.map +1 -0
  100. package/dist/src/printer/print.d.ts +63 -0
  101. package/dist/src/printer/print.d.ts.map +1 -0
  102. package/dist/src/printer/print.js +484 -0
  103. package/dist/src/printer/print.js.map +1 -0
  104. package/dist/src/printer/summary.d.ts +50 -0
  105. package/dist/src/printer/summary.d.ts.map +1 -0
  106. package/dist/src/printer/summary.js +621 -0
  107. package/dist/src/printer/summary.js.map +1 -0
  108. package/dist/src/trace/events.d.ts +183 -0
  109. package/dist/src/trace/events.d.ts.map +1 -0
  110. package/dist/src/trace/events.js +9 -0
  111. package/dist/src/trace/events.js.map +1 -0
  112. package/dist/src/trace/progress.d.ts +58 -0
  113. package/dist/src/trace/progress.d.ts.map +1 -0
  114. package/dist/src/trace/progress.js +83 -0
  115. package/dist/src/trace/progress.js.map +1 -0
  116. package/dist/src/vm/arith.d.ts +29 -0
  117. package/dist/src/vm/arith.d.ts.map +1 -0
  118. package/dist/src/vm/arith.js +258 -0
  119. package/dist/src/vm/arith.js.map +1 -0
  120. package/dist/src/vm/builtins.d.ts +31 -0
  121. package/dist/src/vm/builtins.d.ts.map +1 -0
  122. package/dist/src/vm/builtins.js +1773 -0
  123. package/dist/src/vm/builtins.js.map +1 -0
  124. package/dist/src/vm/emulate.d.ts +75 -0
  125. package/dist/src/vm/emulate.d.ts.map +1 -0
  126. package/dist/src/vm/emulate.js +380 -0
  127. package/dist/src/vm/emulate.js.map +1 -0
  128. package/dist/src/vm/expand.d.ts +68 -0
  129. package/dist/src/vm/expand.d.ts.map +1 -0
  130. package/dist/src/vm/expand.js +744 -0
  131. package/dist/src/vm/expand.js.map +1 -0
  132. package/dist/src/vm/external.d.ts +34 -0
  133. package/dist/src/vm/external.d.ts.map +1 -0
  134. package/dist/src/vm/external.js +228 -0
  135. package/dist/src/vm/external.js.map +1 -0
  136. package/dist/src/vm/fifo.d.ts +66 -0
  137. package/dist/src/vm/fifo.d.ts.map +1 -0
  138. package/dist/src/vm/fifo.js +232 -0
  139. package/dist/src/vm/fifo.js.map +1 -0
  140. package/dist/src/vm/interp.d.ts +209 -0
  141. package/dist/src/vm/interp.d.ts.map +1 -0
  142. package/dist/src/vm/interp.js +1641 -0
  143. package/dist/src/vm/interp.js.map +1 -0
  144. package/dist/src/vm/io.d.ts +180 -0
  145. package/dist/src/vm/io.d.ts.map +1 -0
  146. package/dist/src/vm/io.js +502 -0
  147. package/dist/src/vm/io.js.map +1 -0
  148. package/dist/src/vm/pattern.d.ts +46 -0
  149. package/dist/src/vm/pattern.d.ts.map +1 -0
  150. package/dist/src/vm/pattern.js +498 -0
  151. package/dist/src/vm/pattern.js.map +1 -0
  152. package/dist/src/vm/proc.d.ts +94 -0
  153. package/dist/src/vm/proc.d.ts.map +1 -0
  154. package/dist/src/vm/proc.js +130 -0
  155. package/dist/src/vm/proc.js.map +1 -0
  156. package/dist/src/vm/state.d.ts +104 -0
  157. package/dist/src/vm/state.d.ts.map +1 -0
  158. package/dist/src/vm/state.js +233 -0
  159. package/dist/src/vm/state.js.map +1 -0
  160. package/dist/src/vm/test.d.ts +11 -0
  161. package/dist/src/vm/test.d.ts.map +1 -0
  162. package/dist/src/vm/test.js +277 -0
  163. package/dist/src/vm/test.js.map +1 -0
  164. package/package.json +63 -4
  165. package/src/analysis/analyze.ts +1395 -0
  166. package/src/analysis/fs.ts +121 -0
  167. package/src/analysis/outputs.ts +242 -0
  168. package/src/analysis/value.ts +264 -0
  169. package/src/behaviors/defs/core.ts +970 -0
  170. package/src/behaviors/defs/devtools.ts +400 -0
  171. package/src/behaviors/defs/macos.ts +426 -0
  172. package/src/behaviors/defs/tools.ts +596 -0
  173. package/src/behaviors/dsl.ts +293 -0
  174. package/src/behaviors/index.ts +19 -0
  175. package/src/behaviors/registry.ts +261 -0
  176. package/src/behaviors/roles.ts +187 -0
  177. package/src/behaviors/types.ts +127 -0
  178. package/src/index.ts +27 -0
  179. package/src/integrations/deepsea.ts +259 -0
  180. package/src/machine.ts +483 -0
  181. package/src/parser/ast.ts +472 -0
  182. package/src/parser/parser.ts +1826 -0
  183. package/src/policy/effects.ts +102 -0
  184. package/src/policy/policy.ts +578 -0
  185. package/src/printer/doc.ts +454 -0
  186. package/src/printer/highlight.ts +283 -0
  187. package/src/printer/html.ts +441 -0
  188. package/src/printer/print.ts +528 -0
  189. package/src/printer/summary.ts +601 -0
  190. package/src/trace/events.ts +102 -0
  191. package/src/trace/progress.ts +119 -0
  192. package/src/vm/arith.ts +236 -0
  193. package/src/vm/builtins.ts +1590 -0
  194. package/src/vm/emulate.ts +337 -0
  195. package/src/vm/expand.ts +721 -0
  196. package/src/vm/external.ts +265 -0
  197. package/src/vm/fifo.ts +226 -0
  198. package/src/vm/interp.ts +1623 -0
  199. package/src/vm/io.ts +570 -0
  200. package/src/vm/pattern.ts +477 -0
  201. package/src/vm/proc.ts +147 -0
  202. package/src/vm/state.ts +267 -0
  203. package/src/vm/test.ts +253 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taliesin Beynon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,276 @@
1
- # Temporary Holding Version
1
+ # mash — a bash virtual machine for agent harnesses
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ *mash* sounds like *bash*; the *m* is for *machine*. Source: [github.com/taliesinb/mash](https://github.com/taliesinb/mash).
4
+
5
+ mash parses a bash snippet into a span-preserving AST and analyzes it statically. It then executes the snippet itself, a tree-walking interpreter over real processes and streams, and checks every step against a sandbox policy.
6
+
7
+ It is built for the DeepSea agent harness. There, the model's `bash` tool calls should be:
8
+
9
+ - **Rejected up front** when they would definitely violate the sandbox policy (`rm -rf /etc/foo`, `cd /usr/local && make install`).
10
+ - **Checked step by step** when a step's target is only known at runtime (`d=$(...); rm -rf "$d"`). Each step is denied individually, and the script carries on with exit status 126, as if the OS had refused.
11
+ - **Observable while they run.** Trace events report which command is running, loop iterations, the subshell tree, and the bytes flowing through every pipe and redirection.
12
+
13
+ TypeScript, Node ≥ 24, no runtime dependencies.
14
+
15
+ ```
16
+ $ mash analyze -c 'out=$(mktemp -d); for f in src/*.ts; do wc -l "$f" >> "$out/n"; done; cd /etc && rm -f x.bak'
17
+ verdict: reject
18
+ ✗ line 1: `rm -f x.bak` would delete /etc/x.bak: delete: /etc/x.bak is outside the writable roots (…)
19
+
20
+ commands:
21
+ ✓ L1 [must,$()] mktemp -d
22
+ allow write ⟨$(mktemp):path⟩
23
+ ✓ L1 [must,loop] wc -l src/⟨*.ts:filename⟩
24
+ allow append ⟨$(mktemp):path⟩/n
25
+ ✓ L1 [must] cd /etc
26
+ ✗ L1 [happy] rm -f x.bak
27
+ deny delete /etc/x.bak — …
28
+ variables:
29
+ out: path = ⟨$(mktemp):path⟩
30
+ f: filename = src/⟨*.ts:filename⟩
31
+ ```
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install @taliesinb/mash # library: import { Machine } from '@taliesinb/mash'
37
+ npx @taliesinb/mash analyze -c 'rm -rf /opt/x' # CLI (the binary is called `mash`)
38
+ ```
39
+
40
+ The DeepSea adapter is a subpath export: `import { preflight, runForHarness } from '@taliesinb/mash/integrations/deepsea'`.
41
+
42
+ ## Quick start (from a clone)
43
+
44
+ ```bash
45
+ pnpm install
46
+ pnpm test # ~1970 tests, incl. ~1780 differential tests against /bin/bash
47
+ node bin/mash.ts analyze -c 'rm -rf /opt/x'
48
+ node bin/mash.ts run --trace -c 'for f in a b; do echo $f | tr a-z A-Z; done'
49
+ node bin/mash.ts run --dry-run -c 'rm -rf build && mkdir build && cp src/*.ts build/'
50
+ ```
51
+
52
+ ```ts
53
+ import { Machine, ProgressTracker } from '@taliesinb/mash'
54
+
55
+ const m = new Machine({
56
+ cwd: '/work/repo',
57
+ policy: { mode: 'workspace-write', workspaceRoot: '/work/repo', network: 'deny' },
58
+ hooks: {
59
+ beforeCommand(ctx) { // argv after expansion + predicted effects + policy findings
60
+ if (ctx.argv[0] === 'git' && ctx.argv[1] === 'push') return { action: 'deny', reason: 'ask first' }
61
+ },
62
+ },
63
+ stdout: (chunk) => process.stdout.write(chunk),
64
+ })
65
+
66
+ const script = m.parse(snippet)
67
+ const report = m.analyze(script) // static: commands, effects, types, verdict
68
+ if (report.verdict === 'reject') throw new Error(report.reasons.join('\n'))
69
+
70
+ const progress = new ProgressTracker(report) // static plan + live events
71
+ m.on((e) => progress.update(e))
72
+ m.tap((channel, bytes) => { /* every pipe / redirection / capture */ })
73
+ const { status, denials } = await m.run(script)
74
+ ```
75
+
76
+ ## The four pieces
77
+
78
+ ### 1. Parser → AST for analysis (`src/parser`)
79
+
80
+ `parse(source)` is a scannerless recursive-descent parser. Bash's lexing depends on context (command position, `[[ ]]`, `$(( ))`, `${ }`, here-docs, case patterns), so the parser scans the source directly with context-specific word scanners instead of using a separate tokenizer.
81
+
82
+ - **Every node has an `id` and an exact `span`** (offsets into the source). Trace events carry node ids, so a UI can highlight the running command, and analysis findings point at exact text.
83
+ - **Words keep their quoting structure.** Parts are `Lit`, `SglQuoted`, `DblQuoted`, `Escaped`, `ParamExp` (with a typed `op`), `CmdSubst`, `ArithExp` and `ProcSubst`. Expansion, globbing and the analyzer can therefore tell quoted text from unquoted text.
84
+ - **Arithmetic and `[[ ]]` are parsed into expression trees.** Redirections, here-docs (several per line, `<<-`, quoted delimiters) and assignments (arrays, `+=`, subscripts, arguments of declaration builtins) are structured too.
85
+ - **Trivial wrappers are collapsed.** A `Pipeline` node exists only for two or more stages, and an `AndOr` only for two or more items.
86
+ - **`ParseError` has line and column, and an `incomplete` flag** ("needs more input").
87
+
88
+ ### 2. Execution against a sandbox policy (`src/vm`, `src/policy`)
89
+
90
+ The interpreter runs everything a shell normally does in-process: expansions in bash order (brace, tilde, parameter/command/arithmetic, splitting, globbing including extglob), all compound commands, functions with dynamic scoping, subshells (as copies of the state), about 50 builtins, `set -euo pipefail`, `EXIT`/`ERR`/`RETURN` traps, background jobs, `wait` and `kill`. External commands are **real child processes**.
91
+
92
+ **I/O is modelled on POSIX open file descriptions.** Each shell "process" has an fd table of reference-counted open files: pipes, host files, captures, here-docs. These give the semantics scripts rely on:
93
+
94
+ - EOF arrives when the last writer closes.
95
+ - SIGPIPE: `yes | head -1` ends `yes`, and `while true; do echo; done | head` ends the subshell with status 141.
96
+ - `{ read x; cat; } < f` shares the read position between a builtin and a child process.
97
+ - A command that never reads stdin doesn't swallow the input of a `while read` loop.
98
+
99
+ Every pipe stage runs concurrently. Child stdio is pumped through the VM's channels when something is tracing, and plain files are passed straight through otherwise.
100
+
101
+ **Before any command runs**, its fully expanded argv goes through these steps:
102
+
103
+ 1. Its **behavior definition** predicts effects: read, write, delete, network, exec of subcommands, chdir.
104
+ 2. The effects of its **redirections** are added.
105
+ 3. The **policy** checks every effect. Concrete paths are resolved through symlinks, so `ln -s /etc x; touch x/y` is caught.
106
+ 4. **Hooks** may allow, deny, abort or simulate.
107
+
108
+ Redirections are checked before files are opened, so a denied `> /etc/hosts` truncates nothing. A denied command prints `mash: cmd: denied by policy: …` and returns 126, or aborts the run if `abortOnDeny` is set.
109
+
110
+ **Nested commands run inside the VM.** Without this, these would disappear into an opaque child process:
111
+
112
+ - `bash -c '…'`, `sh script.sh` and executables with a bash/sh shebang (`inlineScripts`).
113
+ - `xargs`, and `find`'s `-exec`/`-delete` actions (`emulate`). mash runs `find … -print0` itself, or reads xargs' input itself, then runs each resulting command through the VM.
114
+
115
+ Either way, every nested command is traced and checked individually with its concrete arguments.
116
+
117
+ `Policy` mirrors the harness vocabulary (`read-only` | `workspace-write` | `danger-full-access`, `workspaceRoot`, `additionalWritableRoots`, temp areas). It adds restrictions an OS sandbox can't express: `network: 'deny'` or a host allow-list (`network: { allow: ['github.com', '*.githubusercontent.com'] }`), `deniedCommands`, `readDeniedRoots`, read policy coverage for credential stores (`readDenyCredentials: true` denies reading `~/.ssh`, `~/.aws`, `~/.config/gh`, keychains and other token files), `missingDeletes: 'allow'` (`rm -f` of a missing path is a no-op, not a violation), and `unknownCommands` / `denyUnknown` for commands whose effects cannot be shown safe. Static reasons for nested code name the inner command: ``would write /etc/x (via `touch /etc/x` (/w/deploy.sh:3))``.
118
+
119
+ ### 3. Behavior definitions (`src/behaviors`)
120
+
121
+ A behavior is a pure function from an invocation to the command's effects. The invocation is the argv (as abstract values), the cwd, the environment and the kind of stdin.
122
+
123
+ ```ts
124
+ const touch: Behavior = {
125
+ names: ['touch'],
126
+ analyze(inv, h) {
127
+ const p = h.parseArgs(inv.argv.slice(1), { short: 'acmfh', shortWithArg: 'rtd' })
128
+ return { effects: p.operands.map((f) => ({ kind: 'write', path: h.resolve(f) })), readsStdin: false }
129
+ },
130
+ }
131
+ ```
132
+
133
+ **The same definition serves runtime enforcement (concrete argv) and static analysis (argv with unknown parts).** Unknown arguments flow through as typed holes. Definitions can also:
134
+
135
+ - recurse into subcommands with `h.subcommand` (`xargs`, `find -exec`, `env`, `timeout`, `sudo`, `command`, …),
136
+ - analyze nested scripts with `h.script` (`bash -c`, script files),
137
+ - declare their stdout type (`mktemp` → a path under `$TMPDIR`, `basename` → a filename, `pwd` → the cwd), which feeds type inference for `x=$(cmd)`,
138
+ - declare whether they read stdin, which the VM uses for stdin plumbing.
139
+
140
+ There are about 440 bundled command names in [`defs/core.ts`](<src/behaviors/defs/core.ts>), [`defs/devtools.ts`](<src/behaviors/defs/devtools.ts>), [`defs/tools.ts`](<src/behaviors/defs/tools.ts>) and [`defs/macos.ts`](<src/behaviors/defs/macos.ts>), chosen by measuring a corpus of real agent commands. They cover coreutils, grep/sed/awk/find/xargs, archives, curl/wget/ssh, git (with its subcommands), npm/pnpm/yarn, interpreters, build tools, system package managers, sqlite3, typst, ffmpeg, yt-dlp, PDF and image tools, openssl, linters, and macOS tools (defaults, plutil, launchctl, codesign, …). Most are declarative tables (`define({ names, spec, operands: 'read*', flags: { o: 'write' } })`, see [docs/behaviors.md](<docs/behaviors.md>)). A command without a definition yields an `unknown` effect: it is allowed by default (the OS sandbox still confines it), and denied with `denyUnknown`.
141
+
142
+ ### 4. Tracing and hooks (`src/trace`)
143
+
144
+ `machine.on(listener)` receives these events in execution order:
145
+
146
+ | Event | When |
147
+ |---|---|
148
+ | `run:start` / `run:end` | Whole run |
149
+ | `step` | Before every command node (simple or compound), with node id, span and line; like bash's DEBUG trap, but with exact spans |
150
+ | `command:start` / `command:spawn` / `command:end` | Expanded argv, kind (builtin / function / external), resolved path, cwd, redirects, OS pid, status, duration |
151
+ | `command:denied` | Policy or hook denial, with the findings |
152
+ | `subshell:start` / `subshell:end` | Virtual pid tree; reason `subshell`, `pipeline`, `cmdsubst`, `procsubst`, `background` or `script` |
153
+ | `loop:iteration`, `branch` | Loop iteration with the loop variable's value; which `if`/`case` arm was taken |
154
+ | `function:enter` / `function:exit`, `assign` | Function calls and variable assignments |
155
+ | `channel:open`, `data` | Each pipe, redirection, capture or here-doc, with a label such as `` `grep foo | wc -l` `` or `> out.txt`, and its bytes |
156
+
157
+ - **`machine.tap(fn)`** intercepts the bytes on every channel: pipes, redirections, command-substitution captures and stdio.
158
+ - **`hooks.beforeCommand(ctx)`** sees the expanded argv, predicted effects and policy findings. It returns `allow`, `deny`, `abort` or `simulate` (fake output without running the command; this is how `--dry-run` works).
159
+ - **`ProgressTracker`** folds the static plan and live events into "3/7 command sites, loop #2, running `npm test`".
160
+
161
+ ## Pretty-printer (`src/printer`)
162
+
163
+ `format(src, options)` returns the snippet re-laid out as readable bash that **parses to the same AST and runs the same**. Words are printed exactly as written; only line breaks, ` \` continuations and indentation change.
164
+
165
+ - Long `&&` chains and pipelines go one item per line.
166
+ - Two or more prefix assignments get their own lines.
167
+ - Short loops stay on one line.
168
+ - Here-docs, comments, and bash's line-by-line error and alias semantics are handled.
169
+
170
+ `formatHtml(src, options)` renders the same layout as syntax-highlighted HTML. It shortens over-long strings to fit the width, with a grey `…` that expands on click. Long here-docs and multi-line strings collapse to "⋯ N more lines".
171
+
172
+ `summarize(src, { width })` gives a one-line summary that keeps the snippet's shape (commands, operators, redirections), e.g. `cd …/harness-port && CI=true bash … -j 8 … > …th.log 2>&1 && echo done`. Every `…` carries what it hides as a tooltip.
173
+
174
+ It also returns where every command ended up in the output, so a UI can highlight the running command in the pretty view. The CLI is `mash fmt`; the design is in [docs/pretty-printer.md](<docs/pretty-printer.md>).
175
+
176
+ ```bash
177
+ $ mash fmt -c 'FOO=bam BAR=baz make -j8 all && cd packages/core && pnpm install --frozen-lockfile && pnpm build 2>&1 | tail -20'
178
+ FOO=bam \
179
+ BAR=baz \
180
+ make -j8 all &&
181
+ cd packages/core &&
182
+ pnpm install --frozen-lockfile &&
183
+ pnpm build 2>&1 | tail -20
184
+ ```
185
+
186
+ ## Static analysis (`src/analysis`)
187
+
188
+ `machine.analyze(src)` performs abstract interpretation over the AST. The abstract environment tracks variables, the cwd, positional parameters and function definitions. Words are expanded into **abstract values**: sets of alternatives made of literal text and typed holes.
189
+
190
+ | Hole type | Example source | Guarantee |
191
+ |---|---|---|
192
+ | `int` | `$((…))`, `$?`, `${#x}` | Never contains `/` |
193
+ | `filename` | Glob matches (`*.txt`), `$(basename …)` | A single path component, never `.` or `..` |
194
+ | `path` | `$(mktemp -d)`, `$(pwd)`, `find` results | Optionally known to be under a directory |
195
+ | `string` | Anything else | None |
196
+
197
+ The types make definite verdicts possible despite unknowns:
198
+
199
+ - `rm /etc/*.conf` is **definitely** inside `/etc`, because a glob match can't contain `/..`.
200
+ - `rm -rf "$(mktemp -d)"` is **definitely** in the temp area.
201
+ - `rm -rf "/etc/$x"` is **not** definite: `$x` could be `../home/…`, so the runtime check decides.
202
+
203
+ The analyzer inlines function bodies at their call sites. It follows `bash -c`, script files, `eval` of known strings, `find -exec`, `xargs` and `env`/`timeout`-style wrappers. Shell builtins such as `cd`, `read`, `declare`, `set` and `shift` update the abstract state.
204
+
205
+ Each command site gets a **reach**:
206
+
207
+ - **`must`**: runs unless the script terminates earlier.
208
+ - **`happy`**: runs if every earlier command succeeds (`cd x && cmd`).
209
+ - **`may`**: runs only under a condition. This covers `if`/`case` arms, anything after `||` or after a predicate (`test -f x && …`, `grep -q … && …`), loop bodies that may not run, and uncalled functions.
210
+
211
+ Some effects are only **possible** (`maybe`): a build tool writing into its directory, `find -delete` on a tree that may contain no matches, `xargs` with empty input. A possible effect never produces a definite denial, statically or at runtime; it is reported as `unknown`.
212
+
213
+ The `cd` builtin only changes the abstract cwd if it can succeed: the directory exists, or the snippet itself created it. `a && b` continues in the state `a` left.
214
+
215
+ The verdict is:
216
+
217
+ - **`reject`** when a definite violation sits on a `must` or `happy` path (configurable with `rejectOn`),
218
+ - **`check-at-runtime`** when violations are only possible,
219
+ - **`allow`** otherwise.
220
+
221
+ ## Fidelity
222
+
223
+ - **Differential tests** ([`tests/differential.test.ts`](<tests/differential.test.ts>), and 1643 more in [`tests/differential-extra.test.ts`](<tests/differential-extra.test.ts>)) run each snippet under `/bin/bash` and under mash in identical fixture directories and compare stdout and exit status. Currently 1780+ cases match. 5 known differences are marked `it.fails`: function pretty-printing in `declare -f`/`type`, `GLOBIGNORE`, and tilde expansion after `=` in plain arguments. macOS ships bash 3.2, so bash 4/5 features (associative arrays, `${x^^}`, `mapfile`, `|&`, `&>>`, `{fd}>`, `;&`, `${x@Q}`, negative indices, `globstar`) are covered by expectation tests in [`tests/runtime.test.ts`](<tests/runtime.test.ts>).
224
+ - **Behavior targets bash 5.** Non-interactive behavior only: no job control, no readline, no history.
225
+
226
+ ## Limitations (prototype)
227
+
228
+ - **Not implemented:** `coproc`, `GLOBIGNORE`, function pretty-printing. Aliases are expanded only when `expand_aliases` is on, by substituting into the command's text.
229
+ - **Extglob is partial:** `@(…)`, `*(…)`, `+(…)` and `?(…)` work in globs and patterns, but `!(…)` is only exact at the end of a pattern.
230
+ - **Traps:** `EXIT`, `ERR` and `RETURN` traps run. Signal traps are recorded but never fire.
231
+ - **Process substitution feeding an external command** uses `/dev/fd/N` with an extra stdio pipe. That works on macOS. On Linux, Node's stdio pipes are sockets, and `/dev/fd/N` can't open a socket, so a FIFO fallback is still needed. `cmd < <(…)` into builtins and loops works everywhere.
232
+ - **Locale:** pathname-expansion results are sorted in C-locale (code unit) order.
233
+ - **Children never get a TTY.** Interactive programs see pipes.
234
+ - **Behaviors only cover what they model.** The static and runtime checks are only as complete as the behavior definitions; arbitrary code (`python script.py`, `make`) is reported as `unknown`. mash is designed to run *on top of* OS confinement, not instead of it (see [docs/deepsea-integration.md](<docs/deepsea-integration.md>)).
235
+ - **Performance:** in-process loops cost about 0.1 ms per command and yield to the event loop every 64 steps, so a runaway `while :; do :; done` can't freeze the host.
236
+
237
+ ## Playground
238
+
239
+ `playground/` is a browser page for trying the static analysis live: pick an example or type a snippet, change the policy, and see the verdict, each command's predicted effects, the inferred types and the AST update as you type. Nothing is executed. It runs the compiled `dist/` in the browser against a small virtual filesystem (`playground/shims/`).
240
+
241
+ ```bash
242
+ pnpm build && node playground/serve.mjs # → http://localhost:8377/playground/
243
+ ```
244
+
245
+ ## Documentation
246
+
247
+ - [AGENTS.md](<AGENTS.md>): orientation for agents (rules, gotchas, map of the docs)
248
+ - [DEVELOPING.md](<DEVELOPING.md>): setup, tools (`scripts/compare.ts`, `scripts/explain.ts`), recipes
249
+ - [docs/architecture.md](<docs/architecture.md>): parser, VM, I/O model, expansion, processes, the Runtime seam
250
+ - [docs/analysis-and-policy.md](<docs/analysis-and-policy.md>): abstract values, reach, path shapes, verdicts
251
+ - [docs/behaviors.md](<docs/behaviors.md>): writing behavior definitions
252
+ - [docs/testing.md](<docs/testing.md>): differential methodology, bash 3.2 vs 5, the `it.fails` workflow
253
+ - [docs/decisions.md](<docs/decisions.md>): design rationale
254
+ - [docs/future-work.md](<docs/future-work.md>): roadmap, with what each item involves
255
+ - [docs/pretty-printer.md](<docs/pretty-printer.md>): the pretty-printer (layout-only, equivalent bash): design, hazards, verification
256
+ - [docs/native-progress.md](<docs/native-progress.md>): proposal for progress from external commands (OSC 9;4, tool parsers, side-channel reporters)
257
+ - [docs/deepsea-integration.md](<docs/deepsea-integration.md>): harness integration options
258
+ - [docs/initial-research-debuggable-bash.md](<docs/initial-research-debuggable-bash.md>): research done before implementation began (DEBUG-trap tracer, survey of existing projects)
259
+
260
+ ## Layout
261
+
262
+ ```
263
+ src/parser/ ast.ts (node types), parser.ts (parse, ParseError, arithText)
264
+ src/vm/ interp.ts (interpreter), expand.ts (word expansion), io.ts (fds, pipes, channels),
265
+ builtins.ts, external.ts (child processes), arith.ts, pattern.ts (globs), test.ts ([[ ]], test)
266
+ src/analysis/ value.ts (abstract values, hole types), analyze.ts (abstract interpreter)
267
+ src/behaviors/ types.ts, registry.ts (parseArgs over abstract values), dsl.ts (define: table definitions), defs/
268
+ src/policy/ policy.ts (modes, roots, abstract path shapes), effects.ts
269
+ src/trace/ events.ts (events + hooks), progress.ts
270
+ src/printer/ doc.ts (layout IR + renderer), print.ts (format), highlight.ts, html.ts (formatHtml), summary.ts (summarize)
271
+ src/integrations/deepsea.ts adapter for the DeepSea harness
272
+ bin/mash.ts CLI
273
+ scripts/ compare.ts (bash vs mash), explain.ts (analysis dump), flip-fixed.mjs (it.fails upkeep)
274
+ playground/ browser playground for live static analysis (index.html, app.js, style.css, examples.js, shims/)
275
+ docs/ see Documentation above
276
+ ```
package/bin/mash.ts ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mash CLI.
4
+ *
5
+ * mash run [options] (-c SCRIPT | FILE) [ARGS...] pre-flight, then run under the policy
6
+ * mash analyze [options] (-c SCRIPT | FILE) static analysis report
7
+ * mash parse (-c SCRIPT | FILE) AST as JSON
8
+ * mash fmt [fmt options] (-c SCRIPT | FILE) pretty-print as equivalent bash
9
+ *
10
+ * Options:
11
+ * --mode MODE read-only | workspace-write (default) | danger-full-access
12
+ * --root DIR workspace root (default: cwd)
13
+ * --allow DIR extra writable root (repeatable)
14
+ * --no-temp temp dirs are not writable
15
+ * --no-network deny network access
16
+ * --allow-host HOST allow network access only to HOST (repeatable; *.example.com for subdomains)
17
+ * --deny-credentials deny reading credential stores (~/.ssh, ~/.aws, ~/.config/gh, keychains, ...)
18
+ * --deny CMD deny a command by name (repeatable)
19
+ * --deny-unknown deny commands whose effects cannot be shown safe
20
+ * --abort-on-deny abort the run at the first denial
21
+ * --no-preflight skip the static pre-flight check
22
+ * --dry-run do not execute external commands; show predicted effects
23
+ * --trace human-readable trace on stderr
24
+ * --trace-json FILE JSONL trace events to FILE
25
+ * --data include stream data in traces
26
+ * --json machine-readable analyze output
27
+ *
28
+ * fmt options (docs/pretty-printer.md):
29
+ * -w, --width N line width (80)
30
+ * --indent N indentation step (2)
31
+ * --operators trailing|leading
32
+ * --assignments auto|always|inline
33
+ * --compounds auto|expanded
34
+ * --statements continuation|newline|preserve
35
+ * --format-substitutions reformat multi-line $(…) bodies
36
+ * --check exit 1 if the input is not already formatted
37
+ */
38
+
39
+ import * as fs from 'node:fs'
40
+ import * as path from 'node:path'
41
+ import { Machine, describe, describeEffect, format, type FormatOptions, type Policy, type TraceEvent } from '../src/index.ts'
42
+
43
+ interface Opts {
44
+ cmd: string
45
+ script?: string
46
+ file?: string
47
+ args: string[]
48
+ mode: Policy['mode']
49
+ root: string
50
+ allow: string[]
51
+ temp: boolean
52
+ network: boolean
53
+ hosts: string[]
54
+ credentials: boolean
55
+ deny: string[]
56
+ denyUnknown: boolean
57
+ abortOnDeny: boolean
58
+ preflight: boolean
59
+ dryRun: boolean
60
+ trace: boolean
61
+ traceJson?: string
62
+ data: boolean
63
+ json: boolean
64
+ fmt: FormatOptions
65
+ check: boolean
66
+ }
67
+
68
+ function usage(code = 2): never {
69
+ const text = fs.readFileSync(new URL(import.meta.url), 'utf8').split('*/')[0].replace(/^#!.*\n\/\*\*?/, '').replace(/^ \* ?/gm, '')
70
+ process.stderr.write(text.trim() + '\n')
71
+ process.exit(code)
72
+ }
73
+
74
+ function parseCli(argv: string[]): Opts {
75
+ const o: Opts = {
76
+ cmd: argv[0] ?? '', args: [], mode: 'workspace-write', root: process.cwd(), allow: [], temp: true, network: true, hosts: [], credentials: false, deny: [],
77
+ denyUnknown: false, abortOnDeny: false, preflight: true, dryRun: false, trace: false, data: false, json: false,
78
+ fmt: {}, check: false,
79
+ }
80
+ if (!['run', 'analyze', 'parse', 'fmt'].includes(o.cmd)) usage()
81
+ let i = 1
82
+ for (; i < argv.length; i++) {
83
+ const a = argv[i]
84
+ const val = (): string => {
85
+ const v = argv[++i]
86
+ if (v === undefined) usage()
87
+ return v
88
+ }
89
+ switch (a) {
90
+ case '-c': o.script = val(); i++; o.args = argv.slice(i); return o
91
+ case '--mode': o.mode = val() as Policy['mode']; break
92
+ case '--root': o.root = path.resolve(val()); break
93
+ case '--allow': o.allow.push(path.resolve(val())); break
94
+ case '--no-temp': o.temp = false; break
95
+ case '--no-network': o.network = false; break
96
+ case '--allow-host': o.hosts.push(val()); break
97
+ case '--deny-credentials': o.credentials = true; break
98
+ case '--deny': o.deny.push(val()); break
99
+ case '--deny-unknown': o.denyUnknown = true; break
100
+ case '--abort-on-deny': o.abortOnDeny = true; break
101
+ case '--no-preflight': o.preflight = false; break
102
+ case '--dry-run': o.dryRun = true; break
103
+ case '--trace': o.trace = true; break
104
+ case '--trace-json': o.traceJson = val(); break
105
+ case '--data': o.data = true; break
106
+ case '--json': o.json = true; break
107
+ case '-w': case '--width': o.fmt.width = Number(val()); break
108
+ case '--indent': o.fmt.indent = Number(val()); break
109
+ case '--operators': o.fmt.operators = val() as FormatOptions['operators']; break
110
+ case '--assignments': o.fmt.assignments = val() as FormatOptions['assignments']; break
111
+ case '--compounds': o.fmt.compounds = val() as FormatOptions['compounds']; break
112
+ case '--statements': o.fmt.statements = val() as FormatOptions['statements']; break
113
+ case '--format-substitutions': o.fmt.formatSubstitutions = true; break
114
+ case '--check': o.check = true; break
115
+ case '-h': case '--help': usage(0)
116
+ default:
117
+ if (a.startsWith('-')) usage()
118
+ o.file = a
119
+ o.args = argv.slice(i + 1)
120
+ return o
121
+ }
122
+ }
123
+ return o
124
+ }
125
+
126
+ const o = parseCli(process.argv.slice(2))
127
+ const source = o.script ?? (o.file ? fs.readFileSync(o.file, 'utf8') : fs.readFileSync(0, 'utf8'))
128
+ const policy: Policy = {
129
+ mode: o.mode,
130
+ workspaceRoot: o.root,
131
+ additionalWritableRoots: o.allow,
132
+ writableTemp: o.temp,
133
+ network: !o.network ? 'deny' : o.hosts.length > 0 ? { allow: o.hosts } : 'allow',
134
+ readDenyCredentials: o.credentials,
135
+ deniedCommands: o.deny,
136
+ }
137
+
138
+ const isTty = process.stderr.isTTY
139
+ const c = (code: string, s: string): string => (isTty ? `\x1b[${code}m${s}\x1b[0m` : s)
140
+
141
+ const machine = new Machine({
142
+ cwd: process.cwd(),
143
+ policy,
144
+ denyUnknown: o.denyUnknown,
145
+ abortOnDeny: o.abortOnDeny,
146
+ dryRun: o.dryRun,
147
+ traceData: o.data,
148
+ arg0: o.file ?? 'mash',
149
+ stdin: process.stdin.isTTY ? undefined : process.stdin,
150
+ stdout: (chunk) => new Promise<void>((r) => (process.stdout.write(chunk) ? r() : process.stdout.once('drain', r))),
151
+ stderr: (chunk) => void process.stderr.write(chunk),
152
+ hooks: o.dryRun
153
+ ? { beforeCommand: (ctx) => void process.stderr.write(c('36', `[dry-run] ${ctx.argv.join(' ')}`) + (ctx.effects.length ? c('2', ` → ${ctx.effects.map(describeEffect).join(', ')}`) : '') + '\n') }
154
+ : undefined,
155
+ })
156
+
157
+ if (o.cmd === 'fmt') {
158
+ const { text } = format(source, o.fmt)
159
+ if (o.check) {
160
+ const same = text === source.replace(/\n$/, '')
161
+ if (!same) process.stderr.write('mash fmt: not formatted\n')
162
+ process.exit(same ? 0 : 1)
163
+ }
164
+ process.stdout.write(text + '\n')
165
+ process.exit(0)
166
+ }
167
+
168
+ if (o.cmd === 'parse') {
169
+ const ast = machine.parse(source)
170
+ process.stdout.write(JSON.stringify(ast, null, 2) + '\n')
171
+ process.exit(0)
172
+ }
173
+
174
+ const script = machine.parse(source, o.file ?? '-c')
175
+ const report = machine.analyze(script)
176
+
177
+ function printReport(): void {
178
+ if (o.json) {
179
+ process.stdout.write(JSON.stringify(report, (k, v) => (typeof v === 'bigint' ? String(v) : v), 2) + '\n')
180
+ return
181
+ }
182
+ const out = (s: string): void => void process.stdout.write(s + '\n')
183
+ out(c('1', `verdict: ${report.verdict}`))
184
+ for (const r of report.reasons) out(c('31', ` ✗ ${r}`))
185
+ out(c('1', '\ncommands:'))
186
+ for (const cmd of report.commands) {
187
+ const flags = [cmd.reach, cmd.context.loop ? 'loop' : '', cmd.context.pipeline ? 'pipe' : '', cmd.context.cmdsubst ? '$()' : '', cmd.context.function ? `fn:${cmd.context.function}` : ''].filter(Boolean).join(',')
188
+ const mark = cmd.verdict === 'deny' ? c('31', '✗') : cmd.verdict === 'unknown' ? c('33', '?') : c('32', '✓')
189
+ out(` ${mark} L${cmd.line} ${c('2', `[${flags}]`)} ${cmd.display}${cmd.complete ? '' : c('33', ' (effects incomplete)')}`)
190
+ for (const f of cmd.findings) {
191
+ if (f.verdict === 'allow' && f.effect.kind !== 'write' && f.effect.kind !== 'delete') continue
192
+ out(` ${f.verdict === 'deny' ? c('31', 'deny') : f.verdict === 'unknown' ? c('33', 'unknown') : c('32', 'allow')} ${describeEffect(f.effect)}${f.verdict !== 'allow' ? c('2', ` — ${f.reason}`) : ''}`)
193
+ }
194
+ }
195
+ if (report.variables.length) {
196
+ out(c('1', '\nvariables:'))
197
+ for (const v of report.variables) out(` ${v.name}: ${c('36', v.type)} = ${v.value}`)
198
+ }
199
+ }
200
+
201
+ if (o.cmd === 'analyze') {
202
+ printReport()
203
+ process.exit(report.verdict === 'reject' ? 1 : 0)
204
+ }
205
+
206
+ // run
207
+ if (o.preflight && report.verdict === 'reject') {
208
+ process.stderr.write(c('31', 'mash: refusing to run: the snippet would definitely violate the sandbox policy\n'))
209
+ for (const r of report.reasons) process.stderr.write(c('31', ` ✗ ${r}\n`))
210
+ process.exit(126)
211
+ }
212
+
213
+ const traceFile = o.traceJson ? fs.openSync(o.traceJson, 'w') : undefined
214
+ const depthOf = new Map<number, number>()
215
+ machine.on((e: TraceEvent) => {
216
+ if (traceFile !== undefined) {
217
+ fs.writeSync(traceFile, JSON.stringify(e, (k, v) => (v instanceof Uint8Array ? new TextDecoder().decode(v) : v)) + '\n')
218
+ }
219
+ if (!o.trace) return
220
+ const ind = (pid: number): string => ' '.repeat(depthOf.get(pid) ?? 0)
221
+ const w = (s: string): void => void process.stderr.write(s + '\n')
222
+ switch (e.type) {
223
+ case 'subshell:start':
224
+ depthOf.set(e.pid, (depthOf.get(e.parentPid) ?? 0) + 1)
225
+ w(c('2', `${ind(e.parentPid)}┌ ${e.reason} pid ${e.pid}`))
226
+ break
227
+ case 'subshell:end':
228
+ w(c('2', `${ind(e.pid).slice(2)}└ pid ${e.pid} exit ${e.status}`))
229
+ break
230
+ case 'command:start':
231
+ w(`${ind(e.pid)}${c('36', '$')} ${e.argv.join(' ')}${e.kind === 'external' ? '' : c('2', ` (${e.kind})`)}`)
232
+ break
233
+ case 'command:end':
234
+ if (e.status !== 0) w(c('33', ` ↳ exit ${e.status} (${e.durationMs}ms)`))
235
+ break
236
+ case 'command:denied':
237
+ w(c('31', `⛔ ${e.argv.join(' ')}: ${e.reason}`))
238
+ break
239
+ case 'loop:iteration':
240
+ w(c('35', `${ind(e.pid)}↻ iteration ${e.iteration}${e.variable ? ` ${e.variable}=${e.value}` : ''}`))
241
+ break
242
+ case 'data':
243
+ if (e.channel.kind !== 'host') w(c('2', ` ⇢ ${e.channel.label}: ${JSON.stringify(new TextDecoder().decode(e.bytes).slice(0, 80))}`))
244
+ break
245
+ default:
246
+ break
247
+ }
248
+ })
249
+
250
+ const result = await machine.run(script, { args: o.args })
251
+ if (traceFile !== undefined) fs.closeSync(traceFile)
252
+ process.exitCode = result.status
253
+ void describe
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mash CLI.
4
+ *
5
+ * mash run [options] (-c SCRIPT | FILE) [ARGS...] pre-flight, then run under the policy
6
+ * mash analyze [options] (-c SCRIPT | FILE) static analysis report
7
+ * mash parse (-c SCRIPT | FILE) AST as JSON
8
+ * mash fmt [fmt options] (-c SCRIPT | FILE) pretty-print as equivalent bash
9
+ *
10
+ * Options:
11
+ * --mode MODE read-only | workspace-write (default) | danger-full-access
12
+ * --root DIR workspace root (default: cwd)
13
+ * --allow DIR extra writable root (repeatable)
14
+ * --no-temp temp dirs are not writable
15
+ * --no-network deny network access
16
+ * --allow-host HOST allow network access only to HOST (repeatable; *.example.com for subdomains)
17
+ * --deny-credentials deny reading credential stores (~/.ssh, ~/.aws, ~/.config/gh, keychains, ...)
18
+ * --deny CMD deny a command by name (repeatable)
19
+ * --deny-unknown deny commands whose effects cannot be shown safe
20
+ * --abort-on-deny abort the run at the first denial
21
+ * --no-preflight skip the static pre-flight check
22
+ * --dry-run do not execute external commands; show predicted effects
23
+ * --trace human-readable trace on stderr
24
+ * --trace-json FILE JSONL trace events to FILE
25
+ * --data include stream data in traces
26
+ * --json machine-readable analyze output
27
+ *
28
+ * fmt options (docs/pretty-printer.md):
29
+ * -w, --width N line width (80)
30
+ * --indent N indentation step (2)
31
+ * --operators trailing|leading
32
+ * --assignments auto|always|inline
33
+ * --compounds auto|expanded
34
+ * --statements continuation|newline|preserve
35
+ * --format-substitutions reformat multi-line $(…) bodies
36
+ * --check exit 1 if the input is not already formatted
37
+ */
38
+ export {};
39
+ //# sourceMappingURL=mash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mash.d.ts","sourceRoot":"","sources":["../../bin/mash.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG"}