@buildinternet/uploads 0.49.0 → 0.50.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/client.d.ts CHANGED
@@ -441,6 +441,8 @@ export interface UsageResult {
441
441
  uploadsRemaining?: number;
442
442
  /** Bytes still on hosted storage (shared-lane residue). */
443
443
  sharedBytes?: number;
444
+ /** Objects still on hosted storage (shared-lane residue). */
445
+ sharedObjects?: number;
444
446
  /** "shared" = BYO bucket active: the storage cap meters only hosted
445
447
  * residue; the customer's own bucket is unmetered. */
446
448
  storageBudgetBasis?: "total" | "shared";
package/dist/commands.js CHANGED
@@ -649,6 +649,13 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
649
649
  // byte-identical.
650
650
  const path = metadata?.path;
651
651
  const state = metadata?.state;
652
+ // Server-derived image dimensions (parity with the bot path's
653
+ // COMMENT_META_KEYS hydration): finite positive numbers only, field
654
+ // omitted entirely when neither parses.
655
+ const imgWidth = Number(metadata?.["image.width"]);
656
+ const imgHeight = Number(metadata?.["image.height"]);
657
+ const hasImgWidth = Number.isFinite(imgWidth) && imgWidth > 0;
658
+ const hasImgHeight = Number.isFinite(imgHeight) && imgHeight > 0;
652
659
  return {
653
660
  key,
654
661
  url,
@@ -657,6 +664,14 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
657
664
  ...(path || state
658
665
  ? { meta: { ...(path ? { path } : {}), ...(state ? { state } : {}) } }
659
666
  : {}),
667
+ ...(hasImgWidth || hasImgHeight
668
+ ? {
669
+ imageMeta: {
670
+ ...(hasImgWidth ? { width: imgWidth } : {}),
671
+ ...(hasImgHeight ? { height: imgHeight } : {}),
672
+ },
673
+ }
674
+ : {}),
660
675
  };
661
676
  }));
662
677
  const galleries = [];
@@ -107,6 +107,11 @@ export interface AttachmentItem {
107
107
  width?: number;
108
108
  height?: number;
109
109
  };
110
+ /** Server-derived pixel dimensions for an image (never client-settable). */
111
+ imageMeta?: {
112
+ width?: number;
113
+ height?: number;
114
+ };
110
115
  }
111
116
  /** A public gallery linked to the PR or issue whose managed comment is syncing. */
112
117
  export interface GalleryCommentItem {
@@ -144,6 +149,11 @@ export declare function attachmentPairWidth(density?: AttachmentDensity): number
144
149
  * only affects managed-comment auto layout; other callers leave it `"dense"`.
145
150
  */
146
151
  export declare function attachmentImageWidth(filename: string, density?: AttachmentDensity): number;
152
+ /**
153
+ * Icons and other small assets: both dimensions known and no edge above this
154
+ * flow inline at natural size rather than each becoming a full row.
155
+ */
156
+ export declare const SMALL_ASSET_MAX_EDGE = 200;
147
157
  /**
148
158
  * Render the one marker-owned GitHub comment. When there are no galleries this
149
159
  * intentionally preserves the legacy attachment-only body byte-for-byte.
@@ -183,19 +183,32 @@ function formatDuration(seconds) {
183
183
  return `${h}:${String(m).padStart(2, "0")}:${ss}`;
184
184
  }
185
185
  /**
186
- * Display width for a video poster. Real dimensions only *select* among the
187
- * density table's tiers — a raw 1920 would blow out the comment column — and
188
- * the result is capped at the real width so a small clip is never upscaled.
186
+ * Display width for an image or video-poster embed. Real dimensions only
187
+ * *select* among the density table's tiers — a raw 1920 would blow out the
188
+ * comment column — and the result is capped at the real width so a small
189
+ * asset is never upscaled into a blurry tile. Without dims (either missing),
190
+ * falls back to the filename heuristic exactly, keeping dimension-less
191
+ * renders byte-identical to before dims existed.
189
192
  */
190
- function posterImageWidth(videoMeta, filename, density = "dense") {
191
- const w = videoMeta?.width ?? 0;
192
- const h = videoMeta?.height ?? 0;
193
+ function naturalMediaWidth(dims, filename, density = "dense") {
194
+ const w = dims?.width ?? 0;
195
+ const h = dims?.height ?? 0;
193
196
  if (w <= 0 || h <= 0)
194
197
  return attachmentImageWidth(filename, density);
195
198
  const table = WIDTH_BY_DENSITY[density];
196
199
  const chosen = h > w ? table.portrait : w / h >= 16 / 9 ? table.wide : table.default;
197
200
  return Math.min(chosen, w);
198
201
  }
202
+ /**
203
+ * Icons and other small assets: both dimensions known and no edge above this
204
+ * flow inline at natural size rather than each becoming a full row.
205
+ */
206
+ export const SMALL_ASSET_MAX_EDGE = 200;
207
+ function isSmallAsset(dims) {
208
+ const w = dims?.width ?? 0;
209
+ const h = dims?.height ?? 0;
210
+ return w > 0 && h > 0 && Math.max(w, h) <= SMALL_ASSET_MAX_EDGE;
211
+ }
199
212
  function escapeHtmlAttr(s) {
200
213
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
201
214
  }
@@ -360,7 +373,7 @@ function renderPairCell(item, label, options, density) {
360
373
  const name = item.key.slice(item.key.lastIndexOf("/") + 1);
361
374
  const src = item.embedUrl ?? item.url;
362
375
  const link = item.pageUrl ?? item.url;
363
- const autoPx = Math.min(attachmentImageWidth(name, density), attachmentPairWidth(density));
376
+ const autoPx = Math.min(naturalMediaWidth(item.imageMeta, name, density), attachmentPairWidth(density));
364
377
  const w = resolvedWidth(autoPx, options);
365
378
  const alt = escapeHtmlAttr(name);
366
379
  const href = escapeHtmlAttr((link ?? src));
@@ -480,7 +493,7 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
480
493
  }
481
494
  if (isPosterVideo) {
482
495
  inlinedImages++;
483
- const autoPx = posterImageWidth(item.videoMeta, name, density);
496
+ const autoPx = naturalMediaWidth(item.videoMeta, name, density);
484
497
  const w = resolvedWidth(autoPx, options);
485
498
  const href = escapeHtmlAttr(link ?? item.posterUrl);
486
499
  lines.push(`<a href="${href}">${imgTag(w, escapeHtmlAttr(name), escapeHtmlAttr(item.posterUrl))}</a>`);
@@ -496,10 +509,53 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
496
509
  lines.push(parts.join(" · "), "");
497
510
  }
498
511
  else if (isImage) {
512
+ // Small-asset flow layout: consecutive captionless small images (icons)
513
+ // join onto one line so GitHub flows them horizontally at natural size
514
+ // instead of stacking a giant column per icon. Auto mode only — a
515
+ // numeric imageWidth override or "full" keeps today's one-per-row form.
516
+ const smallGroupable = (candidate, i) => {
517
+ if (options.imageWidth !== "auto")
518
+ return false;
519
+ if (consumedByPair.has(i) || partnerOf.has(i))
520
+ return false;
521
+ if (!isSmallAsset(candidate.imageMeta))
522
+ return false;
523
+ if (metaCaptionValues(candidate.meta, options).length > 0)
524
+ return false;
525
+ const n = candidate.key.slice(candidate.key.lastIndexOf("/") + 1);
526
+ const s = candidate.embedUrl ?? candidate.url;
527
+ return Boolean(s) && inferContentType(n).startsWith("image/");
528
+ };
529
+ if (smallGroupable(item, idx)) {
530
+ const group = [item];
531
+ let j = idx + 1;
532
+ // Each grouped image still counts toward maxInlineImages.
533
+ while (j < sorted.length &&
534
+ inlinedImages + group.length < options.maxInlineImages &&
535
+ smallGroupable(sorted[j], j)) {
536
+ group.push(sorted[j]);
537
+ j++;
538
+ }
539
+ if (group.length > 1) {
540
+ inlinedImages += group.length;
541
+ lines.push(group
542
+ .map((g) => {
543
+ const gName = g.key.slice(g.key.lastIndexOf("/") + 1);
544
+ const gSrc = (g.embedUrl ?? g.url);
545
+ const gHref = escapeHtmlAttr((g.pageUrl ?? g.url ?? gSrc));
546
+ // Natural width — a small asset is never upscaled.
547
+ const gW = g.imageMeta?.width;
548
+ return `<a href="${gHref}">${imgTag(gW, escapeHtmlAttr(gName), escapeHtmlAttr(gSrc))}</a>`;
549
+ })
550
+ .join(" "), "");
551
+ idx = j - 1;
552
+ continue;
553
+ }
554
+ }
499
555
  inlinedImages++;
500
556
  // Markdown ![]() has no width control — phone frames become full-column giants.
501
557
  // img src uses embed host when available (Camo revalidates); click-through prefers the file page.
502
- const autoPx = attachmentImageWidth(name, density);
558
+ const autoPx = naturalMediaWidth(item.imageMeta, name, density);
503
559
  const w = resolvedWidth(autoPx, options);
504
560
  const alt = escapeHtmlAttr(name);
505
561
  const href = escapeHtmlAttr(link ?? src);
@@ -191,13 +191,30 @@ export const usageResultSchema = objectSchema({
191
191
  workspace: { type: "string" },
192
192
  bytes: { type: "number" },
193
193
  objects: { type: "number" },
194
+ sharedBytes: { type: "number" },
195
+ sharedObjects: { type: "number" },
194
196
  uploadsInPeriod: { type: "number" },
195
197
  periodStart: { type: "string" },
196
198
  updatedAt: { type: "string" },
199
+ storageBudgetBasis: { type: "string", enum: ["total", "shared"] },
197
200
  maxStorageBytes: { type: "number" },
198
201
  storageRemainingBytes: { type: "number" },
199
202
  maxUploadsPerPeriod: { type: "number" },
200
203
  uploadsRemaining: { type: "number" },
204
+ // GET /:workspace/usage (routes/workspace-usage.ts) also stamps these on
205
+ // every response — not part of `usageWithLimits`'s return, so easy to miss.
206
+ scopes: { type: "array", items: { type: "string" } },
207
+ plan: { type: "string" },
208
+ storage: objectSchema({
209
+ mode: { type: "string", enum: ["shared", "byo"] },
210
+ fallbackLanes: { type: "number" },
211
+ health: objectSchema({
212
+ ok: { type: "boolean" },
213
+ code: { type: "string" },
214
+ message: { type: "string" },
215
+ since: { type: "string" },
216
+ }, ["ok"]),
217
+ }, ["mode", "fallbackLanes", "health"]),
201
218
  });
202
219
  export const reconcileResultSchema = objectSchema({
203
220
  workspace: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
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",