@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,596 @@
1
+ /**
2
+ * Behavior definitions for common command-line tools beyond coreutils:
3
+ * databases, document and media tools, downloaders, crypto, and local
4
+ * service CLIs. Most are table definitions (see ../dsl.ts); tools whose
5
+ * argument syntax the table cannot express (ffmpeg, sqlite3) are written out.
6
+ */
7
+
8
+ import type { AValue } from '../../analysis/value.ts'
9
+ import { concat, concrete, hole, known, knownPrefix } from '../../analysis/value.ts'
10
+ import type { Effect } from '../../policy/effects.ts'
11
+ import { define, type Def } from '../dsl.ts'
12
+ import type { Behavior, Invocation } from '../types.ts'
13
+
14
+ const args = (inv: Invocation): AValue[] => inv.argv.slice(1)
15
+ const isDash = (v: AValue): boolean => concrete(v) === '-'
16
+ const isUrl = (v: AValue): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(knownPrefix(v))
17
+
18
+ /** Accepts any flags and operands; no effects of its own (query subcommands). */
19
+ const QUERY: Def = { anyFlags: true, operands: 'arg*', stdin: false }
20
+ /** Changes state the definition cannot describe. */
21
+ const opaque = (detail: string): Def => ({ anyFlags: true, operands: 'arg*', opaque: detail, stdin: 'maybe' })
22
+
23
+ /** The directory part of a path value (`dir/` for `dir/x`), or undefined. */
24
+ function dirOf(p: AValue): AValue | undefined {
25
+ const s = concrete(p)
26
+ if (s === undefined) return undefined
27
+ const i = s.lastIndexOf('/')
28
+ return known(i <= 0 ? (i === 0 ? '/' : '.') : s.slice(0, i))
29
+ }
30
+
31
+ /** `x.typ` → `x.<ext>`: a sibling with another extension (unknown if the name is). */
32
+ function sibling(p: AValue, ext: string): AValue {
33
+ const s = concrete(p)
34
+ if (s !== undefined) return known(s.replace(/\.[^./]*$/, '') + ext)
35
+ const d = dirOf(p)
36
+ return d ? concat(d, known('/'), hole('filename', 'output name')) : hole('path', 'output file')
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // sqlite3
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** Dot commands that only print or set display options. */
44
+ const SQLITE_SAFE_DOT = /^\.(tables|schema|indexes|indices|headers?|mode|width|timer|timeout|nullvalue|separator|print|quit|exit|help|databases|changes|eqp|stats|bail|echo|show|dbinfo|fullschema|lint|selftrace|parameter|limits?|explain|columns|prompt|dump|version|vfsinfo|vfsname|vfslist|sha3sum|binary|crnl|expert|scanstats)\b/
45
+
46
+ function sqlTexts(v: AValue): string[] | undefined {
47
+ const out: string[] = []
48
+ for (const alt of v.alts) {
49
+ let s = ''
50
+ for (const seg of alt) {
51
+ if (typeof seg === 'string') s += seg
52
+ else if (seg.type === 'int') s += '0'
53
+ // a string inside quotes can only be data if it has no quote of its own: not knowable
54
+ else return undefined
55
+ }
56
+ out.push(s)
57
+ }
58
+ return out
59
+ }
60
+
61
+ const SQL_MODIFIES = /\b(insert|update|delete|replace|upsert|create|drop|alter|vacuum|reindex|analyze|begin|commit|savepoint|release|restore)\b|\bpragma\s+\w+(\.\w+)?\s*=|^\s*\.(restore|import)\b/im
62
+ const unquote = (w: string): string => w.replace(/^(['"])(.*)\1$/, '$2')
63
+
64
+ /**
65
+ * The files this SQL (with dot commands) touches beyond the database, or
66
+ * undefined when it can reach outside what can be described: other databases,
67
+ * extensions, the shell's file functions, `.read` scripts, pipes to commands.
68
+ */
69
+ function sqlFiles(text: string): { kind: 'read' | 'write'; file: string }[] | undefined {
70
+ const files: { kind: 'read' | 'write'; file: string }[] = []
71
+ for (const line of text.split('\n')) {
72
+ const t = line.trim()
73
+ if (!t.startsWith('.') || SQLITE_SAFE_DOT.test(t)) continue
74
+ const [cmd, ...words] = t.split(/\s+/).map(unquote)
75
+ const ops = words.filter((w) => !w.startsWith('-'))
76
+ const last = ops[ops.length - 1]
77
+ if (/^\.(backup|save|output|once)$/.test(cmd)) {
78
+ if (cmd === '.backup' || cmd === '.save' ? !last : false) return undefined
79
+ if (last === undefined) continue // `.output` alone: back to stdout
80
+ if (last.startsWith('|') || words.some((w) => /^-[ex]$/.test(w))) return undefined // to a command or an editor
81
+ files.push({ kind: 'write', file: last })
82
+ } else if (cmd === '.import' && ops[0] && !ops[0].startsWith('|')) files.push({ kind: 'read', file: ops[0] })
83
+ else return undefined
84
+ }
85
+ if (/\battach\b|\bvacuum\s+into\b|\b(load_extension|writefile|readfile|edit|fsdir|zipfile|sqlar_compress|sqlar_uncompress|lsmode)\s*\(/i.test(text.split('\n').filter((l) => !l.trim().startsWith('.')).join('\n'))) return undefined
86
+ return files
87
+ }
88
+
89
+ const sqlite3: Behavior = {
90
+ names: ['sqlite3', 'sqlite'],
91
+ summary: 'SQLite shell (sqlite3 [opts] DB [SQL...])',
92
+ analyze(inv, h) {
93
+ const p = h.parseArgs(args(inv), {
94
+ longSingle: true,
95
+ long: {
96
+ readonly: false, header: false, noheader: false, column: false, csv: false, json: false, line: false, list: false, box: false, markdown: false, table: false, html: false, tabs: false, quote: false, ascii: false,
97
+ batch: false, bail: false, echo: false, interactive: false, safe: false, nofollow: false, stats: false, version: false, help: false, memtrace: false, unsafe_testing: false,
98
+ separator: true, newline: true, nullvalue: true, cmd: true, init: true, vfs: true, mmap: true, append: false, deserialize: false, maxsize: true, nonce: true, A: true, zip: false,
99
+ },
100
+ })
101
+ if (p.flags.has('version') || p.flags.has('help')) return { effects: [], readsStdin: false }
102
+ const [db, ...sql] = p.operands
103
+ const effects: Effect[] = []
104
+ let complete = p.unknownFlags.length === 0 && !p.flags.has('A') && !p.flags.has('zip')
105
+ const init = p.flags.get('init')
106
+ if (init && init !== true) {
107
+ effects.push({ kind: 'read', path: h.resolve(init) })
108
+ complete = false
109
+ }
110
+ const programs = [...sql, ...(p.multi.get('cmd') ?? [])]
111
+ for (const s of programs) {
112
+ h.role(s, 'code')
113
+ for (const text of sqlTexts(s) ?? [undefined]) {
114
+ const files = text === undefined ? undefined : sqlFiles(text)
115
+ if (!files) complete = false
116
+ for (const f of files ?? []) effects.push({ kind: f.kind, path: h.resolve(known(f.file)) })
117
+ }
118
+ }
119
+ const name = db === undefined ? '' : concrete(db)
120
+ if (db !== undefined && name !== '' && name !== ':memory:') {
121
+ if (knownPrefix(db).startsWith('file:')) complete = false
122
+ const path = h.resolve(db)
123
+ effects.push({ kind: 'read', path })
124
+ if (!p.flags.has('readonly')) {
125
+ // statements that change the database write it; unknown or stdin SQL may
126
+ const texts = programs.map((v) => sqlTexts(v))
127
+ const modifies = texts.some((t) => t?.some((x) => SQL_MODIFIES.test(x)))
128
+ const unknown = texts.some((t) => t === undefined) || sql.length === 0
129
+ if (modifies || unknown) effects.push({ kind: 'write', path, ...(modifies ? {} : { maybe: true }) })
130
+ // even reads may create the -wal/-shm/-journal siblings
131
+ effects.push({ kind: 'write', path: { alts: ['-wal', '-shm', '-journal'].flatMap((sfx) => path.alts.map((a) => [...a, sfx])) }, maybe: true })
132
+ }
133
+ }
134
+ // no SQL arguments: statements come from stdin
135
+ if (sql.length === 0) {
136
+ if (programs.length === 0) complete = false
137
+ return { effects, readsStdin: true, complete }
138
+ }
139
+ return { effects, readsStdin: false, complete }
140
+ },
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // typst
145
+ // ---------------------------------------------------------------------------
146
+
147
+ const TYPST_GLOBAL = { short: 'hV', long: { help: false, version: false, color: true, cert: true } }
148
+ const TYPST_WORLD = {
149
+ root: true, input: true, 'font-path': true, 'ignore-system-fonts': false, 'ignore-embedded-fonts': false, 'package-path': true, 'package-cache-path': true,
150
+ 'creation-timestamp': true, features: true, 'diagnostic-format': true, jobs: '=j', format: '=f', ppi: true, pages: true, 'pdf-standard': true, 'no-pdf-tags': false,
151
+ 'make-deps': true, deps: true, 'deps-format': true, open: true, timings: true, field: true, one: false, target: true, pretty: false, 'no-serve': false, 'no-reload': false, port: true,
152
+ }
153
+
154
+ /** typst compile/watch/query: reads the project (input's directory or --root), may fetch packages. */
155
+ function typstCompile(withOutput: boolean): Def {
156
+ return {
157
+ spec: { shortWithArg: 'jf', long: TYPST_WORLD },
158
+ operands: withOutput ? 'read write?' : 'read pattern?',
159
+ flags: { 'font-path': 'read:dir', 'make-deps': 'write', deps: 'write', timings: 'write' },
160
+ extra: (r, { inv, h, p, operands }) => {
161
+ const effects = [...r.effects]
162
+ const [input, output] = operands
163
+ const rootFlag = p.flags.get('root')
164
+ const root = rootFlag && rootFlag !== true ? h.resolve(rootFlag) : input && !isDash(input) ? dirOf(h.resolve(input)) : inv.cwd
165
+ if (root) effects.push({ kind: 'read', path: root, recursive: true })
166
+ if (withOutput && input && !output && !isDash(input)) {
167
+ const fmt = concrete((p.flags.get('f') as AValue | undefined) ?? known('pdf')) ?? 'pdf'
168
+ effects.push({ kind: 'write', path: sibling(h.resolve(input), '.' + fmt) })
169
+ }
170
+ // `@preview` packages are downloaded into the cache on first use
171
+ effects.push({ kind: 'network', detail: 'typst packages', maybe: true })
172
+ const cache = p.flags.get('package-cache-path')
173
+ effects.push(cache && cache !== true
174
+ ? { kind: 'write', path: h.resolve(cache), recursive: true, maybe: true }
175
+ : { kind: 'write', path: concat(h.home() ?? known('~'), known(process.platform === 'darwin' ? '/Library/Caches/typst' : '/.cache/typst')), recursive: true, maybe: true })
176
+ const complete = r.complete !== false && !p.flags.has('open')
177
+ return { ...r, effects, complete, readsStdin: input !== undefined && isDash(input) }
178
+ },
179
+ }
180
+ }
181
+
182
+ const typst = define({
183
+ names: ['typst'], summary: 'typesetting', spec: TYPST_GLOBAL,
184
+ subcommands: {
185
+ 'compile|c': typstCompile(true),
186
+ 'watch|w': typstCompile(true),
187
+ 'query|q': { ...typstCompile(false), stdout: { type: 'string' } },
188
+ fonts: { spec: { long: { 'font-path': true, 'ignore-system-fonts': false, 'ignore-embedded-fonts': false, variants: false } }, flags: { 'font-path': 'read:dir' }, stdin: false },
189
+ 'init|update': opaque('typst downloads templates or updates itself'),
190
+ help: QUERY,
191
+ },
192
+ })
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // ffmpeg / ffprobe
196
+ // ---------------------------------------------------------------------------
197
+
198
+ const FFMPEG_BOOL = new Set([
199
+ '-y', '-n', '-nostdin', '-stdin', '-hide_banner', '-stats', '-nostats', '-an', '-vn', '-sn', '-dn', '-shortest', '-re', '-benchmark', '-benchmark_all', '-copyts', '-start_at_zero',
200
+ '-accurate_seek', '-noaccurate_seek', '-autorotate', '-noautorotate', '-ignore_unknown', '-copy_unknown', '-dump', '-hex', '-xerror', '-bitexact', '-debug_ts', '-version', '-h', '-help',
201
+ '-formats', '-codecs', '-encoders', '-decoders', '-filters', '-pix_fmts', '-layouts', '-sample_fmts', '-protocols', '-muxers', '-demuxers', '-devices', '-bsfs', '-buildconf', '-hwaccels',
202
+ '-show_format', '-show_streams', '-show_packets', '-show_frames', '-show_chapters', '-show_programs', '-show_error', '-show_data', '-show_private_data', '-show_program_version',
203
+ '-show_library_versions', '-show_versions', '-show_pixel_formats', '-count_frames', '-count_packets', '-pretty', '-unit', '-prefix', '-byte_binary_prefix', '-sexagesimal', '-show_optional_fields',
204
+ '-shortest_buf_duration', '-dts_delta_threshold', '-vsync', '-fix_sub_duration', '-apad', '-nostdin',
205
+ ])
206
+ /** Options whose value names a file read or written. */
207
+ const FFMPEG_READS = new Set(['-filter_complex_script', '-filter_script', '-attach', '-i'])
208
+ const FFMPEG_WRITES = new Set(['-vstats_file', '-passlogfile', '-progress', '-o'])
209
+ const FFMPEG_PATH_VALUE = new Set([...FFMPEG_READS, ...FFMPEG_WRITES, '-dump_attachment'])
210
+
211
+ const ffmpeg: Behavior = {
212
+ names: ['ffmpeg', 'ffprobe', 'ffplay'],
213
+ summary: 'audio/video conversion',
214
+ analyze(inv, h) {
215
+ const a = args(inv)
216
+ const probe = inv.name !== 'ffmpeg'
217
+ const effects: Effect[] = []
218
+ const inputs: AValue[] = []
219
+ const outputs: AValue[] = []
220
+ let complete = true
221
+ let nostdin = probe
222
+ let stdinInput = false
223
+ for (let i = 0; i < a.length; i++) {
224
+ const s = concrete(a[i])
225
+ if (s === undefined) {
226
+ // an unknown word that may be an option (and then perhaps take a value): not known
227
+ if (knownPrefix(a[i]).startsWith('-')) complete = false
228
+ else (probe ? inputs : outputs).push(a[i])
229
+ continue
230
+ }
231
+ if (s === '-' || !s.startsWith('-')) {
232
+ ;(probe ? inputs : outputs).push(a[i])
233
+ continue
234
+ }
235
+ if (s === '-nostdin') nostdin = true
236
+ if (FFMPEG_BOOL.has(s) || (/^-(no)?(show|count)_/.test(s) && s !== '-show_entries' && s !== '-show_optional_fields')) continue
237
+ if (s === '-report') {
238
+ effects.push({ kind: 'write', path: concat(inv.cwd, known('/'), hole('filename', 'ffmpeg report')) })
239
+ continue
240
+ }
241
+ const v = a[++i]
242
+ if (v === undefined) break
243
+ const name = s.replace(/:.*$/, '')
244
+ // a path-like value for an option not known to take one: maybe a boolean option we do not know, then an output file
245
+ if (!FFMPEG_PATH_VALUE.has(name) && /\/|\.[A-Za-z0-9]{2,4}$/.test(concrete(v) ?? '') && !/^-(metadata|vf|af|filter|filter_complex|lavfi|map|ss|t|to|sseof|f|c|codec|tag)$/.test(name)) complete = false
246
+ if (name === '-i') inputs.push(v)
247
+ else if (FFMPEG_READS.has(name)) effects.push({ kind: 'read', path: h.resolve(v) })
248
+ else if (FFMPEG_WRITES.has(name)) {
249
+ if (!isDash(v) && !isUrl(v) && concrete(v) !== 'pipe:1') effects.push({ kind: 'write', path: h.resolve(v) })
250
+ } else if (/^-(vf|af|filter|filter_complex|lavfi)$/.test(name)) {
251
+ h.role(v, 'code')
252
+ // filters can open (movie=, subtitles=) or write (file=, stats_file=) files
253
+ const t = concrete(v)
254
+ if (t === undefined || /\b(movie|amovie|subtitles|ass|lut3d|lut1d|haldclut|sendcmd|asendcmd|zmq|azmq|file|filename|stats_file|result|f|ocr)\s*=/.test(t) || /\bf=/.test(t)) complete = false
255
+ } else if (name === '-f' && /^(concat|lavfi|tee|segment|hls|dash|image2pipe|fifo)$/.test(concrete(v) ?? '')) {
256
+ // inputs listing other files, filter-graph inputs, several outputs
257
+ if (!/^(segment|hls|dash)$/.test(concrete(v)!)) complete = false
258
+ }
259
+ }
260
+ for (const v of inputs) {
261
+ const t = concrete(v)
262
+ if (t === '-' || t === 'pipe:' || t === 'pipe:0') stdinInput = true
263
+ else if (isUrl(v)) effects.push({ kind: 'network', target: v, detail: inv.name })
264
+ else if (t !== undefined && /^[a-z]+:/.test(t) && !/^file:/.test(t)) complete = false // concat:, subfile:, … protocols
265
+ else effects.push({ kind: 'read', path: h.resolve(v) })
266
+ }
267
+ for (const v of outputs) {
268
+ const t = concrete(v)
269
+ if (t === '-' || t === 'pipe:' || t === 'pipe:1') continue
270
+ if (isUrl(v)) effects.push({ kind: 'network', target: v, detail: inv.name })
271
+ else effects.push({ kind: 'write', path: h.resolve(v) })
272
+ }
273
+ // ffmpeg reads keyboard commands from stdin unless -nostdin
274
+ return { effects, readsStdin: stdinInput || (nostdin ? false : 'maybe'), complete }
275
+ },
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // yt-dlp
280
+ // ---------------------------------------------------------------------------
281
+
282
+ const ytdlp = define({
283
+ names: ['yt-dlp', 'youtube-dl'], summary: 'download videos',
284
+ spec: {
285
+ short: 'FqvsjJgeixwciUN', shortWithArg: 'foPNraRIS',
286
+ long: {
287
+ format: '=f', 'list-formats': '=F', output: '=o', paths: '=P', 'concurrent-fragments': '=N', quiet: '=q', verbose: '=v', 'no-progress': false, progress: false, 'no-part': false,
288
+ 'cookies-from-browser': true, cookies: true, 'extractor-args': true, 'skip-download': false, print: true, 'print-to-file': 2, 'flat-playlist': false, 'dump-json': '=j', 'dump-single-json': '=J',
289
+ simulate: '=s', 'no-simulate': false, version: false, help: false, 'get-url': '=g', 'get-title': '=e', 'extract-audio': '=x', 'audio-format': true, 'audio-quality': true, 'merge-output-format': true,
290
+ 'remux-video': true, 'recode-video': true, 'write-subs': false, 'write-auto-subs': false, 'sub-langs': true, 'sub-format': true, 'embed-subs': false, 'write-thumbnail': false, 'embed-thumbnail': false,
291
+ 'embed-metadata': false, 'no-playlist': false, 'yes-playlist': false, 'playlist-items': '=I', 'limit-rate': '=r', retries: '=R', 'batch-file': '=a', 'no-warnings': false, 'no-overwrites': '=w',
292
+ continue: '=c', newline: false, 'restrict-filenames': false, impersonate: true, 'user-agent': true, referer: true, 'add-header': true, 'socket-timeout': true, 'sleep-interval': true,
293
+ 'ignore-errors': '=i', 'abort-on-error': false, 'download-sections': true, 'force-overwrites': false, 'no-mtime': false, 'write-info-json': false, 'write-description': false, 'list-subs': false,
294
+ 'list-thumbnails': false, 'get-filename': false, 'no-cache-dir': false, 'cache-dir': true, proxy: true, 'geo-bypass': false, 'match-filters': true, 'match-filter': true, 'download-archive': true,
295
+ 'js-runtimes': true, 'remote-components': true, 'format-sort': '=S', 'no-color': false, 'max-downloads': true, 'min-filesize': true, 'max-filesize': true, 'date': true, 'dateafter': true, 'datebefore': true,
296
+ 'exec': true, 'update': '=U', 'external-downloader': true, 'downloader': true, 'postprocessor-args': true, 'ppa': true, 'ffmpeg-location': true, 'split-chapters': false, 'trim-filenames': true, 'windows-filenames': false,
297
+ },
298
+ },
299
+ operands: 'url*', stdin: false,
300
+ flags: { a: 'read', cookies: 'read', 'download-archive': 'append', 'print-to-file': 'append', P: 'write:dir' },
301
+ extra: (r, { inv, h, p }) => {
302
+ const effects = [...r.effects]
303
+ const noDownload = ['F', 'j', 'J', 's', 'g', 'e', 'skip-download', 'list-subs', 'list-thumbnails', 'get-filename', 'version', 'help', 'print'].some((k) => p.flags.has(k)) && !p.flags.has('no-simulate')
304
+ if (p.flags.has('version') || p.flags.has('help')) return { effects: [], readsStdin: false }
305
+ if (!noDownload) {
306
+ const o = p.flags.get('o')
307
+ const P = p.flags.get('P')
308
+ const base = P && P !== true ? h.resolve(P) : inv.cwd
309
+ // the output template (and the merged/temporary files next to it); a template may add directories
310
+ const out = o && o !== true && !isDash(o) ? (knownPrefix(o).startsWith('/') ? o : concat(base, known('/'), o)) : concat(base, known('/'), hole('filename', 'video title'))
311
+ if (!(o && o !== true && isDash(o))) effects.push({ kind: 'write', path: out })
312
+ }
313
+ const cookies = p.flags.get('cookies')
314
+ if (cookies && cookies !== true) effects.push({ kind: 'write', path: h.resolve(cookies), maybe: true })
315
+ // the browser's profile (cookie store)
316
+ if (p.flags.has('cookies-from-browser')) effects.push({ kind: 'read', path: concat(h.home() ?? known('~'), known(process.platform === 'darwin' ? '/Library' : '/.config')), recursive: true, maybe: true })
317
+ if (!p.flags.has('no-cache-dir')) effects.push({ kind: 'write', path: concat(h.home() ?? known('~'), known('/.cache/yt-dlp')), recursive: true, maybe: true })
318
+ // --exec runs commands; --update replaces the program; external downloaders run other programs
319
+ const complete = r.complete !== false && !['exec', 'U', 'external-downloader', 'downloader', 'postprocessor-args', 'ppa'].some((k) => p.flags.has(k))
320
+ // URLs from a batch file (or none given): still network
321
+ if (!effects.some((e) => e.kind === 'network')) effects.unshift({ kind: 'network', detail: inv.name })
322
+ return { ...r, effects, complete, readsStdin: p.flags.has('a') && isDash(p.flags.get('a') as AValue) }
323
+ },
324
+ })
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // PDF tools (poppler), ImageMagick identify
328
+ // ---------------------------------------------------------------------------
329
+
330
+ const POPPLER_SHORT = { short: 'qvh', shortWithArg: 'flrxyWH' }
331
+ const POPPLER_LONG = { opw: true, upw: true, q: false, v: false, h: false, help: false, enc: true, f: true, l: true, r: true, x: true, y: true, W: true, H: true, cropbox: false, nodiag: false }
332
+
333
+ const pdftools: Behavior[] = [
334
+ define({
335
+ names: ['pdftotext'], summary: 'PDF to text (pdftotext in.pdf [out.txt|-])',
336
+ spec: { ...POPPLER_SHORT, longSingle: true, long: { ...POPPLER_LONG, layout: false, fixed: true, raw: false, htmlmeta: false, bbox: false, 'bbox-layout': false, tsv: false, eol: true, nopgbrk: false, colspacing: true } },
337
+ operands: 'read write?', stdin: false,
338
+ extra: (r, { h, operands }) => {
339
+ const [input, output] = operands
340
+ if (output && isDash(output)) return { ...r, effects: r.effects.filter((e) => e.kind !== 'write') }
341
+ if (input && !output && !isDash(input)) return { ...r, effects: [...r.effects, { kind: 'write', path: sibling(h.resolve(input), '.txt') }] }
342
+ return r
343
+ },
344
+ }),
345
+ define({
346
+ names: ['pdfinfo', 'pdffonts', 'pdfsig', 'pdfdetach'], summary: 'inspect PDFs',
347
+ spec: { ...POPPLER_SHORT, longSingle: true, long: { ...POPPLER_LONG, box: false, meta: false, js: false, struct: false, 'struct-text': false, rawdates: false, isodates: false, dests: false, url: false, listenc: false, subst: false, list: false, nodump: false } },
348
+ operands: 'read*', stdin: false,
349
+ when: { saveall: { opaque: 'pdfdetach writes attachments' }, save: { opaque: 'pdfdetach writes attachments' } },
350
+ }),
351
+ define({
352
+ names: ['pdftoppm', 'pdftocairo', 'pdfimages'], summary: 'render PDF pages to images (… in.pdf ROOT)',
353
+ spec: { ...POPPLER_SHORT, longSingle: true, long: { ...POPPLER_LONG, png: false, jpeg: false, tiff: false, ppm: false, pdf: false, ps: false, eps: false, svg: false, j: false, all: false, list: false, p: false, print: false, 'scale-to': true, 'scale-to-x': true, 'scale-to-y': true, singlefile: false, gray: false, mono: false, jpegopt: true, aa: true, aaVector: true, rx: true, ry: true, sep: true, forcenum: false, 'transp': false, 'hide-annotations': false, 'o': false, 'e': false, 'struct': false } },
354
+ operands: 'read arg?', stdin: false,
355
+ // pages are written as ROOT-N.ext (or ROOT.ext): a name starting with ROOT
356
+ extra: (r, { h, operands }) => {
357
+ const root = operands[1]
358
+ if (!root || isDash(root)) return r
359
+ const base = h.resolve(root)
360
+ return { ...r, effects: [...r.effects, { kind: 'write', path: { alts: base.alts.map((a) => [...a, hole('filename', 'page suffix').alts[0][0]]) } }] }
361
+ },
362
+ }),
363
+ define({ names: ['pdfunite'], summary: 'merge PDFs (pdfunite in... out)', spec: { short: 'vh', longSingle: true, long: { help: false } }, operands: 'read* write', stdin: false }),
364
+ define({ names: ['pdfseparate'], summary: 'split a PDF (pdfseparate in out-%d.pdf)', spec: { short: 'vh', shortWithArg: 'fl', longSingle: true }, operands: 'read write', stdin: false }),
365
+ define({
366
+ names: ['identify'], summary: 'describe images (ImageMagick)',
367
+ spec: { longSingle: true, long: { format: true, verbose: false, ping: false, quiet: false, 'regard-warnings': false, density: true, units: true, precision: true, define: true, moments: false, features: true, unique: false, size: true, depth: true, list: true, version: false, help: false, colorspace: true, channel: true, alpha: true, auto_orient: false, debug: true, limit: 2, monitor: false } },
368
+ operands: 'read*', stdin: false,
369
+ // `@list` reads file names from a file; coders like `msl:` run scripts
370
+ extra: (r, { operands }) => (operands.some((o) => /^@|^[a-z]{2,}:/i.test(knownPrefix(o)) || concrete(o) === undefined && knownPrefix(o) === '') ? { ...r, complete: false } : r),
371
+ }),
372
+ ]
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // crypto and keys
376
+ // ---------------------------------------------------------------------------
377
+
378
+ /** openssl SUBCOMMAND with single-dash options: -in/-key/… read, -out/-keyout write. */
379
+ const OPENSSL_READ = new Set(['-in', '-key', '-inkey', '-CAfile', '-CA', '-CAkey', '-cert', '-certfile', '-config', '-signature', '-extfile', '-paramfile', '-kfile', '-verify', '-prverify', '-peerkey', '-sigfile', '-untrusted', '-chain', '-CRLfile', '-passin', '-dcert', '-dkey', '-reqin', '-signkey', '-CAserial', '-rand'])
380
+ const OPENSSL_WRITE = new Set(['-out', '-keyout', '-sessout', '-msgfile', '-signer', '-pubout_file'])
381
+ const OPENSSL_READONLY = new Set(['version', 'rand', 'x509', 'dgst', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'base64', 'enc', 'req', 'rsa', 'ec', 'pkey', 'pkeyutl', 'verify', 'asn1parse', 'crl', 'pkcs7', 'pkcs12', 'cms', 'smime', 'genrsa', 'genpkey', 'ecparam', 'dhparam', 'prime', 'passwd', 'list', 'help', 'speed', 'errstr', 'ciphers', 'kdf', 'mac', 'storeutl', 'nseq', 'spkac', 'ts', 'rsautl', 'gendsa', 'dsa', 'dsaparam', 'crl2pkcs7', 'sess_id', 'info'])
382
+
383
+ const openssl: Behavior = {
384
+ names: ['openssl'],
385
+ summary: 'cryptography toolkit',
386
+ analyze(inv, h) {
387
+ const a = args(inv)
388
+ const sub = concrete(a[0] ?? known('')) ?? ''
389
+ if (a[0]) h.role(a[0], 'subcommand')
390
+ const effects: Effect[] = []
391
+ if (sub === 's_client' || sub === 's_time' || sub === 'ocsp') {
392
+ return { effects: [{ kind: 'network', detail: `openssl ${sub}`, target: a.slice(1).find((_, k) => concrete(a[k]) === '-connect') }], readsStdin: true, complete: true }
393
+ }
394
+ if (!OPENSSL_READONLY.has(sub)) return { effects: [{ kind: 'unknown', detail: `openssl ${sub || '(interactive)'}` }], readsStdin: 'maybe', complete: false }
395
+ let complete = true
396
+ let readsIn = false
397
+ let explicitIn = false
398
+ const operands: AValue[] = []
399
+ for (let i = 1; i < a.length; i++) {
400
+ const s = concrete(a[i])
401
+ if (s === undefined) {
402
+ complete = false
403
+ continue
404
+ }
405
+ if (!s.startsWith('-') || s === '-') {
406
+ operands.push(a[i])
407
+ continue
408
+ }
409
+ const v = a[i + 1]
410
+ if (OPENSSL_READ.has(s) && v) {
411
+ i++
412
+ if (s === '-in') explicitIn = true
413
+ const t = concrete(v)
414
+ if (s === '-passin') {
415
+ if (t?.startsWith('file:')) effects.push({ kind: 'read', path: h.resolve(known(t.slice(5))) })
416
+ continue
417
+ }
418
+ if (t === '-' || t === 'stdin') readsIn = true
419
+ else effects.push({ kind: 'read', path: h.resolve(v) })
420
+ } else if (OPENSSL_WRITE.has(s) && v) {
421
+ i++
422
+ if (!isDash(v)) effects.push({ kind: 'write', path: h.resolve(v) })
423
+ } else if (s === '-engine' || s === '-provider-path' || s === '-provider') {
424
+ // loads code
425
+ complete = false
426
+ i++
427
+ } else if (s === '-sign' || s === '-hmac' || s === '-mac' || s === '-pass' || s === '-passout' || s === '-subj' || s === '-days' || s === '-newkey' || s === '-name' || s === '-digest' || s === '-cipher' || s === '-pkeyopt' || s === '-sigopt' || s === '-nameopt' || s === '-certopt' || s === '-set_serial' || s === '-addext' || s === '-extensions' || s === '-section' || s === '-K' || s === '-iv' || s === '-S' || s === '-md' || s === '-iter' || s === '-inform' || s === '-outform' || s === '-keyform' || s === '-certform' || s === '-algorithm' || s === '-macopt' || s === '-kdfopt' || s === '-keylen' || s === '-bits' || s === '-group' || s === '-servername' || s === '-connect') {
428
+ if (s === '-sign' && v) effects.push({ kind: 'read', path: h.resolve(v) })
429
+ if (s === '-passout' && concrete(v ?? known(''))?.startsWith('file:')) effects.push({ kind: 'write', path: h.resolve(known(concrete(v!)!.slice(5))) })
430
+ i++
431
+ }
432
+ }
433
+ // dgst/sha*/x509 … take files as operands; rand/prime take numbers
434
+ const fileOps = ['dgst', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'asn1parse'].includes(sub)
435
+ if (fileOps) for (const o of operands) if (!isDash(o)) effects.push({ kind: 'read', path: h.resolve(o) })
436
+ const usesStdin = !explicitIn && !['version', 'rand', 'help', 'list', 'genrsa', 'genpkey', 'ecparam', 'prime', 'speed', 'errstr', 'ciphers', 'kdf', 'passwd', 'dhparam', 'gendsa', 'dsaparam'].includes(sub) && !(fileOps && operands.length > 0)
437
+ return { effects, readsStdin: readsIn || usesStdin, complete }
438
+ },
439
+ }
440
+
441
+ const sshKeygen: Behavior = {
442
+ names: ['ssh-keygen'],
443
+ summary: 'create and inspect SSH keys',
444
+ analyze(inv, h) {
445
+ const p = h.parseArgs(args(inv), { short: 'ABcDeEHiklLmMpqQrRuUvyYgK', shortWithArg: 'abCEfFgIJjmNnOPrRstVwYzZDTGSM' })
446
+ const effects: Effect[] = []
447
+ const home = h.home() ?? known('~')
448
+ const f = p.flags.get('f')
449
+ const file = f && f !== true ? h.resolve(f) : undefined
450
+ const knownHosts = file ?? concat(home, known('/.ssh/known_hosts'))
451
+ const query = ['l', 'y', 'B', 'e', 'i', 'D', 'k', 'Q', 'L'].some((k) => p.flags.has(k))
452
+ let complete = p.unknownFlags.length === 0
453
+ if (p.flags.has('R') || p.flags.has('H')) effects.push({ kind: 'write', path: knownHosts })
454
+ else if (p.flags.has('F')) effects.push({ kind: 'read', path: knownHosts })
455
+ else if (p.flags.has('Y')) {
456
+ const mode = concrete(p.flags.get('Y') as AValue) ?? ''
457
+ if (file) effects.push({ kind: 'read', path: file })
458
+ for (const o of p.operands) {
459
+ effects.push({ kind: 'read', path: h.resolve(o) })
460
+ if (mode === 'sign') effects.push({ kind: 'write', path: concat(h.resolve(o), known('.sig')) })
461
+ }
462
+ if (mode !== 'sign' && mode !== 'verify' && mode !== 'check-novalidate' && mode !== 'find-principals') complete = false
463
+ } else if (query || p.flags.has('p') || p.flags.has('c')) {
464
+ if (file) effects.push({ kind: (p.flags.has('p') || p.flags.has('c')) ? 'write' : 'read', path: file })
465
+ else complete = false // prompts for the file
466
+ } else {
467
+ // key generation: FILE and FILE.pub (default ~/.ssh/id_TYPE, asked interactively)
468
+ if (file) effects.push({ kind: 'write', path: file }, { kind: 'write', path: concat(file, known('.pub')) })
469
+ else {
470
+ effects.push({ kind: 'write', path: concat(home, known('/.ssh/'), hole('filename', 'key name')) })
471
+ complete = false
472
+ }
473
+ }
474
+ return { effects, readsStdin: !p.flags.has('N') || !file ? 'maybe' : false, complete }
475
+ },
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // developer tools that do not run project code
480
+ // ---------------------------------------------------------------------------
481
+
482
+ /** tsc: reads the project; emits only without --noEmit (outDir comes from tsconfig: not known). */
483
+ const TSC_VALUE = new Set(['-p', '--project', '--module', '-m', '--moduleResolution', '--target', '-t', '--lib', '--jsx', '--types', '--typeRoots', '--outDir', '--rootDir', '--rootDirs', '--baseUrl', '--declarationDir', '--outFile', '--tsBuildInfoFile', '--generateTrace', '--locale', '--maxNodeModuleJsDepth', '--ignoreDeprecations', '--moduleDetection', '--newLine', '--paths', '--customConditions', '--watchFile', '--watchDirectory', '--fallbackPolling', '--excludeDirectories', '--excludeFiles', '--jsxFactory', '--jsxFragmentFactory', '--jsxImportSource', '--importsNotUsedAsValues', '--moduleSuffixes', '--plugins', '--extendedDiagnostics', '--generateCpuProfile', '--explainFiles', '--listEmittedFiles', '--diagnostics'])
484
+ const tsc: Behavior = {
485
+ names: ['tsc'],
486
+ summary: 'TypeScript compiler',
487
+ analyze(inv, h) {
488
+ const a = args(inv)
489
+ const s = a.map((x) => concrete(x))
490
+ if (s.length === 1 && ['-v', '--version', '-h', '--help', '--all'].includes(s[0] ?? '')) return { effects: [], readsStdin: false }
491
+ let dir: AValue = inv.cwd
492
+ const effects: Effect[] = []
493
+ const files: AValue[] = []
494
+ let noEmit = false
495
+ let complete = true
496
+ for (let i = 0; i < a.length; i++) {
497
+ const t = s[i]
498
+ if (t === undefined) {
499
+ complete = false
500
+ continue
501
+ }
502
+ if (t === '--noEmit' && s[i + 1] !== 'false') noEmit = true
503
+ if (TSC_VALUE.has(t) && a[i + 1]) {
504
+ const v = a[++i]
505
+ if (t === '-p' || t === '--project') dir = h.resolve(v)
506
+ else if (t === '--generateTrace' || t === '--tsBuildInfoFile' || t === '--generateCpuProfile') effects.push({ kind: 'write', path: h.resolve(v), recursive: t === '--generateTrace' })
507
+ else if (t === '--outDir' || t === '--declarationDir' || t === '--outFile') effects.push({ kind: 'write', path: h.resolve(v), recursive: t !== '--outFile', maybe: true })
508
+ continue
509
+ }
510
+ if (t.startsWith('-')) {
511
+ if (s[i + 1] === 'true' || s[i + 1] === 'false') i++
512
+ continue
513
+ }
514
+ files.push(a[i])
515
+ }
516
+ for (const f of files) effects.push({ kind: 'read', path: h.resolve(f) })
517
+ effects.unshift({ kind: 'read', path: dir, recursive: true })
518
+ if (!noEmit) {
519
+ // emits next to the sources or into tsconfig's outDir
520
+ effects.push({ kind: 'write', path: dir, recursive: true, maybe: true })
521
+ complete = false
522
+ }
523
+ return { effects, readsStdin: false, complete }
524
+ },
525
+ }
526
+
527
+ /** Linters and formatters: read their operands (or the cwd); fix/write flags rewrite them. */
528
+ const LINT: Def = {
529
+ anyFlags: true, operands: 'read*', cwdIfNone: true, recursive: true, stdin: false,
530
+ spec: { long: { fix: false, write: false, unsafe: false, 'unsafe-fixes': false, 'fix-only': false, 'fix-dangerously': false, apply: false, 'apply-unsafe': false, diff: false, check: false, config: true, 'config-path': true, format: true, 'output-format': true, select: true, ignore: true, 'extend-select': true, 'target-version': true, 'line-length': true, 'max-diagnostics': true, reporter: true, severity: true, shell: true, exclude: true, include: true } },
531
+ flags: { config: 'read', 'config-path': 'read' },
532
+ when: { 'fix|write|unsafe|unsafe-fixes|fix-only|fix-dangerously|apply|apply-unsafe': { operands: 'write*' } },
533
+ }
534
+ const lintTools = [
535
+ define({ names: ['oxlint', 'shellcheck', 'markdownlint', 'markdownlint-cli2', 'actionlint', 'hadolint', 'yamllint', 'eslint_d'], summary: 'linters (read files; --fix rewrites them)', ...LINT }),
536
+ define({
537
+ names: ['ruff', 'biome', 'taplo'], summary: 'linters / formatters', spec: { long: { version: false, help: false } },
538
+ subcommands: {
539
+ 'check|lint|ci': LINT,
540
+ // `ruff format` and `taplo fmt` rewrite files unless --check/--diff; `biome format` only with --write
541
+ 'format|fmt': { ...LINT, extra: (r, { inv, p }) => (inv.name !== 'biome' && !p.flags.has('check') && !p.flags.has('diff') ? { ...r, effects: r.effects.map((e) => (e.kind === 'read' ? { kind: 'write', path: e.path, recursive: true } : e)) } : r) },
542
+ 'version|help|rule|explain|rage|config|linter|analyze': QUERY,
543
+ '*': opaque('linter subcommand'),
544
+ },
545
+ }),
546
+ ]
547
+
548
+ // ---------------------------------------------------------------------------
549
+ // local daemons and services
550
+ // ---------------------------------------------------------------------------
551
+
552
+ const lms = define({
553
+ names: ['lms'], summary: 'LM Studio CLI', spec: { short: 'h', long: { help: false, version: false } },
554
+ subcommands: {
555
+ 'ps|ls|status|version|help|log': QUERY,
556
+ 'load|unload|server': { ...QUERY, effects: [{ kind: 'process', detail: 'LM Studio server state' }] },
557
+ get: { ...QUERY, effects: [{ kind: 'network', detail: 'lms get' }, { kind: 'write', path: '~/.lmstudio', recursive: true, maybe: true }] },
558
+ '*': opaque('lms'),
559
+ },
560
+ })
561
+
562
+ const tailscale = define({
563
+ names: ['tailscale'], summary: 'Tailscale CLI', spec: { long: { socket: true, help: false, version: false } },
564
+ subcommands: {
565
+ 'status|ip|version|whoami|dns|exit-node|metrics|licenses|bugreport|help|completion': QUERY,
566
+ 'ping|netcheck|nc': { ...QUERY, effects: [{ kind: 'network', detail: 'tailscale' }] },
567
+ 'switch': { ...QUERY, when: { list: QUERY }, opaque: 'tailscale switch changes the account' },
568
+ 'serve|funnel': { ...opaque('tailscale serve changes the node configuration'), when: { json: QUERY } },
569
+ '*': opaque('tailscale changes the node configuration'),
570
+ },
571
+ })
572
+
573
+ const netlify = define({
574
+ names: ['netlify', 'ntl'], summary: 'Netlify CLI', spec: { long: { version: false, help: false, debug: false, auth: true } },
575
+ subcommands: {
576
+ 'status|sites:list|sites:create|api|env:list|env:get|open:admin|watch|logs|logs:function|functions:list|teams:list|help|status:hooks|unlink|switch': { ...QUERY, effects: [{ kind: 'network', detail: 'netlify' }] },
577
+ // builds run project code unless --no-build
578
+ deploy: { anyFlags: true, operands: 'arg*', stdin: false, spec: { long: { dir: true, functions: true, 'no-build': false, prod: false, site: true, message: true, alias: true, json: false } }, flags: { dir: 'read:dir', functions: 'read:dir' }, effects: [{ kind: 'network', detail: 'netlify deploy' }, { kind: 'write', path: '.netlify', recursive: true, maybe: true }], recursive: true, opaque: 'netlify deploy may run the build', when: { 'no-build': { opaque: undefined } } },
579
+ '*': opaque('netlify runs project scripts or changes the site'),
580
+ },
581
+ })
582
+
583
+ const hf = define({
584
+ names: ['hf', 'huggingface-cli'], summary: 'Hugging Face CLI', spec: { long: { help: false, version: false, token: true } },
585
+ subcommands: {
586
+ 'whoami|env|version|help': { ...QUERY, effects: [{ kind: 'network', detail: 'hf', maybe: true }] },
587
+ download: { ...QUERY, spec: { long: { 'local-dir': true, 'cache-dir': true } }, flags: { 'local-dir': 'write:dir', 'cache-dir': 'write:dir' }, effects: [{ kind: 'network', detail: 'hf download' }, { kind: 'write', path: '~/.cache/huggingface', recursive: true, maybe: true }] },
588
+ upload: { anyFlags: true, operands: 'arg read*', stdin: false, recursive: true, effects: [{ kind: 'network', detail: 'hf upload' }] },
589
+ '*': opaque('hf'),
590
+ },
591
+ })
592
+
593
+ export const toolBehaviors: Behavior[] = [
594
+ sqlite3, typst, ffmpeg, ytdlp, ...pdftools, openssl, sshKeygen, tsc, ...lintTools, lms, tailscale, netlify, hf,
595
+ ]
596
+