@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.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- 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
|
+
|