omakit 0.5.0 → 0.6.1
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/README.md +39 -32
- package/blocks/history.json +68 -0
- package/blocks/run/NOTICE +12 -0
- package/blocks/run/Run.qml +242 -0
- package/blocks/run/run-supervisor.py +522 -0
- package/blocks/store/NOTICE +12 -0
- package/blocks/store/Store.qml +157 -0
- package/blocks/store/store-helper.py +431 -0
- package/package.json +12 -5
- package/skills/omarchy-plugin-audit/SKILL.md +11 -5
- package/skills/omarchy-plugin-build/SKILL.md +164 -0
- package/skills/omarchy-plugin-check/SKILL.md +7 -4
- package/skills/omarchy-plugin-submit/SKILL.md +4 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
- package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
- package/tests/fixtures/weigh/clean/Widget.qml +19 -0
- package/tests/fixtures/weigh/clean/manifest.json +9 -0
- package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
- package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
- package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
- package/tests/fixtures/weigh/poller/Service.qml +50 -0
- package/tests/fixtures/weigh/poller/manifest.json +9 -0
- package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
- package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
- package/tests/lab/run/harness/scenarios/controls.sh +6 -0
- package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
- package/tests/lab/run/harness/scenarios/forge.sh +11 -0
- package/tests/lab/run/harness/scenarios/holder.sh +5 -0
- package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
- package/tests/lab/run/harness/scenarios/stall.sh +5 -0
- package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
- package/tests/lab/run/harness/scenarios/tree.sh +7 -0
- package/tests/lab/run/harness/shell.qml +84 -0
- package/tests/lab/run/report.py +217 -0
- package/tests/lab/run/suite.sh +106 -0
- package/tests/lab/store/harness/shell.qml +73 -0
- package/tests/lab/store/report.py +133 -0
- package/tests/lab/store/suite.sh +109 -0
- package/tests/parity/corpus.mjs +8 -3
- package/tests/parity/run.mjs +4 -4
- package/tools/audit/audit.mjs +17 -6
- package/tools/audit/git.mjs +3 -3
- package/tools/audit/report.mjs +31 -5
- package/tools/blocks/add.mjs +138 -0
- package/tools/blocks/commit.json +5 -0
- package/tools/blocks/record-commit.mjs +77 -0
- package/tools/blocks/registry.mjs +191 -0
- package/tools/blocks/stamp.mjs +61 -0
- package/tools/inspect/contract.mjs +37 -6
- package/tools/inspect/functions.mjs +236 -12
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +71 -5
- package/tools/inspect/measure-functions.mjs +12 -3
- package/tools/inspect/patterns.mjs +35 -18
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +33 -6
- package/tools/inspect/writes.mjs +22 -4
- package/tools/lab/guest.mjs +155 -0
- package/tools/lab/harness.sh +119 -0
- package/tools/lab/host.mjs +177 -0
- package/tools/lab/inspect.mjs +240 -0
- package/tools/lab/omarchy.gpg +13 -0
- package/tools/lab/patches/omarchy-iso-test.patch +351 -0
- package/tools/lab/paths.mjs +173 -0
- package/tools/lab/pin.json +42 -0
- package/tools/lab/pin.mjs +64 -0
- package/tools/lab/prune.mjs +68 -0
- package/tools/lab/qemu.mjs +153 -0
- package/tools/lab/qmp-cli.mjs +21 -0
- package/tools/lab/report.mjs +183 -0
- package/tools/lab/run.mjs +344 -0
- package/tools/lab/setup.mjs +430 -0
- package/tools/lab/suites/run.sh +35 -0
- package/tools/lab/suites/store.sh +41 -0
- package/tools/lab/suites/weigh.sh +196 -0
- package/tools/lab/suites.mjs +142 -0
- package/tools/lab/verify.mjs +134 -0
- package/tools/marketplace/README.md +38 -1
- package/tools/marketplace/banner.mjs +23 -2
- package/tools/marketplace/cli.mjs +449 -146
- package/tools/marketplace/completion-check.mjs +27 -1
- package/tools/marketplace/completion.mjs +32 -4
- package/tools/marketplace/doctor.mjs +47 -9
- package/tools/marketplace/github.mjs +52 -6
- package/tools/marketplace/local-transport.mjs +1 -1
- package/tools/marketplace/options.mjs +16 -5
- package/tools/marketplace/outcome.mjs +244 -0
- package/tools/marketplace/pin.mjs +178 -33
- package/tools/marketplace/setup.mjs +16 -15
- package/tools/marketplace/tree.mjs +1 -1
- package/tools/marketplace/upgrade.mjs +5 -5
- package/tools/marketplace/usage.mjs +116 -72
- package/tools/subject/resolve.mjs +19 -6
- package/tools/weigh/audit.mjs +47 -16
- package/tools/weigh/config.mjs +105 -24
- package/tools/weigh/list.mjs +10 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Which files of the tree a `Run {` site starts: the site's argv[0]
|
|
2
|
+
// resolved through the text to a path, never guessed from a base name. A
|
|
3
|
+
// path resolves when the text shows it: a string literal, a `+` chain of
|
|
4
|
+
// literals, `Qt.resolvedUrl("...")` (relative to the QML file) and
|
|
5
|
+
// `Quickshell.env("X")`; an identifier through its `const`, `let`, `var`
|
|
6
|
+
// or assignment in the enclosing text, a `property` binding, or the value
|
|
7
|
+
// a parent QML file binds when it instantiates the component; a call to a
|
|
8
|
+
// function the same file defines, through its return expression with the
|
|
9
|
+
// arguments in place of its parameters; `a ? b : c` through `b`, `a || b`
|
|
10
|
+
// through `a`, and `String()`, `decodeURIComponent()`, `.replace()`,
|
|
11
|
+
// `.trim()`, `.toString()` through their receiver. Anything else, a
|
|
12
|
+
// value from a JavaScript module included, is unresolved, and a helper
|
|
13
|
+
// whose path is unresolved is not read as started through Run. Measured
|
|
14
|
+
// before this: the rule matched a helper by base name in any string
|
|
15
|
+
// literal of the QML, so the plugin's own copy of a script Omarchy starts
|
|
16
|
+
// from its own tree read as Run-started.
|
|
17
|
+
|
|
18
|
+
import { basename, blankComments, closingBracket, propertyValue, blocks, stringLiteral } from "./text.mjs"
|
|
19
|
+
|
|
20
|
+
const DEPTH = 12
|
|
21
|
+
const TREE = "@/"
|
|
22
|
+
|
|
23
|
+
/** Split an expression at a top-level operator, ignoring strings and brackets. */
|
|
24
|
+
function splitTop(text, operator) {
|
|
25
|
+
const parts = []
|
|
26
|
+
let depth = 0
|
|
27
|
+
let quote = null
|
|
28
|
+
let start = 0
|
|
29
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
30
|
+
const ch = text[index]
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (ch === "\\") index += 1
|
|
33
|
+
else if (ch === quote) quote = null
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch
|
|
37
|
+
else if ("([{".includes(ch)) depth += 1
|
|
38
|
+
else if (")]}".includes(ch)) depth -= 1
|
|
39
|
+
else if (depth === 0 && text.startsWith(operator, index) && (operator !== "?" || text[index + 1] !== "?") && (operator !== "+" || text[index + 1] !== "+")) {
|
|
40
|
+
parts.push(text.slice(start, index))
|
|
41
|
+
index += operator.length - 1
|
|
42
|
+
start = index + 1
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
parts.push(text.slice(start))
|
|
46
|
+
return parts.map((part) => part.trim())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A normalised tree-relative path from a directory and a relative reference. */
|
|
50
|
+
function joinTree(dir, relative) {
|
|
51
|
+
const segments = []
|
|
52
|
+
for (const part of `${dir}/${relative}`.split("/")) {
|
|
53
|
+
if (part === "" || part === ".") continue
|
|
54
|
+
if (part === "..") segments.pop()
|
|
55
|
+
else segments.push(part)
|
|
56
|
+
}
|
|
57
|
+
return segments.join("/")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The definition of a function named `name` in `text`: its parameters and body. */
|
|
61
|
+
function functionIn(text, name) {
|
|
62
|
+
const match = new RegExp(`(?<![\\w.])function\\s+${name}\\s*\\(([^)]*)\\)\\s*\\{`).exec(text)
|
|
63
|
+
if (!match) return null
|
|
64
|
+
const open = match.index + match[0].length - 1
|
|
65
|
+
const end = closingBracket(text, open)
|
|
66
|
+
if (end < 0) return null
|
|
67
|
+
return { params: match[1].split(",").map((part) => part.trim()).filter(Boolean), body: text.slice(open + 1, end) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The assignment or declaration of `name` in `text`: the first one, or,
|
|
72
|
+
* given a site offset `at`, the last one before the site (else the first
|
|
73
|
+
* after it: a property declared below its use), as expression text.
|
|
74
|
+
*/
|
|
75
|
+
function assignmentIn(text, name, at = undefined) {
|
|
76
|
+
const patterns = [
|
|
77
|
+
new RegExp(`(?:const|let|var)\\s+${name}\\s*=\\s*([^\\n;]+)`, "g"),
|
|
78
|
+
new RegExp(`(?<![\\w.])(?:readonly\\s+)?property\\s+(?:string|var)\\s+${name}\\s*:\\s*([^\\n]+)`, "g"),
|
|
79
|
+
new RegExp(`(?<![\\w.])${name}\\s*=(?!=)\\s*([^\\n;]+)`, "g"),
|
|
80
|
+
]
|
|
81
|
+
const found = patterns.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ index: match.index, text: match[1].trim() })))
|
|
82
|
+
if (!found.length) return null
|
|
83
|
+
const first = found.sort((a, b) => a.index - b.index)[0]
|
|
84
|
+
if (at === undefined) return first.text
|
|
85
|
+
const before = found.filter((entry) => entry.index < at).sort((a, b) => b.index - a.index)
|
|
86
|
+
return (before[0] || first).text
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The value a parent QML file binds to `name` on an instance of `component`, with the file it sits in. */
|
|
90
|
+
function parentBinding(name, component, files) {
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
if (file.kind !== "qml") continue
|
|
93
|
+
for (const block of blocks(file.text, component)) {
|
|
94
|
+
const value = propertyValue(block.body, name)
|
|
95
|
+
if (value && value.text.trim() !== '""') return { text: value.text.trim(), file }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveCall(callee, inner, scope) {
|
|
102
|
+
const args = splitTop(inner, ",")
|
|
103
|
+
if (callee === "Qt.resolvedUrl") {
|
|
104
|
+
const literal = stringLiteral(args[0] || "")
|
|
105
|
+
return literal === null ? null : `${TREE}${joinTree(scope.file.path.split("/").slice(0, -1).join("/"), literal)}`
|
|
106
|
+
}
|
|
107
|
+
if (callee === "Quickshell.env") {
|
|
108
|
+
const literal = stringLiteral(args[0] || "")
|
|
109
|
+
return literal === null ? null : `$${literal}`
|
|
110
|
+
}
|
|
111
|
+
if (["String", "decodeURIComponent", "encodeURIComponent"].includes(callee)) return resolve(args[0] || "", scope)
|
|
112
|
+
const name = callee.split(".").pop()
|
|
113
|
+
const fn = functionIn(scope.text, name) || functionIn(scope.file.text, name)
|
|
114
|
+
if (!fn) return null
|
|
115
|
+
const params = {}
|
|
116
|
+
fn.params.forEach((param, index) => { params[param] = { text: args[index] || '""', scope } })
|
|
117
|
+
const inner_scope = { ...scope, text: fn.body, params, depth: scope.depth + 1 }
|
|
118
|
+
for (const expression of returnsIn(fn.body)) {
|
|
119
|
+
const value = resolve(expression, inner_scope)
|
|
120
|
+
if (value !== null) return value
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Every `return` expression of a body, cut at a newline, a `;` or the `}` that closes an enclosing block. */
|
|
126
|
+
function returnsIn(body) {
|
|
127
|
+
const found = []
|
|
128
|
+
for (const match of body.matchAll(/(?<![\w.])return\s+/g)) {
|
|
129
|
+
let depth = 0
|
|
130
|
+
let quote = null
|
|
131
|
+
let end = body.length
|
|
132
|
+
for (let index = match.index + match[0].length; index < body.length; index += 1) {
|
|
133
|
+
const ch = body[index]
|
|
134
|
+
if (quote) {
|
|
135
|
+
if (ch === "\\") index += 1
|
|
136
|
+
else if (ch === quote) quote = null
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch
|
|
140
|
+
else if ("([{".includes(ch)) depth += 1
|
|
141
|
+
else if (")]}".includes(ch)) { if (depth === 0) { end = index; break } depth -= 1 }
|
|
142
|
+
else if ((ch === "\n" || ch === ";") && depth === 0) { end = index; break }
|
|
143
|
+
}
|
|
144
|
+
found.push(body.slice(match.index + match[0].length, end).trim())
|
|
145
|
+
}
|
|
146
|
+
return found
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveIdentifier(name, scope) {
|
|
150
|
+
if (scope.params[name]) return resolve(scope.params[name].text, scope.params[name].scope)
|
|
151
|
+
const inner = scope.text === scope.file.text ? null : assignmentIn(scope.text, name)
|
|
152
|
+
if (inner !== null && inner !== '""' && inner !== "''") return resolve(inner, { ...scope, depth: scope.depth + 1 })
|
|
153
|
+
const outer = assignmentIn(scope.file.text, name, scope.at)
|
|
154
|
+
if (outer !== null && outer !== '""' && outer !== "''") return resolve(outer, { ...scope, text: scope.file.text, params: {}, depth: scope.depth + 1 })
|
|
155
|
+
const component = basename(scope.file.path).replace(/\.qml$/, "")
|
|
156
|
+
const bound = parentBinding(name, component, scope.files)
|
|
157
|
+
if (!bound) return null
|
|
158
|
+
return resolve(bound.text, { file: bound.file, files: scope.files, text: bound.file.text, params: {}, depth: scope.depth + 1, at: Infinity })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @returns {string|null} a value: `@/tree/path` for a path under the tree, `$NAME...` for one built on an environment variable, a plain string for a literal; null when the text does not show it
|
|
163
|
+
*/
|
|
164
|
+
export function resolve(text, scope) {
|
|
165
|
+
const expr = String(text || "").trim().replace(/;$/, "")
|
|
166
|
+
if (!expr || scope.depth > DEPTH) return null
|
|
167
|
+
const literal = stringLiteral(expr)
|
|
168
|
+
if (literal !== null) return literal
|
|
169
|
+
const ternary = splitTop(expr, "?")
|
|
170
|
+
if (ternary.length > 1) return resolve(splitTop(ternary.slice(1).join("?"), ":")[0], scope)
|
|
171
|
+
const either = splitTop(expr, "||")
|
|
172
|
+
if (either.length > 1) return resolve(either[0], scope)
|
|
173
|
+
const chain = splitTop(expr, "+")
|
|
174
|
+
if (chain.length > 1) {
|
|
175
|
+
const parts = chain.map((part) => resolve(part, scope))
|
|
176
|
+
return parts.some((part) => part === null) ? null : parts.join("")
|
|
177
|
+
}
|
|
178
|
+
if (expr.startsWith("(") && closingBracket(expr, 0) === expr.length - 1) return resolve(expr.slice(1, -1), scope)
|
|
179
|
+
const method = expr.match(/^(.+)\.(?:replace|trim|toString|toLowerCase|normalize)\s*\(/)
|
|
180
|
+
if (method && closingBracket(expr, expr.lastIndexOf("(", method[1].length + method[0].length)) === expr.length - 1) return resolve(method[1], scope)
|
|
181
|
+
const call = expr.match(/^([\w.]+)\s*\(/)
|
|
182
|
+
if (call && closingBracket(expr, call[0].length - 1) === expr.length - 1) return resolveCall(call[1], expr.slice(call[0].length, -1), scope)
|
|
183
|
+
if (expr === "Quickshell.shellDir") return "$QUICKSHELL_SHELL_DIR"
|
|
184
|
+
const identifier = expr.match(/^(?:root\.|this\.|[a-z][\w]*\.)?([A-Za-z_]\w*)$/)
|
|
185
|
+
if (identifier) return resolveIdentifier(identifier[1], scope)
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The tree files the QML starts through Run: for every Run site of every
|
|
191
|
+
* QML file, argv[0] resolved through the text; only a value under the
|
|
192
|
+
* tree that names a file of the tree counts. Sites whose argv[0] the text
|
|
193
|
+
* does not show resolve to nothing and mark nothing.
|
|
194
|
+
*
|
|
195
|
+
* @param {Array<{ path: string, kind: string, text: string }>} files
|
|
196
|
+
* @param {Array} processes the process rows, Run sites carrying block: "run"
|
|
197
|
+
* @returns {{ helpers: Set<string>, resolved: Map<string, string|null> }} helper paths, and per Run site (file:line) what argv[0] resolved to
|
|
198
|
+
*/
|
|
199
|
+
export function runStartedHelpers(files, processes) {
|
|
200
|
+
const helpers = new Set()
|
|
201
|
+
const resolved = new Map()
|
|
202
|
+
const paths = new Set(files.map((file) => file.path))
|
|
203
|
+
// Comments are blanked first, as every extractor does: an apostrophe in
|
|
204
|
+
// a comment would otherwise open a string for the bracket matcher.
|
|
205
|
+
const blanked = files.map((file) => (file.kind === "qml" ? { ...file, text: blankComments(file.text) } : file))
|
|
206
|
+
for (const row of processes) {
|
|
207
|
+
if (row.declaredIn !== "qml" || row.block !== "run") continue
|
|
208
|
+
const file = blanked.find((entry) => entry.path === row.file)
|
|
209
|
+
const first = Array.isArray(row.argv) ? row.argv[0] : null
|
|
210
|
+
const at = file ? file.text.split("\n").slice(0, row.line).join("\n").length : 0
|
|
211
|
+
const value = file && first ? resolve(first, { file, files: blanked, text: file.text, params: {}, depth: 0, at }) : null
|
|
212
|
+
const path = value && value.startsWith(TREE) ? joinTree("", value.slice(TREE.length)) : null
|
|
213
|
+
resolved.set(`${row.file}:${row.line}`, path ? `${TREE}${path}` : value)
|
|
214
|
+
if (path && paths.has(path)) helpers.add(path)
|
|
215
|
+
}
|
|
216
|
+
return { helpers, resolved }
|
|
217
|
+
}
|
|
@@ -25,6 +25,8 @@ import { extractWrites } from "./writes.mjs"
|
|
|
25
25
|
import { extractTimers } from "./timers.mjs"
|
|
26
26
|
import { extractFunctions } from "./functions.mjs"
|
|
27
27
|
import { evaluatePatterns, heavyShare, overSize, PATTERNS, rankOf, SIZE, sizeScore } from "./patterns.mjs"
|
|
28
|
+
import { recogniseBlockFile, shippedBlocks } from "../blocks/registry.mjs"
|
|
29
|
+
import { runStartedHelpers } from "./helpers.mjs"
|
|
28
30
|
|
|
29
31
|
export const METHOD = "static extraction, regular expressions over qml and shell; observed, not executed"
|
|
30
32
|
|
|
@@ -43,7 +45,40 @@ export const NOT_VISIBLE = Object.freeze([
|
|
|
43
45
|
])
|
|
44
46
|
|
|
45
47
|
/** The failure codes that mean the target could not be read at all: exit 2, the contract's second status. */
|
|
46
|
-
export const NOT_READABLE = Object.freeze(["subject-not-found", "not-a-git-repository", "commit-not-found", "nothing-to-inspect", "usage"])
|
|
48
|
+
export const NOT_READABLE = Object.freeze(["subject-not-found", "not-a-directory", "not-a-git-repository", "commit-not-found", "nothing-to-inspect", "usage"])
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The omakit blocks in the tree, read from the files' own headers and
|
|
52
|
+
* bodies (tools/blocks/registry.mjs): an unmodified copy of a shipped
|
|
53
|
+
* block is omakit's code, tested in this repository, and its lines are
|
|
54
|
+
* not extracted, so it raises no row of its own; a copy whose body is not
|
|
55
|
+
* one omakit shipped is reported as modified and read like any other file.
|
|
56
|
+
* A block is complete when every file the shipped block has is present.
|
|
57
|
+
*
|
|
58
|
+
* @returns {{ blocks: Array<{ name: string, version: string, shippedVersion: string|null, state: "unmodified"|"modified", complete: boolean, files: Array<{ path: string, state: "unmodified"|"modified", version: string }> }>, skip: Set<string> }}
|
|
59
|
+
*/
|
|
60
|
+
export function recogniseBlocks(files) {
|
|
61
|
+
const shipped = shippedBlocks()
|
|
62
|
+
const found = new Map()
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const base = file.path.split("/").pop()
|
|
65
|
+
const seen = recogniseBlockFile(base, file.text, shipped)
|
|
66
|
+
if (!seen) continue
|
|
67
|
+
if (!found.has(seen.name)) found.set(seen.name, { name: seen.name, shippedVersion: seen.shippedVersion, files: [] })
|
|
68
|
+
found.get(seen.name).files.push({ path: file.path, state: seen.state, version: seen.version })
|
|
69
|
+
}
|
|
70
|
+
const blocks = []
|
|
71
|
+
const skip = new Set()
|
|
72
|
+
for (const entry of [...found.values()].sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
73
|
+
const expected = shipped.find((block) => block.name === entry.name)?.files.map((file) => file.file) || []
|
|
74
|
+
const complete = expected.length > 0 && expected.every((name) => entry.files.some((file) => file.path.split("/").pop() === name))
|
|
75
|
+
const state = entry.files.every((file) => file.state === "unmodified") ? "unmodified" : "modified"
|
|
76
|
+
const versions = [...new Set(entry.files.map((file) => file.version))]
|
|
77
|
+
for (const file of entry.files) if (file.state === "unmodified") skip.add(file.path)
|
|
78
|
+
blocks.push({ name: entry.name, version: versions.length === 1 ? versions[0] : versions.join(", "), shippedVersion: entry.shippedVersion, state, complete, files: entry.files.sort((a, b) => (a.path < b.path ? -1 : 1)) })
|
|
79
|
+
}
|
|
80
|
+
return { blocks, skip }
|
|
81
|
+
}
|
|
47
82
|
|
|
48
83
|
export class InspectError extends Error {
|
|
49
84
|
constructor(code, message, remedy = null) {
|
|
@@ -78,6 +113,11 @@ export async function inspectPlugin({ repoRoot, target, offline = false, allowDi
|
|
|
78
113
|
const pluginId = typeof tree.manifest?.id === "string" ? tree.manifest.id.trim() : null
|
|
79
114
|
|
|
80
115
|
onPhase("reading processes, hosts, writes and timers")
|
|
116
|
+
const { blocks, skip } = recogniseBlocks(tree.files)
|
|
117
|
+
// A `Run {` site is a process only where the tree carries the run block
|
|
118
|
+
// whole and unmodified; otherwise the name is the plugin's own.
|
|
119
|
+
const runBlock = blocks.some((block) => block.name === "run" && block.state === "unmodified" && block.complete)
|
|
120
|
+
const storeBlock = runBlock && blocks.some((block) => block.name === "store" && block.state === "unmodified" && block.complete)
|
|
81
121
|
const processes = []
|
|
82
122
|
const hosts = []
|
|
83
123
|
const writes = []
|
|
@@ -85,9 +125,10 @@ export async function inspectPlugin({ repoRoot, target, offline = false, allowDi
|
|
|
85
125
|
const functions = []
|
|
86
126
|
const notResolvable = []
|
|
87
127
|
for (const file of tree.files) {
|
|
128
|
+
if (skip.has(file.path)) continue
|
|
88
129
|
// Each function carries its rank among the listed ones (M12).
|
|
89
130
|
functions.push(...extractFunctions(file).map((entry) => ({ ...entry, percentile: rankOf(entry) })))
|
|
90
|
-
const rows = extractProcesses(file)
|
|
131
|
+
const rows = extractProcesses(file, { runBlock })
|
|
91
132
|
processes.push(...rows)
|
|
92
133
|
for (const row of rows) {
|
|
93
134
|
if (row.argvForm === "computed") notResolvable.push({ file: row.file, line: row.line, kind: "command", text: `command: ${row.commandText}` })
|
|
@@ -95,12 +136,32 @@ export async function inspectPlugin({ repoRoot, target, offline = false, allowDi
|
|
|
95
136
|
const found = extractHosts(file, rows)
|
|
96
137
|
hosts.push(...found.hosts)
|
|
97
138
|
notResolvable.push(...found.notResolvable)
|
|
98
|
-
writes.push(...extractWrites(file, { pluginId }))
|
|
139
|
+
writes.push(...extractWrites(file, { pluginId, storeBlock }))
|
|
99
140
|
const ticking = extractTimers(file)
|
|
100
141
|
timers.push(...ticking.timers)
|
|
101
142
|
notResolvable.push(...ticking.notResolvable)
|
|
102
143
|
}
|
|
103
144
|
|
|
145
|
+
// A helper the QML starts through Run runs in the block's closed
|
|
146
|
+
// environment (PATH=/usr/bin and the named variables, docs/BLOCKS.md),
|
|
147
|
+
// so a bare tool name inside it is not an ambient PATH lookup. A helper
|
|
148
|
+
// is one a Run site's argv[0] resolves to through the text
|
|
149
|
+
// (helpers.mjs): a path the text does not show marks nothing, and only
|
|
150
|
+
// when every QML process site of the tree is a Run site are its shell
|
|
151
|
+
// lines marked closedEnvironment. The hook Omarchy runs, a test script,
|
|
152
|
+
// a helper reached through a value the text does not show: ambient.
|
|
153
|
+
const qmlSites = processes.filter((row) => row.declaredIn === "qml")
|
|
154
|
+
const allRun = qmlSites.length > 0 && qmlSites.every((row) => row.block === "run")
|
|
155
|
+
const started = runStartedHelpers(tree.files, processes)
|
|
156
|
+
const closed = allRun ? started.helpers : new Set()
|
|
157
|
+
for (const row of processes) {
|
|
158
|
+
row.closedEnvironment = row.declaredIn === "shell" && closed.has(row.file)
|
|
159
|
+
if (row.block === "run") {
|
|
160
|
+
const value = started.resolved.get(`${row.file}:${row.line}`)
|
|
161
|
+
row.helper = value && value.startsWith("@/") && started.helpers.has(value.slice(2)) ? value.slice(2) : null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
104
165
|
let marketplaceBaseline
|
|
105
166
|
let blockingRules = []
|
|
106
167
|
if (offline) {
|
|
@@ -138,13 +199,18 @@ export async function inspectPlugin({ repoRoot, target, offline = false, allowDi
|
|
|
138
199
|
uncommittedFiles: subject.uncommittedFiles,
|
|
139
200
|
},
|
|
140
201
|
observed: { processes, hosts, writes, timers, functions },
|
|
202
|
+
// The omakit blocks in the tree, by their headers and body hashes: an
|
|
203
|
+
// unmodified block's files were not read for facts (they are omakit's,
|
|
204
|
+
// tested here), a modified one's were.
|
|
205
|
+
blocks,
|
|
141
206
|
// Size: the functions over the M12 thresholds, longest first. A count of
|
|
142
207
|
// lines, branches and nesting over the text, compared with what 90 of
|
|
143
208
|
// 100 functions in listed trees stay under; never a judgement.
|
|
144
209
|
size: {
|
|
145
210
|
measurement: SIZE.measurement,
|
|
146
|
-
// The listed trees' own heavy shares,
|
|
147
|
-
// the
|
|
211
|
+
// The listed trees' own heavy shares, in the record's row order and
|
|
212
|
+
// only for the trees with a function, so the score can be read from
|
|
213
|
+
// the document alone.
|
|
148
214
|
sample: { trees: SIZE.trees, functions: SIZE.functions, heavyShares: [...SIZE.distribution.heavyShare] },
|
|
149
215
|
thresholds: { lines: SIZE.lines, branches: SIZE.branches, depth: SIZE.depth },
|
|
150
216
|
// The share of this tree's function lines inside functions over the
|
|
@@ -98,14 +98,23 @@ export function buildRecord(trees, { fetchFailed = [], date = new Date().toISOSt
|
|
|
98
98
|
medianLines: median(tree.functions.map((row) => row.lines)),
|
|
99
99
|
functionLines,
|
|
100
100
|
heavyLines,
|
|
101
|
-
|
|
101
|
+
// null, not 0, with no function: a tree with nothing to measure did not measure light.
|
|
102
|
+
heavyShare: functionLines ? Math.round((heavyLines / functionLines) * 10000) / 10000 : null,
|
|
102
103
|
}
|
|
103
104
|
})
|
|
104
105
|
return {
|
|
105
106
|
measurement: "M12",
|
|
106
107
|
date,
|
|
107
108
|
command: "node tools/inspect/measure-functions.mjs",
|
|
108
|
-
method:
|
|
109
|
+
method: [
|
|
110
|
+
`extractFunctions from tools/inspect/functions.mjs over the first ${trees.length} distinct repositories in the pinned catalog's order whose listing is community, laid out as a root plugin and carrying a validated commit (the 2026-09-15 inspect record's rule), each fetched read-only at that commit in reviewer mode.`,
|
|
111
|
+
"A function is a `function name(`, a named arrow function (`const load = (rows) => {`, `this.load = rows => {`), a method shorthand `load(rows) {` inside an object literal or a class, or a multi-line `onSomething: {` handler in QML and JavaScript; a `name() {` or `function name` block in shell; a `def` in Python. Anonymous callbacks are not counted.",
|
|
112
|
+
"Lines are first to last line inclusive; depth is the deepest nesting below the body, the body itself at 0 in every language, a brace that opens an object or array literal or an inline arrow body not a level; branches count if, else if, for, while, switch, case, catch, &&, || and ?: (their shell and Python equivalents).",
|
|
113
|
+
"In shell, a `||` or `&&` followed by one flow word (return, exit, continue, break, true, false, :) with an optional status (a number, `$?` or a variable) and nothing else on the line but a `;` or `;;` is a guard and not a branch, one per line at its end, so `x && return 0 || return 1` keeps its `&&`; the body of a heredoc (`<<WORD`, `<<-WORD`, quoted or backslashed, outside quotes and outside arithmetic) up to its delimiter alone on a line, and a quoted string that spans lines from its opening quote to the line that closes it, count toward the length and toward nothing else, while a `$(...)`, `${...}` or backtick substitution nested in a string reads its own quotes and, spanning lines, is shell and read as shell; a case arm is a `)` with text after it on a line with no `(` before it but its own opening one; a block opens with `if`, `for`, `while`, `until`, `case` or `select` and closes with `fi`, `done` or `esac`, each counted only where a statement can start over the line with its quoted text removed, so a block that opens and closes on one line is no level and a keyword inside a string or as an argument is nothing.",
|
|
114
|
+
"In Python, a line that starts while a bracket is open, inside a triple-quoted string, or after a line ending in a backslash is a continuation of the statement above it, and a def's own parameter list spanning lines is a continuation of the def: it counts toward the length and the branches and never toward nesting; a bracket or backslash continuation ends at the first line at the def's indent that is not a closing bracket, a triple-quoted string at its close. Branch words inside strings, docstrings and comments are prose, not branches, and a def quoted in a docstring is not a function.",
|
|
115
|
+
"Quantiles are nearest-rank over every function in the sample pooled, not per tree. `distribution` is the histogram of each measure over the same functions, value to count, from which a function's percentile rank is read.",
|
|
116
|
+
"Per tree, functionLines is the lines inside every function, heavyLines the lines inside functions over any p90 threshold of this record, and heavyShare their ratio, null with no function; the size score of omakit inspect is a tree's position among the non-null heavyShares.",
|
|
117
|
+
].join(" "),
|
|
109
118
|
sample: { trees: trees.length, functions: pooled.length, fetchFailed },
|
|
110
119
|
quantiles,
|
|
111
120
|
distribution: { lines: histogram(lines), branches: histogram(branches), depth: histogram(depth) },
|
|
@@ -114,7 +123,7 @@ export function buildRecord(trees, { fetchFailed = [], date = new Date().toISOSt
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
const PREVIOUS = "Re-measured on 2026-09-16 after three extraction fixes, so the quantiles are not comparable with the 0.4.3 record (this file at commit 6619c26: 50 trees, 6040 functions, p90 22 lines, 6 branches, nesting 3; and before it 18 trees, 715 functions, p90 18 lines, 7 branches, nesting 2): a Python body now starts at depth 0 the way a brace body does, where it started at 1; a brace that opens an object or array literal is no longer a nesting level; and a named arrow function or a method shorthand is a function, where only `function name(` and `onSomething: {` were."
|
|
126
|
+
const PREVIOUS = "Re-measured on 2026-09-16 after three extraction fixes, so the quantiles are not comparable with the 0.4.3 record (this file at commit 6619c26: 50 trees, 6040 functions, p90 22 lines, 6 branches, nesting 3; and before it 18 trees, 715 functions, p90 18 lines, 7 branches, nesting 2): a Python body now starts at depth 0 the way a brace body does, where it started at 1; a brace that opens an object or array literal is no longer a nesting level; and a named arrow function or a method shorthand is a function, where only `function name(` and `onSomething: {` were. Re-measured again for 0.5.1 after more (the 0.5.0 record is this file at commit 7df451a: 6041 functions, p90 22 lines, 6 branches, nesting 2, six heavyShares of 0 among 50): a Python continuation line inside an open bracket, a triple-quoted string or after a backslash no longer counts as nesting, and a def whose parameter list spans lines is read through to its body where before only the signature was, which is what moved the line p90 from 22 to 25; branch words in Python strings and docstrings are prose; a shell heredoc body and a quoted program spanning lines are no longer read as shell; a shell guard (`|| return 1`) is no longer a branch; and a tree with no function carries heavyShare null instead of 0, so it no longer lifts every other tree's rank. The earlier 0.5.1 cuts, never released, are in this file's history, each corrected by the next: one read a `\"` inside a string and a `\"\"\"` inside a bracket wrongly and stretched two listed Python functions (101 to 133 and 128 to 162 lines); one still read `<<` in arithmetic and a quote inside `$(...)` inside a string wrongly, and counted docstring prose as branches; one counted `if x; then y; fi` on one line as a nesting level; and one closed a block on `done` as a plain argument and peeled two guards from one line."
|
|
118
127
|
|
|
119
128
|
export async function measureFunctions(repoRoot, { count = TREES, cacheRoot = omakitCacheDir(), log = () => {} } = {}) {
|
|
120
129
|
const pin = requirePin(repoRoot)
|
|
@@ -34,11 +34,20 @@ function hasFlag(flags, letter, long = null) {
|
|
|
34
34
|
return flags.some((flag) => flag === `-${letter}` || (long && (flag === long || flag.startsWith(`${long}=`))) || (/^-[A-Za-z]+$/.test(flag) && flag.includes(letter)))
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The tool a process resolves through PATH: its first non-wrapper word
|
|
39
|
+
* when that word is a literal with no slash. A word that is an expression
|
|
40
|
+
* (`[script, name]`, `[root.helperPath("x.sh")]`) is not a name looked up
|
|
41
|
+
* in PATH; it is a value the text does not show, and the argument-grammar
|
|
42
|
+
* class already names the site. Measured on the Theme Manager port
|
|
43
|
+
* (2026-09-17): 12 of its 24 Run sites read as "resolved from PATH" with
|
|
44
|
+
* the tool `script`, an absolute path at run time.
|
|
45
|
+
*/
|
|
38
46
|
function pathResolved(process) {
|
|
39
47
|
if (!Array.isArray(process.argv) || !process.argv.length) return null
|
|
40
|
-
const { tool } = toolOf(process.argv)
|
|
48
|
+
const { tool, index } = toolOf(process.argv)
|
|
41
49
|
if (!tool || tool.includes("/")) return null
|
|
50
|
+
if ((process.expressions || []).some((entry) => entry.index === index)) return null
|
|
42
51
|
return tool
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -89,22 +98,23 @@ function secretLogs(files) {
|
|
|
89
98
|
*/
|
|
90
99
|
export const SIZE = Object.freeze({
|
|
91
100
|
measurement: "M12",
|
|
92
|
-
sample: "50 listed trees,
|
|
101
|
+
sample: "50 listed trees, 6034 functions",
|
|
93
102
|
trees: 50,
|
|
94
|
-
functions:
|
|
95
|
-
lines:
|
|
103
|
+
functions: 6034,
|
|
104
|
+
lines: 25,
|
|
96
105
|
branches: 6,
|
|
97
106
|
depth: 2,
|
|
98
|
-
// The histogram of each measure over the
|
|
107
|
+
// The histogram of each measure over the 6034 functions, value to count,
|
|
99
108
|
// from which a function's percentile rank among listed functions is read;
|
|
100
109
|
// and each listed tree's heavyShare, the share of its function lines in
|
|
101
|
-
// functions over the thresholds, in the record's row order
|
|
102
|
-
// a tree
|
|
110
|
+
// functions over the thresholds, in the record's row order and only for
|
|
111
|
+
// the trees with a function (a tree with nothing to measure has no
|
|
112
|
+
// share), from which a tree's position among listed trees is read.
|
|
103
113
|
distribution: Object.freeze({
|
|
104
|
-
lines: Object.freeze({ 1: 330, 2:
|
|
105
|
-
branches: Object.freeze({ 0:
|
|
106
|
-
depth: Object.freeze({ 0:
|
|
107
|
-
heavyShare: Object.freeze([0.421, 0.3361, 0.4049, 0, 0.
|
|
114
|
+
lines: Object.freeze({ 1: 330, 2: 157, 3: 667, 4: 620, 5: 584, 6: 508, 7: 413, 8: 337, 9: 301, 10: 238, 11: 195, 12: 138, 13: 152, 14: 131, 15: 106, 16: 75, 17: 86, 18: 62, 19: 71, 20: 57, 21: 51, 22: 56, 23: 47, 24: 44, 25: 33, 26: 28, 27: 24, 28: 26, 29: 27, 30: 17, 31: 21, 32: 22, 33: 18, 34: 23, 35: 11, 36: 5, 37: 16, 38: 13, 39: 13, 40: 9, 41: 13, 42: 12, 43: 11, 44: 9, 45: 8, 46: 14, 47: 8, 48: 11, 49: 7, 50: 6, 51: 1, 52: 2, 53: 6, 54: 3, 55: 7, 56: 4, 57: 3, 58: 5, 59: 3, 60: 8, 61: 5, 62: 3, 63: 6, 64: 2, 65: 3, 66: 1, 67: 3, 68: 4, 69: 9, 70: 7, 71: 7, 72: 3, 73: 3, 74: 3, 75: 1, 76: 1, 77: 2, 78: 2, 79: 3, 80: 3, 81: 4, 82: 3, 83: 1, 85: 1, 86: 3, 87: 3, 88: 1, 89: 1, 90: 3, 91: 2, 92: 2, 93: 2, 94: 1, 96: 3, 97: 2, 98: 1, 99: 1, 100: 1, 101: 3, 102: 2, 104: 2, 107: 2, 108: 1, 109: 2, 110: 2, 111: 2, 112: 2, 113: 2, 115: 3, 116: 1, 117: 2, 119: 1, 120: 1, 121: 1, 125: 1, 126: 1, 128: 2, 129: 1, 133: 1, 134: 1, 142: 1, 144: 1, 145: 1, 154: 1, 155: 1, 161: 2, 173: 1, 217: 2, 253: 1, 272: 1, 277: 1, 290: 1, 292: 1, 298: 1, 325: 1, 365: 1, 495: 1 }),
|
|
115
|
+
branches: Object.freeze({ 0: 2302, 1: 1130, 2: 828, 3: 457, 4: 344, 5: 221, 6: 180, 7: 127, 8: 83, 9: 70, 10: 48, 11: 48, 12: 24, 13: 30, 14: 20, 15: 10, 16: 14, 17: 10, 18: 12, 19: 8, 20: 6, 21: 4, 22: 5, 23: 5, 24: 7, 25: 4, 26: 2, 27: 7, 28: 5, 29: 2, 31: 1, 32: 2, 33: 2, 35: 1, 36: 3, 38: 1, 39: 1, 40: 1, 41: 1, 43: 1, 45: 2, 55: 1, 64: 1, 75: 1, 78: 1, 85: 1 }),
|
|
116
|
+
depth: Object.freeze({ 0: 3854, 1: 1478, 2: 482, 3: 140, 4: 47, 5: 27, 6: 5, 7: 1 }),
|
|
117
|
+
heavyShare: Object.freeze([0.421, 0.3361, 0.4049, 0, 0.2979, 0.3647, 0.4107, 0.6167, 0.2635, 0.8434, 0, 0, 0.193, 0.6399, 0.4063, 0.4769, 0.4322, 0.5337, 0.3628, 0.6626, 0.0554, 0.7775, 0.3548, 0.3285, 0, 0.6801, 0.2222, 0.3084, 0.3705, 0.6468, 0.3928, 0.6786, 0.3247, 0.5225, 0, 0.1462, 0, 0.5817, 0.5891, 0.3835, 0.2491, 0.5627, 0.2439, 0.3643, 0.6719, 0.6525, 0.4385, 0.565, 0.5672]),
|
|
108
118
|
}),
|
|
109
119
|
})
|
|
110
120
|
|
|
@@ -142,13 +152,14 @@ export function heavyShare(functions) {
|
|
|
142
152
|
|
|
143
153
|
/**
|
|
144
154
|
* The share of listed trees whose heavyShare is strictly smaller than this
|
|
145
|
-
* one, as a percentage over the trees
|
|
146
|
-
* no function over the thresholds, 100 for one heavier than
|
|
147
|
-
* tree.
|
|
155
|
+
* one, as a percentage over the listed trees that have a share: 0 for a
|
|
156
|
+
* tree with no function over the thresholds, 100 for one heavier than
|
|
157
|
+
* every listed tree.
|
|
148
158
|
*/
|
|
149
159
|
export function treeRank(share) {
|
|
150
|
-
const
|
|
151
|
-
|
|
160
|
+
const shares = SIZE.distribution.heavyShare
|
|
161
|
+
const below = shares.filter((listed) => listed < share).length
|
|
162
|
+
return Math.round((below / shares.length) * 1000) / 10
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
/**
|
|
@@ -220,13 +231,19 @@ export const PATTERNS = Object.freeze([
|
|
|
220
231
|
share: 0.07,
|
|
221
232
|
sample: SAMPLE,
|
|
222
233
|
precondition: ({ processes, hosts }) => {
|
|
223
|
-
|
|
234
|
+
// A line of a helper started through Run resolves its tools in the
|
|
235
|
+
// block's closed PATH (docs/BLOCKS.md); it is counted apart, never as
|
|
236
|
+
// an ambient lookup.
|
|
237
|
+
const ambient = processes.filter((row) => !row.closedEnvironment)
|
|
238
|
+
const resolved = ambient.map((row) => ({ row, tool: pathResolved(row) })).filter((entry) => entry.tool)
|
|
239
|
+
const closed = processes.filter((row) => row.closedEnvironment && pathResolved(row))
|
|
224
240
|
const curls = hosts.filter((row) => row.tool === "curl" && !hasFlag(row.flags, "q", "--disable"))
|
|
225
241
|
const parts = []
|
|
226
242
|
const names = [...new Set(resolved.map((entry) => entry.tool))]
|
|
227
243
|
const named = names.length > 6 ? `${names.slice(0, 6).join(", ")} and ${names.length - 6} more` : names.join(", ")
|
|
228
244
|
if (resolved.length) parts.push(`${plural(resolved.length, "tool")} resolved from PATH (${named}; ${sites(resolved.map((entry) => entry.row))})`)
|
|
229
245
|
if (curls.length) parts.push(`curl without -q (${sites(curls)})`)
|
|
246
|
+
if (closed.length) parts.push(`${plural(closed.length, "tool name")} in ${plural(new Set(closed.map((row) => row.file)).size, "helper")} started through Run, resolved in the block's closed PATH and not counted`)
|
|
230
247
|
return { sites: [...resolved.map((entry) => entry.row), ...curls].map(site), observation: `observed ${parts.join("; ")}` }
|
|
231
248
|
},
|
|
232
249
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Process sites: every `Process {` block in QML (its `command:` inside the
|
|
2
|
-
// block or assigned to its id elsewhere in the file), every
|
|
2
|
+
// block or assigned to its id elsewhere in the file), every `Run {` block
|
|
3
|
+
// where the tree carries the omakit run block unmodified (its deadline and
|
|
4
|
+
// caps are the block's, docs/BLOCKS.md), every
|
|
3
5
|
// `Quickshell.execDetached([...])`, and every command line of a shell
|
|
4
6
|
// script. Each row carries the argv the text shows, whether a deadline is
|
|
5
7
|
// observed for it, what collects its output and whether a producer-side
|
|
@@ -167,6 +169,7 @@ function qmlRow(file, text, block, command, commandOffset) {
|
|
|
167
169
|
deadline: deadlineFor(text, id, resolved.argv),
|
|
168
170
|
output: outputOf(block.body, resolved.argv),
|
|
169
171
|
shellWrapper: isShellWrapper(resolved.argv),
|
|
172
|
+
closedEnvironment: false,
|
|
170
173
|
pipedFrom: null,
|
|
171
174
|
}
|
|
172
175
|
}
|
|
@@ -195,16 +198,42 @@ function detachedRows(file, text) {
|
|
|
195
198
|
deadline: { observed: false, via: null, ms: null },
|
|
196
199
|
output: { collector: "none", capObserved: false, via: null },
|
|
197
200
|
shellWrapper: isShellWrapper(resolved.argv),
|
|
201
|
+
closedEnvironment: false,
|
|
198
202
|
pipedFrom: null,
|
|
199
203
|
})
|
|
200
204
|
}
|
|
201
205
|
return rows
|
|
202
206
|
}
|
|
203
207
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
208
|
+
/**
|
|
209
|
+
* A `Run {` site of the omakit run block: the deadline is the block's
|
|
210
|
+
* (`deadlineMs`, default 10000), the caps are the block's (`maxBytes`),
|
|
211
|
+
* and a shell string is refused unless `allowShellString: true` is on the
|
|
212
|
+
* block's own line, in which case the wrapper is observed as it would be
|
|
213
|
+
* on a Process.
|
|
214
|
+
*/
|
|
215
|
+
function runRow(file, text, block) {
|
|
216
|
+
const inside = propertyValue(block.body, "command")
|
|
217
|
+
const command = inside && !/^\[\s*\]$/.test(inside.text) ? inside : assignedCommand(text, block.id)
|
|
218
|
+
const offset = inside && !/^\[\s*\]$/.test(inside.text) ? block.open + 1 + inside.offset : command ? command.offset : null
|
|
219
|
+
const row = qmlRow(file, text, block, command, offset)
|
|
220
|
+
const deadline = numeric(propertyValue(block.body, "deadlineMs")?.text)
|
|
221
|
+
const allowShellString = propertyValue(block.body, "allowShellString")?.text === "true"
|
|
222
|
+
return {
|
|
223
|
+
...row,
|
|
224
|
+
running: row.running || (block.id ? new RegExp(`(?<![\\w.])${block.id}\\.start\\s*\\(`).test(text) : false),
|
|
225
|
+
block: "run",
|
|
226
|
+
deadline: { observed: true, via: "block-run", ms: deadline ?? 10000 },
|
|
227
|
+
output: { collector: "Run", capObserved: true, via: "maxBytes" },
|
|
228
|
+
shellWrapper: allowShellString ? row.shellWrapper : false,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every `Process {` block and detached exec in a QML file; `Run {` blocks too when the tree carries the run block. */
|
|
233
|
+
export function qmlProcesses(file, { runBlock = false } = {}) {
|
|
206
234
|
const text = blankComments(file.text)
|
|
207
235
|
const rows = []
|
|
236
|
+
if (runBlock) for (const block of blocks(text, "Run")) rows.push(runRow(file, text, block))
|
|
208
237
|
for (const block of blocks(text, "Process")) {
|
|
209
238
|
const inside = propertyValue(block.body, "command")
|
|
210
239
|
// `command: []` is a placeholder for a value set later: the assignment to
|
|
@@ -235,6 +264,9 @@ function commandWords(segment) {
|
|
|
235
264
|
if (words[0] === "for" || words[0] === "case" || words[0] === "select" || words[0] === "function") return null
|
|
236
265
|
words = words.slice(1)
|
|
237
266
|
}
|
|
267
|
+
// `exec cmd` replaces the shell with cmd: a process site, the wrapper
|
|
268
|
+
// dropped; `exec` alone or with only redirections runs nothing.
|
|
269
|
+
while (words.length > 1 && words[0] === "exec" && !/^[<>&\d]/.test(words[1])) words = words.slice(1)
|
|
238
270
|
if (!words.length) return null
|
|
239
271
|
if (/^[A-Za-z_]\w*\(\)$/.test(words[0])) return null
|
|
240
272
|
if (BUILTINS.has(basename(words[0])) && basename(words[0]) !== "eval") return null
|
|
@@ -278,6 +310,7 @@ export function shellProcesses(file) {
|
|
|
278
310
|
deadline: stripped.some((word) => basename(word) === "timeout") ? { observed: true, via: "timeout-argv", ms: deadlineMs } : { observed: false, via: null, ms: null },
|
|
279
311
|
output: { collector: "none", capObserved: lineCap.observed, via: lineCap.via },
|
|
280
312
|
shellWrapper: isShellWrapper(stripped),
|
|
313
|
+
closedEnvironment: false,
|
|
281
314
|
pipedFrom: segment.operator === "|" || segment.operator === "|&" ? previous : null,
|
|
282
315
|
}
|
|
283
316
|
rows.push(row)
|
|
@@ -291,8 +324,8 @@ export function shellProcesses(file) {
|
|
|
291
324
|
* @param {{ path: string, kind: string, text: string }} file
|
|
292
325
|
* @returns {Array} process rows, in file order
|
|
293
326
|
*/
|
|
294
|
-
export function extractProcesses(file) {
|
|
295
|
-
if (file.kind === "qml") return qmlProcesses(file)
|
|
327
|
+
export function extractProcesses(file, options = {}) {
|
|
328
|
+
if (file.kind === "qml") return qmlProcesses(file, options)
|
|
296
329
|
if (file.kind === "shell") return shellProcesses(file)
|
|
297
330
|
return []
|
|
298
331
|
}
|