omakit 0.4.2 → 0.4.3
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 +9 -3
- package/package.json +1 -1
- package/skills/omarchy-plugin-check/SKILL.md +57 -1
- package/tools/inspect/contract.mjs +245 -0
- package/tools/inspect/functions.mjs +129 -0
- package/tools/inspect/hosts.mjs +141 -0
- package/tools/inspect/inspect.mjs +170 -0
- package/tools/inspect/patterns.mjs +326 -0
- package/tools/inspect/processes.mjs +298 -0
- package/tools/inspect/report.mjs +342 -0
- package/tools/inspect/text.mjs +496 -0
- package/tools/inspect/timers.mjs +65 -0
- package/tools/inspect/walk.mjs +102 -0
- package/tools/inspect/writes.mjs +194 -0
- package/tools/marketplace/README.md +20 -1
- package/tools/marketplace/cli.mjs +48 -0
- package/tools/marketplace/local-transport.mjs +16 -4
- package/tools/marketplace/options.mjs +1 -0
- package/tools/marketplace/pin.mjs +18 -10
- package/tools/marketplace/run-baseline.mjs +2 -1
- package/tools/marketplace/style.mjs +3 -0
- package/tools/marketplace/usage.mjs +21 -0
- package/tools/marketplace/verify.mjs +15 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm i -g omakit && omakit setup
|
|
|
13
13
|
npx skills add mtolhuys/omakit
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Runs where [Node 22+](package.json) and Git run; `weigh` and `audit` need a running Omarchy shell.
|
|
17
17
|
|
|
18
18
|
Licence: [MIT](LICENSE).
|
|
19
19
|
|
|
@@ -35,6 +35,12 @@ Checks your submission commits and names the action that re-runs stale validatio
|
|
|
35
35
|
|
|
36
36
|
Compares your installed plugin commits with the marketplace's validated commits. The GIF shows drift rows first: on the author's desktop, 9 of 18 audited plugins ran commits the marketplace never validated.
|
|
37
37
|
|
|
38
|
+
## `omakit inspect <plugin-dir>`
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Reads a plugin's tree and prints what needs attention, biggest first: a size score (10 minus the mean rank of its functions among functions in listed plugins, [M12](docs/MEASUREMENTS.md#m12-how-long-a-plugins-functions-are-in-listed-trees)), the functions over what 90 of 100 listed functions stay under, then each review class the tree shows with the class's measured share of review findings ([M11](docs/MEASUREMENTS.md#m11-what-the-human-review-raises-by-class)) and up to five sites. No verdict, nothing run from the tree; `--full` is every site, `--json` the document. Over 18 listed plugins read at their validated commits, the extraction counted 515 process sites (57 QML `Process` blocks, 458 shell lines), 17 hosts, 63 writes and 40 timers, left 15 rows it could not resolve, and printed 73 pattern rows across 17 of the 18 ([record](docs/evidence/inspect/2026-09-15-listed-sample.json)).
|
|
43
|
+
|
|
38
44
|
## `omakit weigh <plugin>`
|
|
39
45
|
|
|
40
46
|

|
|
@@ -48,13 +54,13 @@ Measures the shell with and without your plugin, reading Pss and CPU. The GIF sh
|
|
|
48
54
|
| Baseline parity | 30/30 identical results, [recorded corpus](docs/evidence/parity/2026-09-12-local-vs-github-2.json), 2026-09-12 |
|
|
49
55
|
| Stale validated commit | 326/519 readable comparisons stale (62.8%); 64/583 unknown, [2026-09-15 data](docs/evidence/staleness/2026-09-15.json) |
|
|
50
56
|
| Registry churn | 4,201/4,293 registry-only commits in 30 days, 2026-09-13, [M7](docs/MEASUREMENTS.md#m7-the-registry-moves-by-the-hour-the-code-and-the-rules-move-by-the-week) |
|
|
51
|
-
| GIFs are recorded output |
|
|
57
|
+
| GIFs are recorded output | 6 GIFs with [captures and scenes](docs/media/README.md) |
|
|
52
58
|
| Posts nothing | 0 marketplace writes, [M10](docs/MEASUREMENTS.md#m10-readme-evidence-and-command-captures) |
|
|
53
59
|
| Zero dependencies | 0 runtime and 0 development dependencies, counted in [package.json](package.json) |
|
|
54
60
|
|
|
55
61
|
## Documentation
|
|
56
62
|
|
|
57
63
|
- Using: [install](docs/INSTALL.md), [commands](docs/COMMANDS.md), [audience](docs/MARKETPLACE.md).
|
|
58
|
-
- Checks and measurements: [submit](docs/SUBMIT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md), [evidence](docs/MEASUREMENTS.md).
|
|
64
|
+
- Checks and measurements: [submit](docs/SUBMIT.md), [inspect](docs/INSPECT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md), [evidence](docs/MEASUREMENTS.md).
|
|
59
65
|
- Method docs: [how](docs/HOW.md), [weigh](docs/WEIGH.md), [upstream contract](docs/UPSTREAM_CONTRACT.md), [palette](docs/PALETTE.md), [terminal](docs/TUI.md).
|
|
60
66
|
- Contributing: [repository rules](AGENTS.md), [releasing](docs/RELEASING.md), [media](docs/media/README.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "The safe place to find out: everything knowable about an Omarchy Quattro plugin submission before you post it, on your own machine. Agent-first, read-only against the marketplace, posts nothing, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Maarten Tolhuijs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: omarchy-plugin-check
|
|
3
|
-
description: Check an Omarchy Quattro plugin while building or changing it, before committing or pushing. Use whenever you create, edit, refactor or test an Omarchy plugin, when asked whether a plugin is marketplace-ready, or before any push to its default branch. Runs the marketplace's own security baseline and submission checks locally, read-only,
|
|
3
|
+
description: Check an Omarchy Quattro plugin while building or changing it, before committing or pushing. Use whenever you create, edit, refactor or test an Omarchy plugin, when asked whether a plugin is marketplace-ready, or before any push to its default branch. Runs the marketplace's own security baseline and submission checks locally, read-only, reports what the marketplace would refuse, and lists what the tree does (processes, hosts, writes, timers) as observations beside the review classes a human reviewer raises most.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Checking a plugin while you build it
|
|
@@ -31,6 +31,62 @@ editorial choice about where the plugin belongs.
|
|
|
31
31
|
needs the network and reads the listed ids from the pin. For the real
|
|
32
32
|
submission, drop it, and use `skills/omarchy-plugin-submit/SKILL.md`.
|
|
33
33
|
|
|
34
|
+
## Read what the tree does before a human does
|
|
35
|
+
|
|
36
|
+
After the two commands above pass, and again whenever you add or change a
|
|
37
|
+
`Process`, a `curl`, a `FileView`, a shell script or a `Timer`, run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
omakit inspect <path-to-the-plugin-repo> --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`inspect` lists what the tree shows, in the order a reviewer reads it: every
|
|
44
|
+
process with its argv and whether a deadline is observed for it, every host
|
|
45
|
+
with its timeout and size-cap flags, every write with whether its path falls
|
|
46
|
+
under a directory the plugin controls, every timer with its interval, and the
|
|
47
|
+
baseline's capabilities. Below the facts, `patterns` holds one entry per
|
|
48
|
+
review class the marketplace's human review has raised, only where the tree
|
|
49
|
+
shows the class's precondition (a process with no deadline, a collector with
|
|
50
|
+
no cap, a write under `/tmp`, `curl` without `-q`), with the class's measured
|
|
51
|
+
share of review findings (`share`, from M11 of `docs/MEASUREMENTS.md`).
|
|
52
|
+
`lookedFor` names the classes whose precondition was not observed.
|
|
53
|
+
`size.over` lists the functions longer, more branched or deeper than 90 of
|
|
54
|
+
100 functions in listed trees (the thresholds are in `size.thresholds`,
|
|
55
|
+
from M12), longest first, each with its `percentile` among them, and
|
|
56
|
+
`size.score` is 10 minus the mean percentile of every function in the
|
|
57
|
+
tree. When the owner asks for simpler code, start with the top of
|
|
58
|
+
`size.over`, and read the score before and after as the measure of the
|
|
59
|
+
change; it is a position among listed plugins, not a grade, so never tell
|
|
60
|
+
the owner a score is good or bad, only that it moved. Read
|
|
61
|
+
the document, not the report: the report a person sees lists at most five
|
|
62
|
+
sites per class and drops classes under five percent, `--full` prints every
|
|
63
|
+
site, and `--json` carries all of it either way.
|
|
64
|
+
|
|
65
|
+
How to read "observed". Every row is what regular expressions found in the
|
|
66
|
+
text at `file:line`, never a runtime fact and never a verdict: `deadline
|
|
67
|
+
observed` means a killing `Timer`, a `timeout` in argv or a destruction
|
|
68
|
+
handler is in the file; `no cap observed` means the argv shows none of
|
|
69
|
+
`head -c`, `--max-filesize` or `timeout`; `observed nothing of this kind`
|
|
70
|
+
means the extraction found nothing, not that the tree is clean. A `▒ ?` row
|
|
71
|
+
(`argvForm: "computed"`, `argv: null`) is a command the text does not show
|
|
72
|
+
as a literal; do not guess it for the owner, read the file. `notVisible`
|
|
73
|
+
names what the method cannot see (commands built at run time, values from
|
|
74
|
+
variables or config, components outside the tree), and a tree that uses
|
|
75
|
+
those has facts `inspect` did not list.
|
|
76
|
+
|
|
77
|
+
What to do with a pattern row. It is not a marketplace rule and not a
|
|
78
|
+
finding: the marketplace's automated baseline blocks, a maintainer reviews,
|
|
79
|
+
and `inspect` only reports that the tree shows something reviewers have
|
|
80
|
+
raised in about N of every 100 findings. Show the owner the row and the
|
|
81
|
+
requirement in the reviewer's own terms from the M11 table (an absolute
|
|
82
|
+
deadline, producer-side bounds, a private 0700 directory, `curl -q`, no
|
|
83
|
+
secret in argv), and let the owner decide; never describe the row as a
|
|
84
|
+
requirement the marketplace enforces, and never say "safe" or "clean" about
|
|
85
|
+
a tree with no rows. The `supply-chain` row is the baseline's own finding
|
|
86
|
+
restated with its evidence sites; the baseline result is what decides there.
|
|
87
|
+
Exit status is 0 whenever a report was produced, so do not read the exit
|
|
88
|
+
code as pass or fail; 2 means the target could not be read.
|
|
89
|
+
|
|
34
90
|
## If omakit is not installed
|
|
35
91
|
|
|
36
92
|
```bash
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// The JSON contract of docs/INSPECT.md, executable. `validateInspectDocument()`
|
|
2
|
+
// returns every way a document departs from it, as sentences, and an empty
|
|
3
|
+
// list when it does not. tests/unit/inspect.test.mjs holds every produced
|
|
4
|
+
// document to it, so the prose and the code cannot drift apart unnoticed.
|
|
5
|
+
//
|
|
6
|
+
// Run as a program: `node tools/inspect/contract.mjs <document.json>` prints
|
|
7
|
+
// the problems and exits 1 on any.
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs"
|
|
10
|
+
import { resolve } from "node:path"
|
|
11
|
+
import { pathToFileURL } from "node:url"
|
|
12
|
+
|
|
13
|
+
const ARGV_FORMS = new Set(["array", "string", "computed"])
|
|
14
|
+
const DEADLINE_VIA = new Set(["timer-kill", "timeout-argv", "destruction", null])
|
|
15
|
+
const COLLECTORS = new Set(["StdioCollector", "SplitParser", "none", "unknown"])
|
|
16
|
+
const CONTROLLED = new Set(["observed", "not-observed", "unknown"])
|
|
17
|
+
const DECLARED_IN = new Set(["qml", "shell"])
|
|
18
|
+
const FILE_KINDS = ["qml", "js", "shell", "python", "other"]
|
|
19
|
+
|
|
20
|
+
const isString = (value) => typeof value === "string"
|
|
21
|
+
const isBool = (value) => typeof value === "boolean"
|
|
22
|
+
const isInt = (value) => Number.isInteger(value)
|
|
23
|
+
const nullOr = (test) => (value) => value === null || test(value)
|
|
24
|
+
|
|
25
|
+
function site(value, at, problems) {
|
|
26
|
+
if (!value || typeof value !== "object") {
|
|
27
|
+
problems.push(`${at} is not a site`)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (!isString(value.file)) problems.push(`${at}.file is not a string`)
|
|
31
|
+
if (!isInt(value.line) || value.line < 1) problems.push(`${at}.line is not a positive integer`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function processRow(row, at, problems) {
|
|
35
|
+
site(row, at, problems)
|
|
36
|
+
if (!DECLARED_IN.has(row.declaredIn)) problems.push(`${at}.declaredIn is neither qml nor shell`)
|
|
37
|
+
if (!nullOr(isString)(row.id)) problems.push(`${at}.id is neither a string nor null`)
|
|
38
|
+
if (!ARGV_FORMS.has(row.argvForm)) problems.push(`${at}.argvForm is not array, string or computed`)
|
|
39
|
+
if (row.argvForm === "computed") {
|
|
40
|
+
if (row.argv !== null) problems.push(`${at}.argv is not null for a computed command`)
|
|
41
|
+
if (!isString(row.commandText)) problems.push(`${at}.commandText is not the expression text of a computed command`)
|
|
42
|
+
} else if (!Array.isArray(row.argv) || row.argv.some((word) => !isString(word))) problems.push(`${at}.argv is not a list of strings`)
|
|
43
|
+
if (!Array.isArray(row.expressions) || row.expressions.some((entry) => !isInt(entry?.index) || !isString(entry?.text))) problems.push(`${at}.expressions is not a list of { index, text }`)
|
|
44
|
+
if (row.argvForm === "array" && Array.isArray(row.argv)) {
|
|
45
|
+
for (const entry of row.expressions || []) if (row.argv[entry.index] !== entry.text) problems.push(`${at}.expressions[${entry.index}] does not name the argv element it stands for`)
|
|
46
|
+
}
|
|
47
|
+
for (const key of ["running", "detached", "shellWrapper"]) if (!isBool(row[key])) problems.push(`${at}.${key} is not a boolean`)
|
|
48
|
+
const deadline = row.deadline || {}
|
|
49
|
+
if (!isBool(deadline.observed)) problems.push(`${at}.deadline.observed is not a boolean`)
|
|
50
|
+
if (!DEADLINE_VIA.has(deadline.via)) problems.push(`${at}.deadline.via is not timer-kill, timeout-argv, destruction or null`)
|
|
51
|
+
if (deadline.observed !== (deadline.via !== null)) problems.push(`${at}.deadline.observed disagrees with deadline.via`)
|
|
52
|
+
if (!nullOr(isInt)(deadline.ms)) problems.push(`${at}.deadline.ms is neither an integer nor null`)
|
|
53
|
+
const output = row.output || {}
|
|
54
|
+
if (!COLLECTORS.has(output.collector)) problems.push(`${at}.output.collector is not StdioCollector, SplitParser, none or unknown`)
|
|
55
|
+
if (!isBool(output.capObserved)) problems.push(`${at}.output.capObserved is not a boolean`)
|
|
56
|
+
if (!nullOr(isString)(output.via)) problems.push(`${at}.output.via is neither a string nor null`)
|
|
57
|
+
if (output.capObserved !== (output.via !== null)) problems.push(`${at}.output.capObserved disagrees with output.via`)
|
|
58
|
+
if (row.pipedFrom !== null && (!isInt(row.pipedFrom?.line) || !isString(row.pipedFrom?.argv0))) problems.push(`${at}.pipedFrom is neither null nor { line, argv0 }`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function host(row, at, problems) {
|
|
62
|
+
site(row, at, problems)
|
|
63
|
+
if (!isString(row.host) || !row.host) problems.push(`${at}.host is not a host name`)
|
|
64
|
+
if (row.scheme !== "http" && row.scheme !== "https") problems.push(`${at}.scheme is neither http nor https`)
|
|
65
|
+
if (!nullOr(isString)(row.tool)) problems.push(`${at}.tool is neither a string nor null`)
|
|
66
|
+
for (const key of ["timeout", "sizeCap"]) {
|
|
67
|
+
const flag = row[key] || {}
|
|
68
|
+
if (!isBool(flag.observed)) problems.push(`${at}.${key}.observed is not a boolean`)
|
|
69
|
+
if (!nullOr(isString)(flag.via)) problems.push(`${at}.${key}.via is neither a string nor null`)
|
|
70
|
+
if (flag.observed !== (flag.via !== null)) problems.push(`${at}.${key}.observed disagrees with ${key}.via`)
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(row.flags) || row.flags.some((flag) => !isString(flag))) problems.push(`${at}.flags is not a list of strings`)
|
|
73
|
+
if (!isBool(row.privateAddress)) problems.push(`${at}.privateAddress is not a boolean`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function write(row, at, problems) {
|
|
77
|
+
site(row, at, problems)
|
|
78
|
+
if (!isString(row.path)) problems.push(`${at}.path is not a string`)
|
|
79
|
+
if (!nullOr(isString)(row.canonicalPath)) problems.push(`${at}.canonicalPath is neither a string nor null`)
|
|
80
|
+
if (!isString(row.via)) problems.push(`${at}.via is not a string`)
|
|
81
|
+
if (!CONTROLLED.has(row.controlledDirectory)) problems.push(`${at}.controlledDirectory is not observed, not-observed or unknown`)
|
|
82
|
+
if ((row.controlledDirectory === "observed") !== isString(row.controlledBy)) problems.push(`${at}.controlledBy names a directory exactly when controlledDirectory is observed`)
|
|
83
|
+
if (row.canonicalPath === null && row.controlledDirectory !== "unknown") problems.push(`${at}.controlledDirectory is decided for a path that could not be read`)
|
|
84
|
+
if (!isBool(row.temp)) problems.push(`${at}.temp is not a boolean`)
|
|
85
|
+
if (!nullOr(isString)(row.mode)) problems.push(`${at}.mode is neither a string nor null`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fn(row, at, problems) {
|
|
89
|
+
site(row, at, problems)
|
|
90
|
+
if (!isString(row.name) || !row.name) problems.push(`${at}.name is not a name`)
|
|
91
|
+
if (row.kind !== "function" && row.kind !== "handler") problems.push(`${at}.kind is neither function nor handler`)
|
|
92
|
+
for (const key of ["lines", "depth", "branches"]) if (!isInt(row[key]) || row[key] < 0) problems.push(`${at}.${key} is not a count`)
|
|
93
|
+
if (isInt(row.lines) && row.lines < 1) problems.push(`${at}.lines is under one`)
|
|
94
|
+
if (typeof row.percentile !== "number" || row.percentile < 0 || row.percentile > 100) problems.push(`${at}.percentile is not a rank between 0 and 100`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function timer(row, at, problems) {
|
|
98
|
+
site(row, at, problems)
|
|
99
|
+
if (!nullOr(isString)(row.id)) problems.push(`${at}.id is neither a string nor null`)
|
|
100
|
+
if (!nullOr(isInt)(row.intervalMs)) problems.push(`${at}.intervalMs is neither an integer nor null`)
|
|
101
|
+
if (!nullOr(isString)(row.intervalText)) problems.push(`${at}.intervalText is neither a string nor null`)
|
|
102
|
+
if (row.intervalMs !== null && row.intervalText !== null) problems.push(`${at} has both an interval and an interval expression`)
|
|
103
|
+
for (const key of ["repeat", "running", "triggeredOnStart"]) if (!nullOr(isBool)(row[key])) problems.push(`${at}.${key} is neither a boolean nor null`)
|
|
104
|
+
if (!nullOr(isString)(row.startedBy)) problems.push(`${at}.startedBy is neither a string nor null`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {object} document
|
|
109
|
+
* @param {{ patternIds?: string[], notVisible?: string[] }} [known] the ids
|
|
110
|
+
* patterns.mjs defines and the fixed blind-spot list, when the caller has
|
|
111
|
+
* them, so a pattern id the code does not define and a rewritten blind
|
|
112
|
+
* spot are both problems.
|
|
113
|
+
* @returns {string[]} problems; empty when the document follows the contract
|
|
114
|
+
*/
|
|
115
|
+
export function validateInspectDocument(document, known = {}) {
|
|
116
|
+
const problems = []
|
|
117
|
+
if (!document || typeof document !== "object") return ["the document is not an object"]
|
|
118
|
+
if (!isString(document.omakit)) problems.push("omakit is not a string")
|
|
119
|
+
if (document.command !== "inspect") problems.push('command is not "inspect"')
|
|
120
|
+
if (!isString(document.method) || !/observed/.test(document.method)) problems.push("method is not the one sentence that says observed")
|
|
121
|
+
const subject = document.subject || {}
|
|
122
|
+
if (!isString(subject.dir)) problems.push("subject.dir is not a string")
|
|
123
|
+
if (!nullOr((value) => /^[0-9a-f]{40}$/.test(value))(subject.commit)) problems.push("subject.commit is neither a 40-character sha nor null")
|
|
124
|
+
if (!subject.repository || !nullOr(isString)(subject.repository.url)) problems.push("subject.repository.url is neither a string nor null")
|
|
125
|
+
const filesRead = subject.filesRead || {}
|
|
126
|
+
for (const kind of FILE_KINDS) if (!isInt(filesRead[kind]) || filesRead[kind] < 0) problems.push(`subject.filesRead.${kind} is not a count`)
|
|
127
|
+
for (const key of Object.keys(filesRead)) if (!FILE_KINDS.includes(key)) problems.push(`subject.filesRead.${key} is not a kind inspect reads`)
|
|
128
|
+
|
|
129
|
+
const observed = document.observed || {}
|
|
130
|
+
for (const [key, check] of [["processes", processRow], ["hosts", host], ["writes", write], ["timers", timer], ["functions", fn]]) {
|
|
131
|
+
if (!Array.isArray(observed[key])) {
|
|
132
|
+
problems.push(`observed.${key} is not a list`)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
for (const [index, row] of observed[key].entries()) check(row, `observed.${key}[${index}]`, problems)
|
|
136
|
+
}
|
|
137
|
+
const counts = document.counts || {}
|
|
138
|
+
if (!counts.processes || typeof counts.processes !== "object") problems.push("counts.processes is not the qml and shell split")
|
|
139
|
+
else {
|
|
140
|
+
for (const key of ["total", "qml", "shell"]) if (!isInt(counts.processes[key]) || counts.processes[key] < 0) problems.push(`counts.processes.${key} is not a count`)
|
|
141
|
+
if (counts.processes.total !== counts.processes.qml + counts.processes.shell) problems.push("counts.processes.total is not qml plus shell")
|
|
142
|
+
if (Array.isArray(observed.processes)) {
|
|
143
|
+
if (counts.processes.total !== observed.processes.length) problems.push("counts.processes.total is not the number of process rows")
|
|
144
|
+
if (counts.processes.qml !== observed.processes.filter((row) => row.declaredIn === "qml").length) problems.push("counts.processes.qml is not the number of qml process rows")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const key of ["hosts", "writes", "timers", "functions"]) {
|
|
148
|
+
if (!isInt(counts[key]) || counts[key] < 0) problems.push(`counts.${key} is not a count`)
|
|
149
|
+
else if (Array.isArray(observed[key]) && counts[key] !== observed[key].length) problems.push(`counts.${key} is not the number of ${key} rows`)
|
|
150
|
+
}
|
|
151
|
+
if (!isInt(counts.notResolvable) || (Array.isArray(document.notResolvable) && counts.notResolvable !== document.notResolvable.length)) problems.push("counts.notResolvable is not the number of notResolvable rows")
|
|
152
|
+
if (!Array.isArray(document.notResolvable)) problems.push("notResolvable is not a list")
|
|
153
|
+
else for (const [index, row] of document.notResolvable.entries()) {
|
|
154
|
+
site(row, `notResolvable[${index}]`, problems)
|
|
155
|
+
if (!isString(row.kind) || !isString(row.text)) problems.push(`notResolvable[${index}] lacks kind and text`)
|
|
156
|
+
}
|
|
157
|
+
// Every computed command is also a site the extraction could not read.
|
|
158
|
+
for (const row of Array.isArray(observed.processes) ? observed.processes : []) {
|
|
159
|
+
if (row.argvForm === "computed" && !(document.notResolvable || []).some((entry) => entry.kind === "command" && entry.file === row.file && entry.line === row.line)) {
|
|
160
|
+
problems.push(`observed.processes at ${row.file}:${row.line} is computed but not listed under notResolvable`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const size = document.size
|
|
165
|
+
if (!size || typeof size !== "object") problems.push("size is missing")
|
|
166
|
+
else {
|
|
167
|
+
if (!isString(size.measurement) || !/^M\d+$/.test(size.measurement)) problems.push("size.measurement is not a measurement id")
|
|
168
|
+
for (const key of ["lines", "branches", "depth"]) if (!isInt(size.thresholds?.[key]) || size.thresholds[key] < 1) problems.push(`size.thresholds.${key} is not a count`)
|
|
169
|
+
if (!size.sample || !isInt(size.sample.trees) || !isInt(size.sample.functions)) problems.push("size.sample is not { trees, functions }")
|
|
170
|
+
const scorable = Array.isArray(observed.functions) && observed.functions.length > 0
|
|
171
|
+
if (size.score === null) {
|
|
172
|
+
if (scorable) problems.push("size.score is null for a tree with functions")
|
|
173
|
+
} else if (typeof size.score !== "number" || size.score < 0 || size.score > 10 || Math.abs(Math.round(size.score * 100) - size.score * 100) > 1e-6) problems.push("size.score is not a number from 0 to 10 with two decimals")
|
|
174
|
+
else if (!scorable) problems.push("size.score is set for a tree with no function")
|
|
175
|
+
else {
|
|
176
|
+
const expected = Math.round((10 - observed.functions.reduce((sum, row) => sum + row.percentile, 0) / observed.functions.length / 10) * 100) / 100
|
|
177
|
+
if (Math.abs(expected - size.score) > 0.011) problems.push(`size.score is ${size.score}; the mean rank of the functions says ${expected}`)
|
|
178
|
+
}
|
|
179
|
+
if (!Array.isArray(size.over)) problems.push("size.over is not a list")
|
|
180
|
+
else {
|
|
181
|
+
for (const [index, row] of size.over.entries()) {
|
|
182
|
+
fn(row, `size.over[${index}]`, problems)
|
|
183
|
+
if (size.thresholds && !(row.lines > size.thresholds.lines || row.branches > size.thresholds.branches || row.depth > size.thresholds.depth)) problems.push(`size.over[${index}] is under every threshold`)
|
|
184
|
+
if (index && size.over[index - 1].lines < row.lines) problems.push(`size.over[${index}] is longer than the one before it; the list is longest first`)
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(observed.functions)) {
|
|
187
|
+
const listed = new Set(size.over.map((row) => `${row.file}:${row.line}`))
|
|
188
|
+
for (const row of observed.functions) {
|
|
189
|
+
const over = row.lines > size.thresholds?.lines || row.branches > size.thresholds?.branches || row.depth > size.thresholds?.depth
|
|
190
|
+
if (over && !listed.has(`${row.file}:${row.line}`)) problems.push(`observed.functions at ${row.file}:${row.line} is over a threshold but not under size.over`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(document.patterns)) problems.push("patterns is not a list")
|
|
196
|
+
else for (const [index, row] of document.patterns.entries()) {
|
|
197
|
+
const at = `patterns[${index}]`
|
|
198
|
+
if (!isString(row.id)) problems.push(`${at}.id is not a string`)
|
|
199
|
+
else if (known.patternIds && !known.patternIds.includes(row.id)) problems.push(`${at}.id ${row.id} is not a pattern patterns.mjs defines`)
|
|
200
|
+
if (!isInt(row.observedCount) || row.observedCount < 1) problems.push(`${at}.observedCount is not a positive count: a pattern is listed only where its precondition was observed`)
|
|
201
|
+
if (!Array.isArray(row.sites) || row.sites.length === 0) problems.push(`${at}.sites is not a non-empty list`)
|
|
202
|
+
else for (const [n, entry] of row.sites.entries()) site(entry, `${at}.sites[${n}]`, problems)
|
|
203
|
+
if (!isString(row.measurement) || !/^M\d+$/.test(row.measurement)) problems.push(`${at}.measurement is not a measurement id`)
|
|
204
|
+
if (typeof row.share !== "number" || !(row.share > 0 && row.share < 1)) problems.push(`${at}.share is not a share between 0 and 1`)
|
|
205
|
+
if (!isString(row.observation) || !/^observed /.test(row.observation)) problems.push(`${at}.observation does not start with the word observed`)
|
|
206
|
+
if (!isString(row.summary) || !row.summary || /\w:\d+/.test(row.summary)) problems.push(`${at}.summary is not the observation without its sites`)
|
|
207
|
+
if (/\b(?:missing|should|fix)\b/i.test(String(row.observation))) problems.push(`${at}.observation reads as a verdict`)
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(document.lookedFor) || document.lookedFor.some((id) => !isString(id))) problems.push("lookedFor is not a list of pattern ids")
|
|
210
|
+
else {
|
|
211
|
+
const listed = new Set((document.patterns || []).map((row) => row.id))
|
|
212
|
+
for (const id of document.lookedFor) {
|
|
213
|
+
if (listed.has(id)) problems.push(`lookedFor names ${id}, which is also under patterns`)
|
|
214
|
+
if (known.patternIds && !known.patternIds.includes(id)) problems.push(`lookedFor names ${id}, which patterns.mjs does not define`)
|
|
215
|
+
}
|
|
216
|
+
if (known.patternIds) {
|
|
217
|
+
for (const id of known.patternIds) if (!listed.has(id) && !document.lookedFor.includes(id)) problems.push(`pattern ${id} is neither under patterns nor under lookedFor`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!Array.isArray(document.notVisible) || document.notVisible.some((line) => !isString(line))) problems.push("notVisible is not a list of sentences")
|
|
221
|
+
else if (known.notVisible && JSON.stringify(document.notVisible) !== JSON.stringify(known.notVisible)) problems.push("notVisible is not the fixed blind-spot list, verbatim")
|
|
222
|
+
|
|
223
|
+
const baseline = document.marketplaceBaseline
|
|
224
|
+
if (!baseline || typeof baseline !== "object") problems.push("marketplaceBaseline is missing")
|
|
225
|
+
else if (baseline.skipped === true) {
|
|
226
|
+
if (!isString(baseline.reason)) problems.push("marketplaceBaseline.reason is not a string")
|
|
227
|
+
} else {
|
|
228
|
+
if (!baseline.pin || !isString(baseline.pin.commit)) problems.push("marketplaceBaseline.pin.commit is not a string")
|
|
229
|
+
if (!isString(baseline.transport)) problems.push("marketplaceBaseline.transport is not a string")
|
|
230
|
+
if (!isBool(baseline.invoked)) problems.push("marketplaceBaseline.invoked is not a boolean")
|
|
231
|
+
if (!isString(baseline.statement)) problems.push("marketplaceBaseline.statement is not a string")
|
|
232
|
+
if (baseline.invoked && (!baseline.official || typeof baseline.official !== "object")) problems.push("marketplaceBaseline.official is missing for an invoked baseline")
|
|
233
|
+
}
|
|
234
|
+
return problems
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const invoked = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
238
|
+
if (invoked) {
|
|
239
|
+
const input = process.argv[2]
|
|
240
|
+
if (!input) throw new Error("usage: node tools/inspect/contract.mjs <document.json>")
|
|
241
|
+
const problems = validateInspectDocument(JSON.parse(readFileSync(resolve(input), "utf8")))
|
|
242
|
+
for (const problem of problems) process.stdout.write(`${problem}\n`)
|
|
243
|
+
process.stdout.write(problems.length ? `${problems.length} problem(s)\n` : "ok: the document follows the contract in docs/INSPECT.md\n")
|
|
244
|
+
process.exit(problems.length ? 1 : 0)
|
|
245
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Function sites: every named function, QML handler, shell function and
|
|
2
|
+
// Python def, with its length in lines, its deepest nesting and its branch
|
|
3
|
+
// count. Size is the one thing a person asks about a tree that regular
|
|
4
|
+
// expressions can answer without a parser: where does the logic pile up.
|
|
5
|
+
// The numbers are counts over the text; the threshold they are compared
|
|
6
|
+
// with is measured over listed trees (M12), never chosen.
|
|
7
|
+
|
|
8
|
+
import { blankComments, closingBracket, lineOf } from "./text.mjs"
|
|
9
|
+
|
|
10
|
+
const JS_BRANCH = /\b(?:if|else if|for|while|do|switch|case|catch)\b|&&|\|\||\?[^.:]/g
|
|
11
|
+
const SHELL_OPEN = /^\s*(?:if|for|while|until|case|select)\b/
|
|
12
|
+
const SHELL_CLOSE = /^\s*(?:fi|done|esac)\b/
|
|
13
|
+
const SHELL_BRANCH = /\b(?:if|elif|for|while|until|case)\b|\|\||&&|^\s*[^)]*\)\s*(?!\s*$)/
|
|
14
|
+
const PY_BRANCH = /^\s*(?:if|elif|for|while|except|with)\b|\band\b|\bor\b/
|
|
15
|
+
|
|
16
|
+
/** Deepest brace nesting inside a body, relative to the body itself. */
|
|
17
|
+
function braceDepth(body) {
|
|
18
|
+
let depth = 0
|
|
19
|
+
let deepest = 0
|
|
20
|
+
let quote = null
|
|
21
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
22
|
+
const ch = body[i]
|
|
23
|
+
if (quote) {
|
|
24
|
+
if (ch === "\\") i += 1
|
|
25
|
+
else if (ch === quote) quote = null
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch
|
|
29
|
+
else if (ch === "{") {
|
|
30
|
+
depth += 1
|
|
31
|
+
if (depth > deepest) deepest = depth
|
|
32
|
+
} else if (ch === "}") depth -= 1
|
|
33
|
+
}
|
|
34
|
+
return deepest
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function jsFunctions(file) {
|
|
38
|
+
const text = blankComments(file.text)
|
|
39
|
+
const rows = []
|
|
40
|
+
// `function name(` and, in QML, `onSomething: {` handlers with a body block.
|
|
41
|
+
const pattern = /(?<![\w.])function\s+([A-Za-z_$][\w$]*)\s*\(|(?<![\w.])(on[A-Z]\w*)\s*:\s*(?=\{)/g
|
|
42
|
+
for (const match of text.matchAll(pattern)) {
|
|
43
|
+
const open = text.indexOf("{", match.index + match[0].length - (match[2] ? 0 : 0))
|
|
44
|
+
if (open < 0) continue
|
|
45
|
+
const end = closingBracket(text, open)
|
|
46
|
+
if (end < 0) continue
|
|
47
|
+
const body = text.slice(open + 1, end)
|
|
48
|
+
const start = lineOf(text, match.index)
|
|
49
|
+
rows.push({
|
|
50
|
+
file: file.path,
|
|
51
|
+
line: start,
|
|
52
|
+
name: match[1] || match[2],
|
|
53
|
+
kind: match[1] ? "function" : "handler",
|
|
54
|
+
lines: lineOf(text, end) - start + 1,
|
|
55
|
+
depth: braceDepth(body),
|
|
56
|
+
branches: (body.match(JS_BRANCH) || []).length,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return rows
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shellFunctions(file) {
|
|
63
|
+
const lines = file.text.split("\n")
|
|
64
|
+
const rows = []
|
|
65
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
66
|
+
const head = lines[index].match(/^\s*(?:function\s+)?([A-Za-z_][\w-]*)\s*\(\)\s*\{?\s*$|^\s*function\s+([A-Za-z_][\w-]*)\s*\{?\s*$/)
|
|
67
|
+
if (!head) continue
|
|
68
|
+
const name = head[1] || head[2]
|
|
69
|
+
// The body runs to the line that is only `}` at the function's own indent.
|
|
70
|
+
const indent = lines[index].match(/^\s*/)[0]
|
|
71
|
+
let end = index
|
|
72
|
+
let depth = 0
|
|
73
|
+
let deepest = 0
|
|
74
|
+
let branches = 0
|
|
75
|
+
for (let at = index + 1; at < lines.length; at += 1) {
|
|
76
|
+
const line = lines[at]
|
|
77
|
+
if (line.trim() === "}" && line.startsWith(indent) && line.match(/^\s*/)[0].length === indent.length) {
|
|
78
|
+
end = at
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
if (SHELL_OPEN.test(line)) {
|
|
82
|
+
depth += 1
|
|
83
|
+
if (depth > deepest) deepest = depth
|
|
84
|
+
}
|
|
85
|
+
if (SHELL_CLOSE.test(line)) depth -= 1
|
|
86
|
+
if (SHELL_BRANCH.test(line.split("#")[0])) branches += 1
|
|
87
|
+
end = at
|
|
88
|
+
}
|
|
89
|
+
rows.push({ file: file.path, line: index + 1, name, kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
90
|
+
index = end
|
|
91
|
+
}
|
|
92
|
+
return rows
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pythonFunctions(file) {
|
|
96
|
+
const lines = file.text.split("\n")
|
|
97
|
+
const rows = []
|
|
98
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
99
|
+
const head = lines[index].match(/^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/)
|
|
100
|
+
if (!head) continue
|
|
101
|
+
const base = head[1].length
|
|
102
|
+
let end = index
|
|
103
|
+
let deepest = 0
|
|
104
|
+
let branches = 0
|
|
105
|
+
for (let at = index + 1; at < lines.length; at += 1) {
|
|
106
|
+
const line = lines[at]
|
|
107
|
+
if (!line.trim()) continue
|
|
108
|
+
const indent = line.match(/^\s*/)[0].length
|
|
109
|
+
if (indent <= base) break
|
|
110
|
+
const level = Math.floor((indent - base) / 4)
|
|
111
|
+
if (level > deepest) deepest = level
|
|
112
|
+
if (PY_BRANCH.test(line)) branches += 1
|
|
113
|
+
end = at
|
|
114
|
+
}
|
|
115
|
+
rows.push({ file: file.path, line: index + 1, name: head[2], kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
116
|
+
}
|
|
117
|
+
return rows
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{ path: string, kind: string, text: string }} file
|
|
122
|
+
* @returns {Array<{ file, line, name, kind, lines, depth, branches }>}
|
|
123
|
+
*/
|
|
124
|
+
export function extractFunctions(file) {
|
|
125
|
+
if (file.kind === "qml" || file.kind === "js") return jsFunctions(file)
|
|
126
|
+
if (file.kind === "shell") return shellFunctions(file)
|
|
127
|
+
if (file.kind === "python") return pythonFunctions(file)
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Host sites: every `http://` or `https://` literal in code or argv, with
|
|
2
|
+
// the host and scheme it names, the tool the literal reaches (the argv it
|
|
3
|
+
// sits in, or the call on its line), and the timeout and size-cap flags
|
|
4
|
+
// that argv shows. A host built from a variable is not a host: a literal
|
|
5
|
+
// whose host part holds `${...}` or stops at `://` is recorded as not
|
|
6
|
+
// resolvable and never guessed.
|
|
7
|
+
|
|
8
|
+
import { basename, blankComments, lineOf, shellSegments, shellWords } from "./text.mjs"
|
|
9
|
+
import { toolOf } from "./processes.mjs"
|
|
10
|
+
|
|
11
|
+
const URL = /https?:\/\/[^\s"'`)\]}>,;]*/g
|
|
12
|
+
const TIMEOUT_FLAGS = ["--max-time", "-m", "--connect-timeout", "--timeout", "-T"]
|
|
13
|
+
const SIZE_FLAGS = ["--max-filesize", "--quota", "-Q"]
|
|
14
|
+
|
|
15
|
+
/** Loopback, link-local and RFC 1918 literals, and the names that resolve there. */
|
|
16
|
+
export function privateAddress(host) {
|
|
17
|
+
const h = String(host).toLowerCase().replace(/^\[|\]$/g, "")
|
|
18
|
+
if (["localhost", "0.0.0.0", "::1", "::"].includes(h)) return true
|
|
19
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
|
|
20
|
+
if (!v4) return /^f[cd][0-9a-f]{2}:|^fe80:/.test(h)
|
|
21
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])]
|
|
22
|
+
return a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254) || (a === 100 && b >= 64 && b <= 127)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The host of a URL literal, or null when the text shows an expression where the host would be. */
|
|
26
|
+
export function hostOf(url) {
|
|
27
|
+
const rest = url.replace(/^https?:\/\//, "")
|
|
28
|
+
const authority = rest.split(/[/?#]/)[0]
|
|
29
|
+
if (!authority || /\$\{|\$\(|["'`+]/.test(authority)) return null
|
|
30
|
+
const host = authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority
|
|
31
|
+
const bare = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0]
|
|
32
|
+
return bare && /^[\w.\-[\]:]+$/.test(bare) ? bare : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The flag with its value: `--max-time 5`, `--max-time=5`, `-m5`. */
|
|
36
|
+
function flagValue(words, names) {
|
|
37
|
+
for (const [index, word] of words.entries()) {
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
if (word === name) return `${name} ${words[index + 1] ?? ""}`.trim()
|
|
40
|
+
if (word.startsWith(`${name}=`)) return word
|
|
41
|
+
if (name.length === 2 && word.startsWith(name) && word.length > 2 && /\d/.test(word[2])) return word
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The words a URL is invoked with: a `-c` script's own words when the URL sits inside one, otherwise the argv. */
|
|
48
|
+
function wordsAround(process, url) {
|
|
49
|
+
const argv = process.argv
|
|
50
|
+
const at = argv.findIndex((word) => word.includes(url))
|
|
51
|
+
if (at < 0 || !process.shellWrapper) return argv
|
|
52
|
+
const script = toolOf(argv).index + 2
|
|
53
|
+
if (at !== script) return argv
|
|
54
|
+
// The URL is a word inside the script string: the words of its own segment.
|
|
55
|
+
for (const segment of shellSegments(argv[at])) {
|
|
56
|
+
if (segment.text.includes(url)) return shellWords(segment.text)
|
|
57
|
+
}
|
|
58
|
+
return shellWords(argv[at])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function caps(words, wholeArgv) {
|
|
62
|
+
const joined = wholeArgv.join(" ")
|
|
63
|
+
const timeout = flagValue(words, TIMEOUT_FLAGS)
|
|
64
|
+
|| (wholeArgv.some((word) => basename(word) === "timeout") ? `timeout ${wholeArgv[wholeArgv.findIndex((word) => basename(word) === "timeout") + 1] ?? ""}`.trim() : null)
|
|
65
|
+
const head = joined.match(/(?:^|[\s|/])head\s+(?:-\S+\s+)*-c\s+(\S+)/)
|
|
66
|
+
const size = flagValue(words, SIZE_FLAGS) || (head ? `head -c ${head[1]}` : null)
|
|
67
|
+
return {
|
|
68
|
+
timeout: { observed: Boolean(timeout), via: timeout },
|
|
69
|
+
sizeCap: { observed: Boolean(size), via: size },
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Shell and Python comments blanked, quotes respected, line count kept. */
|
|
74
|
+
function blankHashComments(text) {
|
|
75
|
+
return text.split("\n").map((line) => {
|
|
76
|
+
let quote = null
|
|
77
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
78
|
+
const ch = line[i]
|
|
79
|
+
if (quote) {
|
|
80
|
+
if (ch === "\\") i += 1
|
|
81
|
+
else if (ch === quote) quote = null
|
|
82
|
+
} else if (ch === '"' || ch === "'") quote = ch
|
|
83
|
+
else if (ch === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i)
|
|
84
|
+
}
|
|
85
|
+
return line
|
|
86
|
+
}).join("\n")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function callOnLine(line, text) {
|
|
90
|
+
if (/(?<![\w.])fetch\s*\(/.test(line)) return "fetch"
|
|
91
|
+
if (/XMLHttpRequest/.test(line) || /XMLHttpRequest/.test(text)) return "XMLHttpRequest"
|
|
92
|
+
const call = line.match(/([A-Za-z_][\w.]*)\s*\([^()]*https?:\/\//)
|
|
93
|
+
return call ? call[1] : null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {{ path: string, kind: string, text: string }} file
|
|
98
|
+
* @param {Array} processes the process rows of the same file
|
|
99
|
+
* @returns {{ hosts: Array, notResolvable: Array }}
|
|
100
|
+
*/
|
|
101
|
+
export function extractHosts(file, processes = []) {
|
|
102
|
+
const text = file.kind === "qml" || file.kind === "js" ? blankComments(file.text) : file.kind === "shell" || file.kind === "python" ? blankHashComments(file.text) : file.text
|
|
103
|
+
const hosts = []
|
|
104
|
+
const notResolvable = []
|
|
105
|
+
for (const match of text.matchAll(URL)) {
|
|
106
|
+
const url = match[0]
|
|
107
|
+
const line = lineOf(text, match.index)
|
|
108
|
+
const scheme = url.startsWith("https") ? "https" : "http"
|
|
109
|
+
const host = hostOf(url)
|
|
110
|
+
if (!host) {
|
|
111
|
+
notResolvable.push({ file: file.path, line, kind: "host", text: url })
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const process = processes.find((row) => row.file === file.path && Array.isArray(row.argv) && row.argv.some((word) => word.includes(url)) && (row.declaredIn !== "shell" || row.line === line))
|
|
115
|
+
let tool = null
|
|
116
|
+
let words = []
|
|
117
|
+
let argv = []
|
|
118
|
+
if (process) {
|
|
119
|
+
argv = process.argv
|
|
120
|
+
words = wordsAround(process, url)
|
|
121
|
+
tool = toolOf(words).tool
|
|
122
|
+
if (tool) tool = basename(tool)
|
|
123
|
+
} else {
|
|
124
|
+
const lineText = text.split("\n")[line - 1] || ""
|
|
125
|
+
tool = callOnLine(lineText, text)
|
|
126
|
+
}
|
|
127
|
+
const { timeout, sizeCap } = caps(words, argv)
|
|
128
|
+
hosts.push({
|
|
129
|
+
host,
|
|
130
|
+
scheme,
|
|
131
|
+
file: file.path,
|
|
132
|
+
line,
|
|
133
|
+
tool,
|
|
134
|
+
timeout,
|
|
135
|
+
sizeCap,
|
|
136
|
+
flags: words.filter((word) => word.startsWith("-") && word !== "-"),
|
|
137
|
+
privateAddress: privateAddress(host),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return { hosts, notResolvable }
|
|
141
|
+
}
|