@buildinternet/uploads 0.52.0 → 0.52.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/dist/commands.js CHANGED
@@ -655,7 +655,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
655
655
  target: { kind: target.kind, num: target.num },
656
656
  });
657
657
  const prefixes = ghListPrefixes(ghKeyPrefix(target), ghPrefix, (id) => ghPrivateKeyPrefix(id, target));
658
- const items = await ghMergedList(prefixes, undefined, async (prefix) => (await client.listAll({ prefix, metadata: true })).map(({ key, url, embedUrl, pageUrl, metadata }) => {
658
+ const items = await ghMergedList(prefixes, undefined, async (prefix) => (await client.listAll({ prefix, metadata: true })).map(({ key, url, embedUrl, pageUrl, size, metadata }) => {
659
659
  // The list endpoint returns every metadata key; the comment
660
660
  // renders only these two. Narrowing here keeps both render paths
661
661
  // byte-identical.
@@ -673,6 +673,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
673
673
  url,
674
674
  embedUrl,
675
675
  pageUrl,
676
+ ...(size != null ? { size } : {}),
676
677
  ...(path || state
677
678
  ? { meta: { ...(path ? { path } : {}), ...(state ? { state } : {}) } }
678
679
  : {}),
@@ -120,6 +120,10 @@ export interface AttachmentItem {
120
120
  width?: number;
121
121
  height?: number;
122
122
  };
123
+ /** Object size in bytes, when known — drives the non-media file table's Size column. */
124
+ size?: number;
125
+ /** Stored content type, when known — preferred over name-based inference for file classification and the table's Type column. */
126
+ contentType?: string;
123
127
  }
124
128
  /** A public gallery linked to the PR or issue whose managed comment is syncing. */
125
129
  export interface GalleryCommentItem {
@@ -285,6 +285,47 @@ function formatMetaCaption(meta, options, mode) {
285
285
  })
286
286
  .join(" · ");
287
287
  }
288
+ /**
289
+ * Decimal-SI byte size for the non-media file table's Size column: whole
290
+ * bytes below 1000, one decimal place at KB/MB/GB and above. `"—"` when the
291
+ * size is unknown.
292
+ */
293
+ function formatBytes(bytes) {
294
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0)
295
+ return "—";
296
+ if (bytes < 1000)
297
+ return `${Math.round(bytes)} B`;
298
+ const units = [
299
+ [1e9, "GB"],
300
+ [1e6, "MB"],
301
+ [1e3, "KB"],
302
+ ];
303
+ for (const [threshold, label] of units) {
304
+ if (bytes >= threshold)
305
+ return `${(bytes / threshold).toFixed(1)} ${label}`;
306
+ }
307
+ return `${Math.round(bytes)} B`;
308
+ }
309
+ /**
310
+ * Type label for the non-media file table: uppercase filename extension
311
+ * first, then the content type's subtype, then a bare "FILE" fallback.
312
+ */
313
+ function fileTypeLabel(name, contentType) {
314
+ const dot = name.lastIndexOf(".");
315
+ if (dot !== -1 && dot < name.length - 1)
316
+ return name.slice(dot + 1).toUpperCase();
317
+ if (contentType) {
318
+ const slash = contentType.indexOf("/");
319
+ if (slash !== -1 && slash < contentType.length - 1) {
320
+ return contentType.slice(slash + 1).toUpperCase();
321
+ }
322
+ }
323
+ return "FILE";
324
+ }
325
+ /** Escape `|` so a filename can't break out of a markdown table cell. */
326
+ function escapeTableCell(s) {
327
+ return s.replace(/\|/g, "\\|");
328
+ }
288
329
  /** Resolved pixel width for an image site, or `null` meaning "omit the width
289
330
  * attribute". `"auto"` defers to the caller's per-item heuristic (`autoPx`);
290
331
  * `"full"` always omits; a number always wins. */
@@ -485,6 +526,10 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
485
526
  const consumedByPair = new Set();
486
527
  let inlinedImages = 0;
487
528
  const overflowImages = [];
529
+ // Non-media attachments (PDFs, archives, text/data files) never inline and
530
+ // never overflow into the <details> link list — they render as one table
531
+ // after the image/video section instead (issue #946).
532
+ const fileItems = [];
488
533
  for (let idx = 0; idx < sorted.length; idx++) {
489
534
  if (consumedByPair.has(idx))
490
535
  continue;
@@ -510,8 +555,21 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
510
555
  const stable = item.url;
511
556
  const src = item.embedUrl ?? item.url;
512
557
  const link = item.pageUrl ?? stable; // click-through: file page when known, else raw
513
- const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
514
- const isPosterVideo = Boolean(item.posterUrl) && inferContentType(name).startsWith("video/");
558
+ // "application/octet-stream" is the server's generic fallback for an
559
+ // object stored without an explicit content type — not a real signal —
560
+ // so it defers to the filename the same as an absent `contentType`.
561
+ const effectiveType = item.contentType && item.contentType !== "application/octet-stream"
562
+ ? item.contentType
563
+ : inferContentType(name);
564
+ const isImage = Boolean(src) && effectiveType.startsWith("image/");
565
+ const isPosterVideo = Boolean(item.posterUrl) && effectiveType.startsWith("video/");
566
+ if (!effectiveType.startsWith("image/") && !effectiveType.startsWith("video/")) {
567
+ // Neither an image nor a video by content type — a non-media
568
+ // attachment goes into the file table, never the bullet list or
569
+ // overflow details.
570
+ fileItems.push(item);
571
+ continue;
572
+ }
515
573
  const inlines = isImage || isPosterVideo;
516
574
  if (inlines && inlinedImages >= options.maxInlineImages) {
517
575
  // Cap hit — defer to the collapsed overflow list below rather than
@@ -604,6 +662,23 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
604
662
  lines.push(`- ${name}${cap ? ` · ${cap}` : ""}`);
605
663
  }
606
664
  }
665
+ if (fileItems.length > 0) {
666
+ lines.push("| File | Type | Size |", "| --- | --- | --- |");
667
+ for (const item of fileItems) {
668
+ const name = item.key.slice(item.key.lastIndexOf("/") + 1);
669
+ const escapedName = escapeTableCell(name);
670
+ let fileCell = item.url ? `[${escapedName}](${item.url})` : escapedName;
671
+ if (item.pageUrl)
672
+ fileCell += ` · [page](${item.pageUrl})`;
673
+ const cap = formatMetaCaption(item.meta, options, "markdown");
674
+ if (cap)
675
+ fileCell += ` · ${cap}`;
676
+ const typeLabel = fileTypeLabel(name, item.contentType);
677
+ const sizeLabel = formatBytes(item.size);
678
+ lines.push(`| ${fileCell} | ${typeLabel} | ${sizeLabel} |`);
679
+ }
680
+ lines.push("");
681
+ }
607
682
  if (overflowImages.length > 0) {
608
683
  const n = overflowImages.length;
609
684
  lines.push(`<details><summary>${n} more attachment${n === 1 ? "" : "s"}</summary>`, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "mcpName": "sh.uploads/mcp",
5
5
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
6
6
  "type": "module",