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.
@@ -0,0 +1,194 @@
1
+ // Write sites: a `FileView {` with a path and a write adapter or a write
2
+ // call in QML; a redirect, `tee`, `cp`, `mv`, `mkdir`, `mktemp`, `install`
3
+ // or `touch` in shell; `writeFile` and its variants in JavaScript; `open()`
4
+ // for writing in Python. Each row says whether the path's literal prefix,
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.
7
+
8
+ import { basename, blankComments, blocks, closingBracket, lineOf, propertyValue, blankShellExpressions, shellLogicalLines, shellPieces, shellWords, stringLiteral, withoutRedirections } from "./text.mjs"
9
+
10
+ /** The directories a plugin controls, by the names the contract lists. */
11
+ export const CONTROLLED = Object.freeze(["$XDG_STATE_HOME", "$XDG_CACHE_HOME", "$XDG_RUNTIME_DIR"])
12
+ export const SHARED_TEMP = Object.freeze(["/tmp", "/var/tmp", "/dev/shm"])
13
+ const DEVICES = new Set(["/dev/null", "/dev/stderr", "/dev/stdout", "/dev/tty"])
14
+
15
+ /**
16
+ * A path's literal prefix in canonical form: `~` and `$HOME` idioms,
17
+ * `${X}` and `Quickshell.env("X")` all read as `$X`, and the three
18
+ * dot-directories under the home read as the XDG names they default to.
19
+ * A JavaScript expression that is a `+` chain of string literals and
20
+ * `Quickshell.env()` calls is joined; any other expression is returned as
21
+ * `null`, which the classification reads as unknown.
22
+ */
23
+ export function canonicalPath(raw) {
24
+ let text = String(raw).trim()
25
+ const literal = stringLiteral(text)
26
+ if (literal !== null) text = literal
27
+ else if (/[+]/.test(text) || /Quickshell\.env|StandardPaths/.test(text)) {
28
+ const pieces = text.split(/\s*\+\s*/)
29
+ const joined = pieces.map((piece) => {
30
+ const part = stringLiteral(piece)
31
+ if (part !== null) return part
32
+ const env = piece.match(/^Quickshell\.env\(\s*(["'])([A-Z_][A-Z0-9_]*)\1\s*\)$/)
33
+ if (env) return `$${env[2]}`
34
+ return null
35
+ })
36
+ if (joined.some((piece) => piece === null)) return null
37
+ text = joined.join("")
38
+ } else if (!/^[~/$.]/.test(text) && !/^[\w.-]+(?:\/[\w.-]*)*$/.test(text)) return null
39
+ text = text.replace(/\$\{(\w+)\}/g, "$$$1")
40
+ if (text === "~" || text.startsWith("~/")) text = `$HOME${text.slice(1)}`
41
+ text = text.replace(/^\$HOME\/\.local\/state(?=\/|$)/, "$XDG_STATE_HOME")
42
+ .replace(/^\$HOME\/\.cache(?=\/|$)/, "$XDG_CACHE_HOME")
43
+ .replace(/^\$HOME\/\.config(?=\/|$)/, "$XDG_CONFIG_HOME")
44
+ return text
45
+ }
46
+
47
+ /**
48
+ * Whether a canonical path is under a directory the plugin controls.
49
+ * @returns {{ controlledDirectory: "observed"|"not-observed"|"unknown", controlledBy: string|null, temp: boolean }}
50
+ */
51
+ export function classifyPath(path, pluginId) {
52
+ if (path === null || path === undefined) return { controlledDirectory: "unknown", controlledBy: null, temp: false }
53
+ for (const prefix of CONTROLLED) {
54
+ if (path === prefix || path.startsWith(`${prefix}/`)) return { controlledDirectory: "observed", controlledBy: prefix, temp: false }
55
+ }
56
+ const own = pluginId ? `$XDG_CONFIG_HOME/omarchy/plugins/${pluginId}` : null
57
+ if (own && (path === own || path.startsWith(`${own}/`))) return { controlledDirectory: "observed", controlledBy: own, temp: false }
58
+ for (const prefix of SHARED_TEMP) {
59
+ if (path === prefix || path.startsWith(`${prefix}/`)) return { controlledDirectory: "not-observed", controlledBy: null, temp: true }
60
+ }
61
+ if (path.startsWith("/") || path.startsWith("$HOME") || path.startsWith("$XDG_")) return { controlledDirectory: "not-observed", controlledBy: null, temp: false }
62
+ return { controlledDirectory: "unknown", controlledBy: null, temp: false }
63
+ }
64
+
65
+ function row(file, line, rawPath, via, pluginId, mode = null) {
66
+ const canonical = canonicalPath(rawPath)
67
+ const classified = classifyPath(canonical, pluginId)
68
+ return { file: file.path, line, path: String(rawPath).trim(), canonicalPath: canonical, via, ...classified, mode }
69
+ }
70
+
71
+ function qmlWrites(file, pluginId) {
72
+ const text = blankComments(file.text)
73
+ const rows = []
74
+ for (const block of blocks(text, "FileView")) {
75
+ const path = propertyValue(block.body, "path")
76
+ if (!path) continue
77
+ const writes = /writeAdapter|blockWrites|atomicWrites|setText\s*\(|(?<![\w.])write\s*\(/.test(block.body)
78
+ || (block.id && new RegExp(`(?<![\\w.])${block.id}\\.(?:writeAdapter|setText|write)\\s*\\(`).test(text))
79
+ if (!writes) continue
80
+ rows.push(row(file, lineOf(text, block.open + 1 + path.offset), path.text, "FileView", pluginId))
81
+ }
82
+ rows.push(...jsWrites({ ...file, text }, pluginId, true))
83
+ return rows
84
+ }
85
+
86
+ function jsWrites(file, pluginId, blanked = false) {
87
+ const text = blanked ? file.text : blankComments(file.text)
88
+ const rows = []
89
+ for (const match of text.matchAll(/(?<![\w.])(writeFileSync|writeFile|appendFileSync|appendFile)\s*\(/g)) {
90
+ const open = match.index + match[0].length - 1
91
+ const end = closingBracket(text, open)
92
+ if (end < 0) continue
93
+ const first = text.slice(open + 1, end).split(",")[0]
94
+ rows.push(row(file, lineOf(text, match.index), first, match[1], pluginId))
95
+ }
96
+ return rows
97
+ }
98
+
99
+ function pythonWrites(file, pluginId) {
100
+ const rows = []
101
+ const lines = file.text.split("\n")
102
+ for (const [index, line] of lines.entries()) {
103
+ const code = line.split("#")[0]
104
+ for (const match of code.matchAll(/(?<![\w.])open\s*\(\s*([^,()]+)\s*,\s*(["'])([rwaxb+]+)\2/g)) {
105
+ if (!/[wax]/.test(match[3])) continue
106
+ rows.push(row(file, index + 1, match[1], "open", pluginId))
107
+ }
108
+ }
109
+ return rows
110
+ }
111
+
112
+ /** The words that are not options, and the value of `-m`/`--mode` when one is given. */
113
+ function operands(all) {
114
+ const words = withoutRedirections(all)
115
+ const out = []
116
+ let mode = null
117
+ for (let index = 1; index < words.length; index += 1) {
118
+ const word = words[index]
119
+ if (word === "-m" || word === "--mode") {
120
+ mode = words[index + 1] ?? null
121
+ index += 1
122
+ } else if (word.startsWith("--mode=")) mode = word.slice("--mode=".length)
123
+ else if (word.startsWith("-") && word !== "-") continue
124
+ else out.push(word)
125
+ }
126
+ return { operands: out, mode }
127
+ }
128
+
129
+ function shellWrites(file, pluginId) {
130
+ const rows = []
131
+ const chmods = []
132
+ let umask = null
133
+ for (const { line, text } of shellLogicalLines(file.text)) {
134
+ const trimmed = blankShellExpressions(text).trim()
135
+ if (!trimmed || trimmed.startsWith("#")) continue
136
+ for (const segment of shellPieces(trimmed)) {
137
+ const words = shellWords(segment.text)
138
+ if (!words.length) continue
139
+ // Redirections anywhere in the segment: `> path`, `>> path`, `>path`, `&> path`.
140
+ for (let i = 0; i < words.length; i += 1) {
141
+ const word = words[i]
142
+ let target = null
143
+ let via = null
144
+ if (/^(?:\d*>>?|&>>?)$/.test(word) && !/^\d*>&/.test(word)) {
145
+ target = words[i + 1]
146
+ via = word.includes(">>") ? ">>" : ">"
147
+ i += 1
148
+ } else if (/^(?:\d*>>?|&>>?)[^&\s]/.test(word)) {
149
+ target = word.replace(/^(?:\d*>>?|&>>?)/, "")
150
+ via = word.includes(">>") ? ">>" : ">"
151
+ }
152
+ // A subshell's closing paren clings to the last word; a bare number
153
+ // or operator where a path would be is a comparison the blanking
154
+ // did not reach, not a write.
155
+ if (target) target = target.replace(/[);]+$/, "")
156
+ if (target && !DEVICES.has(target) && !/^&\d$/.test(target) && !/^[\d=<>!]+$/.test(target)) rows.push(row(file, line, target, via, pluginId))
157
+ }
158
+ const command = basename(words[0])
159
+ if (command === "umask" && words[1]) umask = words[1]
160
+ if (command === "chmod" && words.length >= 3) chmods.push({ mode: words[1], paths: words.slice(2) })
161
+ if (["tee", "mkdir", "touch"].includes(command)) {
162
+ const { operands: paths, mode } = operands(words)
163
+ for (const path of paths) rows.push(row(file, line, path, command, pluginId, mode))
164
+ } else if (["cp", "mv", "install", "ln"].includes(command)) {
165
+ const { operands: paths, mode } = operands(words)
166
+ if (paths.length >= 2) rows.push(row(file, line, paths[paths.length - 1], command, pluginId, mode))
167
+ } else if (command === "mktemp") {
168
+ const { operands: paths } = operands(words)
169
+ const template = paths[0] || (words.includes("-p") ? `${words[words.indexOf("-p") + 1]}/tmp.XXXXXX` : "/tmp/tmp.XXXXXX")
170
+ rows.push(row(file, line, template, "mktemp", pluginId, words.includes("-d") ? "0700" : "0600"))
171
+ }
172
+ }
173
+ }
174
+ for (const write of rows) {
175
+ if (write.mode) continue
176
+ const chmod = chmods.find((entry) => entry.paths.includes(write.path))
177
+ if (chmod) write.mode = `chmod ${chmod.mode}`
178
+ else if (umask) write.mode = `umask ${umask}`
179
+ }
180
+ return rows
181
+ }
182
+
183
+ /**
184
+ * @param {{ path: string, kind: string, text: string }} file
185
+ * @param {{ pluginId?: string|null }} [context]
186
+ * @returns {Array} write rows, in file order
187
+ */
188
+ export function extractWrites(file, { pluginId = null } = {}) {
189
+ if (file.kind === "qml") return qmlWrites(file, pluginId)
190
+ if (file.kind === "js") return jsWrites(file, pluginId)
191
+ if (file.kind === "shell") return shellWrites(file, pluginId)
192
+ if (file.kind === "python") return pythonWrites(file, pluginId)
193
+ return []
194
+ }
@@ -8,7 +8,7 @@ local commit through the transport seam the marketplace tests itself
8
8
  | File | Purpose |
9
9
  | --- | --- |
10
10
  | `pin.mjs` | The pin identity (one home) and the reproducible setup: `omakit pin` fetches exactly that commit into `$XDG_CACHE_HOME/omakit/marketplace` and refuses a modified checkout. |
11
- | `local-transport.mjs` | Answers the four request shapes the official resolver makes, from a local clone at the exact commit. No network, no credentials, no writes. |
11
+ | `local-transport.mjs` | Answers the four request shapes the official resolver makes, from a local clone at the exact commit. No network, no credentials, no writes. With `subdir`, serves a directory below the root as the whole tree, which is how `inspect` keeps the baseline to the plugin's own tree. |
12
12
  | `run-baseline.mjs` | Runs the pinned official baseline over either transport and reports the pin identity beside the result. |
13
13
  | `verify.mjs` | Builds the `marketplaceBaseline` section: pin, transport, assumptions, the official result verbatim, the statement. `omakit verify` renders it for a person (`renderVerify` in `report.mjs`) and prints the document itself behind `--json` and `--out`. |
14
14
  | `preflight.mjs` | Translates that result into what it will cause on submission, using the pinned policy, and renders the marketplace's own report text with its attestation marker stripped and asserted absent. |
@@ -55,8 +55,27 @@ machine (docs/WEIGH.md):
55
55
  | `weigh/report.mjs` | The confirmation and the report for a person, drawn with `style.mjs`; the README sentence and the evidence path come last. |
56
56
  | `weigh/confirm.mjs` | The one question, at a terminal, on stderr. |
57
57
 
58
+ `tools/inspect/` is `omakit inspect`, what a plugin tree does as observations
59
+ (docs/INSPECT.md), regular expressions over QML and shell, read-only against
60
+ the tree, no verdict:
61
+
62
+ | File | Purpose |
63
+ | --- | --- |
64
+ | `inspect/inspect.mjs` | The command: resolve the subject the way `submit` does, walk the tree, run the four extractors, run `verify` for the baseline, evaluate the patterns, build the document. The fixed blind-spot list lives here. |
65
+ | `inspect/walk.mjs` | The installable tree at the commit (through `tree.mjs`) filtered to the kinds inspect reads: `.qml`, `.js`, `.mjs`, `.cjs`, the shell extensions, `.py`, and any blob with a shebang or the executable bit; the manifest for the id; prose never read for facts. |
66
+ | `inspect/text.mjs` | The text primitives every extractor shares: line numbers, brace blocks, a property's value inside a block, string and array literals, the crude shell word and segment split, command substitutions, redirections. |
67
+ | `inspect/processes.mjs` | Process sites: `Process {` blocks with their `command:` or `<id>.command =`, `execDetached` calls, and every command segment of a shell script; argv, deadline, collector, cap, shell wrapper. |
68
+ | `inspect/hosts.mjs` | Every `http` or `https` literal with its host, the tool it reaches, and the timeout and size-cap flags in the same argv; a host behind an expression is not resolvable, never guessed. |
69
+ | `inspect/writes.mjs` | Write sites in QML (`FileView` with a write), shell (redirects, `tee`, `cp`, `mv`, `mkdir`, `mktemp`, `install`, `touch`), JavaScript and Python, with the controlled-directory test over the canonical path prefix and the mode the file shows. |
70
+ | `inspect/timers.mjs` | `Timer {` blocks: interval, repeat, running, triggeredOnStart, and the handler outside the block that starts it. |
71
+ | `inspect/functions.mjs` | Function sites: `function name(` and multi-line handlers in QML and JavaScript, shell functions, Python defs, each with its length in lines, deepest nesting and branch count. |
72
+ | `inspect/patterns.mjs` | The ten review classes of M11 as data: id, label, precondition over the facts, measurement, share, and the phrase for the `not observed` line. `supply-chain` cites the baseline's own findings and detects nothing. Also the M12 size thresholds as data, and the longest-first order over them. |
73
+ | `inspect/contract.mjs` | The JSON contract of docs/INSPECT.md as a validator, run by the unit tests over every fixture document. |
74
+ | `inspect/report.mjs` | The report for a person, drawn with `style.mjs` only: `░ info` for a fact, `▒ ?` for one that could not be read, `▓ note` for a pattern row, `▔ skip` under `--offline`, and the closing word `INSPECTED`. |
75
+
58
76
  ```text
59
77
  omakit pin
78
+ omakit inspect /path/to/plugin-repo # what the tree does, as observations; --json for the document
60
79
  omakit submit /path/to/plugin-repo # asks for the category and tags at a terminal
61
80
  omakit submit /path/to/plugin-repo --category Widgets --tags bar,quickshell
62
81
  omakit submit https://github.com/owner/repo@<40-char sha> --category System --tags system
@@ -7,6 +7,7 @@
7
7
  // omakit verify <target> the official baseline over the local transport, verbatim
8
8
  // omakit parity [--count n] prove the local transport equals the GitHub transport
9
9
  // omakit weigh <plugin> | --all what a plugin weighs on the shell, measured by restarting it
10
+ // omakit inspect <plugin-dir> what a plugin tree does, as observations; decides nothing
10
11
  //
11
12
  // Nothing here writes to the marketplace. There is no POST, PATCH, PUT or
12
13
  // DELETE anywhere in this repository, and `tests/unit/read-only.test.mjs`
@@ -42,6 +43,8 @@ import { listWeighings } from "../weigh/list.mjs"
42
43
  import { askYes } from "../weigh/confirm.mjs"
43
44
  import { auditInstalled } from "../audit/audit.mjs"
44
45
  import { renderAudit } from "../audit/report.mjs"
46
+ import { inspectPlugin, NOT_READABLE } from "../inspect/inspect.mjs"
47
+ import { renderInspect } from "../inspect/report.mjs"
45
48
 
46
49
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
47
50
 
@@ -386,6 +389,49 @@ async function cmdAudit(args) {
386
389
  process.exitCode = document.ok ? 0 : 1
387
390
  }
388
391
 
392
+ /**
393
+ * `omakit inspect`: a report, exit 0 whatever it observed; exit 2 when the
394
+ * target could not be read (no directory, no Git checkout, no commit, no
395
+ * manifest), in the one failure register. There is no exit status for
396
+ * "found something", because finding something is the normal outcome.
397
+ * `--out` writes the document to a file beside whatever stdout gets, the
398
+ * way `audit --out` does.
399
+ */
400
+ async function cmdInspect(args) {
401
+ const parsed = checkArgs(args, ACCEPTED.inspect)
402
+ if (parsed.offending !== null) fail("usage", `${parsed.reason}. Accepted: ${acceptedWords("inspect")}.`, 2, "omakit inspect <plugin-dir> [--full] [--json] [--out FILE] [--offline] [--allow-dirty]")
403
+ const target = parsed.positionals[0]
404
+ if (!target) fail("usage", "inspect needs a plugin directory: `omakit inspect <plugin-dir>`", 2, "omakit inspect <plugin-dir> [--full] [--json] [--out FILE] [--offline] [--allow-dirty]")
405
+ const spinner = spinnerFor(args)
406
+ let document
407
+ try {
408
+ document = await inspectPlugin({
409
+ repoRoot: ROOT,
410
+ target,
411
+ offline: parsed.options.has("--offline"),
412
+ allowDirty: parsed.options.has("--allow-dirty"),
413
+ omakitVersion: VERSION,
414
+ onPhase: spinner.phase,
415
+ })
416
+ } catch (error) {
417
+ spinner.done()
418
+ if (error?.code && typeof error.code === "string") {
419
+ fail(error.code, error.message, NOT_READABLE.includes(error.code) ? 2 : 1, error.remedy || REMEDY[error.code])
420
+ }
421
+ throw error
422
+ }
423
+ spinner.done()
424
+ const json = `${JSON.stringify(document, null, 2)}\n`
425
+ const out = parsed.options.get("--out")
426
+ if (out) {
427
+ mkdirSync(dirname(resolve(out)), { recursive: true })
428
+ writeFileSync(resolve(out), json)
429
+ }
430
+ if (parsed.options.has("--json")) process.stdout.write(json)
431
+ else process.stdout.write(`${renderInspect(document, { full: parsed.options.has("--full") })}\n`)
432
+ process.exitCode = 0
433
+ }
434
+
389
435
  /**
390
436
  * Every way `weigh` stops without weighing, in one register: the closing
391
437
  * word a report would have ended with, negated, then the sentence naming
@@ -562,6 +608,8 @@ if (command === "setup") {
562
608
  await cmdAudit(rest)
563
609
  } else if (command === "weigh") {
564
610
  await cmdWeigh(rest)
611
+ } else if (command === "inspect") {
612
+ await cmdInspect(rest)
565
613
  } else if (command === "help" || command === "--help" || command === "-h" || command === undefined) {
566
614
  if (rest.includes("--agent")) {
567
615
  // The skills ship in the npm package, so this works from a global install
@@ -92,10 +92,14 @@ function fileResponse(buffer, range) {
92
92
  }
93
93
 
94
94
  /**
95
- * @param {{ repoDir: string, repoUrl: string, commitSha: string, defaultBranch?: string }} options
95
+ * @param {{ repoDir: string, repoUrl: string, commitSha: string, defaultBranch?: string, subdir?: string }} options
96
+ * `subdir` names a directory below the repository root whose tree is served
97
+ * as the whole tree: the commit's `<sha>:<subdir>` tree, paths relative to
98
+ * it. `omakit inspect` uses it for a plugin kept below the root of a larger
99
+ * repository, so the baseline sees the plugin's tree and never the root's.
96
100
  * @returns {{ fetchImpl: Function, stats: { api: number, raw: number } }}
97
101
  */
98
- export function createLocalTransport({ repoDir, repoUrl, commitSha, defaultBranch }) {
102
+ export function createLocalTransport({ repoDir, repoUrl, commitSha, defaultBranch, subdir = "" }) {
99
103
  const { owner, repository } = parseRepoUrl(repoUrl)
100
104
  let commit = ""
101
105
  try {
@@ -106,8 +110,16 @@ export function createLocalTransport({ repoDir, repoUrl, commitSha, defaultBranc
106
110
  if (commit.toLowerCase() !== String(commitSha).toLowerCase()) {
107
111
  throw new Error(`local transport: ${repoDir} does not contain commit ${commitSha}`)
108
112
  }
109
- const treeSha = git(repoDir, ["rev-parse", `${commit}^{tree}`]).trim()
110
- const tree = readTree(repoDir, commit)
113
+ const root = subdir ? `${commit}:${subdir.replace(/\/+$/, "")}` : `${commit}^{tree}`
114
+ let treeSha = ""
115
+ try {
116
+ treeSha = git(repoDir, ["rev-parse", "--verify", "-q", root]).trim()
117
+ if (git(repoDir, ["cat-file", "-t", treeSha]).trim() !== "tree") treeSha = ""
118
+ } catch {
119
+ treeSha = ""
120
+ }
121
+ if (!treeSha) throw new Error(`local transport: ${repoDir} has no directory ${subdir} at commit ${commitSha}`)
122
+ const tree = readTree(repoDir, treeSha)
111
123
  const byPath = new Map(tree.map((entry) => [entry.path, entry]))
112
124
  const branch = defaultBranch
113
125
  || (() => {
@@ -28,6 +28,7 @@ export const ACCEPTED = Object.freeze({
28
28
  doctor: Object.freeze({ valued: ["--out"], flags: ["--offline", "--json"], positionals: 0 }),
29
29
  parity: Object.freeze({ valued: ["--count", "--offset", "--out"], flags: [], positionals: 0 }),
30
30
  audit: Object.freeze({ valued: ["--out"], flags: ["--drift", "--json", "--offline"], positionals: 1 }),
31
+ inspect: Object.freeze({ valued: ["--out"], flags: ["--full", "--json", "--offline", "--allow-dirty"], positionals: 1 }),
31
32
  weigh: Object.freeze({ valued: ["--runs", "--window", "--settle", "--out"], flags: ["--all", "--list", "--json", "--yes"], positionals: 1 }),
32
33
  })
33
34
 
@@ -9,7 +9,7 @@
9
9
  // whole list, and tests/unit/pin.test.mjs fails if any module starts reading a
10
10
  // path outside it, because on a partial clone such a read would quietly reach
11
11
  // for the network instead of failing.
12
- import { execFileSync } from "node:child_process"
12
+ import { execFileSync, spawnSync } from "node:child_process"
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs"
14
14
  import { dirname, join, resolve } from "node:path"
15
15
  import { omakitCacheDir } from "./paths.mjs"
@@ -178,18 +178,26 @@ export function ensurePin(repoRoot, log = () => {}, env = process.env) {
178
178
  return { dir, identity, fetched: true }
179
179
  }
180
180
 
181
- /** Human-readable size of the pinned checkout, for `omakit pin` and `omakit doctor`. */
182
- export function pinDiskUsage(dir) {
181
+ /**
182
+ * Human-readable size of the pinned checkout, for `omakit pin` and `omakit
183
+ * doctor`. `env` is the environment `du` is found in; injectable for tests.
184
+ */
185
+ export function pinDiskUsage(dir, env = process.env) {
183
186
  // -H follows a symlink given on the command line (POSIX; GNU's -D). Measured
184
187
  // without it: a checkout reached through a symlink reported 0.0 MB, the size
185
188
  // of the link, while the directory behind it was 15 MB.
186
- try {
187
- const output = execFileSync("du", ["-skH", dir], { encoding: "utf8" }).split(/\s+/)[0]
188
- const mib = Number(output) / 1024
189
- return `${mib < 10 ? mib.toFixed(1) : Math.round(mib)} MB on disk`
190
- } catch {
191
- return "size unknown"
192
- }
189
+ //
190
+ // The total is read whether or not du exited 0. du exits 1 when a file it
191
+ // listed is gone by the time it reaches it, and still prints the total.
192
+ // Measured: with `git status` running on the pin in parallel, 1 of 40 runs
193
+ // warned "cannot access '.git/index.lock'" over its momentary lock file
194
+ // and exited 1, so doctor said "size unknown" for a checkout it had the
195
+ // size of. Only a run that printed no total is unknown.
196
+ const result = spawnSync("du", ["-skH", dir], { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] })
197
+ const output = String(result.stdout || "").trim().split(/\s+/)[0]
198
+ if (result.error || !/^\d+$/.test(output)) return "size unknown"
199
+ const mib = Number(output) / 1024
200
+ return `${mib < 10 ? mib.toFixed(1) : Math.round(mib)} MB on disk`
193
201
  }
194
202
 
195
203
  /** True when the checkout was fetched with only PIN_PATHS, as a fresh one is. */
@@ -18,7 +18,7 @@ async function loadScanner(pinDir) {
18
18
 
19
19
  /**
20
20
  * @param {{ repoRoot: string, repoUrl: string, commitSha: string, transport: "local"|"github",
21
- * repoDir?: string, listedPlugins?: Array, token?: string }} options
21
+ * repoDir?: string, repoSubdir?: string, listedPlugins?: Array, token?: string }} options
22
22
  */
23
23
  export async function runBaseline(options) {
24
24
  const { dir: pinDir, identity } = requirePin(options.repoRoot)
@@ -34,6 +34,7 @@ export async function runBaseline(options) {
34
34
  repoDir: options.repoDir,
35
35
  repoUrl: options.repoUrl,
36
36
  commitSha: options.commitSha,
37
+ subdir: options.repoSubdir || "",
37
38
  })
38
39
  scanOptions.fetchImpl = transport.fetchImpl
39
40
  adapter = {
@@ -220,6 +220,9 @@ export const AUDIT_VERDICTS = Object.freeze({
220
220
  unavailable: "NOT AUDITED",
221
221
  })
222
222
 
223
+ /** The closing word of `omakit inspect`: a report was produced, and it decided nothing. */
224
+ export const INSPECT_VERDICT = "INSPECTED"
225
+
223
226
  /** The width of the widest mark, "█ FAIL"; every mark is padded to it so the names beside them align. */
224
227
  export const MARK_WIDTH = Math.max(...Object.values(STATUS).map((s) => `${s.glyph} ${s.word}`.length))
225
228
 
@@ -91,6 +91,27 @@ export const COMMANDS = Object.freeze([
91
91
  "prints the document itself, and --out writes it to a file.",
92
92
  ],
93
93
  },
94
+ {
95
+ signature: [
96
+ "omakit inspect <target> [--full] [--json] [--out <file>] [--offline]",
97
+ " [--allow-dirty]",
98
+ ],
99
+ lines: [
100
+ "What a plugin tree does, as observations: every process with its argv,",
101
+ "every host with its timeout and size-cap flags, every write with whether",
102
+ "it falls under a directory the plugin controls, every timer with its",
103
+ "interval, and the capabilities the marketplace baseline records. Below",
104
+ "the facts, the review classes the marketplace's human review raised,",
105
+ "each with its measured share, only where the tree shows the class.",
106
+ "Regular expressions over QML and shell, labelled observed; runs nothing",
107
+ "from the tree, decides nothing, exits 0 with a report and 2 when the",
108
+ "target cannot be read. The report opens with a size score, 10 minus",
109
+ "the mean rank of the tree's functions among those in listed trees,",
110
+ "then what needs attention: functions over the measured size, longest",
111
+ "first, then the review classes by measured share, five sites each;",
112
+ "--full is every site with every qualifier; --json prints the document.",
113
+ ],
114
+ },
94
115
  {
95
116
  signature: "omakit help --agent",
96
117
  lines: [
@@ -6,15 +6,25 @@ import { runBaseline } from "./run-baseline.mjs"
6
6
  import { MARKETPLACE_PIN } from "./pin.mjs"
7
7
  import { ASSUMED_BY_ADAPTER } from "./local-transport.mjs"
8
8
 
9
+ /** The one assumption a subtree scan adds: the tree the official code saw is the plugin directory, not the repository root. */
10
+ function subtreeAssumption(subdir) {
11
+ return subdir ? [`tree.root=${subdir.replace(/\/+$/, "")}/ (the plugin directory below the repository root, not the root)`] : []
12
+ }
13
+
9
14
  const MARKETPLACE_STATEMENT =
10
15
  "Official baseline preview over a local snapshot. The marketplace rescans the public commit itself. This is not approval, listing, verification or a security audit."
11
16
 
12
17
  /**
13
- * @param {{ repoRoot: string, subject: { dir: string, commit: string, repository: { url: string|null } }, listedPlugins?: Array }} options
18
+ * @param {{ repoRoot: string, subject: { dir: string, commit: string, repository: { url: string|null } }, listedPlugins?: Array, subdir?: string }} options
19
+ * `subdir`, when given, is a directory below the repository root that the
20
+ * local transport serves as the whole tree, so the official code scans the
21
+ * plugin's directory and not the repository around it; the section records
22
+ * it under `assumedByAdapter`. `verify` and `submit` pass none and scan the
23
+ * root, which for them is the repository the marketplace would fetch.
14
24
  * @returns the `marketplaceBaseline` section: pin, transport, adapter
15
25
  * assumptions, the official result verbatim, and the statement.
16
26
  */
17
- export async function marketplaceBaselineSection({ repoRoot, subject, listedPlugins }) {
27
+ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlugins, subdir = "" }) {
18
28
  const pin = {
19
29
  repository: MARKETPLACE_PIN.repository,
20
30
  commit: MARKETPLACE_PIN.commit,
@@ -40,6 +50,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
40
50
  commitSha: subject.commit,
41
51
  transport: "local",
42
52
  repoDir: subject.dir,
53
+ repoSubdir: subdir,
43
54
  listedPlugins,
44
55
  })
45
56
  } catch (error) {
@@ -49,7 +60,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
49
60
  return {
50
61
  pin,
51
62
  transport: "local-git",
52
- assumedByAdapter: [...ASSUMED_BY_ADAPTER],
63
+ assumedByAdapter: [...ASSUMED_BY_ADAPTER, ...subtreeAssumption(subdir)],
53
64
  invoked: true,
54
65
  skipReason: null,
55
66
  official: { error: { code: error.code, message: error.message, ...(error.details || {}) } },
@@ -61,7 +72,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
61
72
  return {
62
73
  pin: { ...pin, commit: run.pin.commit, baselineVersion: run.pin.baselineVersion, enforcementMode: run.pin.enforcementMode },
63
74
  transport: "local-git",
64
- assumedByAdapter: [...run.adapter.assumedByAdapter],
75
+ assumedByAdapter: [...run.adapter.assumedByAdapter, ...subtreeAssumption(subdir)],
65
76
  invoked: true,
66
77
  skipReason: null,
67
78
  official: run.result,