omakit 0.6.7 → 0.6.8
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/package.json
CHANGED
|
@@ -164,4 +164,8 @@ that asked for each are in `docs/BLOCKS.md`; the ones that shape the call:
|
|
|
164
164
|
`omakit inspect` lists an unmodified store block as one row and each
|
|
165
165
|
`Store {}` site as a write under a directory the plugin controls at mode
|
|
166
166
|
0600; a `FileView` write or a shell redirect that remains names a site
|
|
167
|
-
that still keeps state on its own.
|
|
167
|
+
that still keeps state on its own. A `cp`, `mv`, `install` or `ln` to a
|
|
168
|
+
variable destination is not state Store keeps: the file goes wherever
|
|
169
|
+
the variable points (a hook, a wallpaper), and no block covers that yet;
|
|
170
|
+
the review's requirements for the class are in `docs/MEASUREMENTS.md`
|
|
171
|
+
(M11).
|
package/tools/blocks/commit.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"recordedAt": "2026-09-
|
|
2
|
+
"commit": "0c11dd5671c8ed65a89ea71a2bf44ab5f38b1a90",
|
|
3
|
+
"recordedAt": "2026-09-23T07:35:32.560Z",
|
|
4
4
|
"how": "Written by `node tools/blocks/record-commit.mjs` in the release workflow before `npm pack`, so a packaged omakit, which has no Git checkout, still names the commit its block files come from. In a checkout this file is null and `git rev-parse HEAD` is the source; a package with null here was packed without the release step, and `omakit add` refuses to stamp a header it cannot name."
|
|
5
5
|
}
|
|
@@ -14,7 +14,7 @@ const ARGV_FORMS = new Set(["array", "string", "computed"])
|
|
|
14
14
|
const DEADLINE_VIA = new Set(["timer-kill", "timeout-argv", "destruction", "block-run", null])
|
|
15
15
|
const COLLECTORS = new Set(["StdioCollector", "SplitParser", "Run", "none", "unknown"])
|
|
16
16
|
const BLOCK_STATES = new Set(["unmodified", "modified"])
|
|
17
|
-
const CONTROLLED = new Set(["observed", "not-observed", "unknown"])
|
|
17
|
+
const CONTROLLED = new Set(["observed", "not-observed", "variable", "unknown"])
|
|
18
18
|
const DECLARED_IN = new Set(["qml", "shell"])
|
|
19
19
|
const FILE_KINDS = ["qml", "js", "shell", "python", "other"]
|
|
20
20
|
|
|
@@ -83,9 +83,10 @@ function write(row, at, problems) {
|
|
|
83
83
|
if (!isString(row.path)) problems.push(`${at}.path is not a string`)
|
|
84
84
|
if (!nullOr(isString)(row.canonicalPath)) problems.push(`${at}.canonicalPath is neither a string nor null`)
|
|
85
85
|
if (!isString(row.via)) problems.push(`${at}.via is not a string`)
|
|
86
|
-
if (!CONTROLLED.has(row.controlledDirectory)) problems.push(`${at}.controlledDirectory is not observed, not-observed or unknown`)
|
|
86
|
+
if (!CONTROLLED.has(row.controlledDirectory)) problems.push(`${at}.controlledDirectory is not observed, not-observed, variable or unknown`)
|
|
87
87
|
if ((row.controlledDirectory === "observed") !== isString(row.controlledBy)) problems.push(`${at}.controlledBy names a directory exactly when controlledDirectory is observed`)
|
|
88
88
|
if (row.canonicalPath === null && row.controlledDirectory !== "unknown") problems.push(`${at}.controlledDirectory is decided for a path that could not be read`)
|
|
89
|
+
if (row.controlledDirectory === "variable" && (!/^\$(?:[A-Za-z_]\w*|\d)\/?$/.test(row.canonicalPath ?? "") || /^\$(?:HOME|XDG_)/.test(row.canonicalPath))) problems.push(`${at}.controlledDirectory is variable for a path that is not one variable other than $HOME and the XDG names`)
|
|
89
90
|
if (!isBool(row.temp)) problems.push(`${at}.temp is not a boolean`)
|
|
90
91
|
if (!nullOr(isString)(row.mode)) problems.push(`${at}.mode is neither a string nor null`)
|
|
91
92
|
if ((row.via === "block-store") !== (row.block === "store")) problems.push(`${at}.via block-store and block: "store" go together`)
|
|
@@ -51,6 +51,39 @@ function pathResolved(process) {
|
|
|
51
51
|
return tool
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The verbs that put a file at a destination the caller names. A write by
|
|
56
|
+
* one of them whose destination is one variable (`cp "$src" "$dest"`) is
|
|
57
|
+
* the shape of both file-boundary blockers in one review thread,
|
|
58
|
+
* `install-wallpaper.sh` (`cp -f`, 2026-09-11) and `install-hook.sh`
|
|
59
|
+
* (`cp`, 2026-09-22), and the class cited neither copy: the path was
|
|
60
|
+
* `unknown`, with 52 of that tree's 54 writes. A link planted at the
|
|
61
|
+
* destination picks where the file lands for all four, measured on GNU
|
|
62
|
+
* coreutils 9.11: `cp` writes through a link to a file, and all four put
|
|
63
|
+
* the file inside a linked directory. A redirect or `tee` onto a variable
|
|
64
|
+
* is left out, measured over 15 plugin trees on 2026-09-23: 28 writes by
|
|
65
|
+
* these four verbs to a variable, against 61 redirects and tees, 24 of
|
|
66
|
+
* them `>>` appends to a log (docs/MEASUREMENTS.md, M11).
|
|
67
|
+
*/
|
|
68
|
+
const PLACING = new Set(["cp", "mv", "install", "ln"])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The writes the file and state boundary class cites, by what it cites
|
|
72
|
+
* them for. Each test reads one row alone, so the report can ask it of the
|
|
73
|
+
* rows at one site and show the write the class cited there, not another
|
|
74
|
+
* write on the same line (`printf ... > "$f.tmp" && mv "$f.tmp" "$f"`).
|
|
75
|
+
*/
|
|
76
|
+
export function boundaryWrites(writes) {
|
|
77
|
+
return {
|
|
78
|
+
outside: writes.filter((row) => row.controlledDirectory === "not-observed" && row.via !== "mktemp"),
|
|
79
|
+
placed: writes.filter((row) => row.controlledDirectory === "variable" && PLACING.has(row.via)),
|
|
80
|
+
unmoded: writes.filter((row) => row.via === "mkdir" && row.mode === null),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Words as a list a person reads, in the order given: "cp", "cp and ln", "mv, cp and ln". */
|
|
85
|
+
const listed = (words) => (words.length > 1 ? `${words.slice(0, -1).join(", ")} and ${words.at(-1)}` : words[0])
|
|
86
|
+
|
|
54
87
|
const SECRET = /authorization|bearer|token=|api[_-]?key|password|secret=/i
|
|
55
88
|
const PRIVILEGED = new Set(["sudo", "pkexec", "doas", "docker"])
|
|
56
89
|
|
|
@@ -210,17 +243,17 @@ export const PATTERNS = Object.freeze([
|
|
|
210
243
|
{
|
|
211
244
|
id: "file-and-state-boundary",
|
|
212
245
|
label: "file and state boundary",
|
|
213
|
-
notObserved: "no write outside a controlled directory",
|
|
246
|
+
notObserved: "no write outside a controlled directory or by cp, mv, install or ln to a variable, no mkdir without a mode",
|
|
214
247
|
measurement: "M11",
|
|
215
248
|
share: 0.15,
|
|
216
249
|
sample: SAMPLE,
|
|
217
250
|
precondition: ({ writes }) => {
|
|
218
|
-
const outside
|
|
219
|
-
const unmoded = writes.filter((row) => row.via === "mkdir" && row.mode === null)
|
|
251
|
+
const { outside, placed, unmoded } = boundaryWrites(writes)
|
|
220
252
|
const parts = []
|
|
221
253
|
if (outside.length) parts.push(`${plural(outside.length, "write")} outside a controlled directory (${sites(outside)})`)
|
|
254
|
+
if (placed.length) parts.push(`${plural(placed.length, "write")} by ${listed([...new Set(placed.map((row) => row.via))])} to a variable destination, not resolvable to a controlled directory (${sites(placed)})`)
|
|
222
255
|
if (unmoded.length) parts.push(`${plural(unmoded.length, "mkdir")} with no mode (${sites(unmoded)})`)
|
|
223
|
-
return { sites: [...outside, ...unmoded].map(site), observation: `observed ${parts.join("; ")}` }
|
|
256
|
+
return { sites: [...outside, ...placed, ...unmoded].map(site), observation: `observed ${parts.join("; ")}` }
|
|
224
257
|
},
|
|
225
258
|
},
|
|
226
259
|
{
|
package/tools/inspect/report.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { colourEnabled, field, GUTTER, INSPECT_VERDICT, mark, outputColumns, styler, verdict, wrap } from "../marketplace/style.mjs"
|
|
21
21
|
import { withHomeAbbreviated } from "../marketplace/paths.mjs"
|
|
22
|
-
import { PATTERNS, SIZE } from "./patterns.mjs"
|
|
22
|
+
import { boundaryWrites, PATTERNS, SIZE } from "./patterns.mjs"
|
|
23
23
|
|
|
24
24
|
const NOTHING = "observed nothing of this kind"
|
|
25
25
|
|
|
@@ -110,7 +110,9 @@ function writeRow(write, c) {
|
|
|
110
110
|
? `observed (${write.controlledBy})`
|
|
111
111
|
: write.controlledDirectory === "not-observed"
|
|
112
112
|
? `not observed${write.temp ? ` (${write.canonicalPath.split("/").slice(0, 2).join("/")} is shared)` : ""}`
|
|
113
|
-
:
|
|
113
|
+
: write.controlledDirectory === "variable"
|
|
114
|
+
? `not resolvable (${write.canonicalPath} is a variable)`
|
|
115
|
+
: "unknown (path not readable as a literal prefix)"
|
|
114
116
|
const notes = [`under a directory the plugin controls: ${where}`]
|
|
115
117
|
if (write.mode) notes.push(`mode ${write.mode}`)
|
|
116
118
|
return row("info", site(write), what, c, [notes.join("; ")])
|
|
@@ -207,7 +209,7 @@ function commandLine(process) {
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
/**
|
|
210
|
-
* The fact at a site, in one line: the write
|
|
212
|
+
* The fact at a site, in one line: the write the file class cited there, the
|
|
211
213
|
* host there for the egress class, otherwise the command, then the host,
|
|
212
214
|
* then the write, whichever the site has.
|
|
213
215
|
*/
|
|
@@ -222,7 +224,9 @@ function factAt(document, at, patternId) {
|
|
|
222
224
|
return row ? `${row.scheme}://${row.host}${row.tool ? ` via ${row.tool}` : ""}` : ""
|
|
223
225
|
}
|
|
224
226
|
const write = () => {
|
|
225
|
-
|
|
227
|
+
// On a line with two writes, the one the file class cited, not the first.
|
|
228
|
+
const here = document.observed.writes.filter(same)
|
|
229
|
+
const row = (patternId === "file-and-state-boundary" ? Object.values(boundaryWrites(here)).flat()[0] : null) ?? here[0]
|
|
226
230
|
return row ? `${row.via} ${row.canonicalPath ?? row.path}` : ""
|
|
227
231
|
}
|
|
228
232
|
const order = patternId === "file-and-state-boundary" ? [write, process, host] : patternId === "network-egress" ? [host, process, write] : [process, host, write]
|
package/tools/inspect/writes.mjs
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// or `touch` in shell; `writeFile` and its variants in JavaScript; `open()`
|
|
4
4
|
// for writing in Python. Each row says whether the path's literal prefix,
|
|
5
5
|
// after the `$HOME`, `~` and `XDG_*` idioms are expanded, falls under a
|
|
6
|
-
// directory the plugin controls, and which mode the file shows for it.
|
|
6
|
+
// directory the plugin controls, and which mode the file shows for it. A
|
|
7
|
+
// path that is one variable and nothing else (`$dest`, `$2`, `$target/`)
|
|
8
|
+
// is said apart from any other path the classification cannot place: the
|
|
9
|
+
// write names no directory, and the extraction follows no assignment to
|
|
10
|
+
// find one.
|
|
7
11
|
|
|
8
12
|
import { basename, blankComments, blocks, closingBracket, lineOf, propertyValue, blankShellExpressions, shellLogicalLines, shellPieces, shellWords, stringLiteral, withoutRedirections } from "./text.mjs"
|
|
9
13
|
|
|
@@ -11,6 +15,8 @@ import { basename, blankComments, blocks, closingBracket, lineOf, propertyValue,
|
|
|
11
15
|
export const CONTROLLED = Object.freeze(["$XDG_STATE_HOME", "$XDG_CACHE_HOME", "$XDG_RUNTIME_DIR"])
|
|
12
16
|
export const SHARED_TEMP = Object.freeze(["/tmp", "/var/tmp", "/dev/shm"])
|
|
13
17
|
const DEVICES = new Set(["/dev/null", "/dev/stderr", "/dev/stdout", "/dev/tty"])
|
|
18
|
+
/** A canonical path that is one variable expansion, a name or a positional parameter, with at most a trailing slash. */
|
|
19
|
+
const VARIABLE = /^\$(?:[A-Za-z_]\w*|\d)\/?$/
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* A path's literal prefix in canonical form: `~` and `$HOME` idioms,
|
|
@@ -45,8 +51,12 @@ export function canonicalPath(raw) {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/**
|
|
48
|
-
* Whether a canonical path is under a directory the plugin controls
|
|
49
|
-
*
|
|
54
|
+
* Whether a canonical path is under a directory the plugin controls:
|
|
55
|
+
* `variable` when the path is one variable other than `$HOME` and the XDG
|
|
56
|
+
* names, so the write names no directory; `unknown` for any other path it
|
|
57
|
+
* cannot place, an expression, a relative path or a path under a variable
|
|
58
|
+
* (`$dir/name`).
|
|
59
|
+
* @returns {{ controlledDirectory: "observed"|"not-observed"|"variable"|"unknown", controlledBy: string|null, temp: boolean }}
|
|
50
60
|
*/
|
|
51
61
|
export function classifyPath(path, pluginId) {
|
|
52
62
|
if (path === null || path === undefined) return { controlledDirectory: "unknown", controlledBy: null, temp: false }
|
|
@@ -59,6 +69,7 @@ export function classifyPath(path, pluginId) {
|
|
|
59
69
|
if (path === prefix || path.startsWith(`${prefix}/`)) return { controlledDirectory: "not-observed", controlledBy: null, temp: true }
|
|
60
70
|
}
|
|
61
71
|
if (path.startsWith("/") || path.startsWith("$HOME") || path.startsWith("$XDG_")) return { controlledDirectory: "not-observed", controlledBy: null, temp: false }
|
|
72
|
+
if (VARIABLE.test(path)) return { controlledDirectory: "variable", controlledBy: null, temp: false }
|
|
62
73
|
return { controlledDirectory: "unknown", controlledBy: null, temp: false }
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -126,21 +137,31 @@ function pythonWrites(file, pluginId) {
|
|
|
126
137
|
return rows
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The words that are not options, the value of `-m`/`--mode` when one is
|
|
142
|
+
* given, and, with `targets`, of `-t`/`--target-directory`: for `cp`, `mv`,
|
|
143
|
+
* `install` and `ln` that directory is the destination, and every operand
|
|
144
|
+
* a source.
|
|
145
|
+
*/
|
|
146
|
+
function operands(all, { targets = false } = {}) {
|
|
131
147
|
const words = withoutRedirections(all)
|
|
132
148
|
const out = []
|
|
133
149
|
let mode = null
|
|
150
|
+
let target = null
|
|
134
151
|
for (let index = 1; index < words.length; index += 1) {
|
|
135
152
|
const word = words[index]
|
|
136
153
|
if (word === "-m" || word === "--mode") {
|
|
137
154
|
mode = words[index + 1] ?? null
|
|
138
155
|
index += 1
|
|
139
156
|
} else if (word.startsWith("--mode=")) mode = word.slice("--mode=".length)
|
|
157
|
+
else if (targets && (word === "-t" || word === "--target-directory")) {
|
|
158
|
+
target = words[index + 1] ?? null
|
|
159
|
+
index += 1
|
|
160
|
+
} else if (targets && word.startsWith("--target-directory=")) target = word.slice("--target-directory=".length)
|
|
140
161
|
else if (word.startsWith("-") && word !== "-") continue
|
|
141
162
|
else out.push(word)
|
|
142
163
|
}
|
|
143
|
-
return { operands: out, mode }
|
|
164
|
+
return { operands: out, mode, target }
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
function shellWrites(file, pluginId) {
|
|
@@ -179,8 +200,9 @@ function shellWrites(file, pluginId) {
|
|
|
179
200
|
const { operands: paths, mode } = operands(words)
|
|
180
201
|
for (const path of paths) rows.push(row(file, line, path, command, pluginId, mode))
|
|
181
202
|
} else if (["cp", "mv", "install", "ln"].includes(command)) {
|
|
182
|
-
const { operands: paths, mode } = operands(words)
|
|
183
|
-
if (paths.length >=
|
|
203
|
+
const { operands: paths, mode, target } = operands(words, { targets: true })
|
|
204
|
+
if (target !== null && paths.length >= 1) rows.push(row(file, line, target, command, pluginId, mode))
|
|
205
|
+
else if (paths.length >= 2) rows.push(row(file, line, paths[paths.length - 1], command, pluginId, mode))
|
|
184
206
|
} else if (command === "mktemp") {
|
|
185
207
|
const { operands: paths } = operands(words)
|
|
186
208
|
const template = paths[0] || (words.includes("-p") ? `${words[words.indexOf("-p") + 1]}/tmp.XXXXXX` : "/tmp/tmp.XXXXXX")
|