omakit 0.3.0 → 0.4.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 +11 -1
- package/package.json +1 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +6 -0
- package/tools/marketplace/README.md +3 -0
- package/tools/marketplace/ask.mjs +33 -3
- package/tools/marketplace/cli.mjs +37 -7
- package/tools/marketplace/doctor.mjs +12 -6
- package/tools/marketplace/github.mjs +30 -9
- package/tools/marketplace/options.mjs +1 -1
- package/tools/marketplace/report.mjs +45 -0
- package/tools/marketplace/update-check.mjs +48 -0
- package/tools/marketplace/upgrade.mjs +36 -4
- package/tools/marketplace/usage.mjs +10 -1
- package/tools/marketplace/watch.mjs +71 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ See [docs/INSTALL.md](docs/INSTALL.md) for the clone route, PATH, requirements a
|
|
|
25
25
|
| --- | --- |
|
|
26
26
|
| [`omakit setup`](docs/COMMANDS.md) | The environment, the pin, tab completion, and what to try first. |
|
|
27
27
|
| [`omakit submit <plugin-repo>`](docs/SUBMIT.md) | Every check, the issue title and body; asks for a category and tags at a terminal. |
|
|
28
|
-
| [`omakit watch <issue-url
|
|
28
|
+
| [`omakit watch [<issue-url>]`](docs/VALIDATION_WATCH.md) | Pick your marketplace issues, list them, or check all with `--all`. |
|
|
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
31
|
| [`omakit audit [<plugin>]`](docs/AUDIT.md) | Installed third-party commits against the exact commits the marketplace validated. |
|
|
@@ -35,6 +35,11 @@ See [docs/INSTALL.md](docs/INSTALL.md) for the clone route, PATH, requirements a
|
|
|
35
35
|
| [`omakit upgrade`](docs/COMMANDS.md) | Updates omakit through its own installer: npm, or a fast-forward. |
|
|
36
36
|
| [`omakit help --agent`](docs/COMMANDS.md) | The operating instructions, for the agent running this. |
|
|
37
37
|
|
|
38
|
+
Normal terminal use also checks for a newer npm release at most once daily
|
|
39
|
+
and shows the upgrade command. It installs nothing automatically; scripts
|
|
40
|
+
and JSON stay quiet. `DISABLE_UPDATE_NOTIFIER=1` disables the notice, and
|
|
41
|
+
`omakit doctor` checks explicitly. [Update behaviour](docs/INSTALL.md#updating).
|
|
42
|
+
|
|
38
43
|
### `submit`
|
|
39
44
|
|
|
40
45
|
```bash
|
|
@@ -51,10 +56,15 @@ Read more: [docs/SUBMIT.md](docs/SUBMIT.md).
|
|
|
51
56
|
|
|
52
57
|
```bash
|
|
53
58
|
omakit watch <submission-issue-url>
|
|
59
|
+
omakit watch --all
|
|
60
|
+
omakit watch --list
|
|
61
|
+
omakit watch # choose one or several issues at a terminal
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
It decides whether the marketplace validated the plugin's current commit; [73% of parked submissions have a HEAD the marketplace never saw](docs/MEASUREMENTS.md).
|
|
57
65
|
|
|
66
|
+
Account-wide discovery uses your signed-in `gh` account and reads your open marketplace issues. `--user <login>` reads another public account. Batch output includes baseline results, labels and the latest human discussion; `current` compares commits and does not imply approval or publication. Each command takes one snapshot and posts nothing.
|
|
67
|
+
|
|
58
68
|

|
|
59
69
|
|
|
60
70
|
Read more: [docs/VALIDATION_WATCH.md](docs/VALIDATION_WATCH.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -27,6 +27,8 @@ push came after a review comment had also commented: engaged, and stuck.
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
omakit watch https://github.com/omacom/omarchy-plugin-marketplace/issues/<number>
|
|
30
|
+
omakit watch --all --json
|
|
31
|
+
omakit watch --list --json
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
If `omakit` is not installed: `npm install --global omakit` (Omarchy ships Node
|
|
@@ -35,6 +37,10 @@ the install, `"$(npm prefix --global)/bin/omakit" setup` prints the one line
|
|
|
35
37
|
that puts npm's bin on PATH. `omakit upgrade` keeps it current through npm.
|
|
36
38
|
Add `--json` for a machine-readable result.
|
|
37
39
|
|
|
40
|
+
Use `--all --json` to discover and check every open marketplace issue authored by the signed-in `gh` account. `--list --json` discovers issue URLs without reading every plugin; use an individual URL for focused follow-up. `--user <login>` selects an explicit public author. A bare `omakit watch` opens a numbered picker only at a terminal; an agent must pass a URL, `--all` or `--list` and never wait on keyboard input.
|
|
41
|
+
|
|
42
|
+
Inspect every batch row, including read errors and unknown results; a successful row cannot stand in for a failed one. `current` only says the commit matches HEAD. Read the baseline outcome, labels and latest discussion to understand outstanding work; never equate it with approval, publication or restored catalog verification. A run is a single read-only snapshot, not a background monitor.
|
|
43
|
+
|
|
38
44
|
Read-only. It does not comment, label, or edit anything, and it cannot. It reads
|
|
39
45
|
the default branch through the REST API when a credential is available, taking
|
|
40
46
|
it from the operator's `gh` login (which itself honours `GH_TOKEN` and
|
|
@@ -26,6 +26,7 @@ local commit through the transport seam the marketplace tests itself
|
|
|
26
26
|
| `github.mjs` | Read-only GitHub access. GET only. The credential is your `gh` login, read through one frozen `gh auth token` call, and is never written anywhere. |
|
|
27
27
|
| `style.mjs` | The visual system, defined once: the palette, the status vocabulary, the block ramp, the columns, the motion budgets, and the composition helpers every command draws with. Six states: `▁ ok`, `█ FAIL`, `▓ note`, `░ info`, `▒ ?` for a check that could not be made, and `▔ skip` for a check a flag said not to make, the floor's ink at the ceiling so it is never read as a pass. `docs/TUI.md` explains it. |
|
|
28
28
|
| `report.mjs` | Text rendering of submit, watch, doctor and verify for the agent that runs this tool, and the person reading over its shoulder. |
|
|
29
|
+
| `update-check.mjs` | A passive npm-release notice during normal terminal use, throttled to one check per day with a one-second network budget. Stores only installed/latest version metadata and time under XDG_STATE_HOME/omakit. Offline, JSON, pipes, help and CI skip it; DISABLE_UPDATE_NOTIFIER disables it. Version precedence and the registry GET are shared with doctor/upgrade. |
|
|
29
30
|
| `path-hint.mjs` | Is `omakit` reachable as a bare command, and if not, the one line that makes it so for the install that is here: a symlink for a clone, the npm prefix's `bin` on PATH for a package, said for the shell in `$SHELL`. `setup` and `doctor` print it; nothing writes an rc file. |
|
|
30
31
|
| `usage.mjs` | The help text, as data. |
|
|
31
32
|
| `options.mjs` | Every option every command accepts, in one table, and the parser that reads a command line against it before anything runs; `tests/unit/options.test.mjs` holds the help signatures, and through them the completion scripts, to the table. |
|
|
@@ -99,3 +100,5 @@ online, the subject's default-branch HEAD and the marketplace's current
|
|
|
99
100
|
registry, and `--offline` turns both off; `verify` needs no network at all
|
|
100
101
|
beyond fetching a reviewer-mode subject, and `tests/parity/offline.mjs` proves
|
|
101
102
|
it.
|
|
103
|
+
|
|
104
|
+
`watch --list` discovers the signed-in account's open marketplace issues through `github.mjs`; `--user` bypasses the account lookup. `watch --all` runs the existing single-issue watch with four workers and shared repository HEAD promises, preserving each read failure. A bare terminal invocation uses `ask.mjs` to choose one or several issues. `report.mjs` composes the list and batch views; all options remain in `options.mjs` and `usage.mjs`, which also generate completion. The batch JSON retains complete single-issue reports; `current` compares commits and never substitutes for review or publication.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { createInterface } from "node:readline"
|
|
18
18
|
import { colourEnabled, STEP, action, styler, wrap } from "./style.mjs"
|
|
19
19
|
import { resolveCategory, resolveTags } from "./form.mjs"
|
|
20
|
+
import { watchIssueTitle } from "./watch.mjs"
|
|
20
21
|
|
|
21
22
|
export class AskError extends Error {
|
|
22
23
|
constructor(message) {
|
|
@@ -49,11 +50,13 @@ function reader(input) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/** Ask one question until an answer resolves; `parse` returns { ok, value } or { ok: false, reason }. */
|
|
52
|
-
async function question(lines, output, c, { name, heading, options, defaultIndexes, parse }) {
|
|
53
|
+
async function question(lines, output, c, { name, heading, options, defaultIndexes, parse, wrapOptions = false, eofRemedy }) {
|
|
53
54
|
const step = " ".repeat(STEP)
|
|
54
55
|
output.write(`${wrap(heading, {}, c).join("\n")}\n`)
|
|
55
56
|
for (const [index, option] of options.entries()) {
|
|
56
|
-
output.write(
|
|
57
|
+
output.write(wrapOptions
|
|
58
|
+
? `${wrap(`${String(index + 1).padStart(2)} ${option}`, { indent: STEP }, c).join("\n")}\n`
|
|
59
|
+
: `${step}${c("typeable", String(index + 1).padStart(2))} ${option}\n`)
|
|
57
60
|
}
|
|
58
61
|
const fallback = defaultIndexes.length ? defaultIndexes.map((index) => index + 1).join(",") : null
|
|
59
62
|
const prompt = `${action(fallback ? `${name} [${fallback}]:` : `${name}:`, c, { indent: 0 })[0]} `
|
|
@@ -62,7 +65,7 @@ async function question(lines, output, c, { name, heading, options, defaultIndex
|
|
|
62
65
|
const raw = await lines.next()
|
|
63
66
|
if (raw === null) {
|
|
64
67
|
output.write("\n")
|
|
65
|
-
throw new AskError(`stdin ended before the ${name} was answered; pass --${name} on the command line`)
|
|
68
|
+
throw new AskError(`stdin ended before the ${name} was answered; ${eofRemedy || `pass --${name} on the command line`}`)
|
|
66
69
|
}
|
|
67
70
|
const text = raw.trim() || (fallback ?? "")
|
|
68
71
|
const parsed = parse(text)
|
|
@@ -138,3 +141,30 @@ export async function askChoices({ contract, defaults, missing, input = process.
|
|
|
138
141
|
}
|
|
139
142
|
return answers
|
|
140
143
|
}
|
|
144
|
+
|
|
145
|
+
/** Select one or several discovered issues; nothing is written or opened. */
|
|
146
|
+
export async function askWatchIssues({ issues, input = process.stdin, output = process.stderr, colour = colourEnabled(output) }) {
|
|
147
|
+
const c = styler(colour)
|
|
148
|
+
const lines = reader(input)
|
|
149
|
+
try {
|
|
150
|
+
return await question(lines, output, c, {
|
|
151
|
+
name: "issues",
|
|
152
|
+
heading: "Choose list numbers, comma-separated; all checks every issue, q cancels.",
|
|
153
|
+
options: issues.map((issue) => `#${issue.number} ${watchIssueTitle(issue.title)}`),
|
|
154
|
+
defaultIndexes: [],
|
|
155
|
+
wrapOptions: true,
|
|
156
|
+
eofRemedy: "pass an issue URL, --all or --list on the command line",
|
|
157
|
+
parse: (text) => {
|
|
158
|
+
if (text.toLowerCase() === "q") return { ok: true, value: [] }
|
|
159
|
+
if (text.toLowerCase() === "all") return { ok: true, value: issues }
|
|
160
|
+
const indexes = text.split(",").map((value) => value.trim())
|
|
161
|
+
if (indexes.some((value) => !/^\d+$/.test(value) || Number(value) < 1 || Number(value) > issues.length)) {
|
|
162
|
+
return { ok: false, reason: `Answer with list numbers from 1 to ${issues.length}, all, or q.` }
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, value: [...new Set(indexes.map(Number))].map((index) => issues[index - 1]) }
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
} finally {
|
|
168
|
+
lines.close()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -21,15 +21,16 @@ import { ensurePin, MARKETPLACE_PIN, requirePin } from "./pin.mjs"
|
|
|
21
21
|
import { marketplaceBaselineSection } from "./verify.mjs"
|
|
22
22
|
import { resolveSubject, SubjectError } from "../subject/resolve.mjs"
|
|
23
23
|
import { submitPreflight } from "./submit.mjs"
|
|
24
|
-
import { askChoices } from "./ask.mjs"
|
|
25
|
-
import { validationWatch } from "./watch.mjs"
|
|
26
|
-
import { renderSubmit, renderWatch, renderDoctor, renderVerify } from "./report.mjs"
|
|
24
|
+
import { askChoices, askWatchIssues } from "./ask.mjs"
|
|
25
|
+
import { validationWatch, discoverWatchIssues, validationWatchAll } from "./watch.mjs"
|
|
26
|
+
import { renderSubmit, renderWatch, renderWatchList, renderWatchAll, renderDoctor, renderVerify } from "./report.mjs"
|
|
27
27
|
import { consequence } from "./preflight.mjs"
|
|
28
28
|
import { doctor } from "./doctor.mjs"
|
|
29
29
|
import { completionStep, setup } from "./setup.mjs"
|
|
30
30
|
import { staleCompletionNotice } from "./completion-check.mjs"
|
|
31
31
|
import { ACCEPTED, acceptedWords, checkArgs } from "./options.mjs"
|
|
32
32
|
import { upgrade } from "./upgrade.mjs"
|
|
33
|
+
import { updateCheckEnabled, updateNotice } from "./update-check.mjs"
|
|
33
34
|
import { progress } from "./progress.mjs"
|
|
34
35
|
import { banner, bannerEnabled } from "./banner.mjs"
|
|
35
36
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
@@ -62,6 +63,7 @@ const REMEDY = Object.freeze({
|
|
|
62
63
|
"github-unavailable": "Wait for GitHub, then run it again. `gh auth login` raises the rate limit if that is what ran out.",
|
|
63
64
|
"not-found": "Check the issue URL: it has to be an existing issue on the marketplace repository.",
|
|
64
65
|
"head-unreadable": "Check that the plugin repository is public and its URL is right.",
|
|
66
|
+
"login-required": "gh auth login",
|
|
65
67
|
"not-confirmed": "Run it again and answer y, or pass --yes when the person whose shell it is has agreed.",
|
|
66
68
|
"interrupted": "shell.json was restored; run it again when the desktop is yours to restart.",
|
|
67
69
|
})
|
|
@@ -180,18 +182,41 @@ async function cmdSubmit(args) {
|
|
|
180
182
|
|
|
181
183
|
async function cmdWatch(args) {
|
|
182
184
|
const issueUrl = positionals(args)[0]
|
|
183
|
-
|
|
185
|
+
const all = args.includes("--all")
|
|
186
|
+
const list = args.includes("--list")
|
|
187
|
+
const user = option(args, "--user")
|
|
188
|
+
const interactive = !args.includes("--json") && !option(args, "--out") && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)
|
|
189
|
+
if ((all && list) || (issueUrl && (all || list || user !== undefined))) {
|
|
190
|
+
fail("usage", "Use an issue URL, --all, or --list; --user belongs to account-wide discovery.", 2)
|
|
191
|
+
}
|
|
192
|
+
if (!issueUrl && !all && !list && !interactive) {
|
|
193
|
+
fail("usage", "watch needs an issue or a mode: `omakit watch <issue-url>`, `omakit watch --all`, or `omakit watch --list`. A terminal can pick issues with `omakit watch`.", 2)
|
|
194
|
+
}
|
|
184
195
|
const spinner = spinnerFor(args)
|
|
185
196
|
let result
|
|
186
197
|
try {
|
|
187
|
-
|
|
198
|
+
if (issueUrl) {
|
|
199
|
+
result = await validationWatch({ repoRoot: ROOT, issueUrl, onPhase: spinner.phase })
|
|
200
|
+
} else {
|
|
201
|
+
const discovery = await discoverWatchIssues({ user, onPhase: spinner.phase })
|
|
202
|
+
if (list) {
|
|
203
|
+
result = discovery
|
|
204
|
+
} else {
|
|
205
|
+
if (!all && discovery.issues.length) {
|
|
206
|
+
spinner.done()
|
|
207
|
+
discovery.issues = await askWatchIssues({ issues: discovery.issues })
|
|
208
|
+
}
|
|
209
|
+
result = await validationWatchAll({ repoRoot: ROOT, discovery, onPhase: spinner.phase })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
188
212
|
} catch (error) {
|
|
189
213
|
spinner.done()
|
|
190
214
|
failFrom(error)
|
|
191
215
|
}
|
|
192
216
|
spinner.done()
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
const render = result.mode === "list" ? renderWatchList : result.mode === "all" ? renderWatchAll : renderWatch
|
|
218
|
+
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : render(result))
|
|
219
|
+
process.exit(result.verdict?.state === "unknown" || result.summary?.unknown > 0 ? 2 : 0)
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
async function cmdFrontDoor() {
|
|
@@ -460,6 +485,11 @@ const [command, ...rest] = process.argv.slice(2)
|
|
|
460
485
|
// completion script names another omakit, so `omakit we<TAB>` may not know
|
|
461
486
|
// `weigh`. One stat and one short read; never under a pipe, whose stderr
|
|
462
487
|
// stays empty on success, and never for setup, which is the fix.
|
|
488
|
+
if (updateCheckEnabled({ command, args: rest, stdinTTY: process.stdin.isTTY, stdoutTTY: process.stdout.isTTY, stderrTTY: process.stderr.isTTY })) {
|
|
489
|
+
const notice = await updateNotice({ repoRoot: ROOT, version: VERSION })
|
|
490
|
+
if (notice) process.stderr.write(`${wrap(notice, {}, styler(colourEnabled(process.stderr))).join("\n")}\n`)
|
|
491
|
+
}
|
|
492
|
+
|
|
463
493
|
if (command !== "setup" && process.stderr.isTTY) {
|
|
464
494
|
const notice = staleCompletionNotice({ version: VERSION })
|
|
465
495
|
if (notice) process.stderr.write(`${styler(colourEnabled(process.stderr))("label", notice)}\n`)
|
|
@@ -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 { NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
45
|
+
import { compareVersions, NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
46
46
|
import { pathHint } from "./path-hint.mjs"
|
|
47
47
|
import { completionStatus } from "./completion-check.mjs"
|
|
48
48
|
|
|
@@ -205,11 +205,17 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
205
205
|
phase("asking the npm registry for the newest published version")
|
|
206
206
|
const published = await latestVersion(self.name)
|
|
207
207
|
if (published.version) {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
const comparison = compareVersions(published.version, self.version)
|
|
209
|
+
if (comparison === null) {
|
|
210
|
+
versionCheck("unknown", `${self.version}; the installed or published version is invalid`)
|
|
211
|
+
} else {
|
|
212
|
+
versionCheck(comparison > 0 ? "advice" : "ok",
|
|
213
|
+
comparison === 0 ? `${self.version}, the newest published version`
|
|
214
|
+
: comparison < 0 ? `${self.version}; ahead of the newest published version ${published.version}`
|
|
215
|
+
: `${self.version}; ${published.version} is published`,
|
|
216
|
+
comparison > 0 ? `run \`${upgradeCommand(repoRoot, self.name)}\`` : null,
|
|
217
|
+
published.version, NPM_REGISTRY)
|
|
218
|
+
}
|
|
213
219
|
} else {
|
|
214
220
|
versionCheck("unknown", `${self.version}; could not read the npm registry (${published.error?.code || "error"})`)
|
|
215
221
|
}
|
|
@@ -109,7 +109,7 @@ export function token() {
|
|
|
109
109
|
/** The one host the borrowed gh credential may be sent to. */
|
|
110
110
|
export const CREDENTIAL_HOST = "api.github.com"
|
|
111
111
|
|
|
112
|
-
async function get(url, { accept } = {}) {
|
|
112
|
+
async function get(url, { accept, signal } = {}) {
|
|
113
113
|
const { host } = new URL(url)
|
|
114
114
|
// The credential is GitHub's and goes to GitHub's API and nowhere else.
|
|
115
115
|
// Measured before this held: `omakit upgrade` and `doctor` sent the gh
|
|
@@ -125,7 +125,7 @@ async function get(url, { accept } = {}) {
|
|
|
125
125
|
if (auth) headers.authorization = `Bearer ${auth}`
|
|
126
126
|
let response
|
|
127
127
|
try {
|
|
128
|
-
response = await fetch(url, { method: "GET", headers, redirect: "follow" })
|
|
128
|
+
response = await fetch(url, { method: "GET", headers, redirect: "follow", ...(signal ? { signal } : {}) })
|
|
129
129
|
} catch (error) {
|
|
130
130
|
// Node reports every transport failure as "fetch failed" with the real
|
|
131
131
|
// reason in `cause`. A person needs the reason, and the CLI keys its
|
|
@@ -150,8 +150,8 @@ async function get(url, { accept } = {}) {
|
|
|
150
150
|
return response
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
export async function getJson(url) {
|
|
154
|
-
return (await get(url)).json()
|
|
153
|
+
export async function getJson(url, options) {
|
|
154
|
+
return (await get(url, options)).json()
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export async function getText(url, accept) {
|
|
@@ -171,17 +171,38 @@ export async function issue(owner, repository, number) {
|
|
|
171
171
|
return getJson(`https://api.github.com/repos/${owner}/${repository}/issues/${number}`)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
/** Resolve the account whose credential gh lent us; never persist it. */
|
|
175
|
+
export async function authenticatedUser() {
|
|
176
|
+
if (!token()) throw new GitHubError("login-required", "Account-wide watch needs your GitHub login. Run `gh auth login`, or pass --user <login> to read a public account.")
|
|
177
|
+
const user = await getJson("https://api.github.com/user")
|
|
178
|
+
if (!user?.login) throw new GitHubError("github-unavailable", "GitHub did not return the signed-in account's login")
|
|
179
|
+
return user.login
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Repository issues by their creator. PRs are excluded; pagination never silently truncates. */
|
|
183
|
+
export async function repositoryIssues(owner, repository, creator, { readJson = getJson, maxPages = 100 } = {}) {
|
|
184
|
+
const all = []
|
|
185
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
186
|
+
const query = new URLSearchParams({ creator, state: "open", sort: "updated", direction: "desc", per_page: "100", page: String(page) })
|
|
187
|
+
const batch = await readJson(`https://api.github.com/repos/${owner}/${repository}/issues?${query}`)
|
|
188
|
+
if (!Array.isArray(batch)) throw new GitHubError("github-unavailable", "GitHub did not return an issue list")
|
|
189
|
+
all.push(...batch.filter((item) => !item.pull_request))
|
|
190
|
+
if (batch.length < 100) return all
|
|
191
|
+
}
|
|
192
|
+
throw new GitHubError("issue-list-incomplete", `The issue list exceeded ${maxPages} pages; no complete account-wide result is available`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function issueComments(owner, repository, number, maxPages = 10, readJson = getJson) {
|
|
175
196
|
const all = []
|
|
176
197
|
for (let page = 1; page <= maxPages; page += 1) {
|
|
177
|
-
const batch = await
|
|
198
|
+
const batch = await readJson(
|
|
178
199
|
`https://api.github.com/repos/${owner}/${repository}/issues/${number}/comments?per_page=100&page=${page}`,
|
|
179
200
|
)
|
|
180
|
-
if (!Array.isArray(batch)
|
|
201
|
+
if (!Array.isArray(batch)) throw new GitHubError("github-unavailable", "GitHub did not return issue comments")
|
|
181
202
|
all.push(...batch)
|
|
182
|
-
if (batch.length < 100)
|
|
203
|
+
if (batch.length < 100) return all
|
|
183
204
|
}
|
|
184
|
-
|
|
205
|
+
throw new GitHubError("comments-incomplete", `Issue #${number} exceeded ${maxPages} comment pages; its latest baseline cannot be determined`)
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
/**
|
|
@@ -21,7 +21,7 @@ export const ACCEPTED = Object.freeze({
|
|
|
21
21
|
setup: Object.freeze({ valued: [], flags: ["--yes", "--completion"], positionals: 0 }),
|
|
22
22
|
pin: Object.freeze({ valued: [], flags: [], positionals: 0 }),
|
|
23
23
|
submit: Object.freeze({ valued: ["--category", "--tags", "--notes", "--suggest-tag", "--name", "--out"], flags: ["--offline", "--allow-dirty", "--json"], positionals: 1 }),
|
|
24
|
-
watch: Object.freeze({ valued: ["--out"], flags: ["--json"], positionals: 1 }),
|
|
24
|
+
watch: Object.freeze({ valued: ["--out", "--user"], flags: ["--json", "--all", "--list"], positionals: 1 }),
|
|
25
25
|
verify: Object.freeze({ valued: ["--out"], flags: ["--allow-dirty", "--json"], positionals: 1 }),
|
|
26
26
|
help: Object.freeze({ valued: [], flags: ["--agent"], positionals: 0 }),
|
|
27
27
|
upgrade: Object.freeze({ valued: [], flags: ["--dry-run"], positionals: 0 }),
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
action, colourEnabled, COLUMNS, continuation, field, GUTTER, labelled, mark, section, STEP, styler, verdict, width, wrap,
|
|
22
22
|
} from "./style.mjs"
|
|
23
23
|
import { withHomeAbbreviated } from "./paths.mjs"
|
|
24
|
+
import { watchIssueTitle } from "./watch.mjs"
|
|
24
25
|
|
|
25
26
|
const body = " ".repeat(GUTTER)
|
|
26
27
|
|
|
@@ -244,6 +245,50 @@ export function renderWatch(result, { colour = colourEnabled() } = {}) {
|
|
|
244
245
|
return out.join("\n")
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
/** Discovery is cheap: issue metadata and repeatable commands, no plugin HEAD reads. */
|
|
249
|
+
export function renderWatchList(result, { colour = colourEnabled() } = {}) {
|
|
250
|
+
const c = styler(colour)
|
|
251
|
+
const out = [...field("account", result.account, c), ...field("issues", `${result.issues.length} open marketplace issue(s)`, c), ""]
|
|
252
|
+
for (const issue of result.issues) {
|
|
253
|
+
out.push(...wrap(`#${issue.number} ${watchIssueTitle(issue.title)}`, {}, c))
|
|
254
|
+
out.push(...field("state", `${issue.state}${issue.labels.length ? `; labels ${issue.labels.map(watchIssueTitle).join(", ")}` : ""}`, c))
|
|
255
|
+
out.push(...action(`omakit watch ${issue.url}`, c, { indent: 0 }), "")
|
|
256
|
+
}
|
|
257
|
+
if (!result.issues.length) out.push(...wrap("No open marketplace issues found for this account.", {}, c))
|
|
258
|
+
else out.push(...action(`omakit watch --all --user ${result.account}`, c, { indent: 0 }))
|
|
259
|
+
return out.join("\n")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Compact batch report; exact commits and full discussion remain in JSON. */
|
|
263
|
+
export function renderWatchAll(result, { colour = colourEnabled() } = {}) {
|
|
264
|
+
const c = styler(colour)
|
|
265
|
+
const out = [...field("account", result.account, c), ...field("issues", `${result.summary.total} checked; ${result.summary.current} current, ${result.summary.stale} stale, ${result.summary.unknown} unknown`, c), ""]
|
|
266
|
+
for (const row of result.issues) {
|
|
267
|
+
const state = row.report?.verdict.state || "unknown"
|
|
268
|
+
const style = { current: "pass", stale: "fail", unknown: "unknown" }[state]
|
|
269
|
+
out.push(...verdict(style, state.toUpperCase(), `#${row.issue.number} ${watchIssueTitle(row.report?.read.title || row.issue.title)}`, c))
|
|
270
|
+
out.push(...field("issue", row.issue.url, c, { wrapValue: false }))
|
|
271
|
+
if (row.error) {
|
|
272
|
+
out.push(...field("read error", watchIssueTitle(`${row.error.code}: ${row.error.message}`), c))
|
|
273
|
+
} else {
|
|
274
|
+
const report = row.report
|
|
275
|
+
out.push(...field("baseline", report.validated ? `${report.validated.outcome}; ${report.validated.findings.length} finding(s); capabilities ${report.validated.capabilities.join(", ") || "none"}` : "no complete baseline", c))
|
|
276
|
+
out.push(...field("state", `${report.read.state}; labels ${report.read.labels.map(watchIssueTitle).join(", ") || "none"}`, c))
|
|
277
|
+
if (state !== "current") out.push(...field("comparison", report.verdict.summary, c))
|
|
278
|
+
if (report.discussion) {
|
|
279
|
+
const text = watchIssueTitle(report.discussion.body).replace(/\s+/g, " ").trim()
|
|
280
|
+
out.push(...field("discussion", text.length > 240 ? `${text.slice(0, 240)}…` : text, c))
|
|
281
|
+
if (report.discussion.url) out.push(...field("source", report.discussion.url, c, { wrapValue: false }))
|
|
282
|
+
}
|
|
283
|
+
if (report.verdict.action) out.push(...action(report.verdict.action, c, { indent: 0 }))
|
|
284
|
+
}
|
|
285
|
+
out.push("")
|
|
286
|
+
}
|
|
287
|
+
if (!result.issues.length) out.push(...wrap("No issues selected for checking.", {}, c))
|
|
288
|
+
out.push(...wrap("Current means the validated commit matches HEAD. It does not mean review, approval or publication is complete. Read-only; this run posts nothing.", {}, c))
|
|
289
|
+
return out.join("\n")
|
|
290
|
+
}
|
|
291
|
+
|
|
247
292
|
/** A list of paths at fault, the way a failing check prints them: a tinted dash, the path in the placeholder tint. */
|
|
248
293
|
function pathLines(paths, c, dash = "fail") {
|
|
249
294
|
return paths.flatMap((path) => wrap(`- ${path}`, { indent: GUTTER + STEP, first: GUTTER })
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A passive npm-version notice. Only metadata and a throttle stamp are stored;
|
|
2
|
+
// nothing is installed or executed. Explicit doctor checks bypass this throttle.
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { homedir } from "node:os"
|
|
6
|
+
import { compareVersions, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
7
|
+
|
|
8
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
9
|
+
export const UPDATE_RETRY_INTERVAL_MS = 60 * 60 * 1000
|
|
10
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 1000
|
|
11
|
+
|
|
12
|
+
export function updateCheckEnabled({ command, args = [], stdinTTY, stdoutTTY, stderrTTY, env = process.env }) {
|
|
13
|
+
return Boolean(stdinTTY && stdoutTTY && stderrTTY)
|
|
14
|
+
&& !env.DISABLE_UPDATE_NOTIFIER && !env.CI
|
|
15
|
+
&& !args.some((arg) => arg === "--offline" || arg === "--json" || arg === "--agent" || arg === "--out" || arg.startsWith("--out="))
|
|
16
|
+
&& (command === undefined || ["watch", "submit", "verify", "audit", "weigh"].includes(command))
|
|
17
|
+
&& !args.includes("--help") && !args.includes("-h")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** At most one registry read and one notice per successful day (failure retries after an hour). */
|
|
21
|
+
export async function updateNotice({ repoRoot, version, name = "omakit", env = process.env, now = Date.now(), latest = registryLatest, timeoutMs = UPDATE_CHECK_TIMEOUT_MS }) {
|
|
22
|
+
if (compareVersions(version, version) === null) return null
|
|
23
|
+
const updateFile = join(env.XDG_STATE_HOME || join(homedir(), ".local/state"), "omakit", "update-check.json")
|
|
24
|
+
try {
|
|
25
|
+
const cached = JSON.parse(readFileSync(updateFile, "utf8"))
|
|
26
|
+
const age = now - cached.checkedAt
|
|
27
|
+
const validMetadata = cached.latest === null || compareVersions(cached.latest, cached.latest) !== null
|
|
28
|
+
if (cached.installed === version && cached.name === name && validMetadata && Number.isFinite(age) && age >= 0
|
|
29
|
+
&& age < (cached.latest ? UPDATE_CHECK_INTERVAL_MS : UPDATE_RETRY_INTERVAL_MS)) return null
|
|
30
|
+
} catch { /* Missing or malformed state triggers a fresh bounded read. */ }
|
|
31
|
+
const controller = new AbortController()
|
|
32
|
+
let timer
|
|
33
|
+
let published
|
|
34
|
+
try {
|
|
35
|
+
published = await Promise.race([
|
|
36
|
+
Promise.resolve().then(() => latest(name, { signal: controller.signal })).catch(() => ({ version: null })),
|
|
37
|
+
new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve({ version: null }) }, timeoutMs) }),
|
|
38
|
+
])
|
|
39
|
+
} finally { clearTimeout(timer) }
|
|
40
|
+
const newest = compareVersions(published?.version, published?.version) === null ? null : published.version
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(updateFile), { recursive: true })
|
|
43
|
+
writeFileSync(updateFile, `${JSON.stringify({ name, installed: version, latest: newest, checkedAt: now })}\n`, { mode: 0o600 })
|
|
44
|
+
} catch { /* An unwritable state directory must not break the user's command. */ }
|
|
45
|
+
return newest && compareVersions(newest, version) > 0
|
|
46
|
+
? `Omakit ${newest} is available (installed ${version}). Run \`${upgradeCommand(repoRoot, name)}\`.`
|
|
47
|
+
: null
|
|
48
|
+
}
|
|
@@ -74,9 +74,12 @@ export const NPM_REGISTRY = "https://registry.npmjs.org"
|
|
|
74
74
|
*
|
|
75
75
|
* @returns {Promise<{ version: string|null, error: { code: string, message: string }|null }>}
|
|
76
76
|
*/
|
|
77
|
-
export async function registryLatest(name) {
|
|
77
|
+
export async function registryLatest(name, { signal } = {}) {
|
|
78
78
|
try {
|
|
79
|
-
const meta = await getJson(`${NPM_REGISTRY}/${encodeURIComponent(name)}/latest
|
|
79
|
+
const meta = await getJson(`${NPM_REGISTRY}/${encodeURIComponent(name)}/latest`, { signal })
|
|
80
|
+
if (meta?.version && compareVersions(meta.version, meta.version) === null) {
|
|
81
|
+
return { version: null, error: { code: "invalid-version", message: "the registry did not return a valid semantic version" } }
|
|
82
|
+
}
|
|
80
83
|
return meta?.version
|
|
81
84
|
? { version: meta.version, error: null }
|
|
82
85
|
: { version: null, error: { code: "unpublished", message: "the registry answered without a version" } }
|
|
@@ -85,6 +88,33 @@ export async function registryLatest(name) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/** Semantic precedence, including prereleases; invalid versions are never update targets. */
|
|
92
|
+
export function compareVersions(left, right) {
|
|
93
|
+
const parse = (value) => {
|
|
94
|
+
const match = String(value || "").match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/)
|
|
95
|
+
if (!match) return null
|
|
96
|
+
const pre = match[4]?.split(".") || []
|
|
97
|
+
if (pre.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) return null
|
|
98
|
+
return { core: match.slice(1, 4).map(BigInt), pre }
|
|
99
|
+
}
|
|
100
|
+
const a = parse(left), b = parse(right)
|
|
101
|
+
if (!a || !b) return null
|
|
102
|
+
for (let index = 0; index < 3; index += 1) {
|
|
103
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1
|
|
104
|
+
}
|
|
105
|
+
if (!a.pre.length || !b.pre.length) return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1
|
|
106
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
|
|
107
|
+
const x = a.pre[index], y = b.pre[index]
|
|
108
|
+
if (x === undefined || y === undefined) return x === y ? 0 : x === undefined ? -1 : 1
|
|
109
|
+
if (x === y) continue
|
|
110
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y)
|
|
111
|
+
if (xn && yn) return BigInt(x) > BigInt(y) ? 1 : -1
|
|
112
|
+
if (xn !== yn) return xn ? -1 : 1
|
|
113
|
+
return x > y ? 1 : -1
|
|
114
|
+
}
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
function installedVersion(repoRoot) {
|
|
89
119
|
try {
|
|
90
120
|
return JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version || null
|
|
@@ -300,8 +330,10 @@ async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, ref
|
|
|
300
330
|
if (!newest) {
|
|
301
331
|
return refuse("the npm registry did not answer, so there is nothing to compare against.", "Connect to the network, then run `omakit upgrade` again.")
|
|
302
332
|
}
|
|
303
|
-
|
|
304
|
-
|
|
333
|
+
const compared = compareVersions(newest, current)
|
|
334
|
+
if (compared === null) return refuse("the installed or published version is invalid, so an exact newer release cannot be selected.")
|
|
335
|
+
if (compared <= 0) {
|
|
336
|
+
ok(compared === 0 ? `already current at ${current}, the newest published version` : `${current} is ahead of the newest published version ${newest}; no downgrade applied`)
|
|
305
337
|
out()
|
|
306
338
|
lines(wrap("The marketplace pin is a separate thing and is never touched here. `omakit doctor` says whether it is behind.", {}, c))
|
|
307
339
|
return { ok: true, changed: false, version: current }
|
|
@@ -69,11 +69,18 @@ export const COMMANDS = Object.freeze([
|
|
|
69
69
|
],
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
signature:
|
|
72
|
+
signature: [
|
|
73
|
+
"omakit watch <issue-url> [--json] [--out <file>]",
|
|
74
|
+
"omakit watch [--all | --list] [--user <login>] [--json] [--out <file>]",
|
|
75
|
+
],
|
|
73
76
|
lines: [
|
|
74
77
|
"Compare the commit the marketplace validated on a submission issue with",
|
|
75
78
|
"the plugin repository's current default-branch HEAD, and say what makes",
|
|
76
79
|
"it validate a newer one. Read-only.",
|
|
80
|
+
"--all checks your open marketplace issues; --list lists them first.",
|
|
81
|
+
"Without a URL or either flag, a terminal asks which issues to check.",
|
|
82
|
+
"Uses your gh account, or --user to read another public account. JSON",
|
|
83
|
+
"and pipes never prompt: use --all or --list. Each run is one snapshot.",
|
|
77
84
|
],
|
|
78
85
|
},
|
|
79
86
|
{
|
|
@@ -104,6 +111,8 @@ export const COMMANDS = Object.freeze([
|
|
|
104
111
|
lines: [
|
|
105
112
|
"What is installed, what is pinned, and what has moved since. Reads and",
|
|
106
113
|
"prints; it installs nothing and never moves the pin.",
|
|
114
|
+
"Checks the newest npm release explicitly. Normal terminal use also",
|
|
115
|
+
"checks at most once daily; DISABLE_UPDATE_NOTIFIER=1 disables notices.",
|
|
107
116
|
],
|
|
108
117
|
},
|
|
109
118
|
{
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { join } from "node:path"
|
|
30
30
|
import { pathToFileURL } from "node:url"
|
|
31
31
|
import { MARKETPLACE_PIN, requirePin } from "./pin.mjs"
|
|
32
|
-
import { defaultBranchHead, issue, issueComments, parseIssueUrl, token, GitHubError } from "./github.mjs"
|
|
32
|
+
import { authenticatedUser, repositoryIssues, defaultBranchHead, issue, issueComments, parseIssueUrl, token, GitHubError } from "./github.mjs"
|
|
33
33
|
|
|
34
34
|
export class WatchError extends Error {
|
|
35
35
|
constructor(code, message) {
|
|
@@ -41,6 +41,70 @@ export class WatchError extends Error {
|
|
|
41
41
|
|
|
42
42
|
const MARKETPLACE_SLUG = MARKETPLACE_PIN.repository.replace(/^https:\/\/github\.com\//, "").toLowerCase()
|
|
43
43
|
|
|
44
|
+
/** Text from an issue remains data, including at a terminal. JSON retains the original title. */
|
|
45
|
+
export function watchIssueTitle(value) {
|
|
46
|
+
return String(value || "").replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Discover the account's open marketplace issues, without reading every plugin. */
|
|
50
|
+
export async function discoverWatchIssues({ user, github = {}, onPhase = () => {} } = {}) {
|
|
51
|
+
const read = { authenticatedUser, repositoryIssues, ...github }
|
|
52
|
+
if (user !== undefined && !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(user)) {
|
|
53
|
+
throw new WatchError("usage", "--user needs a GitHub login, not a URL or search query")
|
|
54
|
+
}
|
|
55
|
+
onPhase("reading the GitHub account")
|
|
56
|
+
const account = user || await read.authenticatedUser()
|
|
57
|
+
const [owner, repository] = MARKETPLACE_SLUG.split("/")
|
|
58
|
+
onPhase(`reading open marketplace issues for ${account}`)
|
|
59
|
+
const subjects = await read.repositoryIssues(owner, repository, account)
|
|
60
|
+
// Check the response as well as the server-side creator filter. Never turn
|
|
61
|
+
// another author's issue, or a PR, into an account-wide watch target.
|
|
62
|
+
const seen = new Set()
|
|
63
|
+
const issues = subjects.filter((subject) => {
|
|
64
|
+
if (subject.pull_request || subject.state !== "open" || subject.user?.login?.toLowerCase() !== account.toLowerCase()) return false
|
|
65
|
+
if (!Number.isSafeInteger(subject.number) || subject.number < 1 || seen.has(subject.number)) return false
|
|
66
|
+
seen.add(subject.number)
|
|
67
|
+
return true
|
|
68
|
+
}).map((subject) => ({
|
|
69
|
+
number: subject.number,
|
|
70
|
+
url: `${MARKETPLACE_PIN.repository}/issues/${subject.number}`,
|
|
71
|
+
title: subject.title,
|
|
72
|
+
state: subject.state,
|
|
73
|
+
labels: (subject.labels || []).map((label) => typeof label === "string" ? label : label?.name).filter(Boolean),
|
|
74
|
+
updatedAt: subject.updated_at || null,
|
|
75
|
+
}))
|
|
76
|
+
return { mode: "list", account, marketplace: MARKETPLACE_PIN.repository, issues }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A batch keeps independent read failures visible and shares repository HEAD reads. */
|
|
80
|
+
export async function validationWatchAll({ repoRoot, discovery, github = {}, onPhase = () => {} }) {
|
|
81
|
+
if (discovery.issues.length) requirePin(repoRoot)
|
|
82
|
+
const heads = new Map()
|
|
83
|
+
const readHead = github.defaultBranchHead || defaultBranchHead
|
|
84
|
+
const shared = { ...github, defaultBranchHead: (url) => {
|
|
85
|
+
if (!heads.has(url)) heads.set(url, Promise.resolve().then(() => readHead(url)))
|
|
86
|
+
return heads.get(url)
|
|
87
|
+
} }
|
|
88
|
+
const results = new Array(discovery.issues.length)
|
|
89
|
+
let next = 0
|
|
90
|
+
async function worker() {
|
|
91
|
+
while (next < discovery.issues.length) {
|
|
92
|
+
const index = next++
|
|
93
|
+
const target = discovery.issues[index]
|
|
94
|
+
onPhase(`checking issue #${target.number} (${index + 1}/${discovery.issues.length})`)
|
|
95
|
+
try {
|
|
96
|
+
results[index] = { issue: target, report: await validationWatch({ repoRoot, issueUrl: target.url, github: shared }), error: null }
|
|
97
|
+
} catch (error) {
|
|
98
|
+
results[index] = { issue: target, report: null, error: { code: error.code || "watch-unavailable", message: error.message } }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await Promise.all(Array.from({ length: Math.min(4, discovery.issues.length) }, worker))
|
|
103
|
+
const summary = { total: results.length, current: 0, stale: 0, unknown: 0 }
|
|
104
|
+
for (const result of results) summary[result.report?.verdict.state || "unknown"] += 1
|
|
105
|
+
return { mode: "all", account: discovery.account, marketplace: discovery.marketplace, summary, issues: results }
|
|
106
|
+
}
|
|
107
|
+
|
|
44
108
|
// The one action that re-runs validation, in the register the marketplace itself
|
|
45
109
|
// uses in its own failure feedback.
|
|
46
110
|
export const REFRESH_ACTION =
|
|
@@ -207,6 +271,12 @@ export async function validationWatch({ repoRoot, issueUrl, onPhase, github = {}
|
|
|
207
271
|
baselineError,
|
|
208
272
|
head,
|
|
209
273
|
headError,
|
|
274
|
+
discussion: maintainerComments.length ? {
|
|
275
|
+
body: maintainerComments.at(-1).body || "",
|
|
276
|
+
url: maintainerComments.at(-1).html_url || null,
|
|
277
|
+
createdAt: maintainerComments.at(-1).created_at || null,
|
|
278
|
+
authorAssociation: maintainerComments.at(-1).author_association || null,
|
|
279
|
+
} : null,
|
|
210
280
|
verdict: validationVerdict({ comparable, stale, validated, head, fallback, baselineError, headError, pushedAfterReview, repositoryUrl }),
|
|
211
281
|
}
|
|
212
282
|
}
|