@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
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Declarative behavior definitions: a table form for the common case
3
+ * "parse the arguments; operands and some option values are files that are
4
+ * read, written or deleted", compiled to the same {@link Behavior} interface
5
+ * as hand-written definitions.
6
+ *
7
+ * define({ names: ['sort'], summary: 'sort lines',
8
+ * spec: { short: 'nru', shortWithArg: 'okt', long: { output: '=o' } },
9
+ * operands: 'read*', flags: { o: 'write' } })
10
+ *
11
+ * Operand patterns are space-separated tokens matched against the operands in
12
+ * order. A token is `kind[:dir][~][*|?]`:
13
+ *
14
+ * read write append delete a path effect (resolved against the cwd);
15
+ * `-` as a read operand means stdin
16
+ * url a network effect with the operand as target
17
+ * pattern code subcommand a display hint (h.role), no effect
18
+ * arg any other operand (a mode, a size, a key), no effect
19
+ * cmd the rest of the operands is a command to analyze
20
+ * (wrappers; needs `spec.posix`)
21
+ * :dir display hint: the operand is a directory
22
+ * ~ the effect is only possible (`maybe`)
23
+ * * any number (at most one starred token per pattern)
24
+ * ? optional (documentation only: missing operands just have no effect)
25
+ *
26
+ * Operands beyond a pattern without `*` make the result incomplete. Unknown
27
+ * flags make it incomplete too (as with hand-written definitions).
28
+ *
29
+ * Anything the table cannot say goes into `when` (overrides applied when a
30
+ * flag is present), `subcommands` (a table per subcommand), or `extra` (a
31
+ * function post-processing the result). See docs/behaviors.md.
32
+ */
33
+
34
+ import type { AValue } from '../analysis/value.ts'
35
+ import { concrete, known } from '../analysis/value.ts'
36
+ import type { Effect } from '../policy/effects.ts'
37
+ import type { ArgSpec, Behavior, BehaviorHelpers, BehaviorResult, Invocation, OutputInfo, ParsedArgs } from './types.ts'
38
+
39
+ /** A fixed effect a command always has: paths may start with `~/` (the home directory); relative ones resolve against the cwd. */
40
+ export type StaticEffect =
41
+ | { kind: 'read' | 'write' | 'delete'; path: string; recursive?: boolean; maybe?: boolean }
42
+ | { kind: 'network'; detail: string; maybe?: boolean }
43
+ | { kind: 'process' | 'unknown'; detail: string }
44
+
45
+ export interface Def {
46
+ summary?: string
47
+ /** Option syntax (see {@link ArgSpec}). */
48
+ spec?: ArgSpec
49
+ /** Operand pattern, e.g. `'read*'`, `'arg write*'`, `'read* write'`. Default: no operands expected. */
50
+ operands?: string
51
+ /** Option values by canonical flag name: `{ o: 'write', f: 'read', e: 'pattern' }` (same kinds as operand tokens). */
52
+ flags?: Record<string, string>
53
+ /** Flags not in `spec` are accepted (they do not make the result incomplete): for query-only subcommands. */
54
+ anyFlags?: boolean
55
+ /** Path effects are recursive: always, or when one of these flags is present. */
56
+ recursive?: boolean | string[]
57
+ /** Reads are of names and metadata only (`ls`, `tree`, `du`): marked `names: true`, never content reads. */
58
+ namesOnly?: boolean
59
+ /** Without path operands, the first path token applies to the cwd (`ls`, `rg`). */
60
+ cwdIfNone?: boolean
61
+ /** Does it read stdin? `'operands'`: when no read operand is given or one is `-`. Default: `'operands'` if the pattern has a read token, else `'maybe'`. */
62
+ stdin?: boolean | 'maybe' | 'operands'
63
+ stdout?: OutputInfo
64
+ /** Effects the command always has, in addition to the operand ones. */
65
+ effects?: StaticEffect[]
66
+ /** The command runs code or does things the table cannot describe: an `unknown` effect with this detail, and `complete: false`. */
67
+ opaque?: string
68
+ /** Overrides applied when a flag is present; keys are canonical flag names, `|`-separated alternatives (`'a|append'`). Later entries win. */
69
+ when?: Record<string, Def>
70
+ /**
71
+ * The first operand is a subcommand looked up here (`'list|ls'` keys for
72
+ * aliases, `'*'` for any other, `''` for none). `spec` then describes the
73
+ * global options before it (parsed POSIX-style); the subcommand's own
74
+ * `spec` its options. A subcommand not in the table is incomplete.
75
+ */
76
+ subcommands?: Record<string, Def>
77
+ /** Post-process the computed result (the escape hatch for what the table cannot express). */
78
+ extra?: (r: BehaviorResult, c: { inv: Invocation; h: BehaviorHelpers; p: ParsedArgs; operands: AValue[] }) => BehaviorResult
79
+ }
80
+
81
+ interface Token { kind: string; dir: boolean; maybe: boolean; star: boolean }
82
+
83
+ const KINDS = new Set(['read', 'write', 'append', 'delete', 'url', 'pattern', 'code', 'subcommand', 'arg', 'cmd'])
84
+ const PATH_KINDS = new Set(['read', 'write', 'append', 'delete'])
85
+
86
+ const tokenCache = new Map<string, Token[]>()
87
+ function tokens(pattern: string): Token[] {
88
+ let t = tokenCache.get(pattern)
89
+ if (t) return t
90
+ t = pattern.split(/\s+/).filter(Boolean).map((s): Token => {
91
+ const m = /^([a-z]+)(:dir)?(~)?([*?])?$/.exec(s)
92
+ if (!m || !KINDS.has(m[1])) throw new Error(`bad operand token '${s}'`)
93
+ return { kind: m[1], dir: !!m[2], maybe: !!m[3], star: m[4] === '*' }
94
+ })
95
+ if (t.filter((x) => x.star).length > 1) throw new Error(`more than one starred token in '${pattern}'`)
96
+ tokenCache.set(pattern, t)
97
+ return t
98
+ }
99
+
100
+ /** Assign operands to tokens: fixed tokens before the star from the left, after it from the right. */
101
+ function match(toks: Token[], ops: AValue[]): { pairs: [Token, AValue][]; extra: AValue[] } {
102
+ const star = toks.findIndex((t) => t.star)
103
+ const pairs: [Token, AValue][] = []
104
+ const cmd = toks.findIndex((t) => t.kind === 'cmd')
105
+ if (star === -1) {
106
+ const n = cmd === -1 ? Math.min(toks.length, ops.length) : Math.min(cmd, ops.length)
107
+ for (let i = 0; i < n; i++) pairs.push([toks[i], ops[i]])
108
+ if (cmd !== -1 && ops.length > cmd) return { pairs: [...pairs, [toks[cmd], ops[cmd]]], extra: [] }
109
+ return { pairs, extra: ops.slice(n) }
110
+ }
111
+ const before = toks.slice(0, star)
112
+ const after = toks.slice(star + 1)
113
+ const nb = Math.min(before.length, ops.length)
114
+ for (let i = 0; i < nb; i++) pairs.push([before[i], ops[i]])
115
+ const rest = ops.slice(nb)
116
+ const na = Math.min(after.length, rest.length)
117
+ const mid = rest.slice(0, rest.length - na)
118
+ for (const o of mid) pairs.push([toks[star], o])
119
+ for (let i = 0; i < na; i++) pairs.push([after[after.length - na + i], rest[mid.length + i]])
120
+ return { pairs, extra: [] }
121
+ }
122
+
123
+ function has(p: ParsedArgs, key: string): boolean {
124
+ return key.split('|').some((k) => p.flags.has(k))
125
+ }
126
+
127
+ /** The definition after applying `when` overrides for the flags present. */
128
+ function effective(def: Def, p: ParsedArgs): Def {
129
+ if (!def.when) return def
130
+ let d: Def = { ...def }
131
+ for (const [k, o] of Object.entries(def.when)) if (has(p, k)) d = { ...d, ...o }
132
+ return d
133
+ }
134
+
135
+ function staticEffect(e: StaticEffect, h: BehaviorHelpers): Effect {
136
+ if ('path' in e) {
137
+ const home = h.home()
138
+ const path = e.path === '~' || e.path.startsWith('~/')
139
+ ? { alts: (home ?? known('~')).alts.map((a) => [...a, e.path.slice(1)].filter((s) => s !== '')) }
140
+ : h.resolve(known(e.path))
141
+ return { kind: e.kind, path, ...(e.recursive ? { recursive: true } : {}), ...(e.maybe ? { maybe: true } : {}) }
142
+ }
143
+ if (e.kind === 'network') return { kind: 'network', detail: e.detail, ...(e.maybe ? { maybe: true } : {}) }
144
+ return { kind: e.kind, detail: e.detail }
145
+ }
146
+
147
+ const isDash = (v: AValue): boolean => concrete(v) === '-'
148
+
149
+ /** Analyze `argv` (operands and options, argv[0] excluded) with a definition. */
150
+ export function analyzeDef(def: Def, inv: Invocation, argv: AValue[], h: BehaviorHelpers): BehaviorResult {
151
+ if (def.subcommands) return analyzeSubcommand(def, inv, argv, h)
152
+ const p = h.parseArgs(argv, def.spec ?? {})
153
+ const d = effective(def, p)
154
+ const toks = tokens(d.operands ?? '')
155
+ const recursive = d.recursive === true || (Array.isArray(d.recursive) && d.recursive.some((k) => has(p, k)))
156
+ const effects: Effect[] = []
157
+ let complete = p.unknownFlags.length === 0 || d.anyFlags === true
158
+ let readsStdin: boolean | 'maybe' | undefined
159
+ let stdout = d.stdout
160
+ let sawRead = false
161
+ let sawDash = false
162
+ let sawPath = false
163
+
164
+ const apply = (t: Token, v: AValue, option: boolean): void => {
165
+ if (t.dir) h.role(v, 'dir')
166
+ switch (t.kind) {
167
+ case 'read':
168
+ case 'write':
169
+ case 'append':
170
+ case 'delete': {
171
+ if (isDash(v) && (t.kind === 'read' || option)) {
172
+ if (t.kind === 'read') sawDash = true
173
+ return
174
+ }
175
+ if (t.kind === 'read' && !option) sawRead = true
176
+ if (!option) sawPath = true
177
+ const kind = t.kind === 'append' ? 'write' : t.kind
178
+ effects.push({ kind, path: h.resolve(v), ...(recursive ? { recursive: true } : {}), ...(t.kind === 'append' ? { append: true } : {}), ...(t.maybe ? { maybe: true } : {}) } as Effect)
179
+ return
180
+ }
181
+ case 'url':
182
+ effects.push({ kind: 'network', target: v, detail: inv.name })
183
+ return
184
+ case 'pattern':
185
+ case 'code':
186
+ case 'subcommand':
187
+ h.role(v, t.kind)
188
+ return
189
+ }
190
+ }
191
+
192
+ const { pairs, extra } = match(toks, p.operands)
193
+ if (extra.length > 0) complete = false
194
+ for (const [t, v] of pairs) {
195
+ if (t.kind === 'cmd') {
196
+ const at = p.operands.indexOf(v)
197
+ const sub = h.subcommand(p.operands.slice(at))
198
+ effects.push(...sub.effects)
199
+ if (sub.complete === false) complete = false
200
+ readsStdin = sub.readsStdin
201
+ stdout ??= sub.stdout
202
+ continue
203
+ }
204
+ apply(t, v, false)
205
+ }
206
+ if (d.cwdIfNone && !sawPath) {
207
+ const first = toks.find((t) => PATH_KINDS.has(t.kind))
208
+ if (first) {
209
+ const kind = first.kind === 'append' ? 'write' : first.kind
210
+ effects.push({ kind, path: inv.cwd, ...(recursive ? { recursive: true } : {}), ...(first.maybe ? { maybe: true } : {}) } as Effect)
211
+ if (kind === 'read') sawRead = true
212
+ }
213
+ }
214
+ for (const [k, pattern] of Object.entries(d.flags ?? {})) {
215
+ const t = tokens(pattern)[0]
216
+ for (const v of p.multi.get(k) ?? []) apply(t, v, true)
217
+ }
218
+ for (const e of d.effects ?? []) effects.push(staticEffect(e, h))
219
+ if (d.opaque) {
220
+ effects.push({ kind: 'unknown', detail: d.opaque })
221
+ complete = false
222
+ }
223
+ if (readsStdin === undefined) {
224
+ const stdin = d.stdin ?? (toks.some((t) => t.kind === 'read') ? 'operands' : 'maybe')
225
+ readsStdin = stdin === 'operands' ? !sawRead || sawDash : stdin
226
+ }
227
+ const r: BehaviorResult = { effects, readsStdin, complete, ...(stdout ? { stdout } : {}) }
228
+ return d.extra ? d.extra(r, { inv, h, p, operands: p.operands }) : r
229
+ }
230
+
231
+ function analyzeSubcommand(def: Def, inv: Invocation, argv: AValue[], h: BehaviorHelpers): BehaviorResult {
232
+ const p = h.parseArgs(argv, { ...def.spec, posix: true })
233
+ const d = effective(def, p)
234
+ // the global options' own effects (option values, fixed effects)
235
+ const base: Effect[] = []
236
+ for (const [k, pattern] of Object.entries(d.flags ?? {})) {
237
+ const t = tokens(pattern)[0]
238
+ for (const v of p.multi.get(k) ?? []) {
239
+ if (t.dir) h.role(v, 'dir')
240
+ if (PATH_KINDS.has(t.kind) && !isDash(v)) base.push({ kind: t.kind === 'append' ? 'write' : t.kind, path: h.resolve(v) } as Effect)
241
+ else if (t.kind === 'pattern' || t.kind === 'code') h.role(v, t.kind)
242
+ }
243
+ }
244
+ for (const e of d.effects ?? []) base.push(staticEffect(e, h))
245
+ if (d.opaque) base.push({ kind: 'unknown', detail: d.opaque })
246
+ const globalComplete = (p.unknownFlags.length === 0 || d.anyFlags === true) && !d.opaque
247
+ const [subv, ...rest] = p.operands
248
+ const table = d.subcommands!
249
+ const lookup = (name: string): Def | undefined => {
250
+ for (const [k, v] of Object.entries(table)) if (k !== '*' && k.split('|').includes(name)) return v
251
+ return table['*']
252
+ }
253
+ if (subv === undefined) {
254
+ const none = table['']
255
+ if (!none) return { effects: base, readsStdin: false, complete: globalComplete }
256
+ const r = analyzeDef(none, inv, [], h)
257
+ return { ...r, effects: [...base, ...r.effects], complete: globalComplete && r.complete !== false }
258
+ }
259
+ const name = concrete(subv)
260
+ if (name === undefined) {
261
+ return { effects: [...base, { kind: 'unknown', detail: `${inv.name} subcommand is not known statically` }], readsStdin: 'maybe', complete: false }
262
+ }
263
+ h.role(subv, 'subcommand')
264
+ const sub = lookup(name)
265
+ if (!sub) return { effects: [...base, { kind: 'unknown', detail: `${inv.name} ${name}` }], readsStdin: 'maybe', complete: false }
266
+ // global options are usually accepted after the subcommand too
267
+ const r = analyzeDef({ ...sub, spec: mergeSpec(def.spec, sub.spec) }, inv, rest, h)
268
+ return { ...r, effects: [...base, ...r.effects], complete: globalComplete && r.complete !== false }
269
+ }
270
+
271
+ function mergeSpec(outer: ArgSpec | undefined, inner: ArgSpec | undefined): ArgSpec {
272
+ if (!outer) return inner ?? {}
273
+ if (!inner) return { ...outer, posix: false }
274
+ return {
275
+ ...inner,
276
+ short: (inner.short ?? '') + (outer.short ?? ''),
277
+ shortWithArg: (inner.shortWithArg ?? '') + [...(outer.shortWithArg ?? '')].filter((c) => !(inner.short ?? '').includes(c)).join(''),
278
+ long: { ...outer.long, ...inner.long },
279
+ }
280
+ }
281
+
282
+ /** Compile a table definition to a {@link Behavior}. */
283
+ export function define(def: Def & { names: string[] }): Behavior {
284
+ return {
285
+ names: def.names,
286
+ summary: def.summary,
287
+ analyze: (inv, h) => {
288
+ const r = analyzeDef(def, inv, inv.argv.slice(1), h)
289
+ if (!def.namesOnly) return r
290
+ return { ...r, effects: r.effects.map((e) => (e.kind === 'read' ? { ...e, names: true } : e)) }
291
+ },
292
+ }
293
+ }
@@ -0,0 +1,19 @@
1
+ import { BehaviorRegistry } from './registry.ts'
2
+ import { coreBehaviors } from './defs/core.ts'
3
+ import { devtoolBehaviors } from './defs/devtools.ts'
4
+ import { toolBehaviors } from './defs/tools.ts'
5
+ import { macosBehaviors } from './defs/macos.ts'
6
+
7
+ export * from './types.ts'
8
+ export { BehaviorRegistry, parseArgs, resolvePath, basename } from './registry.ts'
9
+ export { coreBehaviors } from './defs/core.ts'
10
+ export { devtoolBehaviors } from './defs/devtools.ts'
11
+ export { toolBehaviors } from './defs/tools.ts'
12
+ export { macosBehaviors } from './defs/macos.ts'
13
+ export { define, analyzeDef, type Def, type StaticEffect } from './dsl.ts'
14
+
15
+ /** A registry with every bundled definition. */
16
+ export function defaultBehaviors(): BehaviorRegistry {
17
+ return new BehaviorRegistry([...coreBehaviors, ...devtoolBehaviors, ...toolBehaviors, ...macosBehaviors])
18
+ }
19
+ export { argRoles, lexicalRole } from './roles.ts'
@@ -0,0 +1,261 @@
1
+ /**
2
+ * The behavior registry: looks up a definition by command basename and
3
+ * provides the helpers definitions use (abstract path resolution, GNU-style
4
+ * argument parsing over abstract values, nested command analysis).
5
+ */
6
+
7
+ import * as fs from 'node:fs'
8
+ import * as os from 'node:os'
9
+ import type { AValue } from '../analysis/value.ts'
10
+ import { concat, concrete, describe, isHole, known, knownPrefix, unknownString } from '../analysis/value.ts'
11
+ import type { ArgSpec, Behavior, BehaviorHelpers, BehaviorResult, Invocation, ParsedArgs, ScriptInfo, StdinKind } from './types.ts'
12
+
13
+ /** Analyze a shell script (provided by the static analyzer, to avoid a dependency cycle). */
14
+ export type ScriptAnalyzer = (source: AValue, positional: AValue[], inv: Invocation, info?: ScriptInfo) => BehaviorResult
15
+
16
+ const MAX_DEPTH = 8
17
+
18
+ /** Observes helper calls (used to map effects back to the arguments they came from). */
19
+ export interface HelperTracer {
20
+ resolved(input: AValue, output: AValue): void
21
+ role(value: AValue, role: import('./types.ts').ArgRole): void
22
+ }
23
+
24
+ export class BehaviorRegistry {
25
+ private readonly byName = new Map<string, Behavior>()
26
+ scriptAnalyzer: ScriptAnalyzer | null = null
27
+ /** When set, helper calls are reported to it (see roles.ts). */
28
+ tracer: HelperTracer | null = null
29
+
30
+ constructor(behaviors: Behavior[] = []) {
31
+ for (const b of behaviors) this.add(b)
32
+ }
33
+
34
+ add(b: Behavior): this {
35
+ for (const n of b.names) this.byName.set(n, b)
36
+ return this
37
+ }
38
+
39
+ get(name: string): Behavior | undefined {
40
+ return this.byName.get(basename(name))
41
+ }
42
+
43
+ names(): string[] {
44
+ return [...this.byName.keys()].sort()
45
+ }
46
+
47
+ /**
48
+ * Analyze one invocation. Commands without a definition yield a single
49
+ * `unknown` effect and `complete: false`.
50
+ */
51
+ analyze(inv: Invocation, depth = 0): BehaviorResult {
52
+ const name = inv.name
53
+ const b = this.byName.get(name)
54
+ if (!b) {
55
+ const script = this.shellScriptOf(inv)
56
+ if (script !== undefined && this.scriptAnalyzer && depth <= MAX_DEPTH) {
57
+ const r = this.scriptAnalyzer(known(script.text), inv.argv.slice(1), inv, { file: script.path })
58
+ return { ...r, effects: [{ kind: 'read', path: known(script.path), source: name }, ...r.effects] }
59
+ }
60
+ return { effects: [{ kind: 'unknown', detail: `no behavior definition for '${name}'`, source: name }], complete: false, readsStdin: 'maybe' }
61
+ }
62
+ if (depth > MAX_DEPTH) return { effects: [{ kind: 'unknown', detail: 'nesting too deep', source: name }], complete: false }
63
+ const h = this.helpers(inv, depth)
64
+ try {
65
+ const r = b.analyze(inv, h)
66
+ for (const e of r.effects) e.source ??= name
67
+ return { complete: true, readsStdin: 'maybe', ...r }
68
+ } catch (e) {
69
+ return { effects: [{ kind: 'unknown', detail: `behavior '${name}' failed: ${(e as Error).message}`, source: name }], complete: false }
70
+ }
71
+ }
72
+
73
+ /** An executable file with a bash/sh shebang named by argv[0]. */
74
+ private shellScriptOf(inv: Invocation): { path: string; text: string } | undefined {
75
+ const a0 = concrete(inv.argv[0])
76
+ if (a0 === undefined || !a0.includes('/')) return undefined
77
+ const p = concrete(resolvePath(known(a0), inv.cwd))
78
+ if (p === undefined) return undefined
79
+ try {
80
+ const st = fs.statSync(p)
81
+ if (!st.isFile() || st.size > 1024 * 1024) return undefined
82
+ const text = fs.readFileSync(p, 'utf8')
83
+ const m = /^#![ \t]*(\S+)(?:[ \t]+(\S+))?/.exec(text.split('\n')[0])
84
+ if (!m) return undefined
85
+ const interp = basename(m[1]) === 'env' ? m[2] : basename(m[1])
86
+ return interp === 'bash' || interp === 'sh' ? { path: p, text } : undefined
87
+ } catch {
88
+ return undefined
89
+ }
90
+ }
91
+
92
+ helpers(inv: Invocation, depth: number): BehaviorHelpers {
93
+ const tracer = this.tracer
94
+ const resolve = (p: AValue): AValue => {
95
+ const r = resolvePath(p, inv.cwd)
96
+ tracer?.resolved(p, r)
97
+ return r
98
+ }
99
+ return {
100
+ resolve,
101
+ role: (value, role) => tracer?.role(value, role),
102
+ parseArgs,
103
+ subcommand: (argv, opts = {}) => {
104
+ if (argv.length === 0) return { effects: [] }
105
+ const n = concrete(argv[0])
106
+ if (n === undefined) {
107
+ return { effects: [{ kind: 'unknown', detail: `command name ${describe(argv[0])} is not known statically`, source: inv.name }], complete: false }
108
+ }
109
+ const sub: Invocation = { ...inv, name: basename(n), argv, stdin: opts.stdin ?? inv.stdin, cwd: opts.cwd ?? inv.cwd }
110
+ const r = this.analyze(sub, depth + 1)
111
+ return { ...r, effects: [{ kind: 'exec', argv }, ...r.effects] }
112
+ },
113
+ script: (source, positional = [], info) => {
114
+ tracer?.role(source, 'code')
115
+ if (!this.scriptAnalyzer) return { effects: [{ kind: 'unknown', detail: 'no script analyzer', source: inv.name }], complete: false }
116
+ return this.scriptAnalyzer(source, positional, inv, info)
117
+ },
118
+ home: () => inv.env('HOME') ?? known(os.homedir()),
119
+ }
120
+ }
121
+ }
122
+
123
+ export function basename(p: string): string {
124
+ const i = p.lastIndexOf('/')
125
+ return i === -1 ? p : p.slice(i + 1)
126
+ }
127
+
128
+ /** Resolve a path value against a cwd: absolute values stay, relative ones are joined. Literal `.`/`..` are left for the policy shape check. */
129
+ export function resolvePath(p: AValue, cwd: AValue): AValue {
130
+ const alts = p.alts.map((alt): AValue[] => {
131
+ const first = alt[0]
132
+ if (first === undefined) return [cwd]
133
+ if (typeof first === 'string' && first.startsWith('/')) return [{ alts: [alt] }]
134
+ if (isHole(first) && first.type === 'path' && first.under) return [{ alts: [alt] }]
135
+ // a `~` that reaches a behavior is literal: word expansion has already expanded unquoted tildes
136
+ // ("~/x" names ./~/x, as in bash)
137
+ return [cwd, known('/'), { alts: [alt] }]
138
+ })
139
+ const parts = alts.map((a) => concat(...a))
140
+ if (parts.length === 1) return parts[0]
141
+ return { alts: parts.flatMap((v) => v.alts) }
142
+ }
143
+
144
+ /**
145
+ * GNU-style argument parsing over abstract values. Concrete arguments parse
146
+ * normally; an unknown argument whose known prefix starts with `-` is
147
+ * recorded as an unknown flag; one whose first character is unknown could be
148
+ * an option or an operand, so it is returned both as an operand and in
149
+ * `ambiguous`.
150
+ */
151
+ export function parseArgs(args: AValue[], spec: ArgSpec): ParsedArgs {
152
+ const flags = new Map<string, AValue | true>()
153
+ const multi = new Map<string, AValue[]>()
154
+ const operands: AValue[] = []
155
+ const ambiguous: AValue[] = []
156
+ const unknownFlags: string[] = []
157
+ const short = spec.short ?? ''
158
+ const shortArg = spec.shortWithArg ?? ''
159
+ const long = spec.long ?? {}
160
+ const setFlag = (name: string, v: AValue | true): void => {
161
+ flags.set(name, v)
162
+ if (v !== true) {
163
+ const list = multi.get(name) ?? []
164
+ list.push(v)
165
+ multi.set(name, list)
166
+ }
167
+ }
168
+ let onlyOperands = false
169
+ for (let i = 0; i < args.length; i++) {
170
+ const a = args[i]
171
+ const s = concrete(a)
172
+ if (onlyOperands) {
173
+ operands.push(a)
174
+ continue
175
+ }
176
+ if (s === undefined) {
177
+ const prefix = knownPrefix(a)
178
+ if (prefix.startsWith('-') && prefix.length > 1) {
179
+ unknownFlags.push(describe(a))
180
+ } else if (prefix === '') {
181
+ operands.push(a)
182
+ ambiguous.push(a)
183
+ } else {
184
+ operands.push(a)
185
+ if (spec.posix) onlyOperands = true
186
+ }
187
+ continue
188
+ }
189
+ if (s === '--') {
190
+ onlyOperands = true
191
+ continue
192
+ }
193
+ const single = spec.longSingle === true && s.length > 2 && s[0] === '-' && s[1] !== '-'
194
+ if ((s.startsWith('--') && s.length > 2) || single) {
195
+ const body = s.slice(single ? 1 : 2)
196
+ const eq = body.indexOf('=')
197
+ const name = eq === -1 ? body : body.slice(0, eq)
198
+ // allow unambiguous prefixes (not for single-dash options, which fall back to short flags)
199
+ let key = Object.hasOwn(long, name) ? name : undefined
200
+ if (key === undefined && !single) {
201
+ const candidates = Object.keys(long).filter((k) => k.startsWith(name))
202
+ if (candidates.length === 1) key = candidates[0]
203
+ }
204
+ if (key !== undefined) {
205
+ const kind = long[key]
206
+ const canonical = typeof kind === 'string' && kind.startsWith('=') ? kind.slice(1) : key
207
+ const n = typeof kind === 'number' ? kind : typeof kind === 'string' ? (shortArg.includes(canonical) ? spec.arity?.[canonical] ?? 1 : 0) : kind ? 1 : 0
208
+ if (n > 0) {
209
+ if (eq !== -1 && n === 1) setFlag(canonical, known(body.slice(eq + 1)))
210
+ else if (i + n < args.length) {
211
+ i += n
212
+ setFlag(canonical, args[i])
213
+ } else {
214
+ i = args.length
215
+ setFlag(canonical, unknownString(`${s} (missing)`))
216
+ }
217
+ } else setFlag(canonical, true)
218
+ continue
219
+ }
220
+ if (!single) {
221
+ unknownFlags.push(s)
222
+ continue
223
+ }
224
+ }
225
+ if (spec.numeric && /^-\d+$/.test(s)) {
226
+ setFlag(spec.numeric, known(s.slice(1)))
227
+ continue
228
+ }
229
+ if (s.startsWith('-') && s.length > 1) {
230
+ for (let j = 1; j < s.length; j++) {
231
+ const f = s[j]
232
+ if (shortArg.includes(f)) {
233
+ const rest = s.slice(j + 1)
234
+ const n = spec.arity?.[f] ?? 1
235
+ if (rest !== '' && n === 1) setFlag(f, known(rest))
236
+ else {
237
+ const need = rest !== '' ? n - 1 : n
238
+ if (i + need < args.length) {
239
+ i += need
240
+ setFlag(f, args[i])
241
+ } else {
242
+ i = args.length
243
+ setFlag(f, unknownString(`-${f} (missing)`))
244
+ }
245
+ }
246
+ break
247
+ }
248
+ if (short.includes(f)) setFlag(f, true)
249
+ else unknownFlags.push('-' + f)
250
+ }
251
+ continue
252
+ }
253
+ operands.push(a)
254
+ if (spec.posix) onlyOperands = true
255
+ }
256
+ return { flags, multi, operands, ambiguous, unknownFlags }
257
+ }
258
+
259
+ export function stdinOf(kind: StdinKind): StdinKind {
260
+ return kind
261
+ }