@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,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior definitions for developer tools: git, package managers,
|
|
3
|
+
* interpreters and build tools. Interpreters and build tools run arbitrary
|
|
4
|
+
* code, so they report `unknown` effects (and `complete: false`) — the OS
|
|
5
|
+
* sandbox stays responsible for confining them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs'
|
|
9
|
+
import * as path from 'node:path'
|
|
10
|
+
import type { AValue } from '../../analysis/value.ts'
|
|
11
|
+
import { concat, concrete, hole, known, knownPrefix } from '../../analysis/value.ts'
|
|
12
|
+
import type { Effect } from '../../policy/effects.ts'
|
|
13
|
+
import type { Behavior, BehaviorHelpers, BehaviorResult, Invocation } from '../types.ts'
|
|
14
|
+
|
|
15
|
+
const args = (inv: Invocation): AValue[] => inv.argv.slice(1)
|
|
16
|
+
|
|
17
|
+
/** Find the directory containing `marker` at or above `dir` (only for concrete paths). */
|
|
18
|
+
function findUp(dir: string, marker: string): string | undefined {
|
|
19
|
+
let cur = dir
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (fs.existsSync(path.join(cur, marker))) return cur
|
|
22
|
+
const parent = path.dirname(cur)
|
|
23
|
+
if (parent === cur) return undefined
|
|
24
|
+
cur = parent
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GIT_READ = new Set([
|
|
29
|
+
'status', 'log', 'diff', 'show', 'rev-parse', 'ls-files', 'ls-tree', 'blame', 'grep', 'describe', 'shortlog', 'reflog',
|
|
30
|
+
'cat-file', 'symbolic-ref', 'for-each-ref', 'merge-base', 'name-rev', 'rev-list', 'help', 'version', 'check-ignore',
|
|
31
|
+
'whatchanged', 'count-objects', 'show-ref', 'var', 'verify-commit', 'difftool', 'range-diff', 'annotate', 'cherry',
|
|
32
|
+
'diff-tree', 'diff-index', 'diff-files', 'fsck', 'show-branch', 'check-attr', 'check-ref-format', 'check-mailmap', 'verify-pack',
|
|
33
|
+
'verify-tag', 'patch-id', 'get-tar-commit-id', 'column', 'fmt-merge-msg', 'stripspace', 'interpret-trailers', 'show-index',
|
|
34
|
+
])
|
|
35
|
+
const GIT_WRITE = new Set([
|
|
36
|
+
'add', 'commit', 'checkout', 'switch', 'restore', 'reset', 'rm', 'mv', 'merge', 'rebase', 'cherry-pick', 'revert', 'stash',
|
|
37
|
+
'init', 'apply', 'am', 'gc', 'update-index', 'notes', 'clean', 'worktree', 'prune', 'repack', 'update-ref', 'mergetool',
|
|
38
|
+
'bisect', 'sparse-checkout', 'format-patch', 'commit-tree', 'read-tree', 'write-tree', 'hash-object', 'replace', 'restore',
|
|
39
|
+
'maintenance', 'lfs',
|
|
40
|
+
])
|
|
41
|
+
const GIT_NETWORK = new Set(['fetch', 'pull', 'push', 'ls-remote', 'clone', 'submodule', 'remote'])
|
|
42
|
+
|
|
43
|
+
const GIT_NESTED = new Set(['stash', 'remote', 'worktree', 'submodule', 'bisect', 'notes', 'lfs', 'sparse-checkout', 'maintenance', 'reflog'])
|
|
44
|
+
|
|
45
|
+
/** Subcommands that read stdin (patches, object lists, messages): never cut off from it. */
|
|
46
|
+
const GIT_STDIN = new Set(['apply', 'am', 'hash-object', 'patch-id', 'stripspace', 'interpret-trailers', 'fmt-merge-msg', 'mktree', 'mailinfo', 'mailsplit', 'fast-import', 'pack-objects', 'unpack-objects', 'index-pack', 'credential', 'cat-file', 'update-index'])
|
|
47
|
+
|
|
48
|
+
function gitBehavior(): Behavior {
|
|
49
|
+
return {
|
|
50
|
+
names: ['git'],
|
|
51
|
+
summary: 'version control',
|
|
52
|
+
analyze(inv, h) {
|
|
53
|
+
const r = analyzeGit(inv, h)
|
|
54
|
+
if (r.readsStdin === false) {
|
|
55
|
+
const a = args(inv).map((x) => concrete(x))
|
|
56
|
+
const sub = a.find((x) => x !== undefined && !x.startsWith('-'))
|
|
57
|
+
if ((sub && GIT_STDIN.has(sub)) || a.includes('--stdin') || a.includes('-') || a.includes('--batch') || a.includes('--batch-check')) return { ...r, readsStdin: 'maybe' }
|
|
58
|
+
}
|
|
59
|
+
return r
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function analyzeGit(inv: Invocation, h: BehaviorHelpers): BehaviorResult {
|
|
65
|
+
{
|
|
66
|
+
{
|
|
67
|
+
const a = args(inv)
|
|
68
|
+
let cwd = inv.cwd
|
|
69
|
+
let i = 0
|
|
70
|
+
for (; i < a.length; i++) {
|
|
71
|
+
const s = concrete(a[i])
|
|
72
|
+
if (s === undefined) return { effects: [{ kind: 'unknown', detail: 'git with unknown arguments' }], complete: false, readsStdin: false }
|
|
73
|
+
if (s === '-C') {
|
|
74
|
+
const d = a[++i]
|
|
75
|
+
if (d) cwd = h.resolve(d)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (s === '-c' || s === '--git-dir' || s === '--work-tree' || s === '--namespace') {
|
|
79
|
+
i++
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (s.startsWith('-')) continue
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
const sub = concrete(a[i] ?? known('')) ?? ''
|
|
86
|
+
if (a[i]) h.role(a[i], 'subcommand')
|
|
87
|
+
// second-level subcommands: `git stash pop`, `git remote add`, `git worktree list`
|
|
88
|
+
if (GIT_NESTED.has(sub)) {
|
|
89
|
+
const next = a.slice(i + 1).find((x) => !knownPrefix(x).startsWith('-'))
|
|
90
|
+
if (next && /^[a-z][a-z-]*$/.test(concrete(next) ?? '')) h.role(next, 'subcommand')
|
|
91
|
+
}
|
|
92
|
+
const rest = a.slice(i + 1)
|
|
93
|
+
const cwdStr = concrete(cwd)
|
|
94
|
+
const root = cwdStr ? findUp(cwdStr, '.git') : undefined
|
|
95
|
+
const repo: AValue = root ? known(root) : cwdStr ? hole('path', 'git repository root') : hole('path', 'git repository root')
|
|
96
|
+
const effects: Effect[] = []
|
|
97
|
+
const restOps = rest.filter((r) => !knownPrefix(r).startsWith('-'))
|
|
98
|
+
const hasFlag = (...f: string[]): boolean => rest.some((r) => f.includes(concrete(r) ?? ''))
|
|
99
|
+
if (sub === '' || sub === '--version' || sub === 'version' || sub === 'help') return { effects: [], readsStdin: false }
|
|
100
|
+
if (sub === 'config') {
|
|
101
|
+
const write = !hasFlag('--get', '--get-all', '--list', '-l', '--get-regexp', '--show-origin') && restOps.length >= 2
|
|
102
|
+
if (hasFlag('--global')) effects.push({ kind: write ? 'write' : 'read', path: { alts: [[...(h.home()?.alts[0] ?? ['~']), '/.gitconfig']] } })
|
|
103
|
+
else effects.push({ kind: write ? 'write' : 'read', path: { alts: [[...repo.alts[0], '/.git/config']] } })
|
|
104
|
+
return { effects, readsStdin: false }
|
|
105
|
+
}
|
|
106
|
+
if (sub === 'branch' || sub === 'tag') {
|
|
107
|
+
const write = hasFlag('-d', '-D', '-m', '-M', '-c', '-C', '-a', '-s', '-f', '--delete', '--move', '--copy', '--set-upstream-to', '-u') || (restOps.length > 0 && !hasFlag('--list', '-l', '--contains', '--merged', '--no-merged', '--points-at'))
|
|
108
|
+
effects.push({ kind: write ? 'write' : 'read', path: repo, recursive: true })
|
|
109
|
+
return { effects, readsStdin: false }
|
|
110
|
+
}
|
|
111
|
+
if (sub === 'stash' && (restOps.length === 0 ? false : ['list', 'show'].includes(concrete(restOps[0]) ?? ''))) {
|
|
112
|
+
return { effects: [{ kind: 'read', path: repo, recursive: true }], readsStdin: false }
|
|
113
|
+
}
|
|
114
|
+
if (sub === 'remote' && !['add', 'remove', 'rm', 'rename', 'set-url', 'set-head', 'prune', 'update'].includes(concrete(restOps[0] ?? known('')) ?? '')) {
|
|
115
|
+
return { effects: [{ kind: 'read', path: repo, recursive: true }], readsStdin: false }
|
|
116
|
+
}
|
|
117
|
+
if (sub === 'merge-tree') {
|
|
118
|
+
// --write-tree (the default in modern git) stores the merged trees as objects
|
|
119
|
+
effects.push({ kind: 'read', path: repo, recursive: true })
|
|
120
|
+
if (!hasFlag('--trivial-merge')) effects.push({ kind: 'write', path: { alts: [[...repo.alts[0], '/.git']] }, recursive: true, maybe: true })
|
|
121
|
+
return { effects, readsStdin: hasFlag('--stdin') }
|
|
122
|
+
}
|
|
123
|
+
if (sub === 'archive' || sub === 'bundle') {
|
|
124
|
+
effects.push({ kind: 'read', path: repo, recursive: true })
|
|
125
|
+
if (sub === 'archive') {
|
|
126
|
+
if (hasFlag('--remote') || rest.some((r) => knownPrefix(r).startsWith('--remote='))) effects.push({ kind: 'network', detail: 'git archive --remote' })
|
|
127
|
+
for (let j = 0; j < rest.length; j++) {
|
|
128
|
+
const s = concrete(rest[j]) ?? ''
|
|
129
|
+
if ((s === '-o' || s === '--output') && rest[j + 1]) effects.push({ kind: 'write', path: h.resolve(rest[j + 1]) })
|
|
130
|
+
else if (s.startsWith('--output=')) effects.push({ kind: 'write', path: h.resolve(known(s.slice('--output='.length))) })
|
|
131
|
+
}
|
|
132
|
+
return { effects, readsStdin: false }
|
|
133
|
+
}
|
|
134
|
+
const verb = concrete(restOps[0] ?? known('')) ?? ''
|
|
135
|
+
const file = restOps[1]
|
|
136
|
+
if (verb === 'create' && file && concrete(file) !== '-') effects.push({ kind: 'write', path: h.resolve(file) })
|
|
137
|
+
else if ((verb === 'verify' || verb === 'list-heads') && file) effects.push({ kind: 'read', path: h.resolve(file) })
|
|
138
|
+
else if (verb === 'unbundle') effects.push({ kind: 'write', path: repo, recursive: true })
|
|
139
|
+
else if (verb !== 'create') return { effects: [...effects, { kind: 'unknown', detail: `git bundle ${verb}` }], readsStdin: 'maybe', complete: false }
|
|
140
|
+
return { effects, readsStdin: verb === 'unbundle' }
|
|
141
|
+
}
|
|
142
|
+
if (sub === 'filter-branch' || sub === 'credential' || sub === 'filter-repo') {
|
|
143
|
+
// filters and credential helpers are shell commands
|
|
144
|
+
return { effects: [{ kind: 'write', path: repo, recursive: true }, { kind: 'unknown', detail: `git ${sub} runs commands` }], readsStdin: 'maybe', complete: false }
|
|
145
|
+
}
|
|
146
|
+
if (GIT_READ.has(sub)) {
|
|
147
|
+
effects.push({ kind: 'read', path: repo, recursive: true })
|
|
148
|
+
return { effects, readsStdin: false, stdout: sub === 'rev-parse' && hasFlag('--show-toplevel') ? { type: 'path', value: root ? known(root) : undefined } : undefined }
|
|
149
|
+
}
|
|
150
|
+
if (sub === 'clone') {
|
|
151
|
+
const cp = h.parseArgs(rest, {
|
|
152
|
+
short: 'lsqvn', shortWithArg: 'bojcu',
|
|
153
|
+
long: { depth: true, branch: '=b', origin: '=o', config: '=c', reference: true, 'reference-if-able': true, 'separate-git-dir': true, 'upload-pack': '=u', template: true, filter: true, jobs: '=j', 'shallow-since': true, 'shallow-exclude': true, 'bundle-uri': true, 'server-option': true, 'ref-format': true, recursive: false, 'recurse-submodules': true, 'single-branch': false, 'no-single-branch': false, 'no-tags': false, 'shallow-submodules': false, bare: false, mirror: false, quiet: false, verbose: false, progress: false, 'no-checkout': false, local: false, shared: false, dissociate: false, sparse: false, 'also-filter-submodules': false, 'remote-submodules': false, 'no-hardlinks': false, 'no-reject-shallow': false, 'reject-shallow': false },
|
|
154
|
+
})
|
|
155
|
+
const [url, dir] = cp.operands
|
|
156
|
+
const dest = dir ? h.resolve(dir) : { alts: cwd.alts.map((c) => [...c, '/', hole('filename', 'repository name').alts[0][0]]) }
|
|
157
|
+
if (url) effects.push({ kind: 'network', target: url, detail: 'git clone' })
|
|
158
|
+
effects.push({ kind: 'write', path: dest, recursive: true })
|
|
159
|
+
const sep = cp.flags.get('separate-git-dir')
|
|
160
|
+
if (sep && sep !== true) effects.push({ kind: 'write', path: h.resolve(sep), recursive: true })
|
|
161
|
+
// --upload-pack runs a command
|
|
162
|
+
return { effects, readsStdin: false, complete: cp.unknownFlags.length === 0 && !cp.flags.has('u') }
|
|
163
|
+
}
|
|
164
|
+
if (sub === 'init') {
|
|
165
|
+
effects.push({ kind: 'write', path: restOps[0] ? h.resolve(restOps[0]) : cwd, recursive: true })
|
|
166
|
+
return { effects, readsStdin: false }
|
|
167
|
+
}
|
|
168
|
+
if (GIT_NETWORK.has(sub)) {
|
|
169
|
+
effects.push({ kind: 'network', detail: `git ${sub}` })
|
|
170
|
+
if (sub !== 'push' && sub !== 'ls-remote') effects.push({ kind: 'write', path: repo, recursive: true })
|
|
171
|
+
else effects.push({ kind: 'read', path: repo, recursive: true })
|
|
172
|
+
if (sub === 'push') effects.push({ kind: 'write', path: { alts: [[...repo.alts[0], '/.git']] }, recursive: true })
|
|
173
|
+
return { effects, readsStdin: false }
|
|
174
|
+
}
|
|
175
|
+
if (GIT_WRITE.has(sub)) {
|
|
176
|
+
effects.push({ kind: 'write', path: repo, recursive: true })
|
|
177
|
+
if (sub === 'clean' && (hasFlag('-f', '-ff', '-fd', '-fdx', '-df', '-xdf', '--force'))) effects.push({ kind: 'delete', path: cwd, recursive: true })
|
|
178
|
+
// `--fixup=<commit>` needs no message (`--fixup=amend:`/`reword:` open an editor)
|
|
179
|
+
const fixup = rest.some((r) => /^--fixup=(?!amend:|reword:)./.test(concrete(r) ?? ''))
|
|
180
|
+
if (sub === 'commit' && !fixup && !hasFlag('-m', '-F', '--message', '--file', '-C', '--no-edit') && !rest.some((r) => /^-[a-zA-Z]*m|^--(message|file|reuse-message)=/.test(knownPrefix(r)))) {
|
|
181
|
+
effects.push({ kind: 'unknown', detail: 'git commit may open an editor' })
|
|
182
|
+
return { effects, readsStdin: false, complete: false }
|
|
183
|
+
}
|
|
184
|
+
return { effects, readsStdin: false }
|
|
185
|
+
}
|
|
186
|
+
return { effects: [{ kind: 'read', path: repo, recursive: true }, { kind: 'unknown', detail: `git ${sub}` }], complete: false, readsStdin: 'maybe' }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const PM_INSTALL = new Set(['install', 'i', 'ci', 'add', 'remove', 'rm', 'uninstall', 'un', 'update', 'up', 'upgrade', 'link', 'unlink', 'prune', 'dedupe', 'rebuild', 'import'])
|
|
192
|
+
const PM_READ = new Set(['ls', 'list', 'view', 'info', 'show', 'outdated', 'why', 'explain', 'audit', 'search', 'config', 'get', 'root', 'bin', 'prefix', 'whoami', 'help', '-v', '--version', 'version', '-h', '--help', 'doctor', 'licenses'])
|
|
193
|
+
|
|
194
|
+
function packageManager(): Behavior {
|
|
195
|
+
return {
|
|
196
|
+
names: ['npm', 'pnpm', 'yarn', 'bun', 'npx', 'pnpx', 'bunx', 'uvx'],
|
|
197
|
+
summary: 'JavaScript package managers',
|
|
198
|
+
analyze(inv, h) {
|
|
199
|
+
const a = args(inv)
|
|
200
|
+
if (inv.name === 'npx' || inv.name === 'pnpx' || inv.name === 'bunx' || inv.name === 'uvx') {
|
|
201
|
+
// npx [-y] [-p pkg] <binary> args…: the binary is analyzed like any command
|
|
202
|
+
let j = 0
|
|
203
|
+
for (; j < a.length; j++) {
|
|
204
|
+
const s = concrete(a[j])
|
|
205
|
+
if (s === undefined || !s.startsWith('-')) break
|
|
206
|
+
if (['-p', '--package', '--from', '--with', '--python', '--with-requirements', '--index-url', '--index'].includes(s)) j++
|
|
207
|
+
}
|
|
208
|
+
const net: Effect = { kind: 'network', detail: `${inv.name} may download packages` }
|
|
209
|
+
if (j >= a.length || concrete(a[j]) === undefined) return { effects: [net, { kind: 'unknown', detail: `${inv.name} runs package binaries` }], complete: false, readsStdin: 'maybe' }
|
|
210
|
+
const sub = h.subcommand(a.slice(j))
|
|
211
|
+
return { ...sub, effects: [net, ...sub.effects], complete: false }
|
|
212
|
+
}
|
|
213
|
+
let i = 0
|
|
214
|
+
let dir = inv.cwd
|
|
215
|
+
for (; i < a.length; i++) {
|
|
216
|
+
const s = concrete(a[i])
|
|
217
|
+
if (s === undefined) break
|
|
218
|
+
if (s === '-C' || s === '--dir' || s === '--prefix' || s === '--cwd') {
|
|
219
|
+
const d = a[++i]
|
|
220
|
+
if (d) dir = h.resolve(d)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (s === '--filter' || s === '-F' || s === '-w' || s === '--workspace') {
|
|
224
|
+
i++
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (s.startsWith('-') && !PM_READ.has(s)) continue
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
const sub = concrete(a[i] ?? known('')) ?? (a[i] ? '?' : '')
|
|
231
|
+
if (a[i] && sub !== '?' && !sub.startsWith('-')) {
|
|
232
|
+
h.role(a[i], 'subcommand')
|
|
233
|
+
// `npm run build`, `pnpm exec tsc`: the script or binary is a subcommand too
|
|
234
|
+
if (['run', 'run-script', 'exec', 'x', 'dlx'].includes(sub) && a[i + 1] && !knownPrefix(a[i + 1]).startsWith('-')) h.role(a[i + 1], 'subcommand')
|
|
235
|
+
}
|
|
236
|
+
const global = a.some((x) => ['-g', '--global'].includes(concrete(x) ?? ''))
|
|
237
|
+
if (PM_READ.has(sub)) return { effects: [{ kind: 'read', path: dir, recursive: true, maybe: true }, ...(sub === 'view' || sub === 'info' || sub === 'outdated' || sub === 'audit' || sub === 'search' ? [{ kind: 'network', detail: `${inv.name} ${sub}` } as Effect] : [])], readsStdin: false }
|
|
238
|
+
if (PM_INSTALL.has(sub) || (sub === '' && inv.name !== 'npm')) {
|
|
239
|
+
const effects: Effect[] = [
|
|
240
|
+
{ kind: 'network', detail: `${inv.name} ${sub || 'install'}` },
|
|
241
|
+
{ kind: 'write', path: global ? hole('path', 'global package directory') : dir, recursive: true },
|
|
242
|
+
{ kind: 'unknown', detail: 'install scripts may run arbitrary code' },
|
|
243
|
+
]
|
|
244
|
+
return { effects, readsStdin: false, complete: false }
|
|
245
|
+
}
|
|
246
|
+
// run / exec / test / start / custom scripts
|
|
247
|
+
return { effects: [{ kind: 'read', path: dir, recursive: true, maybe: true }, { kind: 'unknown', detail: `${inv.name} ${sub} runs project scripts` }], readsStdin: 'maybe', complete: false }
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function interpreter(names: string[], codeFlags: string[], argFlags: string[]): Behavior {
|
|
253
|
+
return {
|
|
254
|
+
names,
|
|
255
|
+
summary: 'language interpreter',
|
|
256
|
+
analyze(inv, h) {
|
|
257
|
+
const a = args(inv)
|
|
258
|
+
for (let i = 0; i < a.length; i++) {
|
|
259
|
+
const s = concrete(a[i])
|
|
260
|
+
if (s === undefined) break
|
|
261
|
+
if (s === '--version' || s === '-V' || s === '-version' || (s === '-v' && names.includes('node'))) return { effects: [], readsStdin: false }
|
|
262
|
+
if (codeFlags.includes(s)) {
|
|
263
|
+
if (a[i + 1]) h.role(a[i + 1], 'code')
|
|
264
|
+
return { effects: [{ kind: 'unknown', detail: `${inv.name} ${s} runs inline code` }], readsStdin: 'maybe', complete: false }
|
|
265
|
+
}
|
|
266
|
+
if (s === '-m' && names.includes('python3')) {
|
|
267
|
+
const mod = concrete(a[i + 1] ?? known('')) ?? ''
|
|
268
|
+
// `python3 -m pip install x`: the module and its first operand read as subcommands
|
|
269
|
+
if (a[i + 1]) h.role(a[i + 1], 'subcommand')
|
|
270
|
+
const op = a.slice(i + 2).find((x) => !knownPrefix(x).startsWith('-'))
|
|
271
|
+
if (op && mod === 'pip' && /^[a-z][a-z-]*$/.test(concrete(op) ?? '')) h.role(op, 'subcommand')
|
|
272
|
+
if (mod === 'pip') {
|
|
273
|
+
const sub = concrete(a[i + 2] ?? known('')) ?? ''
|
|
274
|
+
if (['install', 'uninstall', 'download', 'wheel'].includes(sub)) return { effects: [{ kind: 'network', detail: 'pip' }, { kind: 'write', path: hole('path', 'python site-packages') }, { kind: 'unknown', detail: 'package build scripts' }], complete: false, readsStdin: false }
|
|
275
|
+
return { effects: [{ kind: 'read', path: hole('path', 'python site-packages') }], readsStdin: false }
|
|
276
|
+
}
|
|
277
|
+
if (mod === 'venv') {
|
|
278
|
+
const d = a.slice(i + 2).find((x) => !knownPrefix(x).startsWith('-'))
|
|
279
|
+
return { effects: d ? [{ kind: 'write', path: h.resolve(d), recursive: true }] : [], readsStdin: false }
|
|
280
|
+
}
|
|
281
|
+
if (mod === 'json.tool') return { effects: a.slice(i + 2).filter((x) => !knownPrefix(x).startsWith('-')).map((f) => ({ kind: 'read' as const, path: h.resolve(f) })), readsStdin: true }
|
|
282
|
+
return { effects: [{ kind: 'unknown', detail: `python module ${mod}` }], readsStdin: 'maybe', complete: false }
|
|
283
|
+
}
|
|
284
|
+
if (argFlags.includes(s)) {
|
|
285
|
+
i++
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
if (s.startsWith('-')) continue
|
|
289
|
+
if (s === '-') break
|
|
290
|
+
return { effects: [{ kind: 'read', path: h.resolve(a[i]) }, { kind: 'unknown', detail: `runs script ${s}` }], readsStdin: 'maybe', complete: false }
|
|
291
|
+
}
|
|
292
|
+
return { effects: [{ kind: 'unknown', detail: `${inv.name} reads a program from stdin` }], readsStdin: true, complete: false }
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildTool(names: string[]): Behavior {
|
|
298
|
+
return {
|
|
299
|
+
names,
|
|
300
|
+
summary: 'build tool',
|
|
301
|
+
analyze(inv, h: BehaviorHelpers) {
|
|
302
|
+
const a = args(inv)
|
|
303
|
+
if (a.some((x) => ['--version', '-v', 'version', '--help'].includes(concrete(x) ?? '')) && a.length === 1) return { effects: [], readsStdin: false }
|
|
304
|
+
let dir = inv.cwd
|
|
305
|
+
for (let i = 0; i < a.length; i++) if (['-C', '--directory'].includes(concrete(a[i]) ?? '') && a[i + 1]) dir = h.resolve(a[i + 1])
|
|
306
|
+
return { effects: [{ kind: 'read', path: dir, recursive: true, maybe: true }, { kind: 'write', path: dir, recursive: true, maybe: true }, { kind: 'unknown', detail: `${inv.name} runs build recipes` }], readsStdin: false, complete: false }
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function systemPackageManager(names: string[], roots: string[]): Behavior {
|
|
312
|
+
return {
|
|
313
|
+
names,
|
|
314
|
+
summary: 'system package manager',
|
|
315
|
+
analyze(inv) {
|
|
316
|
+
const sub = concrete(args(inv).find((x) => !knownPrefix(x).startsWith('-')) ?? known('')) ?? ''
|
|
317
|
+
if (['install', 'upgrade', 'remove', 'uninstall', 'reinstall', 'update', 'tap', 'link', 'unlink', 'cleanup', 'autoremove', 'purge'].includes(sub)) {
|
|
318
|
+
return { effects: [{ kind: 'network', detail: `${inv.name} ${sub}` }, ...roots.map((r): Effect => ({ kind: 'write', path: known(r), recursive: true }))], readsStdin: false }
|
|
319
|
+
}
|
|
320
|
+
return { effects: roots.map((r): Effect => ({ kind: 'read', path: known(r), recursive: true })), readsStdin: false }
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Where CLIs keep their credentials (relative to HOME), and the subcommands that print them. */
|
|
326
|
+
const CREDENTIAL_STORES: Record<string, { stores: string[]; secret: RegExp }> = {
|
|
327
|
+
gh: { stores: ['.config/gh'], secret: /^auth (token|status)/ },
|
|
328
|
+
glab: { stores: ['.config/glab-cli'], secret: /^(auth status|config get token)/ },
|
|
329
|
+
aws: { stores: ['.aws'], secret: /^(configure (get|list|export-credentials)|sts get-session-token|ecr get-login-password|ecr-public get-login-password|codeartifact get-authorization-token)/ },
|
|
330
|
+
gcloud: { stores: ['.config/gcloud'], secret: /^(auth (print-access-token|print-identity-token|application-default print-access-token)|config config-helper)/ },
|
|
331
|
+
az: { stores: ['.azure'], secret: /^account get-access-token/ },
|
|
332
|
+
kubectl: { stores: ['.kube'], secret: /^config view/ },
|
|
333
|
+
helm: { stores: ['.kube'], secret: /(?!)/ },
|
|
334
|
+
docker: { stores: ['.docker/config.json'], secret: /^(login|logout|credential)/ },
|
|
335
|
+
podman: { stores: ['.docker/config.json'], secret: /^(login|logout)/ },
|
|
336
|
+
heroku: { stores: ['.netrc'], secret: /^(auth:token|auth token)/ },
|
|
337
|
+
op: { stores: ['.config/op'], secret: /^(read|inject|item get|document get|run|signin|account get)/ },
|
|
338
|
+
doctl: { stores: ['.config/doctl'], secret: /^auth/ },
|
|
339
|
+
vault: { stores: ['.vault-token'], secret: /^(read|kv get|token lookup|print token|login)/ },
|
|
340
|
+
terraform: { stores: ['.terraform.d/credentials.tfrc.json'], secret: /^login/ },
|
|
341
|
+
tofu: { stores: ['.terraform.d/credentials.tfrc.json'], secret: /^login/ },
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export const devtoolBehaviors: Behavior[] = [
|
|
345
|
+
gitBehavior(),
|
|
346
|
+
packageManager(),
|
|
347
|
+
interpreter(['python', 'python3', 'python2'], ['-c'], ['-W', '-X', '-Q']),
|
|
348
|
+
interpreter(['node', 'deno', 'tsx', 'ts-node'], ['-e', '--eval', '-p', '--print'], ['-r', '--require', '--import', '--loader']),
|
|
349
|
+
interpreter(['ruby'], ['-e'], ['-r', '-I']),
|
|
350
|
+
interpreter(['perl'], ['-e', '-E'], ['-I', '-M']),
|
|
351
|
+
interpreter(['php'], ['-r'], ['-d']),
|
|
352
|
+
interpreter(['java', 'kotlin', 'scala', 'groovy', 'jshell'], [], ['-cp', '-classpath', '--class-path', '-p', '--module-path', '-jar', '-D']),
|
|
353
|
+
interpreter(['lua', 'luajit', 'Rscript', 'julia'], ['-e', '--eval'], ['-l']),
|
|
354
|
+
buildTool(['make', 'gmake', 'cmake', 'ninja', 'cargo', 'go', 'mvn', 'gradle', 'gradlew', 'bazel', 'xcodebuild', 'swift', 'rustc', 'gcc', 'cc', 'clang', 'g++', 'clang++', 'tsc', 'vite', 'webpack', 'esbuild', 'vitest', 'jest', 'pytest', 'tox', 'swiftc', 'zig', 'ld', 'javac', 'kotlinc', 'svelte-check', 'svelte-kit', 'next', 'nuxt', 'astro', 'rollup', 'parcel', 'turbo', 'nx', 'playwright', 'mocha', 'ava', 'eslint', 'prettier', 'stylelint', 'xcodegen', 'tuist', 'bundle', 'rake', 'mix', 'dotnet', 'sbt', 'lein', 'stack', 'cabal', 'ghc', 'dune', 'nim', 'crystal', 'meson', 'scons', 'just', 'task']),
|
|
355
|
+
systemPackageManager(['brew'], ['/opt/homebrew', '/usr/local']),
|
|
356
|
+
systemPackageManager(['apt', 'apt-get', 'dpkg', 'yum', 'dnf', 'apk', 'pacman', 'zypper'], ['/usr', '/var/lib', '/etc']),
|
|
357
|
+
{
|
|
358
|
+
names: ['pip', 'pip3', 'uv', 'pipx', 'poetry', 'conda'],
|
|
359
|
+
summary: 'Python package managers',
|
|
360
|
+
analyze(inv, h) {
|
|
361
|
+
const sub = concrete(args(inv).find((x) => !knownPrefix(x).startsWith('-')) ?? known('')) ?? ''
|
|
362
|
+
if (['install', 'uninstall', 'add', 'remove', 'sync', 'lock', 'update', 'upgrade', 'download', 'venv', 'create'].includes(sub)) {
|
|
363
|
+
return { effects: [{ kind: 'network', detail: `${inv.name} ${sub}` }, { kind: 'write', path: inv.name === 'pip' || inv.name === 'pip3' ? hole('path', 'python site-packages') : inv.cwd, recursive: true }, { kind: 'unknown', detail: 'package build scripts' }], complete: false, readsStdin: false }
|
|
364
|
+
}
|
|
365
|
+
if (['run', 'exec', 'shell'].includes(sub)) return { effects: [{ kind: 'unknown', detail: `${inv.name} ${sub}` }], complete: false, readsStdin: 'maybe' }
|
|
366
|
+
return { effects: [{ kind: 'read', path: inv.cwd, recursive: true }], readsStdin: false }
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
names: ['docker', 'podman', 'container', 'docker-compose', 'kubectl', 'helm', 'terraform', 'tofu', 'pulumi', 'gcloud', 'aws', 'az', 'gh', 'glab', 'vercel', 'fly', 'flyctl', 'heroku', 'wrangler', 'firebase', 'supabase', 'stripe', 'op', 'doctl', 'vault'],
|
|
371
|
+
summary: 'cloud / container CLIs',
|
|
372
|
+
analyze(inv, h) {
|
|
373
|
+
const a = args(inv)
|
|
374
|
+
const sub = concrete(a.find((x) => !knownPrefix(x).startsWith('-')) ?? known('')) ?? ''
|
|
375
|
+
if (a.length === 1 && ['--version', '-v', 'version', 'help', '--help', '-h'].includes(concrete(a[0]) ?? '')) return { effects: [], readsStdin: false }
|
|
376
|
+
const effects: Effect[] = [{ kind: 'network', detail: `${inv.name} ${sub}` }]
|
|
377
|
+
// the credential store: read by (nearly) every call, definitely by the commands that print secrets
|
|
378
|
+
const cred = CREDENTIAL_STORES[inv.name]
|
|
379
|
+
if (cred) {
|
|
380
|
+
const words = a.map((x) => concrete(x) ?? '?').filter((w) => !w.startsWith('-')).join(' ')
|
|
381
|
+
const secret = cred.secret.test(words)
|
|
382
|
+
for (const store of cred.stores) {
|
|
383
|
+
const path = store.startsWith('/') ? known(store) : concat(h.home() ?? known('~'), known('/' + store))
|
|
384
|
+
effects.push({ kind: 'read', path, recursive: true, ...(secret ? {} : { maybe: true }) })
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
effects.push({ kind: 'unknown', detail: `${inv.name} ${sub} acts on remote or container state` })
|
|
388
|
+
return { effects, complete: false, readsStdin: 'maybe' }
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
names: ['open', 'xdg-open', 'osascript', 'say', 'pbcopy', 'pbpaste', 'systemctl', 'afplay', 'shortcuts', 'automator'],
|
|
393
|
+
summary: 'desktop / system integration',
|
|
394
|
+
analyze(inv) {
|
|
395
|
+
if (inv.name === 'pbpaste' || inv.name === 'say') return { effects: [], readsStdin: inv.name === 'say' }
|
|
396
|
+
if (inv.name === 'pbcopy') return { effects: [{ kind: 'process', detail: 'writes the clipboard' }], readsStdin: true }
|
|
397
|
+
return { effects: [{ kind: 'unknown', detail: `${inv.name} acts outside the shell` }], complete: false, readsStdin: false }
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
]
|