@buildinternet/uploads 0.11.0 → 0.11.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 +2 -2
- package/dist/async.d.ts +5 -0
- package/dist/async.js +21 -0
- package/dist/cli-catalog.js +1 -1
- package/dist/commands/install.js +5 -0
- package/dist/commands.d.ts +91 -1
- package/dist/commands.js +350 -139
- 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 +1 -1
- package/dist/mcp/tools.js +170 -94
- package/package.json +1 -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
|
|
@@ -271,6 +281,9 @@ const ATTACH_HELP = `uploads attach <file...> [options]
|
|
|
271
281
|
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
272
282
|
comment. With no target, uses the pull request for the current branch.
|
|
273
283
|
|
|
284
|
+
Multiple files upload in parallel (bounded concurrency). One bad file does not
|
|
285
|
+
block the rest; JSON includes uploads + failures (exit 1 when any failed).
|
|
286
|
+
|
|
274
287
|
Attachments are public and their repo/number/filename keys are predictable.
|
|
275
288
|
Private/internal GitHub repository visibility does not restrict access; upload
|
|
276
289
|
only media that is safe at a public URL.
|
|
@@ -310,6 +323,163 @@ Examples:
|
|
|
310
323
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
311
324
|
uploads attach ./shot.png --meta app=myapp --meta page=settings
|
|
312
325
|
`;
|
|
326
|
+
/**
|
|
327
|
+
* Prepare + put each path as a PR/issue attachment with bounded concurrency.
|
|
328
|
+
* Per-file errors collect in `failures` (does not throw). `firstError` is the
|
|
329
|
+
* original cause of the first failure — for rethrowing single-file CLI paths.
|
|
330
|
+
*/
|
|
331
|
+
export async function uploadAttachments(opts) {
|
|
332
|
+
if (opts.files.some((f) => f === "-")) {
|
|
333
|
+
throw new UsageError("attach does not support stdin; pass one or more file paths");
|
|
334
|
+
}
|
|
335
|
+
const slots = await mapBounded(opts.files, opts.concurrency ?? UPLOAD_BATCH_CONCURRENCY, async (file) => {
|
|
336
|
+
try {
|
|
337
|
+
const sourceName = basename(file);
|
|
338
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
339
|
+
...opts.frame,
|
|
340
|
+
optimize: opts.optimize,
|
|
341
|
+
});
|
|
342
|
+
const result = await opts.client.put(prepared.bytes, {
|
|
343
|
+
filename: prepared.filename,
|
|
344
|
+
key: ghAttachmentKey(opts.target, prepared.filename),
|
|
345
|
+
contentType: prepared.optimized ? prepared.contentType : opts.contentType,
|
|
346
|
+
provenance: buildCliProvenance({
|
|
347
|
+
sourceName,
|
|
348
|
+
client: opts.provenanceClient,
|
|
349
|
+
optimized: prepared.optimized,
|
|
350
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
351
|
+
keepExif: opts.optimize.keepExif === true,
|
|
352
|
+
}),
|
|
353
|
+
metadata: opts.metadata,
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
upload: {
|
|
358
|
+
...result,
|
|
359
|
+
file,
|
|
360
|
+
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
361
|
+
alt: sourceName,
|
|
362
|
+
}),
|
|
363
|
+
optimize: {
|
|
364
|
+
optimized: prepared.optimized,
|
|
365
|
+
skippedReason: prepared.skippedReason,
|
|
366
|
+
originalBytes: prepared.originalBytes,
|
|
367
|
+
outputBytes: prepared.outputBytes,
|
|
368
|
+
filename: prepared.filename,
|
|
369
|
+
},
|
|
370
|
+
frame: prepared.frame,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
return { ok: false, file, err };
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const uploads = [];
|
|
379
|
+
const failures = [];
|
|
380
|
+
let firstError;
|
|
381
|
+
for (const slot of slots) {
|
|
382
|
+
if (slot.ok)
|
|
383
|
+
uploads.push(slot.upload);
|
|
384
|
+
else {
|
|
385
|
+
firstError ??= slot.err;
|
|
386
|
+
failures.push({ file: slot.file, error: errorDetail(slot.err) });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { uploads, failures, firstError };
|
|
390
|
+
}
|
|
391
|
+
function errorDetail(err) {
|
|
392
|
+
if (err instanceof UploadsError)
|
|
393
|
+
return { message: err.message, code: err.code, status: err.status };
|
|
394
|
+
return { message: err instanceof Error ? err.message : String(err) };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Prepare + put each path with put-style key resolution and bounded concurrency.
|
|
398
|
+
* Same partial-failure shape as uploadAttachments.
|
|
399
|
+
*/
|
|
400
|
+
export async function uploadPuts(opts) {
|
|
401
|
+
if (opts.files.length > 1 && opts.files.some((f) => f === "-")) {
|
|
402
|
+
throw new UsageError("stdin (-) cannot be combined with multiple file arguments");
|
|
403
|
+
}
|
|
404
|
+
if (opts.files.length > 1 && opts.explicitKey) {
|
|
405
|
+
throw new UsageError("--key cannot be combined with multiple files");
|
|
406
|
+
}
|
|
407
|
+
if (opts.files.length > 1 && opts.nameOverride) {
|
|
408
|
+
throw new UsageError("--name cannot be combined with multiple files");
|
|
409
|
+
}
|
|
410
|
+
const slots = await mapBounded(opts.files, opts.concurrency ?? UPLOAD_BATCH_CONCURRENCY, async (file) => {
|
|
411
|
+
try {
|
|
412
|
+
const sourceName = opts.nameOverride ??
|
|
413
|
+
(file === "-"
|
|
414
|
+
? opts.explicitKey
|
|
415
|
+
? basename(opts.explicitKey)
|
|
416
|
+
: "stdin.bin"
|
|
417
|
+
: basename(file));
|
|
418
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
419
|
+
...opts.frame,
|
|
420
|
+
optimize: opts.optimize,
|
|
421
|
+
});
|
|
422
|
+
let key = opts.ghTarget
|
|
423
|
+
? ghAttachmentKey(opts.ghTarget, prepared.filename)
|
|
424
|
+
: opts.explicitKey;
|
|
425
|
+
if (key && prepared.optimized)
|
|
426
|
+
key = rewriteKeyExtension(key, prepared.filename);
|
|
427
|
+
const result = await opts.client.put(prepared.bytes, {
|
|
428
|
+
filename: prepared.filename,
|
|
429
|
+
key,
|
|
430
|
+
prefix: opts.prefix,
|
|
431
|
+
repo: opts.repo,
|
|
432
|
+
ref: opts.ref,
|
|
433
|
+
contentType: prepared.optimized ? prepared.contentType : opts.contentType,
|
|
434
|
+
deriveRepoFromGit: opts.deriveRepoFromGit,
|
|
435
|
+
dryRun: opts.dryRun,
|
|
436
|
+
provenance: buildCliProvenance({
|
|
437
|
+
sourceName,
|
|
438
|
+
client: opts.provenanceClient,
|
|
439
|
+
optimized: prepared.optimized,
|
|
440
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
441
|
+
keepExif: opts.optimize.keepExif === true,
|
|
442
|
+
}),
|
|
443
|
+
metadata: opts.metadata,
|
|
444
|
+
});
|
|
445
|
+
const alt = opts.alt ?? basename(sourceName);
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
upload: {
|
|
449
|
+
...result,
|
|
450
|
+
file,
|
|
451
|
+
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
452
|
+
alt,
|
|
453
|
+
width: opts.width,
|
|
454
|
+
}),
|
|
455
|
+
optimize: {
|
|
456
|
+
optimized: prepared.optimized,
|
|
457
|
+
skippedReason: prepared.skippedReason,
|
|
458
|
+
originalBytes: prepared.originalBytes,
|
|
459
|
+
outputBytes: prepared.outputBytes,
|
|
460
|
+
filename: prepared.filename,
|
|
461
|
+
},
|
|
462
|
+
frame: prepared.frame,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
return { ok: false, file, err };
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const uploads = [];
|
|
471
|
+
const failures = [];
|
|
472
|
+
let firstError;
|
|
473
|
+
for (const slot of slots) {
|
|
474
|
+
if (slot.ok)
|
|
475
|
+
uploads.push(slot.upload);
|
|
476
|
+
else {
|
|
477
|
+
firstError ??= slot.err;
|
|
478
|
+
failures.push({ file: slot.file, error: errorDetail(slot.err) });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { uploads, failures, firstError };
|
|
482
|
+
}
|
|
313
483
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
314
484
|
const parsed = parseCommandArgs(args);
|
|
315
485
|
if (help || parsed.help) {
|
|
@@ -339,53 +509,28 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
339
509
|
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
340
510
|
if (Object.keys(metadata).length > 0)
|
|
341
511
|
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
|
-
});
|
|
512
|
+
const logHuman = !ctx.quiet && !ctx.json;
|
|
513
|
+
if (logHuman) {
|
|
514
|
+
const n = parsed.positionals.length;
|
|
515
|
+
process.stderr.write(`>> uploading ${n} file${n === 1 ? "" : "s"}\n`);
|
|
516
|
+
}
|
|
517
|
+
const { uploads, failures, firstError } = await uploadAttachments({
|
|
518
|
+
client: ctx.client,
|
|
519
|
+
target,
|
|
520
|
+
files: parsed.positionals,
|
|
521
|
+
contentType: contentTypeOverride,
|
|
522
|
+
optimize: optimizeOpts,
|
|
523
|
+
frame: frameOpts,
|
|
524
|
+
metadata,
|
|
525
|
+
});
|
|
526
|
+
// Single-file total failure: rethrow so CLI exit codes stay auth/network-aware.
|
|
527
|
+
if (uploads.length === 0 && failures.length === 1 && parsed.positionals.length === 1) {
|
|
528
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
385
529
|
}
|
|
386
530
|
let comment;
|
|
387
531
|
let commentError;
|
|
388
|
-
|
|
532
|
+
// Skip comment refresh when every upload failed — nothing new from this batch.
|
|
533
|
+
if (!parsed.flags.has("--no-comment") && uploads.length > 0) {
|
|
389
534
|
try {
|
|
390
535
|
comment = await syncAttachmentsComment(ctx.client, target, run);
|
|
391
536
|
}
|
|
@@ -395,22 +540,33 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
395
540
|
}
|
|
396
541
|
}
|
|
397
542
|
if (ctx.json) {
|
|
398
|
-
await writeJson({ target, uploads
|
|
543
|
+
await writeJson({ target, uploads, failures, comment, commentError });
|
|
399
544
|
}
|
|
400
545
|
else {
|
|
401
|
-
for (const result of
|
|
546
|
+
for (const result of uploads) {
|
|
547
|
+
if (logHuman) {
|
|
548
|
+
if (result.frame?.framed) {
|
|
549
|
+
process.stderr.write(`>> ${basename(result.file)}: framed with ${result.frame.frameId}\n`);
|
|
550
|
+
}
|
|
551
|
+
const note = formatOptimizeNote(result.optimize);
|
|
552
|
+
if (note)
|
|
553
|
+
process.stderr.write(`>> ${basename(result.file)}: ${note}\n`);
|
|
554
|
+
writeReplacedNote(result.replaced, false);
|
|
555
|
+
}
|
|
402
556
|
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
403
557
|
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
404
558
|
}
|
|
559
|
+
for (const failure of failures) {
|
|
560
|
+
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
561
|
+
}
|
|
405
562
|
if (!ctx.quiet && comment)
|
|
406
563
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
407
|
-
|
|
408
|
-
if (!ctx.quiet) {
|
|
564
|
+
if (!ctx.quiet && uploads.length > 0) {
|
|
409
565
|
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
410
566
|
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
411
567
|
}
|
|
412
568
|
}
|
|
413
|
-
return 0;
|
|
569
|
+
return failures.length === 0 ? 0 : 1;
|
|
414
570
|
}
|
|
415
571
|
export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
416
572
|
if (help) {
|
|
@@ -422,11 +578,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
422
578
|
writeCommandHelp(PUT_HELP);
|
|
423
579
|
return 0;
|
|
424
580
|
}
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
581
|
+
const files = parsed.positionals;
|
|
582
|
+
if (files.length === 0) {
|
|
427
583
|
writeCommandHelp(PUT_HELP);
|
|
428
584
|
return 2;
|
|
429
585
|
}
|
|
586
|
+
const multi = files.length > 1;
|
|
430
587
|
const keyHint = flagString(parsed.flags, "--key");
|
|
431
588
|
const destFlag = flagString(parsed.flags, "--destination");
|
|
432
589
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
@@ -451,6 +608,15 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
451
608
|
}
|
|
452
609
|
if (wantComment && !ghTarget)
|
|
453
610
|
throw new UsageError("--comment requires --pr or --issue");
|
|
611
|
+
if (multi) {
|
|
612
|
+
if (keyHint)
|
|
613
|
+
throw new UsageError("--key cannot be combined with multiple files");
|
|
614
|
+
if (nameFlag !== undefined)
|
|
615
|
+
throw new UsageError("--name cannot be combined with multiple files");
|
|
616
|
+
if (files.some((f) => f === "-")) {
|
|
617
|
+
throw new UsageError("stdin (-) cannot be combined with multiple file arguments");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
454
620
|
if (ghTarget) {
|
|
455
621
|
if (keyHint) {
|
|
456
622
|
throw new UsageError("--key cannot be combined with --pr/--issue; use --name <leaf> to set a clean filename on the stable path");
|
|
@@ -486,8 +652,6 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
486
652
|
catch (err) {
|
|
487
653
|
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
488
654
|
}
|
|
489
|
-
const bytes = readFileArg(fileArg);
|
|
490
|
-
const sourceName = nameFlag ?? (fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg));
|
|
491
655
|
const format = ctx.json
|
|
492
656
|
? "json"
|
|
493
657
|
: (() => {
|
|
@@ -501,14 +665,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
501
665
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
502
666
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
503
667
|
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
668
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
511
|
-
const
|
|
669
|
+
const altFlag = flagString(parsed.flags, "--alt");
|
|
512
670
|
const widthRaw = flagString(parsed.flags, "--width");
|
|
513
671
|
const width = widthRaw && /^\d+$/.test(widthRaw) && Number(widthRaw) > 0
|
|
514
672
|
? Number.parseInt(widthRaw, 10)
|
|
@@ -517,14 +675,6 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
517
675
|
throw new UsageError(`invalid --width: ${widthRaw}`);
|
|
518
676
|
})()
|
|
519
677
|
: 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
678
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
529
679
|
// gh.* metadata: explicit --pr/--issue target wins over --meta; otherwise
|
|
530
680
|
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
@@ -560,66 +710,144 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
560
710
|
}
|
|
561
711
|
}
|
|
562
712
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
713
|
+
const logHuman = !ctx.quiet && format === "human";
|
|
714
|
+
if (logHuman) {
|
|
715
|
+
if (multi) {
|
|
716
|
+
process.stderr.write(`>> ${dryRun ? "dry run for" : "uploading"} ${files.length} files\n`);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
const fileArg = files[0];
|
|
720
|
+
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
721
|
+
}
|
|
722
|
+
if (attachedRef)
|
|
723
|
+
process.stderr.write(`>> attached to ${attachedRef}\n`);
|
|
724
|
+
}
|
|
725
|
+
const { uploads, failures, firstError } = await uploadPuts({
|
|
726
|
+
client: ctx.client,
|
|
727
|
+
files,
|
|
728
|
+
nameOverride: nameFlag,
|
|
729
|
+
explicitKey: keyHint,
|
|
730
|
+
ghTarget,
|
|
572
731
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
573
732
|
repo: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
574
733
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
575
|
-
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
576
734
|
deriveRepoFromGit: !noGit,
|
|
735
|
+
contentType: contentTypeOverride,
|
|
577
736
|
dryRun,
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
optimized: prepared.optimized,
|
|
581
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
582
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
583
|
-
}),
|
|
737
|
+
optimize: optimizeOpts,
|
|
738
|
+
frame: frameOpts,
|
|
584
739
|
metadata,
|
|
740
|
+
alt: altFlag,
|
|
741
|
+
width,
|
|
585
742
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
743
|
+
// Single-file total failure: rethrow so CLI exit codes stay auth/network-aware.
|
|
744
|
+
if (uploads.length === 0 && failures.length > 0 && !multi) {
|
|
745
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
746
|
+
}
|
|
747
|
+
const galleriesByKey = new Map();
|
|
748
|
+
let galleryHadError = false;
|
|
749
|
+
if (galleryId && uploads.length > 0) {
|
|
750
|
+
for (const upload of uploads) {
|
|
751
|
+
const alt = altFlag ?? basename(upload.file === "-" ? upload.optimize.filename : upload.file);
|
|
752
|
+
try {
|
|
753
|
+
// Gallery mutations use optimistic versions. Re-fetch before each add.
|
|
754
|
+
const current = await ctx.client.getGallery(galleryId);
|
|
755
|
+
const item = await ctx.client.addGalleryItem(galleryId, upload.key, {
|
|
756
|
+
expectedVersion: current.version,
|
|
757
|
+
altText: alt,
|
|
758
|
+
});
|
|
759
|
+
galleriesByKey.set(upload.key, { id: galleryId, url: current.url, item });
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
galleryHadError = true;
|
|
763
|
+
galleriesByKey.set(upload.key, { id: galleryId, error: errorDetail(err) });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
let comment;
|
|
768
|
+
let commentError;
|
|
769
|
+
if (wantComment && ghTarget && uploads.length > 0) {
|
|
592
770
|
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 };
|
|
771
|
+
comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
772
|
+
if (logHuman)
|
|
773
|
+
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
601
774
|
}
|
|
602
775
|
catch (err) {
|
|
603
|
-
|
|
776
|
+
commentError = err instanceof Error ? err.message : String(err);
|
|
777
|
+
process.stderr.write(`warning: upload succeeded but the GitHub comment failed (is gh installed and authenticated?): ${commentError}\n`);
|
|
604
778
|
}
|
|
605
779
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
780
|
+
// --- multi-file output ---
|
|
781
|
+
if (multi) {
|
|
782
|
+
if (format === "json") {
|
|
783
|
+
await writeJson({
|
|
784
|
+
uploads: uploads.map((u) => ({
|
|
785
|
+
...u,
|
|
786
|
+
gallery: galleriesByKey.get(u.key),
|
|
787
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
788
|
+
})),
|
|
789
|
+
failures,
|
|
790
|
+
comment,
|
|
791
|
+
commentError,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
for (const result of uploads) {
|
|
796
|
+
if (logHuman) {
|
|
797
|
+
if (result.frame?.framed) {
|
|
798
|
+
process.stderr.write(`>> ${basename(result.file)}: framed with ${result.frame.frameId}\n`);
|
|
799
|
+
}
|
|
800
|
+
const note = formatOptimizeNote(result.optimize);
|
|
801
|
+
if (note)
|
|
802
|
+
process.stderr.write(`>> ${basename(result.file)}: ${note}\n`);
|
|
803
|
+
writeReplacedNote(result.replaced, false, dryRun);
|
|
804
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n`);
|
|
805
|
+
}
|
|
806
|
+
const gallery = galleriesByKey.get(result.key);
|
|
807
|
+
if (format === "url")
|
|
808
|
+
await writeStdout(`${result.url}\n`);
|
|
809
|
+
else if (format === "markdown")
|
|
810
|
+
await writeStdout(`${result.markdown}\n`);
|
|
811
|
+
else {
|
|
812
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
813
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
814
|
+
}
|
|
815
|
+
if (gallery?.error) {
|
|
816
|
+
process.stderr.write(`warning: upload succeeded but adding ${result.key} to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const failure of failures) {
|
|
820
|
+
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
824
|
+
}
|
|
825
|
+
// --- single-file output (flat JSON shape, back-compat) ---
|
|
826
|
+
const result = uploads[0];
|
|
827
|
+
const gallery = galleriesByKey.get(result.key);
|
|
828
|
+
if (logHuman) {
|
|
829
|
+
if (result.frame?.framed) {
|
|
830
|
+
process.stderr.write(`>> framed with ${result.frame.frameId}\n`);
|
|
831
|
+
}
|
|
832
|
+
const note = formatOptimizeNote(result.optimize);
|
|
833
|
+
if (note)
|
|
834
|
+
process.stderr.write(`>> ${note}\n`);
|
|
835
|
+
writeReplacedNote(result.replaced, ctx.quiet, dryRun);
|
|
614
836
|
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
|
|
615
837
|
}
|
|
616
838
|
switch (format) {
|
|
617
839
|
case "json":
|
|
618
840
|
await writeJson({
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
841
|
+
workspace: result.workspace,
|
|
842
|
+
key: result.key,
|
|
843
|
+
url: result.url,
|
|
844
|
+
embedUrl: result.embedUrl,
|
|
845
|
+
size: result.size,
|
|
846
|
+
contentType: result.contentType,
|
|
847
|
+
replaced: result.replaced,
|
|
848
|
+
markdown: result.markdown,
|
|
849
|
+
optimize: result.optimize,
|
|
850
|
+
frame: result.frame,
|
|
623
851
|
gallery,
|
|
624
852
|
...(dryRun ? { dryRun: true } : {}),
|
|
625
853
|
});
|
|
@@ -628,11 +856,11 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
628
856
|
await writeStdout(`${result.url}\n`);
|
|
629
857
|
break;
|
|
630
858
|
case "markdown":
|
|
631
|
-
await writeStdout(`${markdown}\n`);
|
|
859
|
+
await writeStdout(`${result.markdown}\n`);
|
|
632
860
|
break;
|
|
633
861
|
default: {
|
|
634
862
|
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
635
|
-
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
863
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
636
864
|
}
|
|
637
865
|
}
|
|
638
866
|
if (gallery?.url && format !== "human") {
|
|
@@ -641,25 +869,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
641
869
|
if (gallery?.error) {
|
|
642
870
|
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
643
871
|
}
|
|
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
872
|
return gallery?.error ? 1 : 0;
|
|
657
873
|
}
|
|
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
874
|
// --- galleries ---
|
|
664
875
|
const GALLERY_HELP = `uploads gallery <command> [args]
|
|
665
876
|
|
|
@@ -850,7 +1061,7 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
850
1061
|
}));
|
|
851
1062
|
}
|
|
852
1063
|
catch (err) {
|
|
853
|
-
failures.push({ objectKey, error:
|
|
1064
|
+
failures.push({ objectKey, error: errorDetail(err) });
|
|
854
1065
|
}
|
|
855
1066
|
}
|
|
856
1067
|
const output = { galleryId: id, galleryUrl: galleryUrl ?? null, added, failures };
|