omakit 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -217
- package/package.json +2 -2
- package/skills/omarchy-plugin-check/SKILL.md +102 -0
- package/skills/omarchy-plugin-submit/SKILL.md +4 -0
- package/skills/omarchy-plugin-weigh/SKILL.md +128 -0
- package/tools/marketplace/README.md +15 -1
- package/tools/marketplace/cli.mjs +147 -3
- package/tools/marketplace/completion.mjs +3 -3
- package/tools/marketplace/paths.mjs +13 -0
- package/tools/marketplace/report.mjs +3 -2
- package/tools/marketplace/usage.mjs +21 -0
- package/tools/weigh/audit.mjs +666 -0
- package/tools/weigh/commands.mjs +69 -0
- package/tools/weigh/config.mjs +131 -0
- package/tools/weigh/confirm.mjs +32 -0
- package/tools/weigh/contract.mjs +167 -0
- package/tools/weigh/proc.mjs +150 -0
- package/tools/weigh/report.mjs +166 -0
- package/tools/weigh/stats.mjs +63 -0
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
// omakit watch <issue-url> is this submission's validated commit still current?
|
|
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
|
+
// omakit weigh <plugin> | --all what a plugin weighs on the shell, measured by restarting it
|
|
9
10
|
//
|
|
10
11
|
// Nothing here writes to the marketplace. There is no POST, PATCH, PUT or
|
|
11
12
|
// DELETE anywhere in this repository, and `tests/unit/read-only.test.mjs`
|
|
12
|
-
// proves it.
|
|
13
|
+
// proves it. `weigh` is the one command that changes the user's own machine,
|
|
14
|
+
// their shell and its configuration for the duration of a measurement, and
|
|
15
|
+
// it confirms first; docs/WEIGH.md says what it writes and how it restores.
|
|
13
16
|
|
|
14
17
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
15
18
|
import { dirname, resolve } from "node:path"
|
|
@@ -28,8 +31,11 @@ import { upgrade } from "./upgrade.mjs"
|
|
|
28
31
|
import { progress } from "./progress.mjs"
|
|
29
32
|
import { banner, bannerEnabled } from "./banner.mjs"
|
|
30
33
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
31
|
-
import { action, colourEnabled, GUTTER, labelled, mark, styler, wrap } from "./style.mjs"
|
|
34
|
+
import { action, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
|
|
32
35
|
import { omakitCacheDir, withHomeAbbreviated } from "./paths.mjs"
|
|
36
|
+
import { DEFAULTS as WEIGH_DEFAULTS, measureWeigh, planWeigh } from "../weigh/audit.mjs"
|
|
37
|
+
import { confirmationQuestion, renderWeigh, renderPlan } from "../weigh/report.mjs"
|
|
38
|
+
import { askYes } from "../weigh/confirm.mjs"
|
|
33
39
|
|
|
34
40
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
35
41
|
|
|
@@ -51,6 +57,8 @@ const REMEDY = Object.freeze({
|
|
|
51
57
|
"github-unavailable": "Wait for GitHub, then run it again. `gh auth login` raises the rate limit if that is what ran out.",
|
|
52
58
|
"not-found": "Check the issue URL: it has to be an existing issue on the marketplace repository.",
|
|
53
59
|
"head-unreadable": "Check that the plugin repository is public and its URL is right.",
|
|
60
|
+
"not-confirmed": "Run it again and answer y, or pass --yes when the person whose shell it is has agreed.",
|
|
61
|
+
"interrupted": "shell.json was restored; run it again when the desktop is yours to restart.",
|
|
54
62
|
})
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -79,7 +87,7 @@ function option(args, name) {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
function positionals(args) {
|
|
82
|
-
const valued = new Set(["--profile", "--plugin", "--out", "--category", "--tags", "--notes", "--suggest-tag", "--name", "--count", "--offset"])
|
|
90
|
+
const valued = new Set(["--profile", "--plugin", "--out", "--category", "--tags", "--notes", "--suggest-tag", "--name", "--count", "--offset", "--runs", "--window", "--settle"])
|
|
83
91
|
return args.filter((value, index) => !value.startsWith("--") && !valued.has(args[index - 1]))
|
|
84
92
|
}
|
|
85
93
|
|
|
@@ -273,6 +281,140 @@ async function cmdParity(args) {
|
|
|
273
281
|
process.exit(ok ? 0 : 1)
|
|
274
282
|
}
|
|
275
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Strict argument checking for one command: every token is a known option
|
|
286
|
+
* (valued or not), the value of a valued option, or a positional up to
|
|
287
|
+
* the allowed count. Anything else is returned as the offending token, so
|
|
288
|
+
* the command refuses before any preflight instead of running with the
|
|
289
|
+
* defaults as if nothing had been passed (measured: `weigh <plugin> -n 1`
|
|
290
|
+
* ran three runs). `--name=value` is accepted for a valued option. Used by
|
|
291
|
+
* weigh; the other commands still read their flags one by one (see
|
|
292
|
+
* CONTRIBUTING.md, "Debts").
|
|
293
|
+
*
|
|
294
|
+
* @param {string[]} args
|
|
295
|
+
* @param {{ valued: string[], flags: string[], positionals: number }} spec
|
|
296
|
+
* @returns {{ offending: string|null, reason: string|null, options: Map<string, string|true>, positionals: string[] }}
|
|
297
|
+
*/
|
|
298
|
+
function checkArgs(args, spec) {
|
|
299
|
+
const options = new Map()
|
|
300
|
+
const positionals = []
|
|
301
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
302
|
+
const token = args[index]
|
|
303
|
+
const [name, inline] = token.startsWith("--") && token.includes("=") ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)] : [token, undefined]
|
|
304
|
+
if (spec.valued.includes(name)) {
|
|
305
|
+
const value = inline !== undefined ? inline : args[index + 1]
|
|
306
|
+
if (value === undefined || (inline === undefined && value.startsWith("-"))) return { offending: token, reason: `${name} needs a value`, options, positionals }
|
|
307
|
+
options.set(name, value)
|
|
308
|
+
if (inline === undefined) index += 1
|
|
309
|
+
} else if (spec.flags.includes(token)) {
|
|
310
|
+
options.set(token, true)
|
|
311
|
+
} else if (token.startsWith("-")) {
|
|
312
|
+
return { offending: token, reason: `${token} is not an option this command knows`, options, positionals }
|
|
313
|
+
} else if (positionals.length < spec.positionals) {
|
|
314
|
+
positionals.push(token)
|
|
315
|
+
} else {
|
|
316
|
+
return { offending: token, reason: `${JSON.stringify(token)} is one argument more than the command takes`, options, positionals }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { offending: null, reason: null, options, positionals }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** What `weigh` accepts, in the words the refusal prints. */
|
|
323
|
+
const WEIGH_ARGS = Object.freeze({ valued: ["--runs", "--window", "--settle", "--out"], flags: ["--all", "--json", "--yes"], positionals: 1 })
|
|
324
|
+
const WEIGH_ACCEPTED = "--runs N, --window S, --settle S, --all, --json, --out FILE, --yes"
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Every way `weigh` stops without weighing, in one register: the closing
|
|
328
|
+
* word a report would have ended with, negated, then the sentence naming
|
|
329
|
+
* what is missing, then the one thing to do. Exit 2 for a usage error and
|
|
330
|
+
* an unanswered confirmation, 130 for an interrupt, 1 for the rest.
|
|
331
|
+
*/
|
|
332
|
+
function notWeighed(code, message, remedy, exit = 1) {
|
|
333
|
+
const c = styler(colourEnabled(process.stderr))
|
|
334
|
+
const lines = verdict("fail", "NOT WEIGHED", message, c)
|
|
335
|
+
if (remedy) lines.push(...action(remedy, c, { indent: 0 }))
|
|
336
|
+
process.stderr.write(`${lines.join("\n")}\n`)
|
|
337
|
+
process.exit(exit)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function cmdWeigh(args) {
|
|
341
|
+
// Every token is checked before anything else: an option weigh does not
|
|
342
|
+
// know, or a second positional, is refused with the accepted list.
|
|
343
|
+
const parsed = checkArgs(args, WEIGH_ARGS)
|
|
344
|
+
if (parsed.offending !== null) notWeighed("usage", `${parsed.reason}. Accepted: ${WEIGH_ACCEPTED}.`, "omakit weigh <plugin-id-or-dir> [--runs N] [--window S] [--settle S] [--yes] [--json] [--out FILE]", 2)
|
|
345
|
+
const json = parsed.options.has("--json")
|
|
346
|
+
const all = parsed.options.has("--all")
|
|
347
|
+
const target = parsed.positionals[0]
|
|
348
|
+
if (!target && !all) notWeighed("usage", "weigh needs a plugin: `omakit weigh <plugin-id-or-dir>`, or `omakit weigh --all` for every enabled third-party plugin.", "omakit weigh <plugin-id-or-dir>", 2)
|
|
349
|
+
if (target && all) notWeighed("usage", `--all weighs every enabled third-party plugin, so ${JSON.stringify(target)} is one argument more than it takes.`, "omakit weigh --all, or omakit weigh <plugin-id-or-dir>", 2)
|
|
350
|
+
const integer = (name, fallback, letter, min = 1) => {
|
|
351
|
+
if (!parsed.options.has(name)) return fallback
|
|
352
|
+
const raw = parsed.options.get(name)
|
|
353
|
+
if (!/^\d+$/.test(raw) || Number(raw) < min) notWeighed("usage", `${name} needs an integer of at least ${min}, not ${JSON.stringify(raw)}.`, `omakit weigh <plugin-id-or-dir> ${name} ${letter}`, 2)
|
|
354
|
+
return Number(raw)
|
|
355
|
+
}
|
|
356
|
+
const runs = integer("--runs", WEIGH_DEFAULTS.runs, "N")
|
|
357
|
+
const windowSeconds = integer("--window", WEIGH_DEFAULTS.windowSeconds, "S")
|
|
358
|
+
const settleSeconds = integer("--settle", WEIGH_DEFAULTS.settleSeconds, "S", 0)
|
|
359
|
+
let plan
|
|
360
|
+
try {
|
|
361
|
+
plan = planWeigh({ target, all, runs, windowSeconds, settleSeconds, out: parsed.options.get("--out") })
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error?.code && typeof error.code === "string") notWeighed(error.code, `${error.message}.`, error.remedy || REMEDY[error.code], error.code === "usage" ? 2 : 1)
|
|
364
|
+
throw error
|
|
365
|
+
}
|
|
366
|
+
// The narration: what was backed up and with which md5, and what was
|
|
367
|
+
// restored. For a person it is part of the report, on stdout; under
|
|
368
|
+
// --json stdout is the document alone, so it goes to stderr.
|
|
369
|
+
const narrate = json ? process.stderr : process.stdout
|
|
370
|
+
const c = styler(colourEnabled(narrate))
|
|
371
|
+
const say = (line) => narrate.write(`${mark(line.state, c)}${wrap(withHomeAbbreviated(line.text), { indent: GUTTER }, c).join("\n").trimStart()}\n`)
|
|
372
|
+
// The confirmation. The plan is printed either way, so the record says
|
|
373
|
+
// what was agreed to; the question is asked only at a terminal on both
|
|
374
|
+
// ends, and --yes is the only other way past it.
|
|
375
|
+
narrate.write(`${renderPlan(plan, { colour: colourEnabled(narrate) }).join("\n")}\n`)
|
|
376
|
+
if (!parsed.options.has("--yes")) {
|
|
377
|
+
const interactive = !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)
|
|
378
|
+
if (!interactive) notWeighed("not-confirmed", `this restarts the shell ${plan.restarts} times and edits shell.json for the duration; a pipe, an agent or --json cannot answer for the person whose shell it is.`, REMEDY["not-confirmed"], 2)
|
|
379
|
+
const agreed = await askYes({ question: confirmationQuestion(plan) })
|
|
380
|
+
if (!agreed) notWeighed("not-confirmed", "not confirmed; nothing was changed.", REMEDY["not-confirmed"], 2)
|
|
381
|
+
}
|
|
382
|
+
narrate.write("\n")
|
|
383
|
+
// An interrupt is a request to stop, not a reason to leave the user's
|
|
384
|
+
// shell on a measurement configuration: the signal aborts the run, the
|
|
385
|
+
// measurement's own finally restores shell.json and restarts the shell,
|
|
386
|
+
// and only then does the process exit, 130 as an interrupted program does.
|
|
387
|
+
const controller = new AbortController()
|
|
388
|
+
const interrupt = () => {
|
|
389
|
+
if (!controller.signal.aborted) narrate.write(`\n${mark("advisory", c)}interrupted: restoring shell.json before exiting\n`)
|
|
390
|
+
controller.abort()
|
|
391
|
+
}
|
|
392
|
+
process.on("SIGINT", interrupt)
|
|
393
|
+
process.on("SIGTERM", interrupt)
|
|
394
|
+
const spinner = spinnerFor(args)
|
|
395
|
+
let document
|
|
396
|
+
try {
|
|
397
|
+
document = await measureWeigh(plan, { signal: controller.signal, omakitVersion: VERSION, onPhase: spinner.phase, onLine: (line) => { spinner.done(); say(line) } })
|
|
398
|
+
} catch (error) {
|
|
399
|
+
spinner.done()
|
|
400
|
+
if (error?.code === "interrupted") notWeighed("interrupted", "interrupted before the measurement completed.", REMEDY.interrupted, 130)
|
|
401
|
+
if (error?.code && typeof error.code === "string") notWeighed(error.code, `${error.message}.`, error.remedy || REMEDY[error.code])
|
|
402
|
+
throw error
|
|
403
|
+
} finally {
|
|
404
|
+
process.off("SIGINT", interrupt)
|
|
405
|
+
process.off("SIGTERM", interrupt)
|
|
406
|
+
}
|
|
407
|
+
spinner.done()
|
|
408
|
+
if (json) {
|
|
409
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}\n`)
|
|
410
|
+
} else {
|
|
411
|
+
process.stdout.write(`\n${renderWeigh(document)}\n`)
|
|
412
|
+
}
|
|
413
|
+
process.exit(0)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const VERSION = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8")).version
|
|
417
|
+
|
|
276
418
|
const [command, ...rest] = process.argv.slice(2)
|
|
277
419
|
if (command === "setup") {
|
|
278
420
|
await cmdSetup()
|
|
@@ -303,6 +445,8 @@ if (command === "setup") {
|
|
|
303
445
|
await cmdVerify(rest.filter((value, index) => value !== "marketplace" || rest[index - 1] !== "--profile"))
|
|
304
446
|
} else if (command === "parity") {
|
|
305
447
|
await cmdParity(rest)
|
|
448
|
+
} else if (command === "weigh") {
|
|
449
|
+
await cmdWeigh(rest)
|
|
306
450
|
} else if (command === "help" || command === "--help" || command === "-h" || command === undefined) {
|
|
307
451
|
if (rest.includes("--agent")) {
|
|
308
452
|
// The skills ship in the npm package, so this works from a global install
|
|
@@ -18,8 +18,8 @@ import { withHomeAbbreviated } from "./paths.mjs"
|
|
|
18
18
|
/**
|
|
19
19
|
* The completion model, read out of the help data. A subcommand is the word
|
|
20
20
|
* after `omakit` in its signature; a flag is every `--word` in it, valued when
|
|
21
|
-
* a `<placeholder>` follows it; `<target>`
|
|
22
|
-
* positional
|
|
21
|
+
* a `<placeholder>` follows it; `<target>` or `<plugin-id-or-dir>` in the
|
|
22
|
+
* signature means the first positional can be a directory.
|
|
23
23
|
*
|
|
24
24
|
* @param {ReadonlyArray<{ signature: string|string[], lines: string[] }>} commands
|
|
25
25
|
*/
|
|
@@ -33,7 +33,7 @@ export function subcommandsOf(commands = COMMANDS) {
|
|
|
33
33
|
value: placeholder ? placeholderKind(placeholder) : null,
|
|
34
34
|
}))
|
|
35
35
|
const sentence = command.lines.join(" ").replace(/`/g, "").split(/(?<=\.)\s/)[0]
|
|
36
|
-
return { name, description: sentence, flags, target: /<target>/.test(signature) }
|
|
36
|
+
return { name, description: sentence, flags, target: /<target>|<plugin-id-or-dir>/.test(signature) }
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -9,6 +9,19 @@ export function omakitCacheDir(name = "", env = process.env) {
|
|
|
9
9
|
return join(base, "omakit", name)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Omakit's user-writable state, following XDG with the usual ~/.local/state
|
|
14
|
+
* fallback: where `omakit weigh` keeps its documents and its per-restart
|
|
15
|
+
* timing. State, not cache, because a measurement is not something to
|
|
16
|
+
* fetch again.
|
|
17
|
+
*/
|
|
18
|
+
export function omakitStateDir(name = "", env = process.env) {
|
|
19
|
+
const base = env.XDG_STATE_HOME
|
|
20
|
+
? resolve(env.XDG_STATE_HOME)
|
|
21
|
+
: join(env.HOME || homedir(), ".local/state")
|
|
22
|
+
return join(base, "omakit", name)
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* Text for a person, with every path under the home directory written the
|
|
14
27
|
* way a shell would take it: `~/.cache/omakit/marketplace`. Only `$HOME`
|
|
@@ -35,9 +35,10 @@ function stateOf(check) {
|
|
|
35
35
|
/**
|
|
36
36
|
* A check's head line: the mark, the id in bold, and the source in brackets
|
|
37
37
|
* pushed to the right edge, so the sources form a column of their own and the
|
|
38
|
-
* ids form another.
|
|
38
|
+
* ids form another. Exported for the weigh report, whose rows are plugins
|
|
39
|
+
* with their kinds where a check has its source.
|
|
39
40
|
*/
|
|
40
|
-
function head(state, id, source, c) {
|
|
41
|
+
export function head(state, id, source, c) {
|
|
41
42
|
const left = `${mark(state, c)}${c("name", id)}`
|
|
42
43
|
const tag = c("punctuation", `[${source}]`)
|
|
43
44
|
const gap = Math.max(2, COLUMNS - width(left) - width(tag))
|
|
@@ -99,6 +99,27 @@ export const COMMANDS = Object.freeze([
|
|
|
99
99
|
"listed repositories; a packaged install requires --out for evidence.",
|
|
100
100
|
],
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
signature: [
|
|
104
|
+
"omakit weigh <plugin-id-or-dir> [--runs <n>] [--window <s>] [--settle <s>]",
|
|
105
|
+
" [--yes] [--json] [--out <file>]",
|
|
106
|
+
"omakit weigh --all",
|
|
107
|
+
],
|
|
108
|
+
lines: [
|
|
109
|
+
"What a plugin weighs on the shell, measured: the shell is restarted",
|
|
110
|
+
"without it and with it, several runs, and the difference is the weight,",
|
|
111
|
+
"with the baseline's own spread as the noise floor; memory is printed as",
|
|
112
|
+
"the shell's own startup variance, CPU and child processes as the weight.",
|
|
113
|
+
"The one command that changes your machine: it edits shell.json for the",
|
|
114
|
+
"duration, backs it up first, restores it on every exit path, and asks",
|
|
115
|
+
"before the first restart (--yes answers for you): about a minute per",
|
|
116
|
+
"restart, six restarts for one plugin at three runs. --all weighs every",
|
|
117
|
+
"enabled third-party plugin and is sized for a lab machine, not a working",
|
|
118
|
+
"desktop. Writes the document to --out, by default",
|
|
119
|
+
"$XDG_STATE_HOME/omakit/weigh/<date>.json, and ends with the sentence",
|
|
120
|
+
"for the plugin's README.",
|
|
121
|
+
],
|
|
122
|
+
},
|
|
102
123
|
])
|
|
103
124
|
|
|
104
125
|
// A command sits one STEP in from the heading; what it does sits one STEP in
|