@ham2k/extension-tools 0.14.0 → 0.14.1
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 +29 -0
- package/h2kext-publish.mjs +242 -7
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -107,3 +107,32 @@ A channel says how finished a release is — `stable`, `unstable`, `bleeding`
|
|
|
107
107
|
not which app edition is asking. They cascade upward, so `stable` reaches
|
|
108
108
|
everyone and `bleeding` reaches only somebody who asked for it. `--channel` is
|
|
109
109
|
optional and defaults to `stable`.
|
|
110
|
+
|
|
111
|
+
### Screenshots
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
screenshots/
|
|
115
|
+
1-logging.png
|
|
116
|
+
2-map.png
|
|
117
|
+
screenshots.json optional — { "1-logging.png": "Logging a park" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When your project has a `screenshots/` folder (or you name one with
|
|
121
|
+
`--screenshots <dir>`), `h2kext-publish` makes the catalog's screenshots for
|
|
122
|
+
your extension match it, after the bundle is accepted. The folder is the
|
|
123
|
+
whole set: PNG, JPEG or WebP files, at most 8, shown in file-name order
|
|
124
|
+
(`2-…` before `10-…`), captions from `screenshots.json`. Screenshots are never
|
|
125
|
+
packed into the bundle, and they belong to the extension rather than to a
|
|
126
|
+
version: every channel shows the same ones, and
|
|
127
|
+
`h2kext-publish build --screenshots-only` updates them without publishing a
|
|
128
|
+
version.
|
|
129
|
+
|
|
130
|
+
Each screenshot is at most 2 MiB and 3840 pixels on its longest side. The
|
|
131
|
+
command makes the thumbnail the app shows in search results itself, with
|
|
132
|
+
[sharp](https://sharp.pixelplumbing.com) — an optional dependency, so if it
|
|
133
|
+
could not install on your machine the command says so, and you can add your
|
|
134
|
+
screenshots on the site's [/publish](https://catalog.ham2k.net/publish) page
|
|
135
|
+
instead. An unchanged folder sends nothing. A new set waits for a reviewer,
|
|
136
|
+
like a version, while the approved one keeps showing. An empty folder named
|
|
137
|
+
with `--screenshots` takes every screenshot down at once; an empty
|
|
138
|
+
`./screenshots` found on its own is left alone.
|
package/h2kext-publish.mjs
CHANGED
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
//
|
|
9
9
|
// H2K_CATALOG_TOKEN=h2kc_… h2kext-publish <bundle.h2kext | build dir>
|
|
10
10
|
// [--channel <stable|unstable|bleeding>] [--notes "…"] [--catalog <url>]
|
|
11
|
-
// [--force-name]
|
|
11
|
+
// [--force-name] [--screenshots <dir>] [--screenshots-only]
|
|
12
12
|
//
|
|
13
|
-
// One POST of the raw bytes, then one PUT for the notes
|
|
14
|
-
// is the catalog's verdict on these bytes,
|
|
15
|
-
// repeat it. The token is read from the environment for those requests and
|
|
13
|
+
// One POST of the raw bytes, then one PUT for the notes, then the screenshot
|
|
14
|
+
// sync (§4.8); no retries: a refusal is the catalog's verdict on these bytes,
|
|
15
|
+
// and sending them again can only repeat it. The token is read from the environment for those requests and
|
|
16
16
|
// never written anywhere — not to a config file, not to the terminal, not into
|
|
17
17
|
// an error message.
|
|
18
18
|
|
|
19
|
+
import { createHash } from "node:crypto"
|
|
19
20
|
import { realpathSync } from "node:fs"
|
|
20
|
-
import { mkdtemp, readFile, rm, stat } from "node:fs/promises"
|
|
21
|
+
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises"
|
|
21
22
|
import { tmpdir } from "node:os"
|
|
22
|
-
import { join } from "node:path"
|
|
23
|
+
import { extname, join, resolve } from "node:path"
|
|
23
24
|
import { pathToFileURL } from "node:url"
|
|
24
25
|
import { inflateRawSync } from "node:zlib"
|
|
25
26
|
|
|
@@ -43,6 +44,16 @@ export const DEFAULT_CHANNEL = "stable"
|
|
|
43
44
|
/// Checked here so the refusal costs nothing instead of a wasted upload.
|
|
44
45
|
export const MAX_BUNDLE_BYTES = 16 * 1024 * 1024
|
|
45
46
|
|
|
47
|
+
/// Screenshots (§4.8). The catalog checks every one of these again; checking
|
|
48
|
+
/// here turns a refusal after a round trip into one before any upload.
|
|
49
|
+
export const SCREENSHOTS_DIR = "screenshots"
|
|
50
|
+
export const CAPTIONS_FILE = "screenshots.json"
|
|
51
|
+
export const MAX_SCREENSHOTS = 8
|
|
52
|
+
export const MAX_SCREENSHOT_BYTES = 2 * 1024 * 1024
|
|
53
|
+
export const THUMB_SIDE = 480
|
|
54
|
+
export const CAPTION_MAX = 200
|
|
55
|
+
const SCREENSHOT_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" }
|
|
56
|
+
|
|
46
57
|
/// Exit codes. Usage problems — including a missing token — are 2 so a script
|
|
47
58
|
/// can tell "you called this wrong" from "the catalog said no" (1).
|
|
48
59
|
export const EXIT_REFUSED = 1
|
|
@@ -64,6 +75,7 @@ const USAGE = `Publish an extension bundle to the Ham2K extension catalog.
|
|
|
64
75
|
|
|
65
76
|
${TOKEN_ENV}=h2kc_… h2kext-publish <bundle.h2kext | build dir> [--channel <stable|unstable|bleeding>]
|
|
66
77
|
[--notes "<release notes>"] [--catalog <url>] [--force-name]
|
|
78
|
+
[--screenshots <dir>] [--screenshots-only]
|
|
67
79
|
|
|
68
80
|
A directory is packaged first, exactly as h2kext-pack would. A bundle over
|
|
69
81
|
16 MiB is refused here, as the catalog would refuse it.
|
|
@@ -80,6 +92,14 @@ ${DEFAULT_CATALOG}/publish. Nothing here stores it.
|
|
|
80
92
|
--catalog defaults to ${DEFAULT_CATALOG}
|
|
81
93
|
--force-name waive the key naming convention when packaging a directory
|
|
82
94
|
(Ham2K's own builds; the catalog checks it again regardless)
|
|
95
|
+
--screenshots the folder holding the extension's screenshots (default
|
|
96
|
+
./${SCREENSHOTS_DIR}, when it exists). The folder is the whole
|
|
97
|
+
set: PNG, JPEG or WebP files in file-name order, at most
|
|
98
|
+
${MAX_SCREENSHOTS}, captions from an optional ${CAPTIONS_FILE}
|
|
99
|
+
({ "<file name>": "<caption>" }). Thumbnails are made with the
|
|
100
|
+
optional \`sharp\` package. An unchanged folder sends nothing.
|
|
101
|
+
--screenshots-only
|
|
102
|
+
sync the screenshots without publishing the bundle
|
|
83
103
|
|
|
84
104
|
Exits 0 once the catalog holds the submission and its notes, 1 when it refuses
|
|
85
105
|
or cannot be reached, 2 for a usage problem.`
|
|
@@ -226,9 +246,177 @@ export async function setReleaseNotes({ submissionId, notes, catalog, token }, {
|
|
|
226
246
|
throw new PublishError(`${catalog} refused the release notes for submission ${submissionId} with HTTP ${answer.response.status}: ${refusal(answer)}`)
|
|
227
247
|
}
|
|
228
248
|
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Screenshots (§4.8)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
function sha256Of(bytes) {
|
|
254
|
+
return createHash("sha256").update(bytes).digest("hex")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// The type the catalog will read from the bytes, by the same magic numbers,
|
|
258
|
+
/// or null: a file named `.png` that is not one is refused here, by name.
|
|
259
|
+
export function sniffImageType(bytes) {
|
|
260
|
+
const b = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
|
|
261
|
+
if (b.length >= 8 && b.readUInt32BE(0) === 0x89504e47 && b.readUInt32BE(4) === 0x0d0a1a0a) return "image/png"
|
|
262
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return "image/jpeg"
|
|
263
|
+
if (b.length >= 12 && b.toString("latin1", 0, 4) === "RIFF" && b.toString("latin1", 8, 12) === "WEBP") return "image/webp"
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// The folder as a set: every image file in file-name order — numeric-aware,
|
|
268
|
+
/// so `2-…` comes before `10-…` — with its bytes, hash and caption. Anything
|
|
269
|
+
/// else in the folder is ignored, so a README beside the pictures is fine.
|
|
270
|
+
export async function readScreenshotFolder(dir) {
|
|
271
|
+
let entries
|
|
272
|
+
try {
|
|
273
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
274
|
+
} catch (e) {
|
|
275
|
+
throw new PublishError(`${dir}: ${e.code === "ENOENT" ? "no such directory" : e.message}`)
|
|
276
|
+
}
|
|
277
|
+
// Files only: a directory named `old.png` is not a screenshot.
|
|
278
|
+
const names = entries.filter((entry) => entry.isFile()).map((entry) => entry.name)
|
|
279
|
+
let captions = {}
|
|
280
|
+
if (names.includes(CAPTIONS_FILE)) {
|
|
281
|
+
try {
|
|
282
|
+
captions = JSON.parse(await readFile(join(dir, CAPTIONS_FILE), "utf8"))
|
|
283
|
+
} catch (e) {
|
|
284
|
+
throw new PublishError(`${join(dir, CAPTIONS_FILE)} is not valid JSON: ${e.message}`)
|
|
285
|
+
}
|
|
286
|
+
if (typeof captions !== "object" || captions === null || Array.isArray(captions)) {
|
|
287
|
+
throw new PublishError(`${join(dir, CAPTIONS_FILE)} must map file names to captions`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const files = names
|
|
291
|
+
.filter((name) => SCREENSHOT_TYPES[extname(name).toLowerCase()] && !name.startsWith("."))
|
|
292
|
+
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }))
|
|
293
|
+
if (files.length > MAX_SCREENSHOTS) {
|
|
294
|
+
throw new PublishError(`${dir} holds ${files.length} screenshots; the catalog takes at most ${MAX_SCREENSHOTS}`)
|
|
295
|
+
}
|
|
296
|
+
for (const name of Object.keys(captions)) {
|
|
297
|
+
// A caption for a file that is not there is almost always a renamed file.
|
|
298
|
+
if (!files.includes(name)) throw new PublishError(`${join(dir, CAPTIONS_FILE)} has a caption for '${name}', which is not a screenshot in ${dir}`)
|
|
299
|
+
}
|
|
300
|
+
const shots = []
|
|
301
|
+
for (const name of files) {
|
|
302
|
+
const bytes = await readFile(join(dir, name))
|
|
303
|
+
const type = sniffImageType(bytes)
|
|
304
|
+
if (!type) throw new PublishError(`${join(dir, name)} is not a PNG, JPEG or WebP image`)
|
|
305
|
+
if (bytes.length > MAX_SCREENSHOT_BYTES) {
|
|
306
|
+
throw new PublishError(`${join(dir, name)} is ${bytes.length} bytes; a screenshot is at most 2 MiB (${MAX_SCREENSHOT_BYTES} bytes)`)
|
|
307
|
+
}
|
|
308
|
+
const caption = captions[name]
|
|
309
|
+
if (caption !== undefined && caption !== null && typeof caption !== "string") {
|
|
310
|
+
throw new PublishError(`${join(dir, CAPTIONS_FILE)}: the caption for '${name}' must be a string`)
|
|
311
|
+
}
|
|
312
|
+
const trimmed = caption?.trim() || null
|
|
313
|
+
if (trimmed && [...trimmed].length > CAPTION_MAX) {
|
|
314
|
+
throw new PublishError(`${join(dir, CAPTIONS_FILE)}: the caption for '${name}' is longer than ${CAPTION_MAX} characters`)
|
|
315
|
+
}
|
|
316
|
+
shots.push({ name, bytes, type, sha256: sha256Of(bytes), caption: trimmed })
|
|
317
|
+
}
|
|
318
|
+
return shots
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// A thumbnail at most [THUMB_SIDE] pixels on its longest side, in the
|
|
322
|
+
/// image's proportions, as WebP — a PNG screenshot's thumbnail kept as PNG is
|
|
323
|
+
/// often past the catalog's 128 KiB, and every client decodes WebP. `sharp`
|
|
324
|
+
/// is optional so packing and plain publishing never need a native module;
|
|
325
|
+
/// it is loaded only when a new picture needs a thumbnail.
|
|
326
|
+
export async function sharpThumbnail(bytes) {
|
|
327
|
+
let sharp
|
|
328
|
+
try {
|
|
329
|
+
sharp = (await import("sharp")).default
|
|
330
|
+
} catch {
|
|
331
|
+
throw new PublishError(
|
|
332
|
+
"making screenshot thumbnails needs the optional 'sharp' package — `npm install --save-dev sharp` in your project, or manage screenshots on the catalog site instead",
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
const data = await sharp(bytes).resize({ width: THUMB_SIDE, height: THUMB_SIDE, fit: "inside", withoutEnlargement: true }).webp({ quality: 80 }).toBuffer()
|
|
336
|
+
return { bytes: data, type: "image/webp" }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const sameItems = (set, shots) =>
|
|
340
|
+
set !== null &&
|
|
341
|
+
set !== undefined &&
|
|
342
|
+
set.items.length === shots.length &&
|
|
343
|
+
set.items.every((item, i) => item.image === shots[i].sha256 && (item.caption ?? null) === shots[i].caption)
|
|
344
|
+
|
|
345
|
+
/// Makes the catalog's set for [key] the folder's [shots], sending only what
|
|
346
|
+
/// it lacks: nothing at all when the waiting set — or the live one, with none
|
|
347
|
+
/// waiting — already is the folder; otherwise a thumbnail made only for a
|
|
348
|
+
/// picture no set of the key already has one for, the uploads the catalog
|
|
349
|
+
/// does not hold, and the set. Answers what happened, for the result line.
|
|
350
|
+
export async function syncScreenshots({ key, shots, catalog, token }, { fetch = globalThis.fetch, thumbnail = sharpThumbnail } = {}) {
|
|
351
|
+
const base = `/api/v1/extensions/${encodeURIComponent(key)}`
|
|
352
|
+
const auth = { authorization: `Bearer ${token}` }
|
|
353
|
+
const current = await call(fetch, catalog, `${base}/screenshots`, { headers: auth })
|
|
354
|
+
if (!current.response.ok) {
|
|
355
|
+
throw new PublishError(`${catalog} refused to show the screenshots of ${key} with HTTP ${current.response.status}: ${refusal(current)}`)
|
|
356
|
+
}
|
|
357
|
+
const { approved = null, pending = null } = current.body ?? {}
|
|
358
|
+
if (pending ? sameItems(pending, shots) : sameItems(approved, shots)) return { changed: false, set: pending ?? approved }
|
|
359
|
+
|
|
360
|
+
// A picture already in a set keeps that set's thumbnail: making it again
|
|
361
|
+
// with another sharp could give other bytes, and a new set to review.
|
|
362
|
+
const known = new Map()
|
|
363
|
+
for (const set of [approved, pending]) for (const item of set?.items ?? []) known.set(item.image, item.thumb)
|
|
364
|
+
const held = new Set([...known.keys(), ...known.values()])
|
|
365
|
+
|
|
366
|
+
const upload = async (bytes, type, what) => {
|
|
367
|
+
const sha256 = sha256Of(bytes)
|
|
368
|
+
if (held.has(sha256)) return sha256
|
|
369
|
+
const answer = await call(fetch, catalog, `${base}/media/${sha256}`, { method: "PUT", headers: { ...auth, "content-type": type }, body: bytes })
|
|
370
|
+
if (!answer.response.ok) throw new PublishError(`${catalog} refused ${what} with HTTP ${answer.response.status}: ${refusal(answer)}`)
|
|
371
|
+
held.add(sha256)
|
|
372
|
+
return sha256
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const items = []
|
|
376
|
+
for (const shot of shots) {
|
|
377
|
+
const image = await upload(shot.bytes, shot.type, shot.name)
|
|
378
|
+
let thumb = known.get(shot.sha256)
|
|
379
|
+
if (!thumb) {
|
|
380
|
+
let made
|
|
381
|
+
try {
|
|
382
|
+
made = await thumbnail(shot.bytes)
|
|
383
|
+
} catch (e) {
|
|
384
|
+
// A file whose header looks right and whose body does not: said by
|
|
385
|
+
// name, as a refusal, not as a stack trace that hides whether the
|
|
386
|
+
// version went out.
|
|
387
|
+
if (e instanceof PublishError) throw e
|
|
388
|
+
throw new PublishError(`could not make a thumbnail of ${shot.name}: ${e.message}`)
|
|
389
|
+
}
|
|
390
|
+
thumb = await upload(made.bytes, made.type, `the thumbnail of ${shot.name}`)
|
|
391
|
+
}
|
|
392
|
+
items.push({ image, thumb, ...(shot.caption ? { caption: shot.caption } : {}) })
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const answer = await call(fetch, catalog, `${base}/screenshots`, {
|
|
396
|
+
method: "PUT",
|
|
397
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
398
|
+
body: JSON.stringify({ screenshots: items }),
|
|
399
|
+
})
|
|
400
|
+
if (!answer.response.ok || !answer.body?.set) {
|
|
401
|
+
throw new PublishError(`${catalog} refused the screenshots of ${key} with HTTP ${answer.response.status}: ${refusal(answer)}`)
|
|
402
|
+
}
|
|
403
|
+
return { changed: true, set: answer.body.set }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// The result line's words for a set's state.
|
|
407
|
+
function screenshotState({ changed, set }) {
|
|
408
|
+
const count = set?.items.length ?? 0
|
|
409
|
+
const what = count === 0 ? "no screenshots" : `${count} screenshot${count === 1 ? "" : "s"}`
|
|
410
|
+
if (!changed) return `screenshots unchanged (${what}${set?.status === "pending" ? ", pending review" : ""})`
|
|
411
|
+
return `screenshots → ${what} (${STATUS_WORDS[set.status] ?? (set.status === "approved" ? "live" : set.status)})`
|
|
412
|
+
}
|
|
413
|
+
|
|
229
414
|
/// The command. Returns the exit code rather than exiting, so the same code
|
|
230
415
|
/// runs under a test with a fake `fetch` and captured output.
|
|
231
|
-
export async function main(
|
|
416
|
+
export async function main(
|
|
417
|
+
argv,
|
|
418
|
+
{ env = process.env, fetch = globalThis.fetch, stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), thumbnail = sharpThumbnail } = {},
|
|
419
|
+
) {
|
|
232
420
|
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
233
421
|
stdout.write(`${USAGE}\n`)
|
|
234
422
|
return argv.length === 0 ? EXIT_USAGE : 0
|
|
@@ -253,6 +441,8 @@ export async function main(argv, { env = process.env, fetch = globalThis.fetch,
|
|
|
253
441
|
let notes
|
|
254
442
|
let catalog = DEFAULT_CATALOG
|
|
255
443
|
let forceName = false
|
|
444
|
+
let screenshotsDir
|
|
445
|
+
let screenshotsOnly = false
|
|
256
446
|
// An option at the end of the line has no value, and `--notes` with none
|
|
257
447
|
// would silently publish without notes — the one option whose absence
|
|
258
448
|
// looks like success.
|
|
@@ -266,6 +456,8 @@ export async function main(argv, { env = process.env, fetch = globalThis.fetch,
|
|
|
266
456
|
else if (arg === "--notes") notes = valueOf(i++)
|
|
267
457
|
else if (arg === "--catalog") catalog = valueOf(i++)
|
|
268
458
|
else if (arg === "--force-name") forceName = true
|
|
459
|
+
else if (arg === "--screenshots") screenshotsDir = valueOf(i++)
|
|
460
|
+
else if (arg === "--screenshots-only") screenshotsOnly = true
|
|
269
461
|
else if (arg === "--token") throw new PublishError(`there is no --token option; set ${TOKEN_ENV} instead`, EXIT_USAGE)
|
|
270
462
|
else if (arg.startsWith("-")) throw new PublishError(`unknown option '${arg}'`, EXIT_USAGE)
|
|
271
463
|
else if (source === undefined) source = arg
|
|
@@ -285,6 +477,32 @@ export async function main(argv, { env = process.env, fetch = globalThis.fetch,
|
|
|
285
477
|
throw new PublishError(`--catalog must be a URL, not '${catalog}'`, EXIT_USAGE)
|
|
286
478
|
}
|
|
287
479
|
|
|
480
|
+
// The folder is read before anything is sent, so what can be checked
|
|
481
|
+
// without decoding — type, size, count, captions — stops the command
|
|
482
|
+
// before the version is published rather than after; the rest is the
|
|
483
|
+
// catalog's to refuse. Without `--screenshots`, only a folder that exists
|
|
484
|
+
// and holds pictures is a set: a project with none, or a stray empty
|
|
485
|
+
// `screenshots/` where the command happens to run, has not asked for its
|
|
486
|
+
// screenshots to be taken down. Naming the folder is how to ask.
|
|
487
|
+
let shots
|
|
488
|
+
const folder = resolve(cwd, screenshotsDir ?? SCREENSHOTS_DIR)
|
|
489
|
+
if (
|
|
490
|
+
screenshotsDir !== undefined ||
|
|
491
|
+
(await stat(folder).then(
|
|
492
|
+
(s) => s.isDirectory(),
|
|
493
|
+
() => false,
|
|
494
|
+
))
|
|
495
|
+
) {
|
|
496
|
+
shots = await readScreenshotFolder(folder)
|
|
497
|
+
if (screenshotsDir === undefined && shots.length === 0) {
|
|
498
|
+
say(`./${SCREENSHOTS_DIR} holds no screenshots; leaving the catalog's as they are (name it with --screenshots to remove them all)`)
|
|
499
|
+
shots = undefined
|
|
500
|
+
if (screenshotsOnly) return 0
|
|
501
|
+
}
|
|
502
|
+
} else if (screenshotsOnly) {
|
|
503
|
+
throw new PublishError(`--screenshots-only, but there is no ./${SCREENSHOTS_DIR} folder; name one with --screenshots`, EXIT_USAGE)
|
|
504
|
+
}
|
|
505
|
+
|
|
288
506
|
const bytes = await loadBundle(source, { forceName })
|
|
289
507
|
if (bytes.length > MAX_BUNDLE_BYTES) {
|
|
290
508
|
throw new PublishError(
|
|
@@ -297,6 +515,14 @@ export async function main(argv, { env = process.env, fetch = globalThis.fetch,
|
|
|
297
515
|
throw new PublishError(`${MANIFEST_ENTRY} inside the bundle has no '${field}'`)
|
|
298
516
|
}
|
|
299
517
|
}
|
|
518
|
+
const syncShots = async () => {
|
|
519
|
+
say(screenshotState(await syncScreenshots({ key: manifest.key, shots, catalog, token }, { fetch, thumbnail })))
|
|
520
|
+
}
|
|
521
|
+
if (screenshotsOnly) {
|
|
522
|
+
await syncShots()
|
|
523
|
+
return 0
|
|
524
|
+
}
|
|
525
|
+
|
|
300
526
|
say(`publishing ${manifest.key} ${manifest.version}${manifest.name ? ` — ${manifest.name}` : ""} → ${channel} at ${catalog}`)
|
|
301
527
|
|
|
302
528
|
const result = await publish({ bytes, manifest, channel, catalog, token }, { fetch })
|
|
@@ -318,6 +544,15 @@ export async function main(argv, { env = process.env, fetch = globalThis.fetch,
|
|
|
318
544
|
return EXIT_REFUSED
|
|
319
545
|
}
|
|
320
546
|
}
|
|
547
|
+
if (shots !== undefined) {
|
|
548
|
+
try {
|
|
549
|
+
await syncShots()
|
|
550
|
+
} catch (e) {
|
|
551
|
+
if (!(e instanceof PublishError)) throw e
|
|
552
|
+
complain(`warning: ${result.key} ${result.version} is published, but its screenshots were not updated — ${e.message}`)
|
|
553
|
+
return EXIT_REFUSED
|
|
554
|
+
}
|
|
555
|
+
}
|
|
321
556
|
return 0
|
|
322
557
|
} catch (e) {
|
|
323
558
|
if (e instanceof PublishError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ham2k/extension-tools",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Build and package extensions for the Ham2K Logger",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ham2k",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"@ham2k/lib-callsigns": "^1.0.2",
|
|
44
44
|
"yazl": "^3.3.1"
|
|
45
45
|
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"sharp": "^0.34.0"
|
|
48
|
+
},
|
|
46
49
|
"peerDependencies": {
|
|
47
50
|
"esbuild": ">=0.20"
|
|
48
51
|
},
|