@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/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: create/update the attachments comment via your local gh auth
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
- if (items.length === 0)
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({ ...result, markdown, optimize: optimizeMeta, frame: prepared.frame });
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
- export declare function attachmentsCommentBody(items: AttachmentItem[]): string;
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
50
91
  }
51
- export function attachmentsCommentBody(items) {
92
+ function escapeHtmlText(s) {
93
+ return escapeHtmlAttr(s).replace(/'/g, "&#39;").replace(/>/g, "&gt;");
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 lines = [ATTACHMENTS_MARKER, "### 📎 Attachments", ""];
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";