@buildinternet/uploads 0.5.0 → 0.6.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 +47 -12
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +5 -0
- package/dist/cli.js +69 -19
- package/dist/client.d.ts +88 -0
- package/dist/client.js +63 -0
- package/dist/commands/admin-enrollment.js +5 -0
- package/dist/commands/login.js +6 -0
- package/dist/commands/mcp.js +2 -5
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +291 -8
- package/dist/github.d.ts +23 -1
- package/dist/github.js +65 -2
- package/dist/index.d.ts +1 -1
- package/dist/mcp/tools.js +138 -0
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +19 -0
- package/dist/provenance.js +1 -15
- package/dist/update-check.d.ts +30 -0
- package/dist/update-check.js +134 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -6,12 +6,13 @@ import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./co
|
|
|
6
6
|
import { buildMarkdown } from "./embed.js";
|
|
7
7
|
import { UploadsError } from "./errors.js";
|
|
8
8
|
import { writeJson, writeStdout } from "./io.js";
|
|
9
|
-
import { ghAttachmentKey, ghKeyPrefix, attachmentsCommentBody, } from "./github.js";
|
|
9
|
+
import { ghAttachmentKey, ghKeyPrefix, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
10
10
|
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
11
11
|
import { resolvePutPrefix } from "./destinations.js";
|
|
12
12
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
13
13
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
14
14
|
import { buildCliProvenance } from "./provenance.js";
|
|
15
|
+
import { packageVersion } from "./package-version.js";
|
|
15
16
|
// --- put ---
|
|
16
17
|
const PUT_HELP = `uploads put <file> [options]
|
|
17
18
|
|
|
@@ -50,13 +51,15 @@ Options:
|
|
|
50
51
|
--format human|url|markdown|json
|
|
51
52
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
52
53
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
53
|
-
--comment With --pr/--issue:
|
|
54
|
+
--comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
|
|
55
|
+
--gallery <id> Add the uploaded object to this public gallery
|
|
54
56
|
|
|
55
57
|
Examples:
|
|
56
58
|
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
57
59
|
uploads put ./mobile.png --frame phone
|
|
58
60
|
uploads put ./ui.png --frame browser --frame-url "https://app.example/settings"
|
|
59
61
|
uploads put ./shot.png --destination screenshots
|
|
62
|
+
uploads put ./after.png --gallery gal_example
|
|
60
63
|
`;
|
|
61
64
|
/**
|
|
62
65
|
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
@@ -158,11 +161,39 @@ function frameOptionsFromFlags(flags) {
|
|
|
158
161
|
*/
|
|
159
162
|
export async function syncAttachmentsComment(client, target, run) {
|
|
160
163
|
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
|
|
161
|
-
|
|
164
|
+
const galleries = [];
|
|
165
|
+
let cursor;
|
|
166
|
+
do {
|
|
167
|
+
const page = await client.findGalleriesByReference({
|
|
168
|
+
provider: "github",
|
|
169
|
+
// GitHub references intentionally do not distinguish PRs from issues.
|
|
170
|
+
coordinate: `${target.repo.toLowerCase()}#${target.num}`,
|
|
171
|
+
cursor,
|
|
172
|
+
});
|
|
173
|
+
galleries.push(...page.galleries.map(({ id, title, url }) => ({ title, url, id })));
|
|
174
|
+
cursor = page.nextCursor ?? undefined;
|
|
175
|
+
} while (cursor);
|
|
176
|
+
const previewGalleries = await Promise.all(galleries.map(async ({ id, ...gallery }) => {
|
|
177
|
+
try {
|
|
178
|
+
const detail = await client.getGallery(id);
|
|
179
|
+
return {
|
|
180
|
+
...gallery,
|
|
181
|
+
previews: detail.items
|
|
182
|
+
.filter((item) => item.status === "available" && item.url && item.contentType?.startsWith("image/"))
|
|
183
|
+
.slice(0, 3)
|
|
184
|
+
.map((item) => ({ url: item.url, alt: item.altText ?? item.objectKey })),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// A deleted or temporarily unavailable gallery still gets a safe title link.
|
|
189
|
+
return gallery;
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
192
|
+
if (items.length === 0 && previewGalleries.length === 0)
|
|
162
193
|
return { action: "skipped", count: 0 };
|
|
163
|
-
const body = attachmentsCommentBody(items);
|
|
194
|
+
const body = attachmentsCommentBody(items, previewGalleries);
|
|
164
195
|
const { created } = upsertAttachmentsComment(target, body, run);
|
|
165
|
-
return { action: created ? "created" : "updated", count: items.length };
|
|
196
|
+
return { action: created ? "created" : "updated", count: items.length + previewGalleries.length };
|
|
166
197
|
}
|
|
167
198
|
// --- attach ---
|
|
168
199
|
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
@@ -302,6 +333,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
302
333
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
303
334
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
304
335
|
const wantComment = parsed.flags.has("--comment");
|
|
336
|
+
const galleryId = flagString(parsed.flags, "--gallery");
|
|
305
337
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
306
338
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
307
339
|
}
|
|
@@ -386,6 +418,22 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
386
418
|
}),
|
|
387
419
|
});
|
|
388
420
|
const markdown = buildMarkdown(result.url, { alt, width });
|
|
421
|
+
let gallery;
|
|
422
|
+
if (galleryId) {
|
|
423
|
+
try {
|
|
424
|
+
// Gallery mutations use optimistic versions. Fetch immediately before this
|
|
425
|
+
// mutation so `put --gallery` composes safely with other CLI writers.
|
|
426
|
+
const current = await ctx.client.getGallery(galleryId);
|
|
427
|
+
const item = await ctx.client.addGalleryItem(galleryId, result.key, {
|
|
428
|
+
expectedVersion: current.version,
|
|
429
|
+
altText: alt,
|
|
430
|
+
});
|
|
431
|
+
gallery = { id: galleryId, url: current.url, item };
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
gallery = { id: galleryId, error: galleryError(err) };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
389
437
|
const optimizeMeta = {
|
|
390
438
|
optimized: prepared.optimized,
|
|
391
439
|
skippedReason: prepared.skippedReason,
|
|
@@ -398,7 +446,13 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
398
446
|
}
|
|
399
447
|
switch (format) {
|
|
400
448
|
case "json":
|
|
401
|
-
await writeJson({
|
|
449
|
+
await writeJson({
|
|
450
|
+
...result,
|
|
451
|
+
markdown,
|
|
452
|
+
optimize: optimizeMeta,
|
|
453
|
+
frame: prepared.frame,
|
|
454
|
+
gallery,
|
|
455
|
+
});
|
|
402
456
|
break;
|
|
403
457
|
case "url":
|
|
404
458
|
await writeStdout(`${result.url}\n`);
|
|
@@ -407,7 +461,13 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
407
461
|
await writeStdout(`${markdown}\n`);
|
|
408
462
|
break;
|
|
409
463
|
default:
|
|
410
|
-
await writeStdout(`URL: ${result.url}\nMARKDOWN: ${markdown}\n`);
|
|
464
|
+
await writeStdout(`URL: ${result.url}\nMARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
465
|
+
}
|
|
466
|
+
if (gallery?.url && format !== "human") {
|
|
467
|
+
process.stderr.write(`gallery: ${gallery.url}\n`);
|
|
468
|
+
}
|
|
469
|
+
if (gallery?.error) {
|
|
470
|
+
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
411
471
|
}
|
|
412
472
|
if (wantComment && ghTarget) {
|
|
413
473
|
try {
|
|
@@ -421,7 +481,222 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
421
481
|
process.stderr.write(`warning: upload succeeded but the GitHub comment failed (is gh installed and authenticated?): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
422
482
|
}
|
|
423
483
|
}
|
|
424
|
-
return 0;
|
|
484
|
+
return gallery?.error ? 1 : 0;
|
|
485
|
+
}
|
|
486
|
+
function galleryError(err) {
|
|
487
|
+
if (err instanceof UploadsError)
|
|
488
|
+
return { message: err.message, code: err.code, status: err.status };
|
|
489
|
+
return { message: err instanceof Error ? err.message : String(err) };
|
|
490
|
+
}
|
|
491
|
+
// --- galleries ---
|
|
492
|
+
const GALLERY_HELP = `uploads gallery <command> [args]
|
|
493
|
+
|
|
494
|
+
Public galleries can be viewed by anyone who knows the URL. Do not add sensitive media.
|
|
495
|
+
Deleting a gallery only removes the gallery record; it never deletes its uploaded objects.
|
|
496
|
+
|
|
497
|
+
Commands:
|
|
498
|
+
create --title <title> [--description <text>]
|
|
499
|
+
show <gallery-id>
|
|
500
|
+
list [--limit <n>] [--cursor <c>] [--all]
|
|
501
|
+
delete <gallery-id>
|
|
502
|
+
add <gallery-id> <object-key...> [--caption <text>] [--alt <text>]
|
|
503
|
+
link <gallery-id> --github <owner/repo#number|github-url>
|
|
504
|
+
unlink <gallery-id> --github <owner/repo#number|github-url>
|
|
505
|
+
list --github <owner/repo#number|github-url> [--limit <n>] [--cursor <c>] [--all]
|
|
506
|
+
|
|
507
|
+
Examples:
|
|
508
|
+
uploads gallery create --title "Settings redesign"
|
|
509
|
+
uploads gallery add gal_example screenshots/app/after.webp --alt "Updated settings page"
|
|
510
|
+
uploads gallery show gal_example
|
|
511
|
+
uploads gallery link gal_example --github buildinternet/uploads#58
|
|
512
|
+
uploads gallery list --github https://github.com/buildinternet/uploads/pull/58
|
|
513
|
+
`;
|
|
514
|
+
function githubCoordinateFromFlags(flags) {
|
|
515
|
+
const value = flagString(flags, "--github");
|
|
516
|
+
if (!value)
|
|
517
|
+
throw new UsageError("--github requires an owner/repo#number coordinate or GitHub issue/PR URL");
|
|
518
|
+
const normalized = normalizeGithubCoordinate(value);
|
|
519
|
+
if (!normalized)
|
|
520
|
+
throw new UsageError("--github must be owner/repo#number or an https://github.com/.../issues|pull/number URL");
|
|
521
|
+
return normalized.coordinate;
|
|
522
|
+
}
|
|
523
|
+
export async function runGallery(ctx, args, help = false) {
|
|
524
|
+
const parsed = parseCommandArgs(args);
|
|
525
|
+
const action = parsed.positionals[0];
|
|
526
|
+
if (help || parsed.help || !action) {
|
|
527
|
+
process.stderr.write(GALLERY_HELP);
|
|
528
|
+
return help || parsed.help ? 0 : 2;
|
|
529
|
+
}
|
|
530
|
+
switch (action) {
|
|
531
|
+
case "create": {
|
|
532
|
+
const title = flagString(parsed.flags, "--title");
|
|
533
|
+
if (!title)
|
|
534
|
+
throw new UsageError("gallery create requires --title");
|
|
535
|
+
const gallery = await ctx.client.createGallery({
|
|
536
|
+
title,
|
|
537
|
+
description: flagString(parsed.flags, "--description"),
|
|
538
|
+
});
|
|
539
|
+
if (ctx.json)
|
|
540
|
+
await writeJson(gallery);
|
|
541
|
+
else
|
|
542
|
+
await writeStdout(`${gallery.url}\n`);
|
|
543
|
+
if (!ctx.quiet && !ctx.json)
|
|
544
|
+
process.stderr.write("warning: galleries are public to anyone with the URL\n");
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
case "show": {
|
|
548
|
+
const id = parsed.positionals[1];
|
|
549
|
+
if (!id)
|
|
550
|
+
throw new UsageError("gallery show requires a gallery ID");
|
|
551
|
+
const gallery = await ctx.client.getGallery(id);
|
|
552
|
+
if (ctx.json)
|
|
553
|
+
await writeJson(gallery);
|
|
554
|
+
else
|
|
555
|
+
await writeStdout(`${gallery.url}\n`);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
case "list": {
|
|
559
|
+
const limit = flagInt(parsed.flags, "--limit", "--limit");
|
|
560
|
+
const cursor = flagString(parsed.flags, "--cursor");
|
|
561
|
+
const github = parsed.flags.has("--github")
|
|
562
|
+
? githubCoordinateFromFlags(parsed.flags)
|
|
563
|
+
: undefined;
|
|
564
|
+
if (flagBool(parsed.flags, "--all")) {
|
|
565
|
+
const galleries = [];
|
|
566
|
+
let nextCursor = cursor;
|
|
567
|
+
do {
|
|
568
|
+
const page = github
|
|
569
|
+
? await ctx.client.findGalleriesByReference({
|
|
570
|
+
provider: "github",
|
|
571
|
+
coordinate: github,
|
|
572
|
+
limit,
|
|
573
|
+
cursor: nextCursor,
|
|
574
|
+
})
|
|
575
|
+
: await ctx.client.listGalleries({ limit, cursor: nextCursor });
|
|
576
|
+
galleries.push(...page.galleries);
|
|
577
|
+
nextCursor = page.nextCursor ?? undefined;
|
|
578
|
+
} while (nextCursor);
|
|
579
|
+
if (ctx.json)
|
|
580
|
+
await writeJson({ galleries, nextCursor: null });
|
|
581
|
+
else
|
|
582
|
+
for (const gallery of galleries)
|
|
583
|
+
await writeStdout(`${gallery.id} ${gallery.url} ${gallery.title}\n`);
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
const page = github
|
|
587
|
+
? await ctx.client.findGalleriesByReference({
|
|
588
|
+
provider: "github",
|
|
589
|
+
coordinate: github,
|
|
590
|
+
limit,
|
|
591
|
+
cursor,
|
|
592
|
+
})
|
|
593
|
+
: await ctx.client.listGalleries({ limit, cursor });
|
|
594
|
+
if (ctx.json)
|
|
595
|
+
await writeJson(page);
|
|
596
|
+
else {
|
|
597
|
+
for (const gallery of page.galleries)
|
|
598
|
+
await writeStdout(`${gallery.id} ${gallery.url} ${gallery.title}\n`);
|
|
599
|
+
if (page.nextCursor)
|
|
600
|
+
process.stderr.write(`cursor: ${page.nextCursor}\n`);
|
|
601
|
+
}
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
case "link": {
|
|
605
|
+
const id = parsed.positionals[1];
|
|
606
|
+
if (!id)
|
|
607
|
+
throw new UsageError("gallery link requires a gallery ID");
|
|
608
|
+
const coordinate = githubCoordinateFromFlags(parsed.flags);
|
|
609
|
+
const current = await ctx.client.getGallery(id);
|
|
610
|
+
const reference = await ctx.client.linkGalleryExternalReference(id, {
|
|
611
|
+
expectedVersion: current.version,
|
|
612
|
+
provider: "github",
|
|
613
|
+
coordinate,
|
|
614
|
+
});
|
|
615
|
+
if (ctx.json)
|
|
616
|
+
await writeJson({ galleryId: id, reference });
|
|
617
|
+
else
|
|
618
|
+
await writeStdout((reference.canonicalUrl ?? reference.coordinate) + "\n");
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
case "unlink": {
|
|
622
|
+
const id = parsed.positionals[1];
|
|
623
|
+
if (!id)
|
|
624
|
+
throw new UsageError("gallery unlink requires a gallery ID");
|
|
625
|
+
const coordinate = githubCoordinateFromFlags(parsed.flags);
|
|
626
|
+
const references = await ctx.client.listGalleryExternalReferences(id);
|
|
627
|
+
const reference = references.references.find((entry) => entry.provider === "github" && entry.coordinate === coordinate);
|
|
628
|
+
if (!reference) {
|
|
629
|
+
const output = { galleryId: id, coordinate, deleted: false };
|
|
630
|
+
if (ctx.json)
|
|
631
|
+
await writeJson(output);
|
|
632
|
+
else if (!ctx.quiet)
|
|
633
|
+
process.stderr.write("GitHub reference was already absent\n");
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
const current = await ctx.client.getGallery(id);
|
|
637
|
+
const result = await ctx.client.unlinkGalleryExternalReference(id, reference.id, {
|
|
638
|
+
expectedVersion: current.version,
|
|
639
|
+
});
|
|
640
|
+
if (ctx.json)
|
|
641
|
+
await writeJson({ galleryId: id, coordinate, ...result });
|
|
642
|
+
else if (!ctx.quiet)
|
|
643
|
+
process.stderr.write("unlinked " + coordinate + "\n");
|
|
644
|
+
return 0;
|
|
645
|
+
}
|
|
646
|
+
case "delete": {
|
|
647
|
+
const id = parsed.positionals[1];
|
|
648
|
+
if (!id)
|
|
649
|
+
throw new UsageError("gallery delete requires a gallery ID");
|
|
650
|
+
const current = await ctx.client.getGallery(id);
|
|
651
|
+
const result = await ctx.client.deleteGallery(id, { expectedVersion: current.version });
|
|
652
|
+
if (ctx.json)
|
|
653
|
+
await writeJson(result);
|
|
654
|
+
else if (!ctx.quiet)
|
|
655
|
+
process.stderr.write(`deleted gallery ${result.id} (objects kept)\n`);
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
case "add": {
|
|
659
|
+
const id = parsed.positionals[1];
|
|
660
|
+
const keys = parsed.positionals.slice(2);
|
|
661
|
+
if (!id || keys.length === 0)
|
|
662
|
+
throw new UsageError("gallery add requires a gallery ID and one or more object keys");
|
|
663
|
+
const caption = flagString(parsed.flags, "--caption");
|
|
664
|
+
const altText = flagString(parsed.flags, "--alt");
|
|
665
|
+
const added = [];
|
|
666
|
+
let galleryUrl;
|
|
667
|
+
const failures = [];
|
|
668
|
+
for (const objectKey of keys) {
|
|
669
|
+
try {
|
|
670
|
+
// Always re-read before the next write: each add increments the version,
|
|
671
|
+
// and this also avoids stale versions after an independent writer.
|
|
672
|
+
const current = await ctx.client.getGallery(id);
|
|
673
|
+
galleryUrl = current.url;
|
|
674
|
+
added.push(await ctx.client.addGalleryItem(id, objectKey, {
|
|
675
|
+
expectedVersion: current.version,
|
|
676
|
+
caption,
|
|
677
|
+
altText,
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
failures.push({ objectKey, error: galleryError(err) });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const output = { galleryId: id, galleryUrl: galleryUrl ?? null, added, failures };
|
|
685
|
+
if (ctx.json)
|
|
686
|
+
await writeJson(output);
|
|
687
|
+
else {
|
|
688
|
+
if (galleryUrl)
|
|
689
|
+
await writeStdout(`GALLERY: ${galleryUrl}\n`);
|
|
690
|
+
for (const item of added)
|
|
691
|
+
await writeStdout(`${item.objectKey}\n`);
|
|
692
|
+
for (const failure of failures)
|
|
693
|
+
process.stderr.write(`warning: could not add ${failure.objectKey}: ${failure.error.message}\n`);
|
|
694
|
+
}
|
|
695
|
+
return failures.length === 0 ? 0 : 1;
|
|
696
|
+
}
|
|
697
|
+
default:
|
|
698
|
+
throw new UsageError(`unknown gallery command: ${action}`);
|
|
699
|
+
}
|
|
425
700
|
}
|
|
426
701
|
// --- list ---
|
|
427
702
|
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
@@ -474,8 +749,13 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
474
749
|
// --- delete ---
|
|
475
750
|
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
476
751
|
|
|
752
|
+
Options:
|
|
753
|
+
--dry-run Preview without deleting
|
|
754
|
+
--workspace, -w <name>
|
|
755
|
+
|
|
477
756
|
Examples:
|
|
478
757
|
uploads delete screenshots/myapp/42/shot-a1b2c3.png
|
|
758
|
+
uploads delete screenshots/myapp/42/shot-a1b2c3.png --dry-run
|
|
479
759
|
`;
|
|
480
760
|
export async function runDelete(ctx, args, help = false) {
|
|
481
761
|
const parsed = parseCommandArgs(args);
|
|
@@ -643,6 +923,7 @@ Checks API health, token auth, and workspace/token alignment.
|
|
|
643
923
|
Examples:
|
|
644
924
|
uploads --env-file .env doctor
|
|
645
925
|
uploads --workspace acme --env-file .env doctor
|
|
926
|
+
uploads doctor --json
|
|
646
927
|
`;
|
|
647
928
|
/** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
|
|
648
929
|
export async function buildDoctorReport(config, client) {
|
|
@@ -689,6 +970,7 @@ export async function buildDoctorReport(config, client) {
|
|
|
689
970
|
}
|
|
690
971
|
return {
|
|
691
972
|
ok: health.ok && authOk,
|
|
973
|
+
cliVersion: packageVersion(),
|
|
692
974
|
apiUrl: config.apiUrl,
|
|
693
975
|
workspace: config.workspace,
|
|
694
976
|
workspaceSource: config.workspaceSource,
|
|
@@ -713,6 +995,7 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
713
995
|
return report.ok ? 0 : 1;
|
|
714
996
|
}
|
|
715
997
|
const lines = [
|
|
998
|
+
`cli: @buildinternet/uploads@${report.cliVersion}`,
|
|
716
999
|
`config: ${report.configPath}${report.configExists ? "" : " (missing)"}`,
|
|
717
1000
|
`api: ${report.apiUrl} (${report.health.ok ? "ok" : "failed"})`,
|
|
718
1001
|
`workspace: ${report.workspace}`,
|
package/dist/github.d.ts
CHANGED
|
@@ -5,9 +5,16 @@ export interface GhTarget {
|
|
|
5
5
|
kind: GhTargetKind;
|
|
6
6
|
num: number;
|
|
7
7
|
}
|
|
8
|
+
/** A normalized GitHub issue/PR coordinate used for gallery references. */
|
|
9
|
+
export interface GithubCoordinate {
|
|
10
|
+
coordinate: string;
|
|
11
|
+
canonicalUrl: string;
|
|
12
|
+
}
|
|
8
13
|
export declare function isValidRepo(repo: string): boolean;
|
|
9
14
|
/** Parse "owner/name" from a git remote URL (SSH or HTTPS), else undefined. */
|
|
10
15
|
export declare function parseRepoFromRemoteUrl(url: string): string | undefined;
|
|
16
|
+
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
17
|
+
export declare function normalizeGithubCoordinate(value: string): GithubCoordinate | undefined;
|
|
11
18
|
export declare function ghKeyPrefix(target: GhTarget): string;
|
|
12
19
|
/**
|
|
13
20
|
* Stable attachment key: same filename → same key → same public URL, so
|
|
@@ -21,6 +28,17 @@ export interface AttachmentItem {
|
|
|
21
28
|
key: string;
|
|
22
29
|
url: string | null;
|
|
23
30
|
}
|
|
31
|
+
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
32
|
+
export interface GalleryCommentItem {
|
|
33
|
+
title: string;
|
|
34
|
+
/** Canonical URL returned by the API; callers must not synthesize it. */
|
|
35
|
+
url: string;
|
|
36
|
+
/** A bounded set of available images, all of which link back to the gallery. */
|
|
37
|
+
previews?: {
|
|
38
|
+
url: string;
|
|
39
|
+
alt: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
24
42
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
|
25
43
|
export declare const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
26
44
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
@@ -32,4 +50,8 @@ export declare const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
|
32
50
|
* practical signal (we don't re-fetch dimensions when rebuilding the comment).
|
|
33
51
|
*/
|
|
34
52
|
export declare function attachmentImageWidth(filename: string): number;
|
|
35
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
55
|
+
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
56
|
+
*/
|
|
57
|
+
export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[]): string;
|
package/dist/github.js
CHANGED
|
@@ -10,6 +10,47 @@ export function parseRepoFromRemoteUrl(url) {
|
|
|
10
10
|
const repo = match?.[1];
|
|
11
11
|
return repo && isValidRepo(repo) ? repo : undefined;
|
|
12
12
|
}
|
|
13
|
+
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
14
|
+
export function normalizeGithubCoordinate(value) {
|
|
15
|
+
const input = value.trim();
|
|
16
|
+
let match = /^([^/\s#]+)\/([^/\s#]+)#([1-9][0-9]*)$/.exec(input);
|
|
17
|
+
if (!match) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(input);
|
|
20
|
+
if (url.protocol !== "https:" ||
|
|
21
|
+
url.hostname.toLowerCase() !== "github.com" ||
|
|
22
|
+
url.port ||
|
|
23
|
+
url.username ||
|
|
24
|
+
url.password ||
|
|
25
|
+
url.search ||
|
|
26
|
+
url.hash)
|
|
27
|
+
return undefined;
|
|
28
|
+
match = /^\/([^/]+)\/([^/]+)\/(?:issues|pull)\/([1-9][0-9]*)\/?$/.exec(url.pathname);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const [, ownerRaw, repositoryRaw, numberRaw] = match;
|
|
37
|
+
const repo = ownerRaw + "/" + repositoryRaw;
|
|
38
|
+
const number = Number(numberRaw);
|
|
39
|
+
if (!isValidRepo(repo) || !Number.isSafeInteger(number))
|
|
40
|
+
return undefined;
|
|
41
|
+
const owner = ownerRaw.toLowerCase();
|
|
42
|
+
const repository = repositoryRaw.toLowerCase();
|
|
43
|
+
const coordinate = owner + "/" + repository + "#" + number;
|
|
44
|
+
return {
|
|
45
|
+
coordinate,
|
|
46
|
+
canonicalUrl: "https://github.com/" +
|
|
47
|
+
encodeURIComponent(owner) +
|
|
48
|
+
"/" +
|
|
49
|
+
encodeURIComponent(repository) +
|
|
50
|
+
"/issues/" +
|
|
51
|
+
number,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
13
54
|
export function ghKeyPrefix(target) {
|
|
14
55
|
const [owner, name] = target.repo.split("/");
|
|
15
56
|
return `gh/${sanitizeKeySegment(owner)}/${sanitizeKeySegment(name)}/${target.kind}/${target.num}/`;
|
|
@@ -48,9 +89,31 @@ export function attachmentImageWidth(filename) {
|
|
|
48
89
|
function escapeHtmlAttr(s) {
|
|
49
90
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
50
91
|
}
|
|
51
|
-
|
|
92
|
+
function escapeHtmlText(s) {
|
|
93
|
+
return escapeHtmlAttr(s).replace(/'/g, "'").replace(/>/g, ">");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
97
|
+
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
98
|
+
*/
|
|
99
|
+
export function attachmentsCommentBody(items, galleries = []) {
|
|
52
100
|
const sorted = items.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
53
|
-
const
|
|
101
|
+
const sortedGalleries = galleries.toSorted((a, b) => a.title.localeCompare(b.title) || a.url.localeCompare(b.url));
|
|
102
|
+
const lines = [ATTACHMENTS_MARKER];
|
|
103
|
+
if (sortedGalleries.length > 0) {
|
|
104
|
+
lines.push("### 🖼️ Galleries", "");
|
|
105
|
+
for (const gallery of sortedGalleries) {
|
|
106
|
+
const href = escapeHtmlAttr(gallery.url);
|
|
107
|
+
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
|
+
for (const preview of gallery.previews ?? []) {
|
|
109
|
+
lines.push(`<a href="${href}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${escapeHtmlAttr(preview.url)}"></a>`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
112
|
+
}
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
if (sorted.length > 0 || sortedGalleries.length === 0)
|
|
116
|
+
lines.push("### 📎 Attachments", "");
|
|
54
117
|
for (const item of sorted) {
|
|
55
118
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
56
119
|
if (item.url && inferContentType(name).startsWith("image/")) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
3
3
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
4
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
5
5
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
6
|
-
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
6
|
+
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type GalleryItem, type Gallery, type GallerySummary, type GalleryListOptions, type GalleryListResult, type CreateGalleryOptions, type AddGalleryItemOptions, type DeleteGalleryOptions, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
7
7
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
8
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
9
9
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|