omakit 0.1.6 → 0.1.8
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 +31 -11
- package/package.json +1 -1
- package/skills/omarchy-plugin-submit/SKILL.md +34 -7
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -1
- package/tests/parity/run.mjs +145 -113
- package/tools/marketplace/README.md +19 -5
- package/tools/marketplace/cli.mjs +36 -19
- package/tools/marketplace/doctor.mjs +68 -25
- package/tools/marketplace/form.mjs +36 -4
- package/tools/marketplace/registry.mjs +76 -7
- package/tools/marketplace/report.mjs +32 -7
- package/tools/marketplace/style.mjs +13 -3
- package/tools/marketplace/submit.mjs +68 -32
- package/tools/marketplace/usage.mjs +12 -8
- package/tools/marketplace/watch.mjs +21 -11
|
@@ -13,33 +13,51 @@
|
|
|
13
13
|
// contract is read from, and the procedure in docs/UPSTREAM_CONTRACT.md requires
|
|
14
14
|
// re-proving transport parity and committing the evidence afterwards. An
|
|
15
15
|
// `upgrade` that quietly advanced the pin would break the one guarantee this
|
|
16
|
-
// tool sells. So doctor reports that the pin is behind and
|
|
16
|
+
// tool sells. So doctor reports that the pin is behind, and that is all it
|
|
17
|
+
// does: the procedure is the maintainer's, it lives in that document and in
|
|
18
|
+
// this comment, and it is never printed, because the person running doctor
|
|
19
|
+
// is a user of a package that does not even ship docs/. What a user can do
|
|
20
|
+
// is run `omakit upgrade`, since a newer omakit may already carry the new
|
|
21
|
+
// pin, and otherwise open an issue naming the paths that moved.
|
|
17
22
|
//
|
|
18
23
|
// That the pin goes stale unnoticed is, of course, exactly the defect class
|
|
19
24
|
// `omakit watch` exists to report. It would be poor form not to apply it here.
|
|
20
25
|
//
|
|
21
26
|
// And "behind" is not the same as "behind in something omakit reads". Measured
|
|
22
27
|
// on 2026-09-13 (docs/MEASUREMENTS.md M7): 4,201 of the marketplace's 4,293
|
|
23
|
-
// commits in 30 days touched only registry.json, which omakit
|
|
28
|
+
// commits in 30 days touched only registry.json, which omakit reads live,
|
|
24
29
|
// while nothing under scripts/ or .github/ISSUE_TEMPLATE/ changed since the
|
|
25
30
|
// pin. A doctor that only said "behind" would say it about every run. So it
|
|
26
31
|
// compares each path in PIN_PATHS between the pin and HEAD, by tree or blob
|
|
27
|
-
// id, and
|
|
32
|
+
// id, and splits the ones that moved by the same list registry.mjs reads
|
|
33
|
+
// live from: the two data files, which a moved HEAD cannot make stale, and
|
|
34
|
+
// everything else, which only a new pin can carry. Measured on 0.1.7: with
|
|
35
|
+
// only registry.json and site/catalog.json moved, doctor said `note` and
|
|
36
|
+
// pointed a user at docs/UPSTREAM_CONTRACT.md, though the pin was behind in
|
|
37
|
+
// nothing the tool uses.
|
|
28
38
|
|
|
29
39
|
import { execFileSync } from "node:child_process"
|
|
30
40
|
import { readFileSync } from "node:fs"
|
|
31
41
|
import { join } from "node:path"
|
|
32
42
|
import { MARKETPLACE_PIN, PIN_PATHS, marketplacePinDir, pinDiskUsage, pinIsSparse, requirePin } from "./pin.mjs"
|
|
43
|
+
import { LIVE_PATHS } from "./registry.mjs"
|
|
33
44
|
import { credential, defaultBranchHead, getJson, UNAUTHENTICATED_LIMIT, GitHubError } from "./github.mjs"
|
|
34
45
|
import { latestOnRegistry, upgradeCommand } from "./upgrade.mjs"
|
|
35
46
|
import { pathHint } from "./path-hint.mjs"
|
|
36
47
|
|
|
48
|
+
/** "git+https://github.com/owner/name.git" in package.json -> "https://github.com/owner/name", or null. */
|
|
49
|
+
function repositoryPage(repository) {
|
|
50
|
+
const url = typeof repository === "string" ? repository : repository?.url
|
|
51
|
+
const match = String(url || "").match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/)
|
|
52
|
+
return match ? `https://github.com/${match[1]}/${match[2]}` : null
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
function tool(repoRoot) {
|
|
38
56
|
try {
|
|
39
57
|
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
|
|
40
|
-
return { name: pkg.name, version: pkg.version, engines: pkg.engines?.node || null }
|
|
58
|
+
return { name: pkg.name, version: pkg.version, engines: pkg.engines?.node || null, repository: repositoryPage(pkg.repository) }
|
|
41
59
|
} catch {
|
|
42
|
-
return { name: "omakit", version: "unknown", engines: null }
|
|
60
|
+
return { name: "omakit", version: "unknown", engines: null, repository: null }
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -102,40 +120,65 @@ export async function changedPinPaths({ pinDir, headCommit, pinCommit = MARKETPL
|
|
|
102
120
|
return changed
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
/** A PIN_PATHS pattern as a path: "/site/catalog.json" -> "site/catalog.json". */
|
|
124
|
+
const asPath = (pattern) => pattern.replace(/^\/|\/$/g, "")
|
|
125
|
+
|
|
126
|
+
/** "a", "a and b", "a, b and c". */
|
|
127
|
+
function list(items) {
|
|
128
|
+
return items.length < 3 ? items.join(" and ") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`
|
|
129
|
+
}
|
|
130
|
+
|
|
105
131
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
132
|
+
* The pin against the marketplace's HEAD, for a user. `changedPaths` is the
|
|
133
|
+
* answer from changedPinPaths(), split by LIVE_PATHS into `readLive`, the
|
|
134
|
+
* data files a moved HEAD cannot make stale because registry.mjs reads them
|
|
135
|
+
* from HEAD, and `pinned`, everything only a new pin can carry. Only the
|
|
136
|
+
* second set is worth a note; the action for it is a user's, not the
|
|
137
|
+
* maintainer's. Null when the comparison was not made, and then the detail
|
|
138
|
+
* says only that HEAD moved. Full commits in the evidence; short ones in
|
|
139
|
+
* the detail. `issues` is where a user reports a pinned path that moved,
|
|
140
|
+
* read from package.json by the caller.
|
|
109
141
|
*/
|
|
110
|
-
export function pinFreshness(identity, head, changedPaths = null) {
|
|
142
|
+
export function pinFreshness(identity, head, changedPaths = null, { issues = null } = {}) {
|
|
111
143
|
const current = head.commit === identity.commit
|
|
112
144
|
const branch = head.branch || "default"
|
|
145
|
+
const changed = current ? [] : changedPaths || []
|
|
146
|
+
const readLive = changed.filter((pattern) => LIVE_PATHS.includes(asPath(pattern)))
|
|
147
|
+
const pinned = changed.filter((pattern) => !LIVE_PATHS.includes(asPath(pattern)))
|
|
148
|
+
const where = `pin ${identity.commit.slice(0, 7)}; marketplace ${branch} at ${head.commit.slice(0, 7)}`
|
|
113
149
|
const moved = changedPaths === null
|
|
114
|
-
? ""
|
|
115
|
-
:
|
|
116
|
-
? `; changed since the pin: ${
|
|
117
|
-
:
|
|
150
|
+
? "; the paths omakit reads were not compared"
|
|
151
|
+
: pinned.length
|
|
152
|
+
? `; changed since the pin: ${pinned.join(", ")}${readLive.length ? ` (${list(readLive.map(asPath))} moved too, and ${readLive.length === 1 ? "that is" : "those are"} read live)` : ""}`
|
|
153
|
+
: readLive.length
|
|
154
|
+
? `; only ${list(readLive.map(asPath))} moved, and ${readLive.length === 1 ? "that is" : "those are"} read live`
|
|
155
|
+
: "; nothing omakit reads moved"
|
|
156
|
+
const stale = !current && (changedPaths === null || pinned.length > 0)
|
|
118
157
|
return {
|
|
119
158
|
id: "pin.freshness",
|
|
120
|
-
state:
|
|
121
|
-
detail: current
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
159
|
+
state: stale ? "advice" : "ok",
|
|
160
|
+
detail: current ? `the pin is the marketplace's current ${branch}-branch HEAD` : `${where}${moved}`,
|
|
161
|
+
action: stale
|
|
162
|
+
? `A newer omakit may already carry the new pin: run \`omakit upgrade\`. If it does not, open an issue${issues ? ` at ${issues}/issues` : ""} naming the paths above.`
|
|
163
|
+
: null,
|
|
125
164
|
evidence: {
|
|
126
165
|
pinCommit: identity.commit,
|
|
127
166
|
marketplaceHead: head.commit,
|
|
128
167
|
branch,
|
|
129
|
-
...(changedPaths === null ? {} : { changedPaths:
|
|
168
|
+
...(changedPaths === null ? {} : { changedPaths: changed, readLive, pinned }),
|
|
130
169
|
},
|
|
131
170
|
}
|
|
132
171
|
}
|
|
133
172
|
|
|
134
173
|
/**
|
|
135
|
-
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null
|
|
136
|
-
*
|
|
174
|
+
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null,
|
|
175
|
+
* resolveHead?: typeof defaultBranchHead, latest?: typeof latestOnRegistry }} options
|
|
176
|
+
* `env` and `npmPrefix` are injectable for tests of the PATH check;
|
|
177
|
+
* `resolveHead` and `latest` for tests of the two checks that read the
|
|
178
|
+
* network, whose defaults are the tool's one HEAD resolver and its one
|
|
179
|
+
* registry read.
|
|
137
180
|
*/
|
|
138
|
-
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix }) {
|
|
181
|
+
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = latestOnRegistry }) {
|
|
139
182
|
// Optional: told what is being read while the network answers. Never
|
|
140
183
|
// affects the result.
|
|
141
184
|
const phase = onPhase || (() => {})
|
|
@@ -185,13 +228,13 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
185
228
|
if (!offline && identity) {
|
|
186
229
|
phase("reading the marketplace's current default-branch HEAD")
|
|
187
230
|
try {
|
|
188
|
-
const head = await
|
|
231
|
+
const head = await resolveHead(MARKETPLACE_PIN.repository)
|
|
189
232
|
let changedPaths = []
|
|
190
233
|
if (head.commit !== identity.commit) {
|
|
191
234
|
phase("comparing each path omakit reads between the pin and HEAD")
|
|
192
235
|
changedPaths = await changedPinPaths({ pinDir: dir, headCommit: head.commit, pinCommit: identity.commit })
|
|
193
236
|
}
|
|
194
|
-
checks.push(pinFreshness(identity, head, changedPaths))
|
|
237
|
+
checks.push(pinFreshness(identity, head, changedPaths, { issues: self.repository }))
|
|
195
238
|
} catch (error) {
|
|
196
239
|
add("pin.freshness", "unknown", `could not read the marketplace's HEAD (${error.code || "error"})`,
|
|
197
240
|
error.code === "network-unavailable" ? "Connect to the network, or pass --offline to skip the two checks that need it." : null,
|
|
@@ -199,7 +242,7 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
199
242
|
}
|
|
200
243
|
|
|
201
244
|
phase("asking the npm registry for the newest published version")
|
|
202
|
-
const latest = await
|
|
245
|
+
const latest = await latestVersion(self.name)
|
|
203
246
|
if (latest) {
|
|
204
247
|
const current = latest === self.version
|
|
205
248
|
add("omakit.latest", current ? "ok" : "advice",
|
|
@@ -27,6 +27,8 @@ import { 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"
|
|
30
|
+
export const VERIFY_FORM_PATH = ".github/ISSUE_TEMPLATE/verify-plugin.yml"
|
|
31
|
+
export const OFFICIAL_VERIFICATION_MODULE = "scripts/plugin-verification-request.mjs"
|
|
30
32
|
|
|
31
33
|
export class ContractError extends Error {
|
|
32
34
|
constructor(code, message) {
|
|
@@ -50,8 +52,8 @@ function loadOfficial(pinDir) {
|
|
|
50
52
|
return import(pathToFileURL(join(pinDir, OFFICIAL_SUBMISSION_MODULE)).href)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function fieldsOf(form) {
|
|
54
|
-
if (!Array.isArray(form?.body)) throw new ContractError("form-unreadable", `${
|
|
55
|
+
function fieldsOf(form, formPath = SUBMIT_FORM_PATH) {
|
|
56
|
+
if (!Array.isArray(form?.body)) throw new ContractError("form-unreadable", `${formPath} has no body`)
|
|
55
57
|
return form.body
|
|
56
58
|
.filter((item) => item && item.type !== "markdown")
|
|
57
59
|
.map((item) => ({
|
|
@@ -65,12 +67,12 @@ function fieldsOf(form) {
|
|
|
65
67
|
}))
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
function only(fields, predicate, what) {
|
|
70
|
+
function only(fields, predicate, what, formPath = SUBMIT_FORM_PATH) {
|
|
69
71
|
const found = fields.filter(predicate)
|
|
70
72
|
if (found.length !== 1) {
|
|
71
73
|
throw new ContractError(
|
|
72
74
|
"form-shape-changed",
|
|
73
|
-
`${
|
|
75
|
+
`${formPath} no longer has exactly one ${what} (found ${found.length}); the pin changed shape and the submission contract must be re-read before anything is generated`,
|
|
74
76
|
)
|
|
75
77
|
}
|
|
76
78
|
return found[0]
|
|
@@ -135,6 +137,36 @@ export async function submissionContract(options = {}) {
|
|
|
135
137
|
return contract
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The route for a plugin that is already listed: the marketplace's other
|
|
142
|
+
* form, and the one choice on it that lists a newer commit. Both read from
|
|
143
|
+
* the pin. The choice text is the option as the form spells it, found by the
|
|
144
|
+
* name the marketplace's own verification module gives that action; the two
|
|
145
|
+
* must agree, the way the submission form and `scripts/submission.mjs` must,
|
|
146
|
+
* because a choice retyped here would drift by a word and send a person to
|
|
147
|
+
* pick something the form no longer offers.
|
|
148
|
+
*
|
|
149
|
+
* @param {{ repoRoot?: string, pinDir?: string }} [options]
|
|
150
|
+
* @returns {Promise<{ formPath: string, name: string, choice: string }>}
|
|
151
|
+
*/
|
|
152
|
+
export async function newerCommitChoice(options = {}) {
|
|
153
|
+
const pinDir = options.pinDir || requirePin(options.repoRoot).dir
|
|
154
|
+
const form = parseYaml(readFileSync(join(pinDir, VERIFY_FORM_PATH), "utf8"))
|
|
155
|
+
const fields = fieldsOf(form, VERIFY_FORM_PATH)
|
|
156
|
+
const action = only(fields, (f) => f.type === "dropdown" && !f.multiple && f.required, "required single-select dropdown (verification action)", VERIFY_FORM_PATH)
|
|
157
|
+
const official = await import(pathToFileURL(join(pinDir, OFFICIAL_VERIFICATION_MODULE)).href)
|
|
158
|
+
const choice = (action.options || []).find((option) => option === official.upstreamUpdateVerificationAction)
|
|
159
|
+
if (!choice) {
|
|
160
|
+
throw new ContractError(
|
|
161
|
+
"form-shape-changed",
|
|
162
|
+
`${VERIFY_FORM_PATH} offers no "${official.upstreamUpdateVerificationAction}" under ${action.label}; the form and ${OFFICIAL_VERIFICATION_MODULE} at the pin disagree, and nobody is sent to a choice that is not there`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
const name = typeof form.name === "string" ? form.name.trim() : ""
|
|
166
|
+
if (!name) throw new ContractError("form-shape-changed", `${VERIFY_FORM_PATH} has no name`)
|
|
167
|
+
return { formPath: VERIFY_FORM_PATH, name, choice }
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
/**
|
|
139
171
|
* The form and the marketplace's own constants must describe the same
|
|
140
172
|
* submission. Anything else means the pin is internally inconsistent, and
|
|
@@ -107,12 +107,32 @@ export function defaultPresentation(presentation, kinds = []) {
|
|
|
107
107
|
return { category, tags }
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* "https://github.com/Owner/Name.git" and "owner/name" both become
|
|
112
|
+
* "owner/name": the path, without surrounding slashes, without a trailing
|
|
113
|
+
* .git, lowercased. Anything else is "", which matches nothing.
|
|
114
|
+
*/
|
|
110
115
|
function repositorySlug(value) {
|
|
116
|
+
const text = String(value ?? "").trim()
|
|
117
|
+
let path
|
|
111
118
|
try {
|
|
112
|
-
|
|
119
|
+
path = new URL(text).pathname
|
|
113
120
|
} catch {
|
|
114
|
-
|
|
121
|
+
path = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(text) ? text : ""
|
|
115
122
|
}
|
|
123
|
+
return path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").toLowerCase()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The one rule for "the same repository": owner and name, compared
|
|
128
|
+
* case-insensitively, with a trailing .git ignored. Measured on 0.1.6: the
|
|
129
|
+
* author's own listed plugin was rendered as a failed check because the
|
|
130
|
+
* listing and the subject were compared as two strings, so this is the rule
|
|
131
|
+
* and its test, not a comparison typed where it is needed.
|
|
132
|
+
*/
|
|
133
|
+
export function sameRepository(a, b) {
|
|
134
|
+
const slug = repositorySlug(a)
|
|
135
|
+
return Boolean(slug) && slug === repositorySlug(b)
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
/**
|
|
@@ -400,11 +420,14 @@ export function figure(n) {
|
|
|
400
420
|
|
|
401
421
|
/**
|
|
402
422
|
* @param {{ id: string, repositoryUrl?: string|null }} subject
|
|
403
|
-
* @returns {{ ok: boolean, problems: Array<{ code: string, detail: string, repository?: string|null, sameRepository?: boolean }> }}
|
|
423
|
+
* @returns {{ ok: boolean, own: boolean, problems: Array<{ code: string, detail: string, repository?: string|null, sameRepository?: boolean }> }}
|
|
404
424
|
* A `plugin-id-listed` problem names the repository that lists the id
|
|
405
425
|
* (`repository`, a slug, or null when the registry does not say) and
|
|
406
426
|
* whether that is the subject's own (`sameRepository`), because the two
|
|
407
|
-
*
|
|
427
|
+
* are different states: another id is needed, or the plugin is listed and
|
|
428
|
+
* there is nothing to submit. `own` is the second state on its own: the id
|
|
429
|
+
* is listed by this repository and no other code fired, so `ok` is false
|
|
430
|
+
* and nothing is wrong.
|
|
408
431
|
*/
|
|
409
432
|
export function checkIdentity(universe, subject) {
|
|
410
433
|
const problems = []
|
|
@@ -425,18 +448,64 @@ export function checkIdentity(universe, subject) {
|
|
|
425
448
|
}
|
|
426
449
|
if (universe.listedIds.has(id)) {
|
|
427
450
|
const repository = universe.listedBy?.get(id) || null
|
|
428
|
-
const sameRepository = Boolean(repository && slug && repository === slug)
|
|
429
451
|
problems.push({
|
|
430
452
|
code: "plugin-id-listed",
|
|
431
453
|
detail: `"${id}" is already listed${repository ? ` by ${repository}` : ""}`,
|
|
432
454
|
repository,
|
|
433
|
-
sameRepository,
|
|
455
|
+
sameRepository: sameRepository(repository, slug),
|
|
434
456
|
})
|
|
435
457
|
}
|
|
436
458
|
if (slug && universe.listedRepositories.has(slug)) {
|
|
437
459
|
problems.push({ code: "submission-repository-listed", detail: `${slug} is already listed`, repository: slug, sameRepository: true })
|
|
438
460
|
}
|
|
439
|
-
|
|
461
|
+
// The subject's own listing: the id is listed, by this repository, and
|
|
462
|
+
// nothing else is wrong with the id. That is not a problem with the
|
|
463
|
+
// submission, it is the absence of one; the caller reports it as a state.
|
|
464
|
+
const own = problems.length > 0
|
|
465
|
+
&& problems.some((problem) => problem.code === "plugin-id-listed" && problem.sameRepository)
|
|
466
|
+
&& problems.every((problem) => problem.sameRepository)
|
|
467
|
+
return { ok: problems.length === 0, own, problems }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* What the marketplace records about one listing, for the plugin that is
|
|
472
|
+
* already listed by its own repository: read from the catalog first, which
|
|
473
|
+
* carries the verification fields, and from the registry source when the
|
|
474
|
+
* catalog has no entry. Every field is null when nothing records it; nothing
|
|
475
|
+
* is guessed.
|
|
476
|
+
*
|
|
477
|
+
* @param {{ registry: object, catalog: object }} live
|
|
478
|
+
* @param {string} id
|
|
479
|
+
* @returns {{ repository: string|null, id: string, addedAt: string|null, verificationCommit: string|null,
|
|
480
|
+
* verificationStatus: string|null, verificationCheckedAt: string|null }|null}
|
|
481
|
+
*/
|
|
482
|
+
export function listingOf(live, id) {
|
|
483
|
+
const text = (value) => (typeof value === "string" && value ? value : null)
|
|
484
|
+
const plugin = (Array.isArray(live.catalog?.plugins) ? live.catalog.plugins : []).find((entry) => entry?.id === id)
|
|
485
|
+
if (plugin) {
|
|
486
|
+
return {
|
|
487
|
+
repository: text(plugin.repo),
|
|
488
|
+
id,
|
|
489
|
+
addedAt: text(plugin.addedAt) || text(plugin.listedAt),
|
|
490
|
+
verificationCommit: text(plugin.verificationCommit)?.toLowerCase() || text(plugin.listingValidatedCommit)?.toLowerCase() || null,
|
|
491
|
+
verificationStatus: text(plugin.verificationStatus),
|
|
492
|
+
verificationCheckedAt: text(plugin.verificationCheckedAt) || text(plugin.listingValidatedAt),
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const sources = Array.isArray(live.registry?.sources) ? live.registry.sources : Object.values(live.registry?.sources || {})
|
|
496
|
+
const source = sources.find((entry) => {
|
|
497
|
+
const ids = entry?.plugins && typeof entry.plugins === "object" && !Array.isArray(entry.plugins) ? Object.keys(entry.plugins) : []
|
|
498
|
+
return ids.includes(id) || (entry?.automatedSecurityBaseline?.pluginIds || []).includes(id)
|
|
499
|
+
})
|
|
500
|
+
if (!source) return null
|
|
501
|
+
return {
|
|
502
|
+
repository: text(source.repo),
|
|
503
|
+
id,
|
|
504
|
+
addedAt: text(source.addedAt) || text(source.listedAt),
|
|
505
|
+
verificationCommit: text(source.listingValidatedCommit)?.toLowerCase() || null,
|
|
506
|
+
verificationStatus: text(source.automatedSecurityBaseline?.outcome),
|
|
507
|
+
verificationCheckedAt: text(source.listingValidatedAt),
|
|
508
|
+
}
|
|
440
509
|
}
|
|
441
510
|
|
|
442
511
|
export { repositorySlug }
|
|
@@ -23,10 +23,11 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const body = " ".repeat(GUTTER)
|
|
25
25
|
|
|
26
|
-
/** The state a check renders in: an advisory failure is a note, not a FAIL,
|
|
26
|
+
/** The state a check renders in: an advisory failure is a note, not a FAIL, a check that waited on another is a question, and a check a flag skipped says so. */
|
|
27
27
|
function stateOf(check) {
|
|
28
28
|
if (check.verdict === "pass") return "pass"
|
|
29
29
|
if (check.verdict === "unknown") return "unknown"
|
|
30
|
+
if (check.verdict === "skipped") return "skipped"
|
|
30
31
|
return check.severity === "advisory" ? "advisory" : "fail"
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -101,10 +102,10 @@ export function renderSubmit(result, { colour = colourEnabled() } = {}) {
|
|
|
101
102
|
out.push(...field("marketplace", `${result.pin.commit}, baseline ${result.pin.baselineVersion}, ${result.pin.enforcementMode}`, c))
|
|
102
103
|
out.push("")
|
|
103
104
|
|
|
104
|
-
// Passing checks run together, and so
|
|
105
|
-
//
|
|
106
|
-
// collapsed where two blocks meet.
|
|
107
|
-
const quiet = (state) => state === "pass" || state === "unknown"
|
|
105
|
+
// Passing checks run together, and so do a check that waited on another
|
|
106
|
+
// and one a flag skipped: all are two quiet lines. Anything else gets a
|
|
107
|
+
// blank line on each side, collapsed where two blocks meet.
|
|
108
|
+
const quiet = (state) => state === "pass" || state === "unknown" || state === "skipped"
|
|
108
109
|
let previous = "pass"
|
|
109
110
|
for (const [index, check] of result.checks.entries()) {
|
|
110
111
|
const state = stateOf(check)
|
|
@@ -122,7 +123,26 @@ export function renderSubmit(result, { colour = colourEnabled() } = {}) {
|
|
|
122
123
|
out.push("")
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
if (
|
|
126
|
+
if (result.outcome === "listed") {
|
|
127
|
+
// The third outcome. Nothing was refused and nothing is wrong: the plugin
|
|
128
|
+
// is listed by this repository, so there is no body and no reproduce
|
|
129
|
+
// line, and the route to a newer commit is the marketplace's other form.
|
|
130
|
+
// Measured on 0.1.6: this state was drawn as FAIL and REFUSED, and closed
|
|
131
|
+
// with "Fix it, then run submit again" under a remedy that said there was
|
|
132
|
+
// nothing to submit.
|
|
133
|
+
const listing = result.listing
|
|
134
|
+
out.push(...verdict("pass", "LISTED", `${listing.id} is already listed by this repository, so the submission form is not the route.`, c))
|
|
135
|
+
out.push("")
|
|
136
|
+
out.push(...field("listed", listing.verificationCommit ? c("name", listing.verificationCommit) : "no verification commit recorded", c, { wrapValue: false }))
|
|
137
|
+
out.push(...continuation(`${listing.verificationStatus || "status unrecorded"}, checked ${listing.verificationCheckedAt || "at an unrecorded time"}, read from the ${listing.source === "head" ? "marketplace's current HEAD" : "pin"}`, c))
|
|
138
|
+
out.push(...field("local HEAD", c("name", listing.localCommit), c, { wrapValue: false }))
|
|
139
|
+
out.push(...continuation(listing.sameCommit ? "the same commit" : "not the listed commit", c))
|
|
140
|
+
out.push("")
|
|
141
|
+
out.push(...wrap(`${listing.sameCommit ? "To get a newer commit listed later" : "To get it listed"}, open the marketplace's "${listing.updateRoute.form}" form and choose "${listing.updateRoute.choice}". \`omakit watch <the submission issue>\` shows which commit is listed now.`, {}, c))
|
|
142
|
+
return out.join("\n")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.outcome === "refused") {
|
|
126
146
|
// Root causes only: a check that waited on a failed one is not listed,
|
|
127
147
|
// and the count says how many waited.
|
|
128
148
|
const failed = result.checks.filter((check) => result.blocking.includes(check.id))
|
|
@@ -149,7 +169,12 @@ export function renderSubmit(result, { colour = colourEnabled() } = {}) {
|
|
|
149
169
|
const validation = result.validationCommit.defaultBranchHead
|
|
150
170
|
? `${result.validationCommit.local} is the ${result.validationCommit.branch || "default"}-branch HEAD, so it is the commit the marketplace will validate.`
|
|
151
171
|
: `${result.validationCommit.local} is the local commit; the marketplace validates the default-branch HEAD it resolves when the issue is opened.`
|
|
152
|
-
|
|
172
|
+
// A skipped check is said on the READY line itself, so the one line an
|
|
173
|
+
// agent quotes does not read as "everything passed" when one check never
|
|
174
|
+
// ran. The count is the whole `skipped` list; today only --offline skips.
|
|
175
|
+
const skipped = (result.skipped || []).length
|
|
176
|
+
const skippedNote = skipped ? ` ${skipped === 1 ? "1 check" : `${skipped} checks`} skipped (--offline).` : ""
|
|
177
|
+
out.push(...verdict("pass", "READY", `every blocking check passed.${skippedNote} ${validation}`, c))
|
|
153
178
|
out.push("")
|
|
154
179
|
out.push(...section("issue title", c))
|
|
155
180
|
out.push(c("heading", result.issue.title))
|
|
@@ -162,6 +162,7 @@ export const DENSITY = Object.freeze({
|
|
|
162
162
|
medium: "▒", // ▒ unknown: neither here nor there
|
|
163
163
|
light: "░", // ░ information: present, weightless
|
|
164
164
|
floor: "▁", // ▁ settled: a pass, a rule, the track the scanner runs on
|
|
165
|
+
ceiling: "▔", // ▔ skipped: the floor's own ink, never landed, because a flag said not to look
|
|
165
166
|
})
|
|
166
167
|
|
|
167
168
|
/** The glyph that starts the one line that fixes things. */
|
|
@@ -170,15 +171,23 @@ export const ARROW = "→"
|
|
|
170
171
|
// --- the status vocabulary ---------------------------------------------------
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
|
-
*
|
|
174
|
+
* Six states, and only six. Each has one glyph, one word and one tint, and
|
|
174
175
|
* every command prints them through `mark()` below. `pass` and `fail` are
|
|
175
176
|
* verdicts; `advisory` is a failure that does not block; `info` is a fact with
|
|
176
|
-
* no verdict; `unknown` is a check that could not be made
|
|
177
|
+
* no verdict; `unknown` is a check that could not be made; `skipped` is a
|
|
178
|
+
* check that was not made because a flag said not to (`--offline`).
|
|
177
179
|
*
|
|
178
180
|
* The glyph is the density ramp read as severity: the more ink, the more it
|
|
179
181
|
* matters. The word is the same thing in letters, and its case carries it too:
|
|
180
182
|
* FAIL is the only upper-case mark, so it is the one the eye lands on in a
|
|
181
183
|
* column of lower-case ones, with or without colour.
|
|
184
|
+
*
|
|
185
|
+
* `skipped` and `unknown` are one family: neither has a verdict, both wear the
|
|
186
|
+
* same tint, and only the glyph and the word tell them apart. Measured on
|
|
187
|
+
* 0.1.6: `--offline` drew the skipped validation-commit check as `▁ ok`, and
|
|
188
|
+
* `--json` said `"verdict": "pass"`, for a comparison that never happened. The
|
|
189
|
+
* skipped mark is the floor's ink at the ceiling: the same weight as a pass,
|
|
190
|
+
* visibly not landed on one, on a theme that shows no hue.
|
|
182
191
|
*/
|
|
183
192
|
export const STATUS = Object.freeze({
|
|
184
193
|
pass: Object.freeze({ glyph: DENSITY.floor, word: "ok", tint: "pass" }),
|
|
@@ -186,6 +195,7 @@ export const STATUS = Object.freeze({
|
|
|
186
195
|
advisory: Object.freeze({ glyph: DENSITY.dark, word: "note", tint: "advisory" }),
|
|
187
196
|
info: Object.freeze({ glyph: DENSITY.light, word: "info", tint: "info" }),
|
|
188
197
|
unknown: Object.freeze({ glyph: DENSITY.medium, word: "?", tint: "unknown" }),
|
|
198
|
+
skipped: Object.freeze({ glyph: DENSITY.ceiling, word: "skip", tint: "unknown" }),
|
|
189
199
|
})
|
|
190
200
|
|
|
191
201
|
/** The width of the widest mark, "█ FAIL"; every mark is padded to it so the names beside them align. */
|
|
@@ -360,7 +370,7 @@ export function wrap(text, { indent = 0, width: total = COLUMNS, first = indent
|
|
|
360
370
|
* after it starts at GUTTER. This is the only place a status is turned into
|
|
361
371
|
* characters.
|
|
362
372
|
*
|
|
363
|
-
* @param {"pass"|"fail"|"advisory"|"info"|"unknown"} state
|
|
373
|
+
* @param {"pass"|"fail"|"advisory"|"info"|"unknown"|"skipped"} state
|
|
364
374
|
* @param {(name: string, text: string) => string} c
|
|
365
375
|
*/
|
|
366
376
|
export function mark(state, c) {
|