@buildinternet/uploads 0.49.0 → 0.50.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/cli-catalog.js
CHANGED
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 = [];
|
|
@@ -794,6 +809,12 @@ takes effect with zero files and cannot combine with --branch/--issue/
|
|
|
794
809
|
--no-promote. Promotion never applies to issues. Staged files stay findable
|
|
795
810
|
with "uploads find gh.branch=<branch>" either way.
|
|
796
811
|
|
|
812
|
+
If the branch was renamed or deleted before the PR opened, pass
|
|
813
|
+
"--from-branch <old-name>" with "--pr <num>". With no file arguments, this
|
|
814
|
+
promotes the stale branch prefix and refreshes the managed comment. With file
|
|
815
|
+
or existing-key arguments, it promotes the stale prefix before the normal
|
|
816
|
+
attach flow.
|
|
817
|
+
|
|
797
818
|
Options:
|
|
798
819
|
--pr <num> Attach to this pull request
|
|
799
820
|
--issue <num> Attach to this issue
|
|
@@ -802,6 +823,8 @@ Options:
|
|
|
802
823
|
--promote No files: promote branch-staged attachments into the
|
|
803
824
|
resolved PR and refresh the comment; not with
|
|
804
825
|
--branch/--issue/--no-promote
|
|
826
|
+
--from-branch <name> Promote staged attachments from this branch instead of
|
|
827
|
+
the current branch; requires a pull-request target
|
|
805
828
|
--no-promote Skip auto-promoting branch-staged attachments (default path only)
|
|
806
829
|
--move With an already-uploaded key/URL argument: delete the source
|
|
807
830
|
object after a successful server-side copy (default: copy)
|
|
@@ -833,6 +856,7 @@ Examples:
|
|
|
833
856
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
834
857
|
uploads attach ./shot.png --meta path=/settings --state after
|
|
835
858
|
uploads attach ./shot.png --branch
|
|
859
|
+
uploads attach --pr 123 --from-branch old/branch
|
|
836
860
|
uploads attach ./shot.png --branch feature/new-settings
|
|
837
861
|
uploads attach --promote
|
|
838
862
|
`;
|
|
@@ -1189,7 +1213,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1189
1213
|
if (parsed.flags.has("--move") && typeof parsed.flags.get("--move") === "string") {
|
|
1190
1214
|
throw new UsageError("--move takes no value — place it after the file arguments");
|
|
1191
1215
|
}
|
|
1192
|
-
if (parsed.flags.has("--
|
|
1216
|
+
if (parsed.flags.has("--from-branch") && !flagString(parsed.flags, "--from-branch")) {
|
|
1217
|
+
throw new UsageError("--from-branch requires a branch name");
|
|
1218
|
+
}
|
|
1219
|
+
const fromBranch = flagString(parsed.flags, "--from-branch");
|
|
1220
|
+
if (parsed.flags.has("--promote") ||
|
|
1221
|
+
(fromBranch !== undefined && parsed.positionals.length === 0)) {
|
|
1193
1222
|
if (parsed.positionals.length > 0) {
|
|
1194
1223
|
throw new UsageError("--promote takes no file arguments — attaching a file to a PR already auto-promotes " +
|
|
1195
1224
|
"staged files; use `uploads attach <file> --pr <num>` instead");
|
|
@@ -1221,6 +1250,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1221
1250
|
throw new UsageError("--branch cannot be combined with --comment");
|
|
1222
1251
|
return runAttachBranch(ctx, parsed, branchArg, run);
|
|
1223
1252
|
}
|
|
1253
|
+
if (fromBranch !== undefined && parsed.flags.has("--issue")) {
|
|
1254
|
+
throw new UsageError("--from-branch cannot be combined with --issue");
|
|
1255
|
+
}
|
|
1256
|
+
if (fromBranch !== undefined && parsed.flags.has("--no-promote")) {
|
|
1257
|
+
throw new UsageError("--from-branch cannot be combined with --no-promote");
|
|
1258
|
+
}
|
|
1224
1259
|
const explicitTarget = ghTargetFromFlags(parsed.flags, run);
|
|
1225
1260
|
const target = explicitTarget ??
|
|
1226
1261
|
resolveCurrentPullRequest(resolveRepo(flagString(parsed.flags, "--repo"), run), run);
|
|
@@ -1309,11 +1344,14 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1309
1344
|
let promotion;
|
|
1310
1345
|
let promotedBranch;
|
|
1311
1346
|
if (target.kind === "pull" && !parsed.flags.has("--no-promote")) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1347
|
+
promotedBranch = fromBranch;
|
|
1348
|
+
if (promotedBranch === undefined) {
|
|
1349
|
+
try {
|
|
1350
|
+
promotedBranch = resolveCurrentBranch(run);
|
|
1351
|
+
}
|
|
1352
|
+
catch {
|
|
1353
|
+
promotedBranch = undefined;
|
|
1354
|
+
}
|
|
1317
1355
|
}
|
|
1318
1356
|
if (promotedBranch !== undefined) {
|
|
1319
1357
|
promotion = await attemptPromoteBranch(ctx.client, target, promotedBranch);
|
|
@@ -1321,8 +1359,10 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1321
1359
|
}
|
|
1322
1360
|
let comment;
|
|
1323
1361
|
let commentError;
|
|
1324
|
-
//
|
|
1325
|
-
|
|
1362
|
+
// Existing-key attach syncs server-side, but a stale-branch promotion runs
|
|
1363
|
+
// afterward. Refresh once more so that copy is visible in the same command.
|
|
1364
|
+
const shouldRefreshComment = uploads.length > 0 || (fromBranch !== undefined && attachedExisting.length > 0);
|
|
1365
|
+
if (!parsed.flags.has("--no-comment") && shouldRefreshComment) {
|
|
1326
1366
|
try {
|
|
1327
1367
|
comment = await syncAttachmentsComment(ctx.client, target, run, ctx.config.workspace);
|
|
1328
1368
|
}
|
|
@@ -1524,7 +1564,10 @@ async function runAttachPromoteOnly(ctx, parsed, run) {
|
|
|
1524
1564
|
const explicitTarget = ghTargetFromFlags(parsed.flags, run);
|
|
1525
1565
|
const target = explicitTarget ??
|
|
1526
1566
|
resolveCurrentPullRequest(resolveRepo(flagString(parsed.flags, "--repo"), run), run);
|
|
1527
|
-
|
|
1567
|
+
if (target.kind !== "pull") {
|
|
1568
|
+
throw new UsageError("--from-branch only promotes into a pull request");
|
|
1569
|
+
}
|
|
1570
|
+
const branch = flagString(parsed.flags, "--from-branch") ?? resolveCurrentBranch(run);
|
|
1528
1571
|
const promotion = await attemptPromoteBranch(ctx.client, target, branch);
|
|
1529
1572
|
let comment;
|
|
1530
1573
|
let commentError;
|
|
@@ -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
|
|
187
|
-
* density table's tiers — a raw 1920 would blow out the
|
|
188
|
-
* the result is capped at the real width so a small
|
|
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
|
|
191
|
-
const w =
|
|
192
|
-
const h =
|
|
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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
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(
|
|
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 =
|
|
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 =
|
|
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" },
|