omakit 0.2.1 → 0.3.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 +2 -0
- package/package.json +1 -1
- package/skills/omarchy-plugin-audit/SKILL.md +44 -0
- package/tools/audit/audit.mjs +223 -0
- package/tools/audit/git.mjs +63 -0
- package/tools/audit/report.mjs +56 -0
- package/tools/marketplace/cli.mjs +39 -1
- package/tools/marketplace/completion.mjs +14 -5
- package/tools/marketplace/form.mjs +5 -3
- package/tools/marketplace/options.mjs +1 -0
- package/tools/marketplace/style.mjs +8 -2
- package/tools/marketplace/upgrade.mjs +4 -1
- package/tools/marketplace/usage.mjs +11 -0
- package/tools/weigh/confirm.mjs +22 -5
- package/tools/weigh/list.mjs +17 -4
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ See [docs/INSTALL.md](docs/INSTALL.md) for the clone route, PATH, requirements a
|
|
|
28
28
|
| [`omakit watch <issue-url>`](docs/VALIDATION_WATCH.md) | The commit the marketplace validated, against the plugin's current HEAD. |
|
|
29
29
|
| [`omakit verify <plugin-repo>`](docs/COMMANDS.md) | The official security baseline over the local transport; `--json` for the document. |
|
|
30
30
|
| [`omakit parity`](docs/COMMANDS.md) | The baseline over GitHub versus the local transport, on real listings; writes the evidence. |
|
|
31
|
+
| [`omakit audit [<plugin>]`](docs/AUDIT.md) | Installed third-party commits against the exact commits the marketplace validated. |
|
|
31
32
|
| [`omakit weigh <plugin>`](docs/WEIGH.md) | What a plugin weighs on the shell, measured by restarting it without and with the plugin; asks first. |
|
|
32
33
|
| [`omakit doctor`](docs/COMMANDS.md) | What is installed, what is pinned, and what has moved. |
|
|
33
34
|
| [`omakit pin`](docs/COMMANDS.md) | What setup does for the pin, on its own. |
|
|
@@ -95,6 +96,7 @@ Committed evidence records a digest of each side rather than the results themsel
|
|
|
95
96
|
| [docs/INSTALL.md](docs/INSTALL.md) | install details, PATH, requirements, upgrading, and what Socket reports and why |
|
|
96
97
|
| [docs/HOW.md](docs/HOW.md) | what omakit is doing, why it uses Node, the baseline and check labels |
|
|
97
98
|
| [docs/COMMANDS.md](docs/COMMANDS.md) | command details, authentication and network behaviour |
|
|
99
|
+
| [docs/AUDIT.md](docs/AUDIT.md) | installed plugin drift against marketplace-validated commits, with JSON origins |
|
|
98
100
|
| [docs/SUBMIT.md](docs/SUBMIT.md) | every check and what it decides |
|
|
99
101
|
| [docs/WEIGH.md](docs/WEIGH.md) | what `weigh` measures, the noise floor, the `shell.json` mutation and its restore, and the JSON contract |
|
|
100
102
|
| [docs/VALIDATION_WATCH.md](docs/VALIDATION_WATCH.md) | the validation watch: what the marketplace validated, and what moves it |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: omarchy-plugin-audit
|
|
3
|
+
description: Compare installed Omarchy Quattro plugin commits with the exact commits the marketplace validated. Use before touching an installed third-party plugin, before enabling or updating one, or when asked whether installed plugins match marketplace review. Read-only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Audit installed plugin commits
|
|
7
|
+
|
|
8
|
+
Run `omakit audit` before touching an installed third-party plugin. Run
|
|
9
|
+
`omakit audit <plugin-id-or-dir>` when only one installed plugin is in scope.
|
|
10
|
+
The command reads the shell's installed list, the marketplace catalog and local
|
|
11
|
+
Git facts. It changes nothing.
|
|
12
|
+
|
|
13
|
+
Read the primary state first:
|
|
14
|
+
|
|
15
|
+
- `validated` means installed HEAD is one of the commits the marketplace
|
|
16
|
+
records as validated.
|
|
17
|
+
- `ahead` means a validated commit is an ancestor, but HEAD contains later
|
|
18
|
+
commits the marketplace did not validate.
|
|
19
|
+
- `diverged` means installed history does not descend from a validated commit,
|
|
20
|
+
or its origin is not the listed repository. A validated object missing from
|
|
21
|
+
local history is diverged, with the clone's shallow status stated.
|
|
22
|
+
- `unverified` means the listing records no validated commit. Its status is stated.
|
|
23
|
+
- `unlisted` means neither plugin id nor origin identifies a listing.
|
|
24
|
+
- `unknown` means Git or the source directory could not answer. Keep the error.
|
|
25
|
+
|
|
26
|
+
Then report every stacking flag: `modified`, `disabled`, and `upstream moved`.
|
|
27
|
+
Do not collapse a flag into the primary state. First-party plugins are counted
|
|
28
|
+
and excluded because they ship with the shell.
|
|
29
|
+
|
|
30
|
+
Use `--json` when another tool needs the document. Every figure carries its
|
|
31
|
+
origin. Use `--offline` when the live catalog must not be read; the header will
|
|
32
|
+
name the pin. `--drift` is a view filter, not a different measurement.
|
|
33
|
+
|
|
34
|
+
An `ahead` or `diverged` report prints the exact checkout command for the
|
|
35
|
+
validated commit and names the marketplace form for validating a newer one.
|
|
36
|
+
Never run that checkout command without the person's explicit request. Never
|
|
37
|
+
replace it with `omarchy plugin update`; update moves to mutable HEAD and does
|
|
38
|
+
not establish marketplace validation.
|
|
39
|
+
|
|
40
|
+
`AUDITED`, exit 0, means every audited row is validated, or there was nothing
|
|
41
|
+
third-party to audit. `DRIFT`, exit 1, means drift or an unknown answer.
|
|
42
|
+
Exit 2 means the invocation was refused. `NOT AUDITED` because the shell or
|
|
43
|
+
catalog did not answer is a
|
|
44
|
+
result to report, not a reason to substitute a directory scan.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Compare installed third-party checkouts with commits recorded by the live
|
|
2
|
+
// marketplace catalog. Nothing here fetches or changes a checkout.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
5
|
+
import { resolve } from "node:path"
|
|
6
|
+
import { newerCommitChoice } from "../marketplace/form.mjs"
|
|
7
|
+
import { liveRegistry, sameRepository } from "../marketplace/registry.mjs"
|
|
8
|
+
import { installedPlugins } from "../weigh/list.mjs"
|
|
9
|
+
import { ancestorOf, commitsAfter, hasCommit, isShallow, readCheckout } from "./git.mjs"
|
|
10
|
+
|
|
11
|
+
export class AuditError extends Error {
|
|
12
|
+
constructor(code, message, remedy = null) {
|
|
13
|
+
super(message)
|
|
14
|
+
this.name = "AuditError"
|
|
15
|
+
this.code = code
|
|
16
|
+
this.remedy = remedy
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const text = (value) => (typeof value === "string" && value ? value : null)
|
|
21
|
+
const lower = (value) => text(value)?.toLowerCase() || null
|
|
22
|
+
const figure = (value, origin) => (value === null || value === undefined ? null : { value, origin })
|
|
23
|
+
const catalogFigure = (listing, name) => figure(text(listing?.[name]), `site/catalog.json ${name}`)
|
|
24
|
+
const short = (value) => value.slice(0, 8)
|
|
25
|
+
const ROW_ORDER = ["diverged", "ahead", "modified", "unverified", "unlisted", "unknown", "validated"]
|
|
26
|
+
const rowRank = (row) => ROW_ORDER.indexOf(["diverged", "ahead"].includes(row.state) ? row.state : row.flags.includes("modified") ? "modified" : row.state)
|
|
27
|
+
|
|
28
|
+
function targetId(target) {
|
|
29
|
+
if (!target || !existsSync(target)) return target || null
|
|
30
|
+
try {
|
|
31
|
+
return text(JSON.parse(readFileSync(resolve(target, "manifest.json"), "utf8"))?.id)
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function chooseListing(plugin, checkout, listings) {
|
|
38
|
+
const byId = listings.find((entry) => entry?.id === plugin.id) || null
|
|
39
|
+
const byOrigin = listings.find((entry) => sameRepository(entry?.repo, checkout.repository)) || null
|
|
40
|
+
if (byId && byOrigin && byId !== byOrigin) {
|
|
41
|
+
return { listing: null, conflict: `manifest id matches ${byId.id}, but origin matches ${byOrigin.id}; neither listing was used` }
|
|
42
|
+
}
|
|
43
|
+
return { listing: byId || byOrigin, conflict: null }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validatedFigures(listing) {
|
|
47
|
+
const figures = []
|
|
48
|
+
const add = (prefix) => {
|
|
49
|
+
const commit = lower(listing?.[`${prefix}ValidatedCommit`])
|
|
50
|
+
if (!commit || figures.some((entry) => entry.commit.value === commit)) return
|
|
51
|
+
figures.push({
|
|
52
|
+
kind: prefix,
|
|
53
|
+
commit: figure(commit, `site/catalog.json ${prefix}ValidatedCommit`),
|
|
54
|
+
date: catalogFigure(listing, `${prefix}ValidatedAt`),
|
|
55
|
+
branch: catalogFigure(listing, prefix === "listing" ? "listingValidatedBranch" : "upstreamObservedBranch"),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
add("upstream")
|
|
59
|
+
add("listing")
|
|
60
|
+
return figures
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rowFact(plugin, checkout, listing, conflict, git, env) {
|
|
64
|
+
const flags = []
|
|
65
|
+
if (checkout.status) flags.push("modified")
|
|
66
|
+
if (plugin.enabled !== true) flags.push("disabled")
|
|
67
|
+
const validated = validatedFigures(listing)
|
|
68
|
+
const observed = lower(listing?.upstreamObservedCommit)
|
|
69
|
+
if (observed && observed !== checkout.commit && !validated.some((entry) => entry.commit.value === observed)) flags.push("upstream moved")
|
|
70
|
+
|
|
71
|
+
const common = {
|
|
72
|
+
id: plugin.id,
|
|
73
|
+
flags,
|
|
74
|
+
installed: {
|
|
75
|
+
commit: figure(checkout.commit, "git rev-parse HEAD"),
|
|
76
|
+
enabled: figure(plugin.enabled === true, "omarchy plugin list --json enabled"),
|
|
77
|
+
modified: figure(Boolean(checkout.status), "git status --porcelain"),
|
|
78
|
+
repository: figure(checkout.repository, "git remote get-url origin"),
|
|
79
|
+
},
|
|
80
|
+
validated,
|
|
81
|
+
upstream: {
|
|
82
|
+
observedCommit: catalogFigure(listing, "upstreamObservedCommit"),
|
|
83
|
+
observedBranch: catalogFigure(listing, "upstreamObservedBranch"),
|
|
84
|
+
checkedAt: catalogFigure(listing, "upstreamCheckedAt"),
|
|
85
|
+
checkStatus: catalogFigure(listing, "upstreamCheckStatus"),
|
|
86
|
+
},
|
|
87
|
+
sourceDir: plugin.sourceDir,
|
|
88
|
+
listing: listing ? { id: listing.id, repository: listing.repo } : null,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!listing) return { ...common, state: "unlisted", fact: conflict || "manifest id and origin match no marketplace listing", aheadBy: null, matchedValidated: null }
|
|
92
|
+
if (validated.some((entry) => entry.commit.value === checkout.commit)) {
|
|
93
|
+
const matched = validated.find((entry) => entry.commit.value === checkout.commit)
|
|
94
|
+
return { ...common, state: "validated", fact: `validated ${short(matched.commit.value)} on ${matched.date?.value || "an unrecorded date"}`, aheadBy: null, matchedValidated: matched }
|
|
95
|
+
}
|
|
96
|
+
if (!validated.length) {
|
|
97
|
+
return { ...common, state: "unverified", fact: `listed with verificationStatus ${listing.verificationStatus || "unrecorded"}, and no validated commit`, aheadBy: null, matchedValidated: null }
|
|
98
|
+
}
|
|
99
|
+
const missing = []
|
|
100
|
+
for (const candidate of validated) {
|
|
101
|
+
if (!git.hasCommit(plugin.sourceDir, candidate.commit.value, { env })) {
|
|
102
|
+
missing.push(candidate)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (git.ancestor(plugin.sourceDir, candidate.commit.value, { env })) {
|
|
106
|
+
const count = git.count(plugin.sourceDir, candidate.commit.value, { env })
|
|
107
|
+
return { ...common, state: "ahead", fact: `${count} commit${count === 1 ? "" : "s"} ahead of validated ${short(candidate.commit.value)} (${candidate.date?.value || "unrecorded date"}); HEAD ${short(checkout.commit)}`, aheadBy: figure(count, `git rev-list --count ${candidate.commit.value}..HEAD`), matchedValidated: candidate }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (missing.length) {
|
|
111
|
+
const shallow = figure(git.shallow(plugin.sourceDir, { env }), "git rev-parse --is-shallow-repository")
|
|
112
|
+
return { ...common, state: "diverged", fact: `validated commit not in local history; shallow clone: ${shallow.value}`, shallow, aheadBy: null, matchedValidated: missing[0] }
|
|
113
|
+
}
|
|
114
|
+
const moved = sameRepository(checkout.repository, listing.repo)
|
|
115
|
+
return { ...common, state: "diverged", fact: moved ? "installed commit does not descend from a validated commit" : `origin does not match ${listing.repo}`, aheadBy: null, matchedValidated: validated[0] }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Audit the installed third-party plugin set, or one target from that set.
|
|
120
|
+
* All external readers are injectable so tests establish every state without
|
|
121
|
+
* touching the person's shell or plugin directories.
|
|
122
|
+
*/
|
|
123
|
+
export async function auditInstalled(options = {}) {
|
|
124
|
+
const env = options.env || process.env
|
|
125
|
+
const readers = {
|
|
126
|
+
installed: options.installed || ((args) => installedPlugins(args)),
|
|
127
|
+
registry: options.registry || ((args) => liveRegistry(args)),
|
|
128
|
+
checkout: options.checkout || ((dir, args) => readCheckout(dir, args)),
|
|
129
|
+
hasCommit: options.hasCommit || ((dir, sha, args) => hasCommit(dir, sha, args)),
|
|
130
|
+
shallow: options.shallow || ((dir, args) => isShallow(dir, args)),
|
|
131
|
+
ancestor: options.ancestor || ((dir, sha, args) => ancestorOf(dir, sha, args)),
|
|
132
|
+
count: options.count || ((dir, sha, args) => commitsAfter(dir, sha, args)),
|
|
133
|
+
route: options.route || ((args) => newerCommitChoice(args)),
|
|
134
|
+
}
|
|
135
|
+
let installed
|
|
136
|
+
try {
|
|
137
|
+
installed = readers.installed({ env })
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new AuditError(error?.code || "shell-not-running", error?.message || String(error))
|
|
140
|
+
}
|
|
141
|
+
let live
|
|
142
|
+
try {
|
|
143
|
+
live = await readers.registry({ repoRoot: options.repoRoot, offline: options.offline })
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new AuditError(error?.code || "marketplace-unavailable", error?.message || String(error), error?.remedy || "omakit pin")
|
|
146
|
+
}
|
|
147
|
+
const listings = Array.isArray(live.catalog?.plugins) ? live.catalog.plugins : null
|
|
148
|
+
if (!listings) throw new AuditError("catalog-unreadable", "the marketplace catalog has no plugins array", "omakit pin")
|
|
149
|
+
|
|
150
|
+
const wanted = targetId(options.target)
|
|
151
|
+
let selected = installed
|
|
152
|
+
if (options.target) {
|
|
153
|
+
const path = existsSync(options.target) ? resolve(options.target) : null
|
|
154
|
+
selected = installed.filter((plugin) => plugin.id === wanted || (path && plugin.sourceDir && resolve(plugin.sourceDir) === path))
|
|
155
|
+
if (!selected.length) throw new AuditError("plugin-not-installed", `${JSON.stringify(options.target)} is not an installed plugin`, "omakit audit")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let firstPartyCount = 0
|
|
159
|
+
const rows = []
|
|
160
|
+
for (const plugin of selected) {
|
|
161
|
+
const idListing = listings.find((entry) => entry?.id === plugin.id)
|
|
162
|
+
if (plugin.firstParty === true || idListing?.sourceType === "builtin") {
|
|
163
|
+
firstPartyCount += 1
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (!plugin.sourceDir) {
|
|
167
|
+
rows.push({ id: plugin.id, state: "unknown", flags: plugin.enabled === true ? [] : ["disabled"], installed: { enabled: figure(plugin.enabled === true, "omarchy plugin list --json enabled") }, validated: [], upstream: {}, sourceDir: null, listing: idListing ? { id: idListing.id, repository: idListing.repo } : null, fact: "omarchy-plugin-catalog records no source directory", error: "source directory unavailable", aheadBy: null, matchedValidated: null })
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
let checkout
|
|
171
|
+
try {
|
|
172
|
+
checkout = readers.checkout(plugin.sourceDir, { env })
|
|
173
|
+
} catch (error) {
|
|
174
|
+
rows.push({ id: plugin.id, state: "unknown", flags: plugin.enabled === true ? [] : ["disabled"], installed: { enabled: figure(plugin.enabled === true, "omarchy plugin list --json enabled") }, validated: [], upstream: {}, sourceDir: plugin.sourceDir, listing: idListing ? { id: idListing.id, repository: idListing.repo } : null, fact: `git failed: ${error.message}`, error: error.message, aheadBy: null, matchedValidated: null })
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const { listing, conflict } = chooseListing(plugin, checkout, listings)
|
|
179
|
+
if (listing?.sourceType === "builtin") {
|
|
180
|
+
firstPartyCount += 1
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
rows.push(rowFact(plugin, checkout, listing, conflict, readers, env))
|
|
184
|
+
} catch (error) {
|
|
185
|
+
rows.push({ id: plugin.id, state: "unknown", flags: [checkout.status ? "modified" : null, plugin.enabled === true ? null : "disabled"].filter(Boolean), installed: { commit: figure(checkout.commit, "git rev-parse HEAD"), enabled: figure(plugin.enabled === true, "omarchy plugin list --json enabled"), modified: figure(Boolean(checkout.status), "git status --porcelain"), repository: figure(checkout.repository, "git remote get-url origin") }, validated: validatedFigures(idListing), upstream: {}, sourceDir: plugin.sourceDir, listing: idListing ? { id: idListing.id, repository: idListing.repo } : null, fact: `git failed: ${error.message}`, error: error.message, aheadBy: null, matchedValidated: null })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
rows.sort((a, b) => rowRank(a) - rowRank(b))
|
|
190
|
+
const needsAction = rows.some((row) => row.state === "ahead" || row.state === "diverged")
|
|
191
|
+
let updateRoute = null
|
|
192
|
+
let updateRouteError = null
|
|
193
|
+
if (needsAction) {
|
|
194
|
+
try {
|
|
195
|
+
updateRoute = await readers.route({ repoRoot: options.repoRoot })
|
|
196
|
+
} catch (error) {
|
|
197
|
+
updateRouteError = error?.message || String(error)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const drift = rows.filter((row) => row.state !== "validated")
|
|
201
|
+
return {
|
|
202
|
+
command: "audit",
|
|
203
|
+
catalog: {
|
|
204
|
+
source: live.source,
|
|
205
|
+
commit: figure(live.commit, live.source === "head" ? "marketplace default-branch HEAD" : "marketplace pin"),
|
|
206
|
+
readAt: figure(live.fetchedAt, live.source === "head" ? "liveRegistry fetchedAt" : "--offline or liveRegistry fallback"),
|
|
207
|
+
offline: options.offline === true,
|
|
208
|
+
reason: live.reason,
|
|
209
|
+
},
|
|
210
|
+
counts: {
|
|
211
|
+
installed: figure(installed.length, "omarchy plugin list --json length"),
|
|
212
|
+
selected: figure(selected.length, options.target ? "target selection" : "omarchy plugin list --json length"),
|
|
213
|
+
firstPartyExcluded: figure(firstPartyCount, "sourceType builtin or omarchy plugin list --json firstParty"),
|
|
214
|
+
audited: figure(rows.length, "audited row count"),
|
|
215
|
+
validated: figure(rows.length - drift.length, "rows whose state is validated"),
|
|
216
|
+
drift: figure(drift.length, "rows whose state is not validated"),
|
|
217
|
+
},
|
|
218
|
+
rows: options.drift ? drift : rows,
|
|
219
|
+
updateRoute,
|
|
220
|
+
updateRouteError,
|
|
221
|
+
ok: drift.length === 0,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The local Git facts used by `omakit audit`. Every invocation is read-only,
|
|
2
|
+
// has a fixed verb and argument shape, and is passed to Git without a shell.
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from "node:child_process"
|
|
5
|
+
|
|
6
|
+
function runGit(sourceDir, args, env = process.env) {
|
|
7
|
+
const result = spawnSync("git", ["-C", sourceDir, ...args], {
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
env: { ...env, GIT_NO_LAZY_FETCH: "1" },
|
|
10
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11
|
+
})
|
|
12
|
+
if (result.status !== 0 || result.error) {
|
|
13
|
+
const detail = (result.stderr || result.error?.message || `git exited ${result.status}`).trim()
|
|
14
|
+
throw new Error(`${args.join(" ")}: ${detail}`)
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.trim()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Read HEAD, tree state and origin from one checkout. */
|
|
20
|
+
export function readCheckout(sourceDir, { env = process.env } = {}) {
|
|
21
|
+
return {
|
|
22
|
+
commit: runGit(sourceDir, ["rev-parse", "HEAD"], env).toLowerCase(),
|
|
23
|
+
status: runGit(sourceDir, ["status", "--porcelain"], env),
|
|
24
|
+
repository: runGit(sourceDir, ["remote", "get-url", "origin"], env),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A missing object is a local-history fact, not a failed ancestry check. */
|
|
29
|
+
export function hasCommit(sourceDir, commit, { env = process.env } = {}) {
|
|
30
|
+
const result = spawnSync("git", ["-C", sourceDir, "cat-file", "-e", commit], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
env: { ...env, GIT_NO_LAZY_FETCH: "1" },
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
})
|
|
35
|
+
if (result.error || result.status === null) throw new Error(result.error?.message || "git could not run")
|
|
36
|
+
return result.status === 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isShallow(sourceDir, { env = process.env } = {}) {
|
|
40
|
+
const value = runGit(sourceDir, ["rev-parse", "--is-shallow-repository"], env)
|
|
41
|
+
if (!["true", "false"].includes(value)) throw new Error(`rev-parse --is-shallow-repository returned ${JSON.stringify(value)}`)
|
|
42
|
+
return value === "true"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Is a recorded commit an ancestor of the running checkout? */
|
|
46
|
+
export function ancestorOf(sourceDir, commit, { env = process.env } = {}) {
|
|
47
|
+
const result = spawnSync("git", ["-C", sourceDir, "merge-base", "--is-ancestor", commit, "HEAD"], {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
env: { ...env, GIT_NO_LAZY_FETCH: "1" },
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
+
})
|
|
52
|
+
if (result.status === 0) return true
|
|
53
|
+
if (result.status === 1) return false
|
|
54
|
+
const detail = (result.stderr || result.error?.message || `git exited ${result.status}`).trim()
|
|
55
|
+
throw new Error(`merge-base --is-ancestor ${commit} HEAD: ${detail}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Count commits after one recorded ancestor. */
|
|
59
|
+
export function commitsAfter(sourceDir, commit, { env = process.env } = {}) {
|
|
60
|
+
const value = runGit(sourceDir, ["rev-list", "--count", `${commit}..HEAD`], env)
|
|
61
|
+
if (!/^\d+$/.test(value)) throw new Error(`rev-list --count returned ${JSON.stringify(value)}`)
|
|
62
|
+
return Number(value)
|
|
63
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { action, AUDIT_VERDICTS, colourEnabled, field, GUTTER, mark, styler, verdict, wrap } from "../marketplace/style.mjs"
|
|
2
|
+
|
|
3
|
+
const short = (value) => value ? String(value).slice(0, 8) : "unrecorded"
|
|
4
|
+
const flagText = (flags) => flags.length ? `; ${flags.join(", ")}` : ""
|
|
5
|
+
const markFor = (row) => {
|
|
6
|
+
if (row.flags.includes("modified")) return "advisory"
|
|
7
|
+
if (row.state === "validated") return "pass"
|
|
8
|
+
if (["ahead", "diverged", "unverified"].includes(row.state)) return "advisory"
|
|
9
|
+
if (row.state === "unlisted") return "info"
|
|
10
|
+
if (row.flags.includes("disabled")) return "info"
|
|
11
|
+
return "unknown"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function catalogText(catalog) {
|
|
15
|
+
if (catalog.source === "head") return `live HEAD ${short(catalog.commit.value)} at ${catalog.readAt?.value || "an unrecorded time"}`
|
|
16
|
+
return `pin ${short(catalog.commit.value)}${catalog.offline ? " (offline)" : ""}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A terminal report made only from the shared style vocabulary. */
|
|
20
|
+
export function renderAudit(document, { colour = colourEnabled() } = {}) {
|
|
21
|
+
const c = styler(colour)
|
|
22
|
+
const out = []
|
|
23
|
+
out.push(...field("catalog", catalogText(document.catalog), c))
|
|
24
|
+
out.push(...field("installed", `${document.counts.installed.value}`, c))
|
|
25
|
+
out.push(...field("first-party", `${document.counts.firstPartyExcluded.value} left out`, c))
|
|
26
|
+
out.push(...field("audited", `${document.counts.audited.value}`, c))
|
|
27
|
+
out.push("")
|
|
28
|
+
for (const row of document.rows) {
|
|
29
|
+
const installed = row.installed?.commit?.value
|
|
30
|
+
const validated = row.matchedValidated?.commit?.value
|
|
31
|
+
const date = row.matchedValidated?.date?.value
|
|
32
|
+
const detail = ["validated", "ahead"].includes(row.state)
|
|
33
|
+
? `${row.fact}${flagText(row.flags)}`
|
|
34
|
+
: `${row.state}: ${row.fact}${installed ? `; HEAD ${short(installed)}` : ""}${validated ? `; validated ${short(validated)}${date ? ` on ${date}` : ""}` : ""}${flagText(row.flags)}`
|
|
35
|
+
out.push(`${mark(markFor(row), c)}${c("name", row.id)}`)
|
|
36
|
+
out.push(...wrap(detail, { indent: GUTTER }, c))
|
|
37
|
+
if ((row.state === "ahead" || row.state === "diverged") && validated) {
|
|
38
|
+
out.push(...action(`git -C ${JSON.stringify(row.sourceDir)} checkout ${validated}`, c))
|
|
39
|
+
}
|
|
40
|
+
out.push("")
|
|
41
|
+
}
|
|
42
|
+
const total = document.counts.audited.value
|
|
43
|
+
const good = document.counts.validated.value
|
|
44
|
+
const drift = document.counts.drift.value
|
|
45
|
+
if (document.updateRoute) {
|
|
46
|
+
out.push(...action(`To validate a newer commit: ${document.updateRoute.url}`, c))
|
|
47
|
+
out.push(...wrap(`${document.updateRoute.name}; choose ${JSON.stringify(document.updateRoute.choice)}.`, { indent: GUTTER }, c))
|
|
48
|
+
out.push("")
|
|
49
|
+
}
|
|
50
|
+
if (document.updateRouteError) {
|
|
51
|
+
out.push(...wrap(`Verification route unavailable: ${document.updateRouteError}; run omakit pin.`, { indent: GUTTER }, c))
|
|
52
|
+
out.push("")
|
|
53
|
+
}
|
|
54
|
+
out.push(...verdict(document.ok ? "pass" : "fail", document.ok ? AUDIT_VERDICTS.validated : AUDIT_VERDICTS.drift, `${good} of ${total} run a commit the marketplace validated; ${drift} run one it never saw.`, c))
|
|
55
|
+
return out.join("\n")
|
|
56
|
+
}
|
|
@@ -33,12 +33,14 @@ import { upgrade } from "./upgrade.mjs"
|
|
|
33
33
|
import { progress } from "./progress.mjs"
|
|
34
34
|
import { banner, bannerEnabled } from "./banner.mjs"
|
|
35
35
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
36
|
-
import { action, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
|
|
36
|
+
import { action, AUDIT_VERDICTS, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
|
|
37
37
|
import { omakitCacheDir, withHomeAbbreviated } from "./paths.mjs"
|
|
38
38
|
import { DEFAULTS as WEIGH_DEFAULTS, measureWeigh, planWeigh } from "../weigh/audit.mjs"
|
|
39
39
|
import { confirmationQuestion, renderList, renderWeigh, renderPlan } from "../weigh/report.mjs"
|
|
40
40
|
import { listWeighings } from "../weigh/list.mjs"
|
|
41
41
|
import { askYes } from "../weigh/confirm.mjs"
|
|
42
|
+
import { auditInstalled } from "../audit/audit.mjs"
|
|
43
|
+
import { renderAudit } from "../audit/report.mjs"
|
|
42
44
|
|
|
43
45
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
44
46
|
|
|
@@ -293,6 +295,40 @@ async function cmdParity(args) {
|
|
|
293
295
|
process.exit(ok ? 0 : 1)
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
function notAudited(message, remedy = null, exit = 1) {
|
|
299
|
+
const c = styler(colourEnabled(process.stderr))
|
|
300
|
+
const lines = verdict("fail", AUDIT_VERDICTS.unavailable, message, c)
|
|
301
|
+
if (remedy) lines.push(...action(remedy, c, { indent: 0 }))
|
|
302
|
+
process.stderr.write(`${lines.join("\n")}\n`)
|
|
303
|
+
process.exit(exit)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function cmdAudit(args) {
|
|
307
|
+
const parsed = checkArgs(args, ACCEPTED.audit)
|
|
308
|
+
if (parsed.offending !== null) notAudited(`${parsed.reason}. Accepted: ${acceptedWords("audit")}.`, "omakit audit [<plugin-id-or-dir>] [--drift] [--json] [--out FILE] [--offline]", 2)
|
|
309
|
+
let document
|
|
310
|
+
try {
|
|
311
|
+
document = await auditInstalled({
|
|
312
|
+
repoRoot: ROOT,
|
|
313
|
+
target: parsed.positionals[0],
|
|
314
|
+
drift: parsed.options.has("--drift"),
|
|
315
|
+
offline: parsed.options.has("--offline"),
|
|
316
|
+
})
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error?.code && typeof error.code === "string") notAudited(`${error.message}.`, error.remedy || REMEDY[error.code])
|
|
319
|
+
throw error
|
|
320
|
+
}
|
|
321
|
+
const json = `${JSON.stringify(document, null, 2)}\n`
|
|
322
|
+
const out = parsed.options.get("--out")
|
|
323
|
+
if (out) {
|
|
324
|
+
mkdirSync(dirname(resolve(out)), { recursive: true })
|
|
325
|
+
writeFileSync(resolve(out), json)
|
|
326
|
+
}
|
|
327
|
+
if (parsed.options.has("--json")) process.stdout.write(json)
|
|
328
|
+
else process.stdout.write(`${renderAudit(document)}\n`)
|
|
329
|
+
process.exit(document.ok ? 0 : 1)
|
|
330
|
+
}
|
|
331
|
+
|
|
296
332
|
/**
|
|
297
333
|
* Every way `weigh` stops without weighing, in one register: the closing
|
|
298
334
|
* word a report would have ended with, negated, then the sentence naming
|
|
@@ -458,6 +494,8 @@ if (command === "setup") {
|
|
|
458
494
|
await cmdVerify(rest)
|
|
459
495
|
} else if (command === "parity") {
|
|
460
496
|
await cmdParity(rest)
|
|
497
|
+
} else if (command === "audit") {
|
|
498
|
+
await cmdAudit(rest)
|
|
461
499
|
} else if (command === "weigh") {
|
|
462
500
|
await cmdWeigh(rest)
|
|
463
501
|
} else if (command === "help" || command === "--help" || command === "-h" || command === undefined) {
|
|
@@ -47,7 +47,8 @@ export function subcommandsOf(commands = COMMANDS) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* The plugin ids a TAB offers for `omakit weigh <TAB
|
|
50
|
+
* The plugin ids a TAB offers for `omakit weigh <TAB>` and `omakit audit <TAB>`:
|
|
51
|
+
* what the running
|
|
51
52
|
* shell reports through `omarchy-shell shell listPlugins`, filtered with
|
|
52
53
|
* `jq` at TAB time, enabled ids first and whole bars left out, since a bar
|
|
53
54
|
* cannot be weighed. No node process behind the TAB: a shell that does not
|
|
@@ -56,7 +57,15 @@ export function subcommandsOf(commands = COMMANDS) {
|
|
|
56
57
|
* expression is exported so a test can run it through jq.
|
|
57
58
|
*/
|
|
58
59
|
export const PLUGIN_IDS_JQ = "[.[] | select(((.kinds // []) | index(\"bar\")) | not)] | sort_by(.enabled | not) | .[].id"
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The pipeline, in the shell's own syntax. `timeout 1` bounds the wait
|
|
62
|
+
* where coreutils has it; a system without `timeout` (measured: the macOS
|
|
63
|
+
* runner in CI, where the TAB fell back to directories) runs the command
|
|
64
|
+
* unbounded rather than never, since `omarchy-shell` itself gives up on
|
|
65
|
+
* its IPC timeout.
|
|
66
|
+
*/
|
|
67
|
+
export const PLUGIN_IDS_COMMAND = `omarchy-shell shell listPlugins 2>/dev/null | jq -r '${PLUGIN_IDS_JQ}' 2>/dev/null`
|
|
68
|
+
export const PLUGIN_IDS_TIMED = `timeout 1 ${PLUGIN_IDS_COMMAND}`
|
|
60
69
|
|
|
61
70
|
/** What a valued flag takes, by its placeholder: a controlled value, a file, or free text. */
|
|
62
71
|
function placeholderKind(placeholder) {
|
|
@@ -175,7 +184,7 @@ function bash({ subcommands, categories, tags, pin, version }) {
|
|
|
175
184
|
lines.push("# left out, read at TAB time; nothing when the shell does not answer in a")
|
|
176
185
|
lines.push("# second, and the caller falls back to a directory.")
|
|
177
186
|
lines.push("_omakit_plugin_ids() {")
|
|
178
|
-
lines.push(` ${PLUGIN_IDS_COMMAND}`)
|
|
187
|
+
lines.push(` if command -v timeout >/dev/null 2>&1; then ${PLUGIN_IDS_TIMED}; else ${PLUGIN_IDS_COMMAND}; fi`)
|
|
179
188
|
lines.push("}")
|
|
180
189
|
lines.push("")
|
|
181
190
|
lines.push("# A controlled value may contain a space, so each match is one line and is")
|
|
@@ -243,7 +252,7 @@ function zsh({ subcommands, categories, tags, pin, version }) {
|
|
|
243
252
|
lines.push("# left out, read at TAB time; a directory when the shell does not answer.")
|
|
244
253
|
lines.push("_omakit_plugins() {")
|
|
245
254
|
lines.push(" local -a ids")
|
|
246
|
-
lines.push(` ids=(\${(f)"$(${PLUGIN_IDS_COMMAND})"})`)
|
|
255
|
+
lines.push(` if (( $+commands[timeout] )); then ids=(\${(f)"$(${PLUGIN_IDS_TIMED})"}); else ids=(\${(f)"$(${PLUGIN_IDS_COMMAND})"}); fi`)
|
|
247
256
|
lines.push(" if (( ${#ids} )); then compadd -a ids; else _directories; fi")
|
|
248
257
|
lines.push("}")
|
|
249
258
|
lines.push("")
|
|
@@ -263,7 +272,7 @@ function fish({ subcommands, categories, tags, pin, version }) {
|
|
|
263
272
|
lines.push("# left out, read at TAB time; nothing when the shell does not answer, and")
|
|
264
273
|
lines.push("# the directories offered beside them stand.")
|
|
265
274
|
lines.push("function __omakit_plugin_ids")
|
|
266
|
-
lines.push(` ${PLUGIN_IDS_COMMAND}`)
|
|
275
|
+
lines.push(` if command -q timeout; ${PLUGIN_IDS_TIMED}; else; ${PLUGIN_IDS_COMMAND}; end`)
|
|
267
276
|
lines.push("end")
|
|
268
277
|
lines.push("")
|
|
269
278
|
for (const sub of subcommands) {
|
|
@@ -23,7 +23,7 @@ import { readFileSync } from "node:fs"
|
|
|
23
23
|
import { join } from "node:path"
|
|
24
24
|
import { pathToFileURL } from "node:url"
|
|
25
25
|
import { parseYaml } from "./yaml.mjs"
|
|
26
|
-
import { requirePin } from "./pin.mjs"
|
|
26
|
+
import { MARKETPLACE_PIN, requirePin } from "./pin.mjs"
|
|
27
27
|
|
|
28
28
|
export const SUBMIT_FORM_PATH = ".github/ISSUE_TEMPLATE/submit-plugin.yml"
|
|
29
29
|
export const OFFICIAL_SUBMISSION_MODULE = "scripts/submission.mjs"
|
|
@@ -147,7 +147,7 @@ export async function submissionContract(options = {}) {
|
|
|
147
147
|
* pick something the form no longer offers.
|
|
148
148
|
*
|
|
149
149
|
* @param {{ repoRoot?: string, pinDir?: string }} [options]
|
|
150
|
-
* @returns {Promise<{ formPath: string, name: string, choice: string }>}
|
|
150
|
+
* @returns {Promise<{ formPath: string, name: string, choice: string, url: string }>}
|
|
151
151
|
*/
|
|
152
152
|
export async function newerCommitChoice(options = {}) {
|
|
153
153
|
const pinDir = options.pinDir || requirePin(options.repoRoot).dir
|
|
@@ -164,7 +164,9 @@ export async function newerCommitChoice(options = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
const name = typeof form.name === "string" ? form.name.trim() : ""
|
|
166
166
|
if (!name) throw new ContractError("form-shape-changed", `${VERIFY_FORM_PATH} has no name`)
|
|
167
|
-
|
|
167
|
+
const url = new URL(`${MARKETPLACE_PIN.repository.replace(/\/$/, "")}/issues/new`)
|
|
168
|
+
url.searchParams.set("template", VERIFY_FORM_PATH.split("/").at(-1))
|
|
169
|
+
return { formPath: VERIFY_FORM_PATH, name, choice, url: url.href }
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
/**
|
|
@@ -27,6 +27,7 @@ export const ACCEPTED = Object.freeze({
|
|
|
27
27
|
upgrade: Object.freeze({ valued: [], flags: ["--dry-run"], positionals: 0 }),
|
|
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
|
+
audit: Object.freeze({ valued: ["--out"], flags: ["--drift", "--json", "--offline"], positionals: 1 }),
|
|
30
31
|
weigh: Object.freeze({ valued: ["--runs", "--window", "--settle", "--out"], flags: ["--all", "--list", "--json", "--yes"], positionals: 1 }),
|
|
31
32
|
})
|
|
32
33
|
|
|
@@ -198,6 +198,12 @@ export const STATUS = Object.freeze({
|
|
|
198
198
|
skipped: Object.freeze({ glyph: DENSITY.ceiling, word: "skip", tint: "unknown" }),
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
+
export const AUDIT_VERDICTS = Object.freeze({
|
|
202
|
+
validated: "AUDITED",
|
|
203
|
+
drift: "DRIFT",
|
|
204
|
+
unavailable: "NOT AUDITED",
|
|
205
|
+
})
|
|
206
|
+
|
|
201
207
|
/** The width of the widest mark, "█ FAIL"; every mark is padded to it so the names beside them align. */
|
|
202
208
|
export const MARK_WIDTH = Math.max(...Object.values(STATUS).map((s) => `${s.glyph} ${s.word}`.length))
|
|
203
209
|
|
|
@@ -416,10 +422,10 @@ export function continuation(value, c) {
|
|
|
416
422
|
* under itself. There is exactly one of these under any failure, and it is the
|
|
417
423
|
* only line in the tool that starts with an arrow, so it can be found by shape.
|
|
418
424
|
*/
|
|
419
|
-
export function action(text, c, { indent = GUTTER } = {}) {
|
|
425
|
+
export function action(text, c, { indent = GUTTER, width: total = COLUMNS } = {}) {
|
|
420
426
|
// Painted after wrapping, and all of it cyan: the whole line is the thing
|
|
421
427
|
// to do, so a backticked word inside it has nothing to stand out from.
|
|
422
|
-
const lines = wrap(text, { indent: indent + 2 })
|
|
428
|
+
const lines = wrap(text, { indent: indent + 2, width: total })
|
|
423
429
|
return lines.map((line, index) => (index === 0
|
|
424
430
|
? `${" ".repeat(indent)}${c("typeable.bold", ARROW)} ${c("typeable", line.trimStart())}`
|
|
425
431
|
: `${" ".repeat(indent + 2)}${c("typeable", line.trimStart())}`))
|
|
@@ -155,7 +155,10 @@ export const COMPLETION_REFRESH_ARGS = Object.freeze(["setup", "--completion"])
|
|
|
155
155
|
|
|
156
156
|
function refreshCompletionWith(root, stream) {
|
|
157
157
|
const entryPoint = join(root, "bin/omakit")
|
|
158
|
-
|
|
158
|
+
// The reason names the relative path: the root is printed above it, and a
|
|
159
|
+
// temporary directory on macOS is long enough to push an absolute one
|
|
160
|
+
// past eighty columns in a sentence (measured in CI: 109).
|
|
161
|
+
if (!existsSync(entryPoint)) return { ran: false, reason: "this install has no bin/omakit under its root" }
|
|
159
162
|
try {
|
|
160
163
|
const out = execFileSync(process.execPath, [entryPoint, ...COMPLETION_REFRESH_ARGS], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
161
164
|
stream.write(out)
|
|
@@ -39,6 +39,17 @@ export const COMMANDS = Object.freeze([
|
|
|
39
39
|
`Read-only, exact commit ${MARKETPLACE_PIN.commit}.`,
|
|
40
40
|
],
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
signature: [
|
|
44
|
+
"omakit audit <plugin-id-or-dir> [--drift] [--json] [--out <file>] [--offline]",
|
|
45
|
+
"omakit audit [--drift] [--json] [--out <file>] [--offline]",
|
|
46
|
+
],
|
|
47
|
+
lines: [
|
|
48
|
+
"Compare every installed third-party plugin's running commit with the",
|
|
49
|
+
"exact commits the marketplace records as validated. Read-only. --drift",
|
|
50
|
+
"shows only rows that are not validated; --offline reads the pin.",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
42
53
|
{
|
|
43
54
|
signature: [
|
|
44
55
|
"omakit submit <target> --category <c> --tags <a,b> [--notes <text>]",
|
package/tools/weigh/confirm.mjs
CHANGED
|
@@ -5,13 +5,30 @@
|
|
|
5
5
|
// typed here is written anywhere.
|
|
6
6
|
|
|
7
7
|
import { createInterface } from "node:readline"
|
|
8
|
-
import { action, colourEnabled, styler } from "../marketplace/style.mjs"
|
|
8
|
+
import { action, colourEnabled, COLUMNS, styler } from "../marketplace/style.mjs"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The question as it is written to the terminal: the arrow line, wrapped
|
|
12
|
+
* at the contract width like every other action, with `[y/N]:` at the end
|
|
13
|
+
* of the last line, and no newline after it, so the cursor waits there.
|
|
14
|
+
* Measured on 0.2.1: the question grew a clause about --runs, wrapped to
|
|
15
|
+
* two lines, and only the first was written, so a person saw "for a" and
|
|
16
|
+
* no prompt, pressed Enter to see the rest, and the empty line was No.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} question
|
|
19
|
+
* @param {(name: string, text: string) => string} c
|
|
20
|
+
* @param {{ width?: number }} [options]
|
|
21
|
+
* @returns {string} every line, joined, ending in `[y/N]: `
|
|
13
22
|
*/
|
|
14
|
-
export function
|
|
23
|
+
export function renderQuestion(question, c, { width = COLUMNS } = {}) {
|
|
24
|
+
return `${action(`${question} [y/N]:`, c, { indent: 0, width }).join("\n")} `
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream, colour?: boolean, question: string, width?: number }} options
|
|
29
|
+
* @returns {Promise<boolean>} true only for `y` or `yes`, in any case; an empty line and the end of stdin are no
|
|
30
|
+
*/
|
|
31
|
+
export function askYes({ input = process.stdin, output = process.stderr, colour = colourEnabled(output), question, width = COLUMNS }) {
|
|
15
32
|
const c = styler(colour)
|
|
16
33
|
return new Promise((resolve) => {
|
|
17
34
|
const rl = createInterface({ input, terminal: false })
|
|
@@ -27,6 +44,6 @@ export function askYes({ input = process.stdin, output = process.stderr, colour
|
|
|
27
44
|
if (!answered) output.write("\n")
|
|
28
45
|
settle(false)
|
|
29
46
|
})
|
|
30
|
-
output.write(
|
|
47
|
+
output.write(renderQuestion(question, c, { width }))
|
|
31
48
|
})
|
|
32
49
|
}
|
package/tools/weigh/list.mjs
CHANGED
|
@@ -43,12 +43,13 @@ export function lastWeighings(stateDir) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* The installed-plugin list joined to the manifest catalog by id. This is the
|
|
47
|
+
* one join used by both `weigh --list` and `audit`, so a source directory has
|
|
48
|
+
* one authority throughout the tool.
|
|
48
49
|
*
|
|
49
50
|
* @param {{ env?: NodeJS.ProcessEnv }} [options]
|
|
50
51
|
*/
|
|
51
|
-
export function
|
|
52
|
+
export function installedPlugins({ env = process.env } = {}) {
|
|
52
53
|
const listed = run("listPlugins", { env })
|
|
53
54
|
if (!listed.ok) throw new WeighError("shell-not-running", "omarchy plugin list did not answer, and the list is what the shell has installed", "omarchy-restart-shell")
|
|
54
55
|
let installed
|
|
@@ -57,6 +58,7 @@ export function listWeighings({ env = process.env } = {}) {
|
|
|
57
58
|
} catch {
|
|
58
59
|
throw new WeighError("shell-unreadable", "omarchy plugin list did not answer with JSON", "omarchy-restart-shell, then run it again.")
|
|
59
60
|
}
|
|
61
|
+
if (!Array.isArray(installed)) throw new WeighError("shell-unreadable", "omarchy plugin list did not answer with a JSON array", "omarchy-restart-shell, then run it again.")
|
|
60
62
|
const catalogRun = run("catalog", { env })
|
|
61
63
|
let catalog = []
|
|
62
64
|
try {
|
|
@@ -65,6 +67,17 @@ export function listWeighings({ env = process.env } = {}) {
|
|
|
65
67
|
catalog = []
|
|
66
68
|
}
|
|
67
69
|
const sourceDirOf = (id) => catalog.find((entry) => entry.id === id)?.sourceDir || null
|
|
70
|
+
return installed.map((plugin) => ({ ...plugin, sourceDir: sourceDirOf(plugin.id) }))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One row per installed plugin, sorted: enabled and not weighed first (by
|
|
75
|
+
* id), then enabled and weighed, oldest weighing first, then disabled.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ env?: NodeJS.ProcessEnv }} [options]
|
|
78
|
+
*/
|
|
79
|
+
export function listWeighings({ env = process.env } = {}) {
|
|
80
|
+
const installed = installedPlugins({ env })
|
|
68
81
|
const stateDir = omakitStateDir("weigh", env)
|
|
69
82
|
const latest = lastWeighings(stateDir)
|
|
70
83
|
const rows = installed.map((plugin) => {
|
|
@@ -76,7 +89,7 @@ export function listWeighings({ env = process.env } = {}) {
|
|
|
76
89
|
kinds,
|
|
77
90
|
enabled: plugin.enabled === true,
|
|
78
91
|
firstParty: plugin.firstParty === true,
|
|
79
|
-
sourceDir:
|
|
92
|
+
sourceDir: plugin.sourceDir,
|
|
80
93
|
weighable: !kinds.includes("bar"),
|
|
81
94
|
lastWeighed: last ? { date: last.date, readme: last.readme, summary: last.summary, document: last.document } : null,
|
|
82
95
|
enable: plugin.enabled === true ? null : `omarchy plugin enable ${plugin.id}`,
|