omakit 0.1.8 → 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 -212
- 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 +17 -2
- package/tools/marketplace/cli.mjs +149 -5
- package/tools/marketplace/completion.mjs +5 -4
- package/tools/marketplace/doctor.mjs +24 -14
- package/tools/marketplace/path-hint.mjs +1 -1
- package/tools/marketplace/paths.mjs +31 -0
- package/tools/marketplace/report.mjs +13 -5
- package/tools/marketplace/setup.mjs +7 -4
- package/tools/marketplace/upgrade.mjs +22 -4
- 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"
|
|
32
|
-
import { omakitCacheDir } from "./paths.mjs"
|
|
34
|
+
import { action, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
|
|
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()
|
|
@@ -284,7 +426,7 @@ if (command === "setup") {
|
|
|
284
426
|
// ensurePin narrates: a state line to keep, then a fetch it is about to
|
|
285
427
|
// start. The fetch is the slow part, so it gets the progress line.
|
|
286
428
|
if (line.state === "fetching") spinner.phase(line.text)
|
|
287
|
-
else process.stdout.write(`${mark(line.state, c)}${wrap(line.text, { indent: GUTTER }, c).join("\n").trimStart()}\n`)
|
|
429
|
+
else process.stdout.write(`${mark(line.state, c)}${wrap(withHomeAbbreviated(line.text), { indent: GUTTER }, c).join("\n").trimStart()}\n`)
|
|
288
430
|
})
|
|
289
431
|
} catch (error) {
|
|
290
432
|
spinner.done()
|
|
@@ -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
|
|
@@ -13,12 +13,13 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
13
13
|
import { basename, dirname, join } from "node:path"
|
|
14
14
|
import { tagSlug } from "./form.mjs"
|
|
15
15
|
import { COMMANDS, COMPLETION_SHELLS } from "./usage.mjs"
|
|
16
|
+
import { withHomeAbbreviated } from "./paths.mjs"
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The completion model, read out of the help data. A subcommand is the word
|
|
19
20
|
* after `omakit` in its signature; a flag is every `--word` in it, valued when
|
|
20
|
-
* a `<placeholder>` follows it; `<target>`
|
|
21
|
-
* positional
|
|
21
|
+
* a `<placeholder>` follows it; `<target>` or `<plugin-id-or-dir>` in the
|
|
22
|
+
* signature means the first positional can be a directory.
|
|
22
23
|
*
|
|
23
24
|
* @param {ReadonlyArray<{ signature: string|string[], lines: string[] }>} commands
|
|
24
25
|
*/
|
|
@@ -32,7 +33,7 @@ export function subcommandsOf(commands = COMMANDS) {
|
|
|
32
33
|
value: placeholder ? placeholderKind(placeholder) : null,
|
|
33
34
|
}))
|
|
34
35
|
const sentence = command.lines.join(" ").replace(/`/g, "").split(/(?<=\.)\s/)[0]
|
|
35
|
-
return { name, description: sentence, flags, target: /<target>/.test(signature) }
|
|
36
|
+
return { name, description: sentence, flags, target: /<target>|<plugin-id-or-dir>/.test(signature) }
|
|
36
37
|
})
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -221,7 +222,7 @@ export function completionInstall(env = process.env) {
|
|
|
221
222
|
const shell = basename(env.SHELL || "")
|
|
222
223
|
const home = env.HOME || ""
|
|
223
224
|
if (!home || !COMPLETION_SHELLS.includes(shell)) return null
|
|
224
|
-
const tilde = (dir) => (dir
|
|
225
|
+
const tilde = (dir) => withHomeAbbreviated(dir, env)
|
|
225
226
|
if (shell === "bash") {
|
|
226
227
|
const dir = join(env.XDG_DATA_HOME || join(home, ".local/share"), "bash-completion/completions")
|
|
227
228
|
return { shell, path: join(dir, "omakit"), display: `${tilde(dir)}/omakit`, note: null }
|
|
@@ -42,7 +42,7 @@ import { join } from "node:path"
|
|
|
42
42
|
import { MARKETPLACE_PIN, PIN_PATHS, marketplacePinDir, pinDiskUsage, pinIsSparse, requirePin } from "./pin.mjs"
|
|
43
43
|
import { LIVE_PATHS } from "./registry.mjs"
|
|
44
44
|
import { credential, defaultBranchHead, getJson, UNAUTHENTICATED_LIMIT, GitHubError } from "./github.mjs"
|
|
45
|
-
import {
|
|
45
|
+
import { NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
46
46
|
import { pathHint } from "./path-hint.mjs"
|
|
47
47
|
|
|
48
48
|
/** "git+https://github.com/owner/name.git" in package.json -> "https://github.com/owner/name", or null. */
|
|
@@ -172,13 +172,13 @@ export function pinFreshness(identity, head, changedPaths = null, { issues = nul
|
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null,
|
|
175
|
-
* resolveHead?: typeof defaultBranchHead, latest?: typeof
|
|
175
|
+
* resolveHead?: typeof defaultBranchHead, latest?: typeof registryLatest }} options
|
|
176
176
|
* `env` and `npmPrefix` are injectable for tests of the PATH check;
|
|
177
177
|
* `resolveHead` and `latest` for tests of the two checks that read the
|
|
178
178
|
* network, whose defaults are the tool's one HEAD resolver and its one
|
|
179
179
|
* registry read.
|
|
180
180
|
*/
|
|
181
|
-
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion =
|
|
181
|
+
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest }) {
|
|
182
182
|
// Optional: told what is being read while the network answers. Never
|
|
183
183
|
// affects the result.
|
|
184
184
|
const phase = onPhase || (() => {})
|
|
@@ -192,7 +192,27 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
192
192
|
})
|
|
193
193
|
|
|
194
194
|
const self = tool(repoRoot)
|
|
195
|
-
|
|
195
|
+
// What is installed and what is published, one check: the two facts are
|
|
196
|
+
// one question, "is this the current omakit", and were two lines before.
|
|
197
|
+
// Offline, the first fact alone, as information. The evidence carries both
|
|
198
|
+
// and where the second came from.
|
|
199
|
+
const versionCheck = (state, detail, action = null, latest = null, source = null) =>
|
|
200
|
+
add("omakit.version", state, detail, action, { installed: self.version, latest, source })
|
|
201
|
+
if (offline) {
|
|
202
|
+
versionCheck("info", `${self.version}; the newest published version is not checked (--offline)`)
|
|
203
|
+
} else {
|
|
204
|
+
phase("asking the npm registry for the newest published version")
|
|
205
|
+
const published = await latestVersion(self.name)
|
|
206
|
+
if (published.version) {
|
|
207
|
+
const current = published.version === self.version
|
|
208
|
+
versionCheck(current ? "ok" : "advice",
|
|
209
|
+
current ? `${self.version}, the newest published version` : `${self.version}; ${published.version} is published`,
|
|
210
|
+
current ? null : `run \`${upgradeCommand(repoRoot, self.name)}\``,
|
|
211
|
+
published.version, NPM_REGISTRY)
|
|
212
|
+
} else {
|
|
213
|
+
versionCheck("unknown", `${self.version}; could not read the npm registry (${published.error?.code || "error"})`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
196
216
|
|
|
197
217
|
// Reachable as a bare command, or the one line that makes it so for this
|
|
198
218
|
// install (path-hint.mjs). Measured: an npm prefix whose bin is not on PATH
|
|
@@ -241,16 +261,6 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
241
261
|
{ pinCommit: identity.commit, marketplaceHead: null, branch: null })
|
|
242
262
|
}
|
|
243
263
|
|
|
244
|
-
phase("asking the npm registry for the newest published version")
|
|
245
|
-
const latest = await latestVersion(self.name)
|
|
246
|
-
if (latest) {
|
|
247
|
-
const current = latest === self.version
|
|
248
|
-
add("omakit.latest", current ? "ok" : "advice",
|
|
249
|
-
current ? `${latest} is the newest published version` : `${latest} is published, this is ${self.version}`,
|
|
250
|
-
current ? null : upgradeCommand(repoRoot, self.name))
|
|
251
|
-
} else {
|
|
252
|
-
add("omakit.latest", "unknown", "the npm registry did not answer, or this version is unpublished")
|
|
253
|
-
}
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
// Where the credential comes from, said out loud. Borrowing someone's `gh`
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// it so, for the install that is actually here.
|
|
3
3
|
//
|
|
4
4
|
// Measured: after `npm install --global omakit` on a machine whose npm prefix
|
|
5
|
-
// was ~/.local/share
|
|
5
|
+
// was a directory under ~/.local/share, the command was missing, because that
|
|
6
6
|
// prefix's `bin` was not on PATH. `omakit setup` then printed the `ln -s` hint
|
|
7
7
|
// written for a clone, which points at a bin/omakit that npm did not lay out
|
|
8
8
|
// where the hint assumes. An npm install needs the npm prefix's `bin` on PATH,
|
|
@@ -8,3 +8,34 @@ export function omakitCacheDir(name = "", env = process.env) {
|
|
|
8
8
|
: join(env.HOME || homedir(), ".cache")
|
|
9
9
|
return join(base, "omakit", name)
|
|
10
10
|
}
|
|
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
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Text for a person, with every path under the home directory written the
|
|
27
|
+
* way a shell would take it: `~/.cache/omakit/marketplace`. Only `$HOME`
|
|
28
|
+
* counts, because `~` is what the shell expands to `$HOME` and nothing else;
|
|
29
|
+
* with it unset, or for a path outside it, the text is returned as it came.
|
|
30
|
+
* Human output only: `--json` keeps every path absolute, so this is applied
|
|
31
|
+
* where text is rendered, never where a result is built.
|
|
32
|
+
*/
|
|
33
|
+
export function withHomeAbbreviated(text, env = process.env) {
|
|
34
|
+
const home = String(env.HOME || "").replace(/\/+$/, "")
|
|
35
|
+
if (!home || !home.startsWith("/")) return String(text)
|
|
36
|
+
// The home directory where a path starts: at the start of the text or after
|
|
37
|
+
// the characters a path follows in prose or a command, and followed by a
|
|
38
|
+
// separator or the end, so /tmp/home/me/x and /home/meh/x are left alone.
|
|
39
|
+
const literal = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
40
|
+
return String(text).replace(new RegExp(`(^|[\\s"'(=:])${literal}(?=/|$|[\\s"'),:])`, "g"), "$1~")
|
|
41
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import {
|
|
21
21
|
action, colourEnabled, COLUMNS, continuation, field, GUTTER, labelled, mark, section, STEP, styler, verdict, width, wrap,
|
|
22
22
|
} from "./style.mjs"
|
|
23
|
+
import { withHomeAbbreviated } from "./paths.mjs"
|
|
23
24
|
|
|
24
25
|
const body = " ".repeat(GUTTER)
|
|
25
26
|
|
|
@@ -34,9 +35,10 @@ function stateOf(check) {
|
|
|
34
35
|
/**
|
|
35
36
|
* A check's head line: the mark, the id in bold, and the source in brackets
|
|
36
37
|
* pushed to the right edge, so the sources form a column of their own and the
|
|
37
|
-
* 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.
|
|
38
40
|
*/
|
|
39
|
-
function head(state, id, source, c) {
|
|
41
|
+
export function head(state, id, source, c) {
|
|
40
42
|
const left = `${mark(state, c)}${c("name", id)}`
|
|
41
43
|
const tag = c("punctuation", `[${source}]`)
|
|
42
44
|
const gap = Math.max(2, COLUMNS - width(left) - width(tag))
|
|
@@ -317,7 +319,13 @@ export function renderVerify(document, { colour = colourEnabled(), blockingRules
|
|
|
317
319
|
|
|
318
320
|
const DOCTOR_STATE = { ok: "pass", advice: "advisory", problem: "fail", info: "info", unknown: "unknown" }
|
|
319
321
|
|
|
320
|
-
|
|
322
|
+
/**
|
|
323
|
+
* @param {{ colour?: boolean, env?: object }} [options] `env` is where `$HOME`
|
|
324
|
+
* is read from: a path under it is printed as `~/...` for a person, the
|
|
325
|
+
* way a shell takes it, while the result itself, and so `--json`, keeps
|
|
326
|
+
* every path absolute.
|
|
327
|
+
*/
|
|
328
|
+
export function renderDoctor(result, { colour = colourEnabled(), env = process.env } = {}) {
|
|
321
329
|
const c = styler(colour)
|
|
322
330
|
const out = []
|
|
323
331
|
let previous = false
|
|
@@ -326,8 +334,8 @@ export function renderDoctor(result, { colour = colourEnabled() } = {}) {
|
|
|
326
334
|
const loud = state === "fail" || state === "advisory"
|
|
327
335
|
if (index > 0 && (loud || previous)) out.push("")
|
|
328
336
|
out.push(`${mark(state, c)}${c("name", check.id)}`)
|
|
329
|
-
out.push(...wrap(check.detail, { indent: GUTTER }, c))
|
|
330
|
-
if (check.action) out.push(...action(check.action, c))
|
|
337
|
+
out.push(...wrap(withHomeAbbreviated(check.detail, env), { indent: GUTTER }, c))
|
|
338
|
+
if (check.action) out.push(...action(withHomeAbbreviated(check.action, env), c))
|
|
331
339
|
previous = loud
|
|
332
340
|
}
|
|
333
341
|
out.push("")
|
|
@@ -20,6 +20,7 @@ import { TAGLINE } from "./usage.mjs"
|
|
|
20
20
|
import { installCompletion } from "./completion.mjs"
|
|
21
21
|
import { submissionContract } from "./form.mjs"
|
|
22
22
|
import { pathHint } from "./path-hint.mjs"
|
|
23
|
+
import { withHomeAbbreviated } from "./paths.mjs"
|
|
23
24
|
|
|
24
25
|
function version(command) {
|
|
25
26
|
try {
|
|
@@ -32,16 +33,18 @@ function version(command) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream }} options
|
|
36
|
+
* @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream, env?: object }} options
|
|
37
|
+
* `env` is where `$HOME` is read from: this is output for a person, so a
|
|
38
|
+
* path under it is printed as `~/...`.
|
|
36
39
|
*/
|
|
37
|
-
export async function setup({ repoRoot, entryPoint, stream = process.stdout }) {
|
|
40
|
+
export async function setup({ repoRoot, entryPoint, stream = process.stdout, env = process.env }) {
|
|
38
41
|
const c = styler(colourEnabled(stream))
|
|
39
42
|
const out = (line = "") => stream.write(`${line}\n`)
|
|
40
43
|
// A step is a status line: the mark, then the fact, wrapped under itself.
|
|
41
|
-
const step = (state, text) => out(`${mark(state, c)}${wrap(text, { indent: GUTTER }, c).join("\n").trimStart()}`)
|
|
44
|
+
const step = (state, text) => out(`${mark(state, c)}${wrap(withHomeAbbreviated(text, env), { indent: GUTTER }, c).join("\n").trimStart()}`)
|
|
42
45
|
// The one action under a step sits in the step's body; under a sentence it
|
|
43
46
|
// sits where the sentence does.
|
|
44
|
-
const fix = (text, indent = GUTTER) => { for (const line of action(text, c, { indent })) out(line) }
|
|
47
|
+
const fix = (text, indent = GUTTER) => { for (const line of action(withHomeAbbreviated(text, env), c, { indent })) out(line) }
|
|
45
48
|
|
|
46
49
|
// The one place the wordmark runs through `ttfx` (effect.mjs): a first run
|
|
47
50
|
// already spending seconds fetching the pin.
|
|
@@ -59,11 +59,29 @@ export const NPM_UPGRADE_ARGS = Object.freeze(["install", "--global", "--ignore-
|
|
|
59
59
|
|
|
60
60
|
/** The newest published version, or null when the registry did not answer. */
|
|
61
61
|
export async function latestOnRegistry(name) {
|
|
62
|
+
return (await registryLatest(name)).version
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The registry the newest version is read from, named in doctor's evidence. */
|
|
66
|
+
export const NPM_REGISTRY = "https://registry.npmjs.org"
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The newest published version, with the reason when there is none: the
|
|
70
|
+
* failure code from the one GET call site (network-unavailable,
|
|
71
|
+
* github-unavailable's npm sibling, not-found for an unpublished name), or
|
|
72
|
+
* `unpublished` when the registry answered without a version. `doctor` prints
|
|
73
|
+
* the code; `upgrade` only needs the version.
|
|
74
|
+
*
|
|
75
|
+
* @returns {Promise<{ version: string|null, error: { code: string, message: string }|null }>}
|
|
76
|
+
*/
|
|
77
|
+
export async function registryLatest(name) {
|
|
62
78
|
try {
|
|
63
|
-
const meta = await getJson(
|
|
64
|
-
return meta?.version
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
const meta = await getJson(`${NPM_REGISTRY}/${encodeURIComponent(name)}/latest`)
|
|
80
|
+
return meta?.version
|
|
81
|
+
? { version: meta.version, error: null }
|
|
82
|
+
: { version: null, error: { code: "unpublished", message: "the registry answered without a version" } }
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { version: null, error: { code: error?.code || "error", message: String(error?.message || error) } }
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
|
|
@@ -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
|