@buildinternet/uploads 0.11.0 → 0.12.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 +14 -3
- package/dist/async.d.ts +5 -0
- package/dist/async.js +21 -0
- package/dist/cli-catalog.d.ts +7 -0
- package/dist/cli-catalog.js +52 -1
- package/dist/cli.js +12 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +13 -7
- package/dist/commands/completion.js +15 -5
- package/dist/commands/install.js +5 -0
- package/dist/commands/screenshot.d.ts +6 -0
- package/dist/commands/screenshot.js +317 -0
- package/dist/commands.d.ts +162 -2
- package/dist/commands.js +440 -143
- package/dist/config-file.d.ts +28 -2
- package/dist/config-file.js +44 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/mcp/batch-error.d.ts +16 -0
- package/dist/mcp/batch-error.js +24 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +16 -0
- package/dist/mcp/tools.d.ts +8 -1
- package/dist/mcp/tools.js +393 -94
- package/dist/screenshot-local.d.ts +64 -0
- package/dist/screenshot-local.js +310 -0
- package/dist/screenshot-remote.d.ts +23 -0
- package/dist/screenshot-remote.js +79 -0
- package/dist/screenshot.d.ts +74 -0
- package/dist/screenshot.js +231 -0
- package/package.json +4 -1
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { mapBounded } from "./async.js";
|
|
4
|
+
import { createUploadsClient, } from "./client.js";
|
|
4
5
|
import { parseCommandArgs, flagString, flagBool, flagInt, flagValues, UsageError, } from "./cli-args.js";
|
|
5
6
|
import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./config.js";
|
|
6
7
|
import { buildMarkdown } from "./embed.js";
|
|
@@ -18,6 +19,10 @@ import { formatByteSize } from "./format-bytes.js";
|
|
|
18
19
|
import { formatUsageHuman } from "./format-usage.js";
|
|
19
20
|
import { packageVersion } from "./package-version.js";
|
|
20
21
|
import { colorEnabled, writeCommandHelp } from "./cli-style.js";
|
|
22
|
+
/** Parallel fan-out for multi-file put/attach (matches files-sdk bulk default). */
|
|
23
|
+
export const UPLOAD_BATCH_CONCURRENCY = 8;
|
|
24
|
+
/** @deprecated Use UPLOAD_BATCH_CONCURRENCY. */
|
|
25
|
+
export const ATTACH_CONCURRENCY = UPLOAD_BATCH_CONCURRENCY;
|
|
21
26
|
export { formatUsageHuman } from "./format-usage.js";
|
|
22
27
|
/** Read a local file (or `-` for stdin). Missing path → FILE_NOT_FOUND (exit 2). */
|
|
23
28
|
export function readFileArg(fileArg) {
|
|
@@ -32,9 +37,13 @@ export function readFileArg(fileArg) {
|
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
// --- put ---
|
|
35
|
-
const PUT_HELP = `uploads put <file
|
|
40
|
+
const PUT_HELP = `uploads put <file...> [options]
|
|
36
41
|
|
|
37
|
-
Upload
|
|
42
|
+
Upload one or more images for GitHub embeds. Use "-" for stdin (single file only).
|
|
43
|
+
|
|
44
|
+
Multiple files upload in parallel (bounded concurrency). One bad file does not
|
|
45
|
+
block the rest; multi-file JSON is { uploads, failures } (exit 1 when any failed).
|
|
46
|
+
Single-file JSON stays a flat object (back-compat).
|
|
38
47
|
|
|
39
48
|
Still images (PNG/JPEG/…) are optimized to WebP by default (long edge capped,
|
|
40
49
|
high quality; EXIF stripped) so GitHub embeds stay lean. Original bytes are kept
|
|
@@ -57,13 +66,13 @@ Human/json output includes durable url and (when dual-host applies) embedUrl.
|
|
|
57
66
|
MARKDOWN prefers embedUrl for GitHub. Override: UPLOADS_EMBED_PUBLIC_BASE_URL.
|
|
58
67
|
|
|
59
68
|
Options:
|
|
60
|
-
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
61
|
-
--name <leaf> Clean key leaf + default alt (no '/'); keeps --pr/default path. Not with --key
|
|
69
|
+
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>). Single file only
|
|
70
|
+
--name <leaf> Clean key leaf + default alt (no '/'); keeps --pr/default path. Single file only. Not with --key
|
|
62
71
|
--destination <id> Typed root: screenshots | gh | f (sets --prefix)
|
|
63
72
|
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
64
73
|
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
65
74
|
--ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
|
|
66
|
-
--alt <text> Alt text (default:
|
|
75
|
+
--alt <text> Alt text (default: each file's name; with multiple files applies to all)
|
|
67
76
|
--width <px> <img width=…> markdown (or UPLOADS_DEFAULT_WIDTH)
|
|
68
77
|
--content-type <mime> Override Content-Type (ignored when optimize rewrites the body)
|
|
69
78
|
--frame <id> Device/browser frame before optimize (phone|browser|iphone-16-pro)
|
|
@@ -81,18 +90,19 @@ Options:
|
|
|
81
90
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
82
91
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
83
92
|
--comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
|
|
84
|
-
--gallery <id> Add the uploaded object to this public gallery
|
|
93
|
+
--gallery <id> Add the uploaded object(s) to this public gallery
|
|
85
94
|
--meta <k=v> Queryable custom metadata (repeatable; value may contain "="): key ^[a-z][a-z0-9._-]{0,63}$, value 1-512 printable ASCII, max 24 pairs
|
|
86
95
|
Re-uploading to an existing key WITH --meta replaces that file's
|
|
87
96
|
entire metadata set; without --meta the existing metadata is
|
|
88
97
|
preserved. Use "uploads meta set" to edit individual keys.
|
|
89
98
|
--dry-run Print key + public URL without uploading; reports if the key would replace an existing object. Not with --comment/--gallery
|
|
90
99
|
|
|
91
|
-
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
|
|
100
|
+
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other (incl. partial multi-file failure).
|
|
92
101
|
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
93
102
|
|
|
94
103
|
Examples:
|
|
95
104
|
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
105
|
+
uploads put ./before.png ./after.png
|
|
96
106
|
uploads put ./mobile.png --frame phone
|
|
97
107
|
uploads put ./ui.png --frame browser --frame-url "https://app.example/settings"
|
|
98
108
|
uploads put ./shot.png --destination screenshots
|
|
@@ -115,7 +125,7 @@ export function makeGhTarget(pr, issue, repoArg, run) {
|
|
|
115
125
|
return { repo, kind: pr !== undefined ? "pull" : "issues", num: (pr ?? issue) };
|
|
116
126
|
}
|
|
117
127
|
/** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
|
|
118
|
-
function ghTargetFromFlags(flags, run) {
|
|
128
|
+
export function ghTargetFromFlags(flags, run) {
|
|
119
129
|
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
120
130
|
}
|
|
121
131
|
/**
|
|
@@ -195,7 +205,50 @@ export async function prepareImageForUpload(bytes, filename, opts) {
|
|
|
195
205
|
const optimized = await optimizeImageForUpload(currentBytes, currentName, opts.optimize);
|
|
196
206
|
return { ...optimized, frame: frameMeta };
|
|
197
207
|
}
|
|
198
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Shared bytes-oriented upload tail: frame + optimize the bytes, resolve the
|
|
210
|
+
* object key (gh attachment key wins over an explicit key; extension
|
|
211
|
+
* rewritten post-optimize), put, and build the GitHub embed markdown. Used by
|
|
212
|
+
* the screenshot CLI command, the MCP screenshot tool, and the MCP put
|
|
213
|
+
* tool's contentBase64 path — the three in-memory-bytes call sites.
|
|
214
|
+
* uploadPuts/uploadAttachments loop over file paths with their own bounded
|
|
215
|
+
* concurrency and delegate here per item.
|
|
216
|
+
*/
|
|
217
|
+
export async function uploadPreparedImage(client, bytes, sourceName, opts) {
|
|
218
|
+
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
219
|
+
frameId: opts.frame.frameId,
|
|
220
|
+
frameUrl: opts.frame.frameUrl,
|
|
221
|
+
frameFit: opts.frame.frameFit,
|
|
222
|
+
optimize: opts.optimize,
|
|
223
|
+
});
|
|
224
|
+
let key = opts.ghTarget ? ghAttachmentKey(opts.ghTarget, prepared.filename) : opts.key;
|
|
225
|
+
if (key && prepared.optimized)
|
|
226
|
+
key = rewriteKeyExtension(key, prepared.filename);
|
|
227
|
+
const result = await client.put(prepared.bytes, {
|
|
228
|
+
filename: prepared.filename,
|
|
229
|
+
key,
|
|
230
|
+
prefix: opts.prefix,
|
|
231
|
+
repo: opts.repo,
|
|
232
|
+
ref: opts.ref,
|
|
233
|
+
contentType: prepared.optimized ? prepared.contentType : opts.contentType,
|
|
234
|
+
deriveRepoFromGit: opts.deriveRepoFromGit,
|
|
235
|
+
dryRun: opts.dryRun,
|
|
236
|
+
provenance: buildCliProvenance({
|
|
237
|
+
sourceName,
|
|
238
|
+
client: opts.provenanceClient,
|
|
239
|
+
optimized: prepared.optimized,
|
|
240
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
241
|
+
keepExif: opts.optimize.keepExif === true,
|
|
242
|
+
}),
|
|
243
|
+
metadata: opts.metadata,
|
|
244
|
+
});
|
|
245
|
+
const markdown = buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
246
|
+
alt: opts.alt(prepared),
|
|
247
|
+
width: opts.width,
|
|
248
|
+
});
|
|
249
|
+
return { result, prepared, markdown };
|
|
250
|
+
}
|
|
251
|
+
export function frameOptionsFromFlags(flags) {
|
|
199
252
|
const raw = flagString(flags, "--frame");
|
|
200
253
|
let frameId;
|
|
201
254
|
try {
|
|
@@ -271,6 +324,9 @@ const ATTACH_HELP = `uploads attach <file...> [options]
|
|
|
271
324
|
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
272
325
|
comment. With no target, uses the pull request for the current branch.
|
|
273
326
|
|
|
327
|
+
Multiple files upload in parallel (bounded concurrency). One bad file does not
|
|
328
|
+
block the rest; JSON includes uploads + failures (exit 1 when any failed).
|
|
329
|
+
|
|
274
330
|
Attachments are public and their repo/number/filename keys are predictable.
|
|
275
331
|
Private/internal GitHub repository visibility does not restrict access; upload
|
|
276
332
|
only media that is safe at a public URL.
|
|
@@ -310,6 +366,148 @@ Examples:
|
|
|
310
366
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
311
367
|
uploads attach ./shot.png --meta app=myapp --meta page=settings
|
|
312
368
|
`;
|
|
369
|
+
/**
|
|
370
|
+
* Prepare + put each path as a PR/issue attachment with bounded concurrency.
|
|
371
|
+
* Per-file errors collect in `failures` (does not throw). `firstError` is the
|
|
372
|
+
* original cause of the first failure — for rethrowing single-file CLI paths.
|
|
373
|
+
*/
|
|
374
|
+
export async function uploadAttachments(opts) {
|
|
375
|
+
if (opts.files.some((f) => f === "-")) {
|
|
376
|
+
throw new UsageError("attach does not support stdin; pass one or more file paths");
|
|
377
|
+
}
|
|
378
|
+
const slots = await mapBounded(opts.files, opts.concurrency ?? UPLOAD_BATCH_CONCURRENCY, async (file) => {
|
|
379
|
+
try {
|
|
380
|
+
const sourceName = basename(file);
|
|
381
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
382
|
+
...opts.frame,
|
|
383
|
+
optimize: opts.optimize,
|
|
384
|
+
});
|
|
385
|
+
const result = await opts.client.put(prepared.bytes, {
|
|
386
|
+
filename: prepared.filename,
|
|
387
|
+
key: ghAttachmentKey(opts.target, prepared.filename),
|
|
388
|
+
contentType: prepared.optimized ? prepared.contentType : opts.contentType,
|
|
389
|
+
provenance: buildCliProvenance({
|
|
390
|
+
sourceName,
|
|
391
|
+
client: opts.provenanceClient,
|
|
392
|
+
optimized: prepared.optimized,
|
|
393
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
394
|
+
keepExif: opts.optimize.keepExif === true,
|
|
395
|
+
}),
|
|
396
|
+
metadata: opts.metadata,
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
upload: {
|
|
401
|
+
...result,
|
|
402
|
+
file,
|
|
403
|
+
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
404
|
+
alt: sourceName,
|
|
405
|
+
}),
|
|
406
|
+
optimize: {
|
|
407
|
+
optimized: prepared.optimized,
|
|
408
|
+
skippedReason: prepared.skippedReason,
|
|
409
|
+
originalBytes: prepared.originalBytes,
|
|
410
|
+
outputBytes: prepared.outputBytes,
|
|
411
|
+
filename: prepared.filename,
|
|
412
|
+
},
|
|
413
|
+
frame: prepared.frame,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
return { ok: false, file, err };
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
const uploads = [];
|
|
422
|
+
const failures = [];
|
|
423
|
+
let firstError;
|
|
424
|
+
for (const slot of slots) {
|
|
425
|
+
if (slot.ok)
|
|
426
|
+
uploads.push(slot.upload);
|
|
427
|
+
else {
|
|
428
|
+
firstError ??= slot.err;
|
|
429
|
+
failures.push({ file: slot.file, error: errorDetail(slot.err) });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return { uploads, failures, firstError };
|
|
433
|
+
}
|
|
434
|
+
function errorDetail(err) {
|
|
435
|
+
if (err instanceof UploadsError)
|
|
436
|
+
return { message: err.message, code: err.code, status: err.status };
|
|
437
|
+
return { message: err instanceof Error ? err.message : String(err) };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Prepare + put each path with put-style key resolution and bounded concurrency.
|
|
441
|
+
* Same partial-failure shape as uploadAttachments.
|
|
442
|
+
*/
|
|
443
|
+
export async function uploadPuts(opts) {
|
|
444
|
+
if (opts.files.length > 1 && opts.files.some((f) => f === "-")) {
|
|
445
|
+
throw new UsageError("stdin (-) cannot be combined with multiple file arguments");
|
|
446
|
+
}
|
|
447
|
+
if (opts.files.length > 1 && opts.explicitKey) {
|
|
448
|
+
throw new UsageError("--key cannot be combined with multiple files");
|
|
449
|
+
}
|
|
450
|
+
if (opts.files.length > 1 && opts.nameOverride) {
|
|
451
|
+
throw new UsageError("--name cannot be combined with multiple files");
|
|
452
|
+
}
|
|
453
|
+
const slots = await mapBounded(opts.files, opts.concurrency ?? UPLOAD_BATCH_CONCURRENCY, async (file) => {
|
|
454
|
+
try {
|
|
455
|
+
const sourceName = opts.nameOverride ??
|
|
456
|
+
(file === "-"
|
|
457
|
+
? opts.explicitKey
|
|
458
|
+
? basename(opts.explicitKey)
|
|
459
|
+
: "stdin.bin"
|
|
460
|
+
: basename(file));
|
|
461
|
+
const { result, prepared, markdown } = await uploadPreparedImage(opts.client, readFileArg(file), sourceName, {
|
|
462
|
+
frame: opts.frame,
|
|
463
|
+
optimize: opts.optimize,
|
|
464
|
+
ghTarget: opts.ghTarget,
|
|
465
|
+
key: opts.explicitKey,
|
|
466
|
+
prefix: opts.prefix,
|
|
467
|
+
repo: opts.repo,
|
|
468
|
+
ref: opts.ref,
|
|
469
|
+
deriveRepoFromGit: opts.deriveRepoFromGit,
|
|
470
|
+
contentType: opts.contentType,
|
|
471
|
+
dryRun: opts.dryRun,
|
|
472
|
+
metadata: opts.metadata,
|
|
473
|
+
provenanceClient: opts.provenanceClient,
|
|
474
|
+
alt: () => opts.alt ?? basename(sourceName),
|
|
475
|
+
width: opts.width,
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
upload: {
|
|
480
|
+
...result,
|
|
481
|
+
file,
|
|
482
|
+
markdown,
|
|
483
|
+
optimize: {
|
|
484
|
+
optimized: prepared.optimized,
|
|
485
|
+
skippedReason: prepared.skippedReason,
|
|
486
|
+
originalBytes: prepared.originalBytes,
|
|
487
|
+
outputBytes: prepared.outputBytes,
|
|
488
|
+
filename: prepared.filename,
|
|
489
|
+
},
|
|
490
|
+
frame: prepared.frame,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
return { ok: false, file, err };
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
const uploads = [];
|
|
499
|
+
const failures = [];
|
|
500
|
+
let firstError;
|
|
501
|
+
for (const slot of slots) {
|
|
502
|
+
if (slot.ok)
|
|
503
|
+
uploads.push(slot.upload);
|
|
504
|
+
else {
|
|
505
|
+
firstError ??= slot.err;
|
|
506
|
+
failures.push({ file: slot.file, error: errorDetail(slot.err) });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { uploads, failures, firstError };
|
|
510
|
+
}
|
|
313
511
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
314
512
|
const parsed = parseCommandArgs(args);
|
|
315
513
|
if (help || parsed.help) {
|
|
@@ -339,53 +537,28 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
339
537
|
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
340
538
|
if (Object.keys(metadata).length > 0)
|
|
341
539
|
validateMetaMap(metadata);
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const result = await ctx.client.put(prepared.bytes, {
|
|
360
|
-
filename: prepared.filename,
|
|
361
|
-
key: ghAttachmentKey(target, prepared.filename),
|
|
362
|
-
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
363
|
-
provenance: buildCliProvenance({
|
|
364
|
-
sourceName,
|
|
365
|
-
optimized: prepared.optimized,
|
|
366
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
367
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
368
|
-
}),
|
|
369
|
-
metadata,
|
|
370
|
-
});
|
|
371
|
-
writeReplacedNote(result.replaced, ctx.quiet || ctx.json);
|
|
372
|
-
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
373
|
-
results.push({
|
|
374
|
-
...result,
|
|
375
|
-
markdown: buildMarkdown(embedSrc, { alt: sourceName }),
|
|
376
|
-
optimize: {
|
|
377
|
-
optimized: prepared.optimized,
|
|
378
|
-
skippedReason: prepared.skippedReason,
|
|
379
|
-
originalBytes: prepared.originalBytes,
|
|
380
|
-
outputBytes: prepared.outputBytes,
|
|
381
|
-
filename: prepared.filename,
|
|
382
|
-
},
|
|
383
|
-
frame: prepared.frame,
|
|
384
|
-
});
|
|
540
|
+
const logHuman = !ctx.quiet && !ctx.json;
|
|
541
|
+
if (logHuman) {
|
|
542
|
+
const n = parsed.positionals.length;
|
|
543
|
+
process.stderr.write(`>> uploading ${n} file${n === 1 ? "" : "s"}\n`);
|
|
544
|
+
}
|
|
545
|
+
const { uploads, failures, firstError } = await uploadAttachments({
|
|
546
|
+
client: ctx.client,
|
|
547
|
+
target,
|
|
548
|
+
files: parsed.positionals,
|
|
549
|
+
contentType: contentTypeOverride,
|
|
550
|
+
optimize: optimizeOpts,
|
|
551
|
+
frame: frameOpts,
|
|
552
|
+
metadata,
|
|
553
|
+
});
|
|
554
|
+
// Single-file total failure: rethrow so CLI exit codes stay auth/network-aware.
|
|
555
|
+
if (uploads.length === 0 && failures.length === 1 && parsed.positionals.length === 1) {
|
|
556
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
385
557
|
}
|
|
386
558
|
let comment;
|
|
387
559
|
let commentError;
|
|
388
|
-
|
|
560
|
+
// Skip comment refresh when every upload failed — nothing new from this batch.
|
|
561
|
+
if (!parsed.flags.has("--no-comment") && uploads.length > 0) {
|
|
389
562
|
try {
|
|
390
563
|
comment = await syncAttachmentsComment(ctx.client, target, run);
|
|
391
564
|
}
|
|
@@ -395,22 +568,33 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
395
568
|
}
|
|
396
569
|
}
|
|
397
570
|
if (ctx.json) {
|
|
398
|
-
await writeJson({ target, uploads
|
|
571
|
+
await writeJson({ target, uploads, failures, comment, commentError });
|
|
399
572
|
}
|
|
400
573
|
else {
|
|
401
|
-
for (const result of
|
|
574
|
+
for (const result of uploads) {
|
|
575
|
+
if (logHuman) {
|
|
576
|
+
if (result.frame?.framed) {
|
|
577
|
+
process.stderr.write(`>> ${basename(result.file)}: framed with ${result.frame.frameId}\n`);
|
|
578
|
+
}
|
|
579
|
+
const note = formatOptimizeNote(result.optimize);
|
|
580
|
+
if (note)
|
|
581
|
+
process.stderr.write(`>> ${basename(result.file)}: ${note}\n`);
|
|
582
|
+
writeReplacedNote(result.replaced, false);
|
|
583
|
+
}
|
|
402
584
|
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
403
585
|
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
404
586
|
}
|
|
587
|
+
for (const failure of failures) {
|
|
588
|
+
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
589
|
+
}
|
|
405
590
|
if (!ctx.quiet && comment)
|
|
406
591
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
407
|
-
|
|
408
|
-
if (!ctx.quiet) {
|
|
592
|
+
if (!ctx.quiet && uploads.length > 0) {
|
|
409
593
|
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
410
594
|
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
411
595
|
}
|
|
412
596
|
}
|
|
413
|
-
return 0;
|
|
597
|
+
return failures.length === 0 ? 0 : 1;
|
|
414
598
|
}
|
|
415
599
|
export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
416
600
|
if (help) {
|
|
@@ -422,11 +606,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
422
606
|
writeCommandHelp(PUT_HELP);
|
|
423
607
|
return 0;
|
|
424
608
|
}
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
609
|
+
const files = parsed.positionals;
|
|
610
|
+
if (files.length === 0) {
|
|
427
611
|
writeCommandHelp(PUT_HELP);
|
|
428
612
|
return 2;
|
|
429
613
|
}
|
|
614
|
+
const multi = files.length > 1;
|
|
430
615
|
const keyHint = flagString(parsed.flags, "--key");
|
|
431
616
|
const destFlag = flagString(parsed.flags, "--destination");
|
|
432
617
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
@@ -451,6 +636,15 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
451
636
|
}
|
|
452
637
|
if (wantComment && !ghTarget)
|
|
453
638
|
throw new UsageError("--comment requires --pr or --issue");
|
|
639
|
+
if (multi) {
|
|
640
|
+
if (keyHint)
|
|
641
|
+
throw new UsageError("--key cannot be combined with multiple files");
|
|
642
|
+
if (nameFlag !== undefined)
|
|
643
|
+
throw new UsageError("--name cannot be combined with multiple files");
|
|
644
|
+
if (files.some((f) => f === "-")) {
|
|
645
|
+
throw new UsageError("stdin (-) cannot be combined with multiple file arguments");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
454
648
|
if (ghTarget) {
|
|
455
649
|
if (keyHint) {
|
|
456
650
|
throw new UsageError("--key cannot be combined with --pr/--issue; use --name <leaf> to set a clean filename on the stable path");
|
|
@@ -486,8 +680,6 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
486
680
|
catch (err) {
|
|
487
681
|
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
488
682
|
}
|
|
489
|
-
const bytes = readFileArg(fileArg);
|
|
490
|
-
const sourceName = nameFlag ?? (fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg));
|
|
491
683
|
const format = ctx.json
|
|
492
684
|
? "json"
|
|
493
685
|
: (() => {
|
|
@@ -501,14 +693,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
501
693
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
502
694
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
503
695
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
504
|
-
// Optimize even on --dry-run so the preview key extension/hash match a real put.
|
|
505
|
-
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
506
|
-
...frameOpts,
|
|
507
|
-
optimize: optimizeOpts,
|
|
508
|
-
});
|
|
509
|
-
const filename = prepared.filename;
|
|
510
696
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
511
|
-
const
|
|
697
|
+
const altFlag = flagString(parsed.flags, "--alt");
|
|
512
698
|
const widthRaw = flagString(parsed.flags, "--width");
|
|
513
699
|
const width = widthRaw && /^\d+$/.test(widthRaw) && Number(widthRaw) > 0
|
|
514
700
|
? Number.parseInt(widthRaw, 10)
|
|
@@ -517,14 +703,6 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
517
703
|
throw new UsageError(`invalid --width: ${widthRaw}`);
|
|
518
704
|
})()
|
|
519
705
|
: defaults.width;
|
|
520
|
-
if (!ctx.quiet && format === "human") {
|
|
521
|
-
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
522
|
-
if (prepared.frame?.framed)
|
|
523
|
-
process.stderr.write(`>> framed with ${prepared.frame.frameId}\n`);
|
|
524
|
-
const note = formatOptimizeNote(prepared);
|
|
525
|
-
if (note)
|
|
526
|
-
process.stderr.write(`>> ${note}\n`);
|
|
527
|
-
}
|
|
528
706
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
529
707
|
// gh.* metadata: explicit --pr/--issue target wins over --meta; otherwise
|
|
530
708
|
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
@@ -560,66 +738,144 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
560
738
|
}
|
|
561
739
|
}
|
|
562
740
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
741
|
+
const logHuman = !ctx.quiet && format === "human";
|
|
742
|
+
if (logHuman) {
|
|
743
|
+
if (multi) {
|
|
744
|
+
process.stderr.write(`>> ${dryRun ? "dry run for" : "uploading"} ${files.length} files\n`);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
const fileArg = files[0];
|
|
748
|
+
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
749
|
+
}
|
|
750
|
+
if (attachedRef)
|
|
751
|
+
process.stderr.write(`>> attached to ${attachedRef}\n`);
|
|
752
|
+
}
|
|
753
|
+
const { uploads, failures, firstError } = await uploadPuts({
|
|
754
|
+
client: ctx.client,
|
|
755
|
+
files,
|
|
756
|
+
nameOverride: nameFlag,
|
|
757
|
+
explicitKey: keyHint,
|
|
758
|
+
ghTarget,
|
|
572
759
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
573
760
|
repo: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
574
761
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
575
|
-
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
576
762
|
deriveRepoFromGit: !noGit,
|
|
763
|
+
contentType: contentTypeOverride,
|
|
577
764
|
dryRun,
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
optimized: prepared.optimized,
|
|
581
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
582
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
583
|
-
}),
|
|
765
|
+
optimize: optimizeOpts,
|
|
766
|
+
frame: frameOpts,
|
|
584
767
|
metadata,
|
|
768
|
+
alt: altFlag,
|
|
769
|
+
width,
|
|
585
770
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
771
|
+
// Single-file total failure: rethrow so CLI exit codes stay auth/network-aware.
|
|
772
|
+
if (uploads.length === 0 && failures.length > 0 && !multi) {
|
|
773
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
774
|
+
}
|
|
775
|
+
const galleriesByKey = new Map();
|
|
776
|
+
let galleryHadError = false;
|
|
777
|
+
if (galleryId && uploads.length > 0) {
|
|
778
|
+
for (const upload of uploads) {
|
|
779
|
+
const alt = altFlag ?? basename(upload.file === "-" ? upload.optimize.filename : upload.file);
|
|
780
|
+
try {
|
|
781
|
+
// Gallery mutations use optimistic versions. Re-fetch before each add.
|
|
782
|
+
const current = await ctx.client.getGallery(galleryId);
|
|
783
|
+
const item = await ctx.client.addGalleryItem(galleryId, upload.key, {
|
|
784
|
+
expectedVersion: current.version,
|
|
785
|
+
altText: alt,
|
|
786
|
+
});
|
|
787
|
+
galleriesByKey.set(upload.key, { id: galleryId, url: current.url, item });
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
galleryHadError = true;
|
|
791
|
+
galleriesByKey.set(upload.key, { id: galleryId, error: errorDetail(err) });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
let comment;
|
|
796
|
+
let commentError;
|
|
797
|
+
if (wantComment && ghTarget && uploads.length > 0) {
|
|
592
798
|
try {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const item = await ctx.client.addGalleryItem(galleryId, result.key, {
|
|
597
|
-
expectedVersion: current.version,
|
|
598
|
-
altText: alt,
|
|
599
|
-
});
|
|
600
|
-
gallery = { id: galleryId, url: current.url, item };
|
|
799
|
+
comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
800
|
+
if (logHuman)
|
|
801
|
+
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
601
802
|
}
|
|
602
803
|
catch (err) {
|
|
603
|
-
|
|
804
|
+
commentError = err instanceof Error ? err.message : String(err);
|
|
805
|
+
process.stderr.write(`warning: upload succeeded but the GitHub comment failed (is gh installed and authenticated?): ${commentError}\n`);
|
|
604
806
|
}
|
|
605
807
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
808
|
+
// --- multi-file output ---
|
|
809
|
+
if (multi) {
|
|
810
|
+
if (format === "json") {
|
|
811
|
+
await writeJson({
|
|
812
|
+
uploads: uploads.map((u) => ({
|
|
813
|
+
...u,
|
|
814
|
+
gallery: galleriesByKey.get(u.key),
|
|
815
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
816
|
+
})),
|
|
817
|
+
failures,
|
|
818
|
+
comment,
|
|
819
|
+
commentError,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
for (const result of uploads) {
|
|
824
|
+
if (logHuman) {
|
|
825
|
+
if (result.frame?.framed) {
|
|
826
|
+
process.stderr.write(`>> ${basename(result.file)}: framed with ${result.frame.frameId}\n`);
|
|
827
|
+
}
|
|
828
|
+
const note = formatOptimizeNote(result.optimize);
|
|
829
|
+
if (note)
|
|
830
|
+
process.stderr.write(`>> ${basename(result.file)}: ${note}\n`);
|
|
831
|
+
writeReplacedNote(result.replaced, false, dryRun);
|
|
832
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n`);
|
|
833
|
+
}
|
|
834
|
+
const gallery = galleriesByKey.get(result.key);
|
|
835
|
+
if (format === "url")
|
|
836
|
+
await writeStdout(`${result.url}\n`);
|
|
837
|
+
else if (format === "markdown")
|
|
838
|
+
await writeStdout(`${result.markdown}\n`);
|
|
839
|
+
else {
|
|
840
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
841
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
842
|
+
}
|
|
843
|
+
if (gallery?.error) {
|
|
844
|
+
process.stderr.write(`warning: upload succeeded but adding ${result.key} to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const failure of failures) {
|
|
848
|
+
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
852
|
+
}
|
|
853
|
+
// --- single-file output (flat JSON shape, back-compat) ---
|
|
854
|
+
const result = uploads[0];
|
|
855
|
+
const gallery = galleriesByKey.get(result.key);
|
|
856
|
+
if (logHuman) {
|
|
857
|
+
if (result.frame?.framed) {
|
|
858
|
+
process.stderr.write(`>> framed with ${result.frame.frameId}\n`);
|
|
859
|
+
}
|
|
860
|
+
const note = formatOptimizeNote(result.optimize);
|
|
861
|
+
if (note)
|
|
862
|
+
process.stderr.write(`>> ${note}\n`);
|
|
863
|
+
writeReplacedNote(result.replaced, ctx.quiet, dryRun);
|
|
614
864
|
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
|
|
615
865
|
}
|
|
616
866
|
switch (format) {
|
|
617
867
|
case "json":
|
|
618
868
|
await writeJson({
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
869
|
+
workspace: result.workspace,
|
|
870
|
+
key: result.key,
|
|
871
|
+
url: result.url,
|
|
872
|
+
embedUrl: result.embedUrl,
|
|
873
|
+
size: result.size,
|
|
874
|
+
contentType: result.contentType,
|
|
875
|
+
replaced: result.replaced,
|
|
876
|
+
markdown: result.markdown,
|
|
877
|
+
optimize: result.optimize,
|
|
878
|
+
frame: result.frame,
|
|
623
879
|
gallery,
|
|
624
880
|
...(dryRun ? { dryRun: true } : {}),
|
|
625
881
|
});
|
|
@@ -628,11 +884,11 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
628
884
|
await writeStdout(`${result.url}\n`);
|
|
629
885
|
break;
|
|
630
886
|
case "markdown":
|
|
631
|
-
await writeStdout(`${markdown}\n`);
|
|
887
|
+
await writeStdout(`${result.markdown}\n`);
|
|
632
888
|
break;
|
|
633
889
|
default: {
|
|
634
890
|
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
635
|
-
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
891
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
636
892
|
}
|
|
637
893
|
}
|
|
638
894
|
if (gallery?.url && format !== "human") {
|
|
@@ -641,25 +897,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
641
897
|
if (gallery?.error) {
|
|
642
898
|
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
643
899
|
}
|
|
644
|
-
if (wantComment && ghTarget) {
|
|
645
|
-
try {
|
|
646
|
-
const sync = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
647
|
-
if (!ctx.quiet && format === "human") {
|
|
648
|
-
process.stderr.write(`>> attachments comment ${sync.action}\n`);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
catch (err) {
|
|
652
|
-
// Upload already succeeded; the comment is best-effort by design.
|
|
653
|
-
process.stderr.write(`warning: upload succeeded but the GitHub comment failed (is gh installed and authenticated?): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
900
|
return gallery?.error ? 1 : 0;
|
|
657
901
|
}
|
|
658
|
-
function galleryError(err) {
|
|
659
|
-
if (err instanceof UploadsError)
|
|
660
|
-
return { message: err.message, code: err.code, status: err.status };
|
|
661
|
-
return { message: err instanceof Error ? err.message : String(err) };
|
|
662
|
-
}
|
|
663
902
|
// --- galleries ---
|
|
664
903
|
const GALLERY_HELP = `uploads gallery <command> [args]
|
|
665
904
|
|
|
@@ -850,7 +1089,7 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
850
1089
|
}));
|
|
851
1090
|
}
|
|
852
1091
|
catch (err) {
|
|
853
|
-
failures.push({ objectKey, error:
|
|
1092
|
+
failures.push({ objectKey, error: errorDetail(err) });
|
|
854
1093
|
}
|
|
855
1094
|
}
|
|
856
1095
|
const output = { galleryId: id, galleryUrl: galleryUrl ?? null, added, failures };
|
|
@@ -1223,8 +1462,52 @@ Examples:
|
|
|
1223
1462
|
uploads --workspace acme --env-file .env doctor
|
|
1224
1463
|
uploads doctor --json
|
|
1225
1464
|
`;
|
|
1465
|
+
/**
|
|
1466
|
+
* Best-effort local-browser detection for doctor. fs scans only, no browser
|
|
1467
|
+
* launch. Guarded for non-Node runtimes as a precaution for any future
|
|
1468
|
+
* non-Node consumer of this module — apps/mcp today only imports
|
|
1469
|
+
* `buildMarkdown`/`buildScreenshotKey` from the package root, not
|
|
1470
|
+
* `buildDoctorReport`, so this guard isn't exercised on that path currently.
|
|
1471
|
+
*/
|
|
1472
|
+
async function detectBrowserForDoctor(detectRoots) {
|
|
1473
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
1474
|
+
return {
|
|
1475
|
+
supported: false,
|
|
1476
|
+
found: false,
|
|
1477
|
+
autoBackend: "remote",
|
|
1478
|
+
candidates: [],
|
|
1479
|
+
note: "browser detection is not supported in this runtime",
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
const { detectLocalBrowser } = await import("./screenshot-local.js");
|
|
1484
|
+
const { candidates, winner } = detectLocalBrowser(detectRoots);
|
|
1485
|
+
return {
|
|
1486
|
+
supported: true,
|
|
1487
|
+
found: Boolean(winner),
|
|
1488
|
+
autoBackend: winner ? "local" : "remote",
|
|
1489
|
+
candidates: candidates.map((c) => ({
|
|
1490
|
+
source: c.source,
|
|
1491
|
+
kind: c.kind,
|
|
1492
|
+
executablePath: c.executablePath,
|
|
1493
|
+
})),
|
|
1494
|
+
winner: winner
|
|
1495
|
+
? { source: winner.source, kind: winner.kind, executablePath: winner.executablePath }
|
|
1496
|
+
: undefined,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
catch (err) {
|
|
1500
|
+
return {
|
|
1501
|
+
supported: true,
|
|
1502
|
+
found: false,
|
|
1503
|
+
autoBackend: "remote",
|
|
1504
|
+
candidates: [],
|
|
1505
|
+
note: err instanceof Error ? err.message : String(err),
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1226
1509
|
/** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
|
|
1227
|
-
export async function buildDoctorReport(config, client) {
|
|
1510
|
+
export async function buildDoctorReport(config, client, detectRoots) {
|
|
1228
1511
|
const mismatch = workspaceMismatch(config);
|
|
1229
1512
|
const hints = [];
|
|
1230
1513
|
if (mismatch)
|
|
@@ -1232,7 +1515,12 @@ export async function buildDoctorReport(config, client) {
|
|
|
1232
1515
|
if (config.apiUrl.includes("localhost") || config.apiUrl.includes("127.0.0.1")) {
|
|
1233
1516
|
hints.push("local API uses dev KV — prod tokens won't work unless minted with --local");
|
|
1234
1517
|
}
|
|
1235
|
-
|
|
1518
|
+
// Independent checks — fs-based browser detection doesn't depend on the
|
|
1519
|
+
// network health probe (or vice versa).
|
|
1520
|
+
const [browser, health] = await Promise.all([
|
|
1521
|
+
detectBrowserForDoctor(detectRoots),
|
|
1522
|
+
client.health(),
|
|
1523
|
+
]);
|
|
1236
1524
|
let authOk = false;
|
|
1237
1525
|
let authError;
|
|
1238
1526
|
try {
|
|
@@ -1280,6 +1568,7 @@ export async function buildDoctorReport(config, client) {
|
|
|
1280
1568
|
usage,
|
|
1281
1569
|
warning: mismatch,
|
|
1282
1570
|
hints,
|
|
1571
|
+
browser,
|
|
1283
1572
|
};
|
|
1284
1573
|
}
|
|
1285
1574
|
export async function runDoctor(ctx, args, help = false) {
|
|
@@ -1304,6 +1593,14 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
1304
1593
|
? `usage: ${formatByteSize(report.usage.bytes ?? 0)}, ${formatCount(report.usage.objects ?? 0)} objects, ${formatCount(report.usage.uploadsInPeriod ?? 0)} uploads this period`
|
|
1305
1594
|
: `usage: failed — ${report.usage.error ?? "unknown"}`);
|
|
1306
1595
|
}
|
|
1596
|
+
if (report.browser.supported) {
|
|
1597
|
+
lines.push(report.browser.found
|
|
1598
|
+
? `browser: found (${report.browser.winner?.source}/${report.browser.winner?.kind}) — screenshot --via auto uses local`
|
|
1599
|
+
: `browser: none found — screenshot --via auto uses remote`);
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
lines.push(`browser: ${report.browser.note ?? "not supported in this runtime"}`);
|
|
1603
|
+
}
|
|
1307
1604
|
if (report.warning)
|
|
1308
1605
|
lines.push(`warning: ${report.warning}`);
|
|
1309
1606
|
for (const h of report.hints)
|