@buildinternet/uploads 0.42.2 → 0.44.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/cli-help.js +1 -1
- package/dist/client.d.ts +36 -1
- package/dist/client.js +15 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/hook.d.ts +6 -2
- package/dist/commands/hook.js +31 -9
- package/dist/commands/screenshot.js +96 -20
- package/dist/commands.d.ts +76 -0
- package/dist/commands.js +326 -78
- package/dist/comment-config.generated.d.ts +6 -0
- package/dist/comment-config.generated.js +27 -1
- package/dist/comment-render.generated.d.ts +4 -1
- package/dist/comment-render.generated.js +15 -2
- package/dist/config-file.d.ts +4 -1
- package/dist/config-file.js +10 -0
- package/dist/mcp/output-schemas.js +24 -1
- package/dist/mcp/tools.js +154 -49
- package/dist/project-context-nudge.d.ts +9 -0
- package/dist/project-context-nudge.js +39 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -16,6 +16,7 @@ import { mergeSidecarMeta } from "./sidecar.js";
|
|
|
16
16
|
import { ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, ghBranchKeyPrefix, ghKeyPrefix, ghPrivateKeyPrefix, ghPrivateBranchKeyPrefix, ghMetadataFromTarget, parseGhKey, parseGhPrivateKey, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, AUTO_RENDER_OPTIONS, GH_FALLBACK_AUTHOR_NOTE, normalizeGithubCoordinate, } from "./github.js";
|
|
17
17
|
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
18
18
|
import { deriveRepoFromGit, deriveRepoSlugFromGit } from "./keys.js";
|
|
19
|
+
import { noProjectContextNudge } from "./project-context-nudge.js";
|
|
19
20
|
import { resolvePutPrefix } from "./destinations.js";
|
|
20
21
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
21
22
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
@@ -151,6 +152,7 @@ Options:
|
|
|
151
152
|
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
152
153
|
--auto Resolve current PR/issue and stamp gh.* metadata (default on)
|
|
153
154
|
--no-auto Skip gh.* auto-resolution (also skipped by --no-git or UPLOADS_NO_AUTO_META=1)
|
|
155
|
+
--no-pr Skip auto-PR context (or UPLOADS_NO_AUTO_PR=1) — see below
|
|
154
156
|
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
155
157
|
--format human|url|markdown|json
|
|
156
158
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
@@ -174,9 +176,17 @@ Options:
|
|
|
174
176
|
--dry-run Print key + public URL without uploading; reports if the key would replace
|
|
175
177
|
(or, on a strict key, be refused). Not with --gallery
|
|
176
178
|
|
|
177
|
-
A bare put (no --pr/--issue/--key) on a
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
A bare put (no --pr/--issue/--key/--ref/--prefix/--destination) on a git branch
|
|
180
|
+
that maps to exactly one open PR now behaves as if --pr <n> had been passed
|
|
181
|
+
(issue #700): stable gh/ key, managed comment sync — instead of the #403
|
|
182
|
+
branch-staging default. A one-line note announces this (stderr in human mode,
|
|
183
|
+
the "hint" field in --format json). Opt out with --no-pr, UPLOADS_NO_AUTO_PR=1,
|
|
184
|
+
or config UPLOADS_NO_AUTO_PR=1; never fires outside a git repo, on the default
|
|
185
|
+
branch, with --no-git, or when no single open PR can be resolved (falls back to
|
|
186
|
+
branch staging, then the plain dated layout). When it doesn't fire and the
|
|
187
|
+
upload lands on the dated layout with a detectable PR, a similar one-line nudge
|
|
188
|
+
names the PR and a ready-made follow-up (uploads attach --pr <n> <key>...).
|
|
189
|
+
Suppress either note with --quiet, UPLOADS_NO_NUDGE=1, or config UPLOADS_NO_NUDGE=1.
|
|
180
190
|
|
|
181
191
|
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other (incl. partial multi-file failure).
|
|
182
192
|
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
@@ -646,7 +656,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
646
656
|
}
|
|
647
657
|
// Append only on the local-gh path: bot posts already carry the uploads-sh
|
|
648
658
|
// bot identity, so this note would be wrong there.
|
|
649
|
-
const body = `${attachmentsCommentBody(items, previewGalleries, marker, renderOptions)}\n${GH_FALLBACK_AUTHOR_NOTE}`;
|
|
659
|
+
const body = `${attachmentsCommentBody(items, previewGalleries, marker, renderOptions, target)}\n${GH_FALLBACK_AUTHOR_NOTE}`;
|
|
650
660
|
const count = items.length + previewGalleries.length;
|
|
651
661
|
// Empty (count 0) renders the neutral empty-state body but must not create a
|
|
652
662
|
// comment — it only rewrites one that already exists (`action: "skipped"`
|
|
@@ -682,6 +692,15 @@ derived metadata (path/url/env/viewport/state) is merged in automatically —
|
|
|
682
692
|
explicit --meta/--state always win. A regenerated or edited file loses its
|
|
683
693
|
sidecar silently (hash no longer matches).
|
|
684
694
|
|
|
695
|
+
An argument that doesn't exist on disk but resolves as an already-uploaded
|
|
696
|
+
object — a bare key (e.g. "f/AbC123/shot.webp") or an uploads.sh URL (storage
|
|
697
|
+
host, embed host, or /f/ page) — attaches via a server-side copy instead of a
|
|
698
|
+
re-upload: the source's own derived metadata (path/url/viewport/state/…)
|
|
699
|
+
rides along, and gh.repo/gh.kind/gh.number/gh.ref are stamped fresh. Copy by
|
|
700
|
+
default; --move deletes the source after a successful copy. A path that
|
|
701
|
+
exists on disk always wins as a local file, even if it also happens to look
|
|
702
|
+
like a key.
|
|
703
|
+
|
|
685
704
|
Branch staging (pre-PR): --branch [name] stages files against a git branch
|
|
686
705
|
before a pull request exists, e.g. for a coding agent working a branch that
|
|
687
706
|
hasn't opened a PR yet. Key: gh/<owner>/<repo>/branch/<branch>/<filename>
|
|
@@ -713,6 +732,8 @@ Options:
|
|
|
713
732
|
resolved PR and refresh the comment; not with
|
|
714
733
|
--branch/--issue/--no-promote
|
|
715
734
|
--no-promote Skip auto-promoting branch-staged attachments (default path only)
|
|
735
|
+
--move With an already-uploaded key/URL argument: delete the source
|
|
736
|
+
object after a successful server-side copy (default: copy)
|
|
716
737
|
--repo <owner/repo> Repository (default: gh/git inference)
|
|
717
738
|
--no-comment Upload only; don't create/update the managed comment
|
|
718
739
|
--content-type <mime> Override Content-Type (applied to every file; ignored when optimize rewrites)
|
|
@@ -767,6 +788,57 @@ sentMetadata) {
|
|
|
767
788
|
const missingPath = uploads.some((u, i) => u.contentType.startsWith("image/") && !sentMetadata[i]?.path);
|
|
768
789
|
return missingPath ? "tip: add --meta path=/route so this shot is findable by page" : undefined;
|
|
769
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Bounded fan-out for `uploads attach`'s already-uploaded-object args (issue
|
|
793
|
+
* #702) — attach args are independent server calls (no shared batch state
|
|
794
|
+
* like `uploadAttachments`'s optimize/frame prep), so this stays a thin
|
|
795
|
+
* wrapper rather than a variant of that function.
|
|
796
|
+
*/
|
|
797
|
+
const ATTACH_EXISTING_CONCURRENCY = 4;
|
|
798
|
+
async function attachExistingBatch(client, target, sources, move) {
|
|
799
|
+
const outcomes = await mapBounded(sources, ATTACH_EXISTING_CONCURRENCY, async (source) => {
|
|
800
|
+
try {
|
|
801
|
+
const result = await client.attachExisting({
|
|
802
|
+
source,
|
|
803
|
+
repo: target.repo,
|
|
804
|
+
pr: target.kind === "pull" ? target.num : undefined,
|
|
805
|
+
issue: target.kind === "issues" ? target.num : undefined,
|
|
806
|
+
move,
|
|
807
|
+
});
|
|
808
|
+
return { ok: true, source, result };
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
const message = err instanceof UploadsError && err.code === "NOT_FOUND"
|
|
812
|
+
? `not a local file, and no such object in this workspace: ${source}`
|
|
813
|
+
: err instanceof Error
|
|
814
|
+
? err.message
|
|
815
|
+
: String(err);
|
|
816
|
+
return {
|
|
817
|
+
ok: false,
|
|
818
|
+
source,
|
|
819
|
+
cause: err,
|
|
820
|
+
error: {
|
|
821
|
+
message,
|
|
822
|
+
code: err instanceof UploadsError ? err.code : undefined,
|
|
823
|
+
status: err instanceof UploadsError ? err.status : undefined,
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
const results = [];
|
|
829
|
+
const failures = [];
|
|
830
|
+
let firstError;
|
|
831
|
+
for (const outcome of outcomes) {
|
|
832
|
+
if (outcome.ok) {
|
|
833
|
+
results.push(outcome.result);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
failures.push({ file: outcome.source, error: outcome.error });
|
|
837
|
+
firstError ??= outcome.cause;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return { results, failures, firstError };
|
|
841
|
+
}
|
|
770
842
|
/**
|
|
771
843
|
* Shared prepare + put loop for both PR/issue attach (`uploadAttachments`)
|
|
772
844
|
* and branch-staged attach (`uploadBranchAttachments`) — bounded concurrency,
|
|
@@ -1020,6 +1092,9 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1020
1092
|
if (parsed.flags.has("--no-promote") && typeof parsed.flags.get("--no-promote") === "string") {
|
|
1021
1093
|
throw new UsageError("--no-promote takes no value — place it after the file arguments");
|
|
1022
1094
|
}
|
|
1095
|
+
if (parsed.flags.has("--move") && typeof parsed.flags.get("--move") === "string") {
|
|
1096
|
+
throw new UsageError("--move takes no value — place it after the file arguments");
|
|
1097
|
+
}
|
|
1023
1098
|
if (parsed.flags.has("--promote")) {
|
|
1024
1099
|
if (parsed.positionals.length > 0) {
|
|
1025
1100
|
throw new UsageError("--promote takes no file arguments — attaching a file to a PR already auto-promotes " +
|
|
@@ -1072,23 +1147,61 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1072
1147
|
};
|
|
1073
1148
|
if (Object.keys(metadata).length > 0)
|
|
1074
1149
|
validateMetaMap(metadata);
|
|
1150
|
+
// Args that exist on disk are always local files, even if they'd also
|
|
1151
|
+
// parse as a key/URL. Everything else is a candidate for the server-side
|
|
1152
|
+
// attach-existing path (issue #702) — resolved/validated server-side, so a
|
|
1153
|
+
// typo'd path and a genuinely-missing object key report the same way.
|
|
1154
|
+
const localFiles = parsed.positionals.filter((p) => existsSync(p));
|
|
1155
|
+
const remoteArgs = parsed.positionals.filter((p) => !existsSync(p));
|
|
1156
|
+
const moveExisting = parsed.flags.has("--move");
|
|
1157
|
+
if (moveExisting && remoteArgs.length === 0) {
|
|
1158
|
+
throw new UsageError("--move only applies to already-uploaded key/URL arguments");
|
|
1159
|
+
}
|
|
1075
1160
|
const logHuman = !ctx.quiet && !ctx.json;
|
|
1076
|
-
if (logHuman) {
|
|
1077
|
-
const n =
|
|
1161
|
+
if (logHuman && localFiles.length > 0) {
|
|
1162
|
+
const n = localFiles.length;
|
|
1078
1163
|
process.stderr.write(`>> uploading ${n} file${n === 1 ? "" : "s"}\n`);
|
|
1079
1164
|
}
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1165
|
+
const uploadResult = localFiles.length > 0
|
|
1166
|
+
? await uploadAttachments({
|
|
1167
|
+
client: ctx.client,
|
|
1168
|
+
target,
|
|
1169
|
+
files: localFiles,
|
|
1170
|
+
contentType: contentTypeOverride,
|
|
1171
|
+
optimize: optimizeOpts,
|
|
1172
|
+
frame: frameOpts,
|
|
1173
|
+
metadata,
|
|
1174
|
+
deriveImageFacts: derivedMetaEnabled(parsed.flags, defaults),
|
|
1175
|
+
})
|
|
1176
|
+
: { uploads: [], failures: [], firstError: undefined, sentMetadata: [] };
|
|
1177
|
+
const { uploads, sentMetadata } = uploadResult;
|
|
1178
|
+
const localFailures = uploadResult.failures;
|
|
1179
|
+
if (logHuman && remoteArgs.length > 0) {
|
|
1180
|
+
const n = remoteArgs.length;
|
|
1181
|
+
process.stderr.write(`>> attaching ${n} existing object${n === 1 ? "" : "s"}\n`);
|
|
1182
|
+
}
|
|
1183
|
+
const remoteResult = remoteArgs.length > 0
|
|
1184
|
+
? await attachExistingBatch(ctx.client, target, remoteArgs, moveExisting)
|
|
1185
|
+
: {
|
|
1186
|
+
results: [],
|
|
1187
|
+
failures: [],
|
|
1188
|
+
firstError: undefined,
|
|
1189
|
+
};
|
|
1190
|
+
const { results: attachedExisting, failures: remoteFailures } = remoteResult;
|
|
1191
|
+
const failures = [...localFailures, ...remoteFailures];
|
|
1192
|
+
const firstError = localFailures.length > 0 ? uploadResult.firstError : remoteResult.firstError;
|
|
1193
|
+
// Single-arg total failure: rethrow so CLI exit codes stay auth/network-aware.
|
|
1194
|
+
// The remote-attach path's friendlier not-found message (attachExistingBatch)
|
|
1195
|
+
// wins over the raw client error text, but the original error's class/code
|
|
1196
|
+
// (UploadsError) is preserved so exit-code mapping stays unaffected.
|
|
1197
|
+
if (uploads.length === 0 &&
|
|
1198
|
+
attachedExisting.length === 0 &&
|
|
1199
|
+
failures.length === 1 &&
|
|
1200
|
+
parsed.positionals.length === 1) {
|
|
1201
|
+
const only = failures[0];
|
|
1202
|
+
if (firstError instanceof UploadsError && firstError.message !== only.error.message) {
|
|
1203
|
+
throw new UploadsError(only.error.message, firstError.code, firstError.status);
|
|
1204
|
+
}
|
|
1092
1205
|
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
1093
1206
|
}
|
|
1094
1207
|
// Auto-promote: before the comment sync, best-effort promote this
|
|
@@ -1130,6 +1243,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1130
1243
|
await writeJson({
|
|
1131
1244
|
target,
|
|
1132
1245
|
uploads,
|
|
1246
|
+
attachedExisting,
|
|
1133
1247
|
failures,
|
|
1134
1248
|
comment,
|
|
1135
1249
|
commentError,
|
|
@@ -1151,6 +1265,13 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
1151
1265
|
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
1152
1266
|
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
1153
1267
|
}
|
|
1268
|
+
for (const attached of attachedExisting) {
|
|
1269
|
+
if (logHuman) {
|
|
1270
|
+
process.stderr.write(`>> ${attached.source.key}: attached${attached.moved ? " (moved)" : ""} as ${attached.key}\n`);
|
|
1271
|
+
}
|
|
1272
|
+
const embedLine = attached.embedUrl ? `EMBED: ${attached.embedUrl}\n` : "";
|
|
1273
|
+
await writeStdout(`URL: ${attached.url}\n${embedLine}`);
|
|
1274
|
+
}
|
|
1154
1275
|
for (const failure of failures) {
|
|
1155
1276
|
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
1156
1277
|
}
|
|
@@ -1346,36 +1467,54 @@ async function runAttachPromoteOnly(ctx, parsed, run) {
|
|
|
1346
1467
|
* enough to never be felt as a hang. */
|
|
1347
1468
|
const PUT_NUDGE_GH_TIMEOUT_MS = 3000;
|
|
1348
1469
|
/**
|
|
1349
|
-
* The bare-put nudge's wording (issue #393
|
|
1350
|
-
* as an upgrade from a targetless `put`.
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1470
|
+
* The bare-put nudge's wording (issue #393, made concrete by issue #700):
|
|
1471
|
+
* teaches `--pr`/`attach --branch` as an upgrade from a targetless `put`.
|
|
1472
|
+
* `pr` present → names the PR and, once upload `keys` are known, appends a
|
|
1473
|
+
* ready-made follow-up command naming them verbatim (e.g. `uploads attach
|
|
1474
|
+
* --pr 1250 f/abc123.webp`); otherwise a generic variant that still points
|
|
1475
|
+
* at `--pr <num>`. Used verbatim for both the human-mode stderr line and the
|
|
1476
|
+
* JSON `hint` field.
|
|
1353
1477
|
*/
|
|
1354
|
-
function putNudgeText(branch, pr) {
|
|
1478
|
+
export function putNudgeText(branch, pr, keys = []) {
|
|
1355
1479
|
const prClause = pr !== undefined ? ` (PR #${pr} open) — rerun with --pr ${pr}` : ` — rerun with --pr <num>`;
|
|
1356
|
-
|
|
1357
|
-
`that collects this PR's media, or stage pre-PR files with: uploads attach <file> --branch
|
|
1480
|
+
const base = `note: on branch ${branch}${prClause} for a stable key plus a managed comment ` +
|
|
1481
|
+
`that collects this PR's media, or stage pre-PR files with: uploads attach <file> --branch`;
|
|
1482
|
+
if (pr === undefined || keys.length === 0)
|
|
1483
|
+
return base;
|
|
1484
|
+
return `${base}. Already uploaded? uploads attach --pr ${pr} ${keys.join(" ")}`;
|
|
1358
1485
|
}
|
|
1359
1486
|
/**
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1487
|
+
* Auto-PR note (issue #700): announces at the moment it fires that a bare
|
|
1488
|
+
* put/screenshot on this branch was auto-attached to `pr` — the default
|
|
1489
|
+
* behavior change this issue introduces — and how to opt out.
|
|
1490
|
+
*/
|
|
1491
|
+
export function autoPrNoteText(pr) {
|
|
1492
|
+
return (`note: branch maps to open PR #${pr} — auto-attached (stable key + managed comment sync). ` +
|
|
1493
|
+
`Opt out with --no-pr or UPLOADS_NO_AUTO_PR=1.`);
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Best-effort bare-put/screenshot nudge context (issue #393): resolves the
|
|
1497
|
+
* branch and, when detectable, the open PR for it — fires only when there is
|
|
1498
|
+
* no targeting flag at all (`--pr`/`--issue`/`--key`; `--branch` too, though
|
|
1362
1499
|
* `put` doesn't currently accept it — defensive parity with `attach`), is
|
|
1363
1500
|
* inside a git repo (reusing `deriveRepoFromGit`, the same detection the
|
|
1364
1501
|
* default screenshot key's repo segment uses), and the current branch isn't
|
|
1365
1502
|
* the default one. Never throws — any failure (not a repo, detached HEAD,
|
|
1366
1503
|
* `gh` missing/unauthenticated/timed out) degrades to "no nudge" or, once a
|
|
1367
|
-
* branch is already known, to
|
|
1368
|
-
* put's exit code, stdout, or upload
|
|
1504
|
+
* branch is already known, to a context with `pr: undefined` (the generic
|
|
1505
|
+
* no-PR wording). Must never affect put's exit code, stdout, or upload
|
|
1506
|
+
* behavior. Callers turn the result into text via `putNudgeText`, once any
|
|
1507
|
+
* upload keys are known.
|
|
1369
1508
|
*/
|
|
1370
|
-
function
|
|
1371
|
-
const {
|
|
1372
|
-
if (
|
|
1509
|
+
export function resolvePutNudgeContext(opts) {
|
|
1510
|
+
const { quiet, noNudge, ghTarget, keyHint, hasBranchFlag, noGit, repoArg, run } = opts;
|
|
1511
|
+
if (quiet)
|
|
1373
1512
|
return undefined;
|
|
1374
|
-
if (
|
|
1513
|
+
if (noNudge)
|
|
1375
1514
|
return undefined;
|
|
1376
1515
|
if (ghTarget || keyHint || noGit)
|
|
1377
1516
|
return undefined;
|
|
1378
|
-
if (
|
|
1517
|
+
if (hasBranchFlag)
|
|
1379
1518
|
return undefined; // not a real put flag today; defensive only
|
|
1380
1519
|
try {
|
|
1381
1520
|
if (deriveRepoFromGit(run) === undefined)
|
|
@@ -1400,19 +1539,69 @@ function resolvePutNudge(opts) {
|
|
|
1400
1539
|
// fast/fake, and execFileSync's `timeout` option is meaningless
|
|
1401
1540
|
// against anything that isn't actually shelling out.
|
|
1402
1541
|
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1403
|
-
const repoArg = flagString(flags, "--repo") ?? defaults.repo;
|
|
1404
1542
|
const repo = resolveRepo(repoArg, timed);
|
|
1405
1543
|
pr = resolveCurrentPullRequest(repo, timed).num;
|
|
1406
1544
|
}
|
|
1407
1545
|
catch {
|
|
1408
1546
|
pr = undefined; // gh missing/unauthenticated/timed out/no open PR — generic wording
|
|
1409
1547
|
}
|
|
1410
|
-
return
|
|
1548
|
+
return { branch, pr };
|
|
1411
1549
|
}
|
|
1412
1550
|
catch {
|
|
1413
1551
|
return undefined;
|
|
1414
1552
|
}
|
|
1415
1553
|
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Auto-PR context (issue #700): when a bare put/screenshot has no explicit
|
|
1556
|
+
* destination flag at all (`--pr`/`--issue`/`--key`/`--ref`/`--prefix`/
|
|
1557
|
+
* `--destination`, and for `screenshot` no explicit `--branch`) and runs on a
|
|
1558
|
+
* branch that maps to exactly one open PR, this resolves that PR so the
|
|
1559
|
+
* caller can behave as if `--pr <n>` had been passed — stable key + managed
|
|
1560
|
+
* comment sync — instead of the #403/#469 staging default or the plain dated
|
|
1561
|
+
* layout. `resolveCurrentPullRequest`'s `gh pr view <branch>` lookup is
|
|
1562
|
+
* already the unambiguous case: it names the single open PR whose head is
|
|
1563
|
+
* that branch, or fails (no open PR, or `gh` unavailable/unauthenticated) —
|
|
1564
|
+
* there is no "ambiguous, more than one" state to further disambiguate.
|
|
1565
|
+
* Opt-out: `noAutoPr` (the caller folds in `--no-pr` and
|
|
1566
|
+
* `UPLOADS_NO_AUTO_PR=1`/config). Never fires outside a git checkout, on the
|
|
1567
|
+
* default branch, or with `--no-git`; any failure (not a repo, detached
|
|
1568
|
+
* HEAD, gh missing/unauthenticated/timed out, no open PR) degrades to
|
|
1569
|
+
* undefined so the caller falls back to its normal staging/dated behavior.
|
|
1570
|
+
*/
|
|
1571
|
+
export function resolveAutoPrTarget(opts) {
|
|
1572
|
+
const { ghTarget, keyHint, refArg, prefixArg, destinationArg, branchArg, noGit, noAutoPr, repoArg, run, } = opts;
|
|
1573
|
+
if (noAutoPr)
|
|
1574
|
+
return undefined;
|
|
1575
|
+
if (ghTarget || keyHint || noGit)
|
|
1576
|
+
return undefined;
|
|
1577
|
+
if (refArg || prefixArg || destinationArg || branchArg !== undefined)
|
|
1578
|
+
return undefined;
|
|
1579
|
+
try {
|
|
1580
|
+
if (deriveRepoFromGit(run) === undefined)
|
|
1581
|
+
return undefined; // not a (usable) git repo
|
|
1582
|
+
let branch;
|
|
1583
|
+
try {
|
|
1584
|
+
branch = resolveCurrentBranch(run);
|
|
1585
|
+
}
|
|
1586
|
+
catch {
|
|
1587
|
+
return undefined; // detached HEAD, or git unavailable
|
|
1588
|
+
}
|
|
1589
|
+
const defaultBranch = resolveDefaultBranch(run);
|
|
1590
|
+
const onDefaultBranch = defaultBranch
|
|
1591
|
+
? branch === defaultBranch
|
|
1592
|
+
: branch === "main" || branch === "master"; // undetermined: err toward the old default
|
|
1593
|
+
if (onDefaultBranch)
|
|
1594
|
+
return undefined;
|
|
1595
|
+
// Same bounded-timeout treatment as the #393 nudge's `gh pr view` call —
|
|
1596
|
+
// this must never be felt as a hang.
|
|
1597
|
+
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1598
|
+
const repo = resolveRepo(repoArg, timed);
|
|
1599
|
+
return resolveCurrentPullRequest(repo, timed);
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
return undefined; // gh/git unavailable, no open PR, or repo unresolvable
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1416
1605
|
/**
|
|
1417
1606
|
* Bare-put branch-staging trigger (issue #403): put on a non-default git
|
|
1418
1607
|
* branch stages to the branch prefix by default — the branch becomes the
|
|
@@ -1637,10 +1826,39 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1637
1826
|
});
|
|
1638
1827
|
}
|
|
1639
1828
|
const multi = files.length > 1;
|
|
1829
|
+
// Resolved early (issue #700): both the auto-PR opt-out default and the
|
|
1830
|
+
// `--no-git`-gated staging/auto-PR detection below need it before the rest
|
|
1831
|
+
// of put's flag parsing.
|
|
1832
|
+
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
1833
|
+
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
1640
1834
|
const keyHint = flagString(parsed.flags, "--key");
|
|
1641
1835
|
const destFlag = flagString(parsed.flags, "--destination");
|
|
1642
1836
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
1643
1837
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
1838
|
+
if (parsed.flags.has("--no-pr") && typeof parsed.flags.get("--no-pr") === "string") {
|
|
1839
|
+
throw new UsageError("--no-pr takes no value");
|
|
1840
|
+
}
|
|
1841
|
+
const noAutoPr = flagBool(parsed.flags, "--no-pr") || defaults.noAutoPr === true;
|
|
1842
|
+
// Auto-PR context (issue #700): a bare put (no --pr/--issue/--key/--ref/
|
|
1843
|
+
// --prefix/--destination, not --no-git/--no-pr) on a branch that maps to
|
|
1844
|
+
// exactly one open PR behaves as if `--pr <n>` had been passed — see
|
|
1845
|
+
// resolveAutoPrTarget. Supersedes both the #403 staging default and the
|
|
1846
|
+
// #393 nudge for this case; computed before the gh.* metadata resolution
|
|
1847
|
+
// below since it takes over that resolution entirely.
|
|
1848
|
+
const autoPrTarget = ghTarget
|
|
1849
|
+
? undefined
|
|
1850
|
+
: resolveAutoPrTarget({
|
|
1851
|
+
ghTarget,
|
|
1852
|
+
keyHint,
|
|
1853
|
+
refArg: flagString(parsed.flags, "--ref"),
|
|
1854
|
+
prefixArg: prefixFlag,
|
|
1855
|
+
destinationArg: destFlag,
|
|
1856
|
+
noGit,
|
|
1857
|
+
noAutoPr,
|
|
1858
|
+
repoArg: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1859
|
+
run,
|
|
1860
|
+
});
|
|
1861
|
+
const effectiveGhTarget = ghTarget ?? autoPrTarget;
|
|
1644
1862
|
// Comment sync runs by default with --pr/--issue (matches `attach`); opt
|
|
1645
1863
|
// out with --no-comment. --comment is accepted as a redundant no-op for
|
|
1646
1864
|
// back-compat with scripts written before this default flipped (#537).
|
|
@@ -1675,7 +1893,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1675
1893
|
if (parsed.flags.has("--no-auto") && typeof parsed.flags.get("--no-auto") === "string") {
|
|
1676
1894
|
throw new UsageError("--no-auto takes no value");
|
|
1677
1895
|
}
|
|
1678
|
-
if (parsed.flags.has("--no-comment") && !
|
|
1896
|
+
if (parsed.flags.has("--no-comment") && !effectiveGhTarget) {
|
|
1679
1897
|
throw new UsageError("--no-comment requires --pr or --issue");
|
|
1680
1898
|
}
|
|
1681
1899
|
if (multi) {
|
|
@@ -1712,7 +1930,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1712
1930
|
destination: destFlag,
|
|
1713
1931
|
prefix: prefixFlag,
|
|
1714
1932
|
key: keyHint,
|
|
1715
|
-
ghAttachment: Boolean(
|
|
1933
|
+
ghAttachment: Boolean(effectiveGhTarget),
|
|
1716
1934
|
});
|
|
1717
1935
|
}
|
|
1718
1936
|
catch (err) {
|
|
@@ -1728,7 +1946,6 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1728
1946
|
return raw;
|
|
1729
1947
|
throw new UsageError(`invalid --format: ${raw}`);
|
|
1730
1948
|
})();
|
|
1731
|
-
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
1732
1949
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
1733
1950
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
1734
1951
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
@@ -1741,16 +1958,16 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1741
1958
|
throw new UsageError(`invalid --width: ${widthRaw}`);
|
|
1742
1959
|
})()
|
|
1743
1960
|
: defaults.width;
|
|
1744
|
-
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
1745
1961
|
// Bare-put branch staging (issue #403): a bare put (no --pr/--issue/--key/
|
|
1746
1962
|
// --ref/--prefix/--destination, not --no-git) on a non-default git branch
|
|
1747
1963
|
// stages to the branch prefix — identical key/metadata to `attach
|
|
1748
1964
|
// --branch` — instead of the dated layout. Computed before gh.* metadata
|
|
1749
1965
|
// resolution below since it takes over that resolution entirely (branch
|
|
1750
1966
|
// metadata, not PR/issue metadata) and supersedes the #393 nudge for this
|
|
1751
|
-
// case.
|
|
1967
|
+
// case. `effectiveGhTarget` (explicit --pr/--issue OR the #700 auto-PR
|
|
1968
|
+
// match) wins over staging, same as it wins over the dated layout.
|
|
1752
1969
|
const stagingTarget = resolvePutStagingTarget({
|
|
1753
|
-
ghTarget,
|
|
1970
|
+
ghTarget: effectiveGhTarget,
|
|
1754
1971
|
keyHint,
|
|
1755
1972
|
refArg: flagString(parsed.flags, "--ref"),
|
|
1756
1973
|
prefixArg: prefixFlag,
|
|
@@ -1759,15 +1976,16 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1759
1976
|
repoArg: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1760
1977
|
run,
|
|
1761
1978
|
});
|
|
1762
|
-
// gh.* metadata: explicit --pr/--issue target
|
|
1763
|
-
// wins over --meta the same way (matches attach
|
|
1764
|
-
// best-effort auto resolution (on by default) where
|
|
1765
|
-
// --no-auto, or UPLOADS_NO_AUTO_META disable auto;
|
|
1766
|
-
// config default but never past --no-git (no repo
|
|
1979
|
+
// gh.* metadata: explicit --pr/--issue target (or the #700 auto-PR match)
|
|
1980
|
+
// wins over --meta; staging wins over --meta the same way (matches attach
|
|
1981
|
+
// --branch); otherwise best-effort auto resolution (on by default) where
|
|
1982
|
+
// --meta wins. --no-git, --no-auto, or UPLOADS_NO_AUTO_META disable auto;
|
|
1983
|
+
// --auto forces past the config default but never past --no-git (no repo
|
|
1984
|
+
// to resolve).
|
|
1767
1985
|
let metadata = userMeta;
|
|
1768
1986
|
let attachedRef;
|
|
1769
|
-
if (
|
|
1770
|
-
const merged = { ...userMeta, ...ghMetadataFromTargetWithTitle(
|
|
1987
|
+
if (effectiveGhTarget) {
|
|
1988
|
+
const merged = { ...userMeta, ...ghMetadataFromTargetWithTitle(effectiveGhTarget, run) };
|
|
1771
1989
|
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
1772
1990
|
metadata = merged;
|
|
1773
1991
|
attachedRef = merged["gh.ref"];
|
|
@@ -1812,23 +2030,31 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1812
2030
|
if (slug)
|
|
1813
2031
|
metadata = mergeDerivedMeta(metadata, { repo: slug });
|
|
1814
2032
|
}
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
|
|
1820
|
-
//
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
|
|
2033
|
+
// #692 follow-up: a path-tagged upload with no repo/gh.repo/app context and
|
|
2034
|
+
// no real (non-local) origin lands in the screenshots page's fallback
|
|
2035
|
+
// buckets — one advisory line at the moment the context went missing.
|
|
2036
|
+
// --no-git is an explicit choice, so it suppresses the nudge too.
|
|
2037
|
+
const contextNudge = !ctx.quiet && !defaults.noNudge && !noGit ? noProjectContextNudge(metadata) : undefined;
|
|
2038
|
+
// Bare-put nudge (issue #393): only relevant when neither auto-PR nor
|
|
2039
|
+
// staging took over — once `effectiveGhTarget`/`stagingTarget` resolves,
|
|
2040
|
+
// that IS the upgrade the nudge used to point at, so this is skipped
|
|
2041
|
+
// entirely rather than firing redundantly. Still fires as before for a
|
|
2042
|
+
// bare put that lands on the dated layout with a detectable PR (e.g. an
|
|
2043
|
+
// explicit --ref/--prefix opts out of staging AND auto-PR, or --no-pr/
|
|
2044
|
+
// UPLOADS_NO_AUTO_PR opts out of auto-PR specifically). The concrete
|
|
2045
|
+
// key-naming text (issue #700) is finished below, once upload keys exist.
|
|
2046
|
+
// Best-effort — see resolvePutNudgeContext; never affects exit code,
|
|
2047
|
+
// stdout, or the upload.
|
|
2048
|
+
const nudgeContext = effectiveGhTarget || stagingTarget
|
|
1824
2049
|
? undefined
|
|
1825
|
-
:
|
|
1826
|
-
ctx,
|
|
1827
|
-
|
|
2050
|
+
: resolvePutNudgeContext({
|
|
2051
|
+
quiet: ctx.quiet,
|
|
2052
|
+
noNudge: defaults.noNudge === true,
|
|
1828
2053
|
ghTarget,
|
|
1829
2054
|
keyHint,
|
|
2055
|
+
hasBranchFlag: parsed.flags.has("--branch"),
|
|
1830
2056
|
noGit,
|
|
1831
|
-
defaults,
|
|
2057
|
+
repoArg: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1832
2058
|
run,
|
|
1833
2059
|
});
|
|
1834
2060
|
// Staging note (issue #403): same suppression as the #393 nudge
|
|
@@ -1837,6 +2063,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1837
2063
|
const stagingNote = stagingTarget && !ctx.quiet && !defaults.noNudge
|
|
1838
2064
|
? putStagingNoteText(stagingTarget.branch)
|
|
1839
2065
|
: undefined;
|
|
2066
|
+
// Auto-PR note (issue #700): announces the default-behavior change at the
|
|
2067
|
+
// moment it fires, so a bare put that silently became a --pr attach isn't
|
|
2068
|
+
// a surprise — names the PR and how to opt out. Same suppression as the
|
|
2069
|
+
// other advisories (--quiet, UPLOADS_NO_NUDGE=1); NOT gated by --no-pr/
|
|
2070
|
+
// UPLOADS_NO_AUTO_PR since those are what prevent it from firing at all.
|
|
2071
|
+
const autoPrNote = autoPrTarget && !ctx.quiet && !defaults.noNudge ? autoPrNoteText(autoPrTarget.num) : undefined;
|
|
1840
2072
|
const logHuman = !ctx.quiet && format === "human";
|
|
1841
2073
|
if (logHuman) {
|
|
1842
2074
|
if (multi) {
|
|
@@ -1854,7 +2086,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1854
2086
|
files,
|
|
1855
2087
|
nameOverride: nameFlag,
|
|
1856
2088
|
explicitKey: keyHint,
|
|
1857
|
-
ghTarget,
|
|
2089
|
+
ghTarget: effectiveGhTarget,
|
|
1858
2090
|
ghBranchTarget: stagingTarget,
|
|
1859
2091
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
1860
2092
|
repo: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
@@ -1874,6 +2106,13 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1874
2106
|
if (uploads.length === 0 && failures.length > 0 && !multi) {
|
|
1875
2107
|
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
1876
2108
|
}
|
|
2109
|
+
// Concrete bare-put nudge text (issue #700): built once upload keys exist,
|
|
2110
|
+
// so the ready-made follow-up names them, e.g.
|
|
2111
|
+
// "uploads attach --pr 1250 f/abc123.webp". Falls back to the plain
|
|
2112
|
+
// #393 wording when there are no successful uploads to name.
|
|
2113
|
+
const nudge = nudgeContext && uploads.length > 0
|
|
2114
|
+
? putNudgeText(nudgeContext.branch, nudgeContext.pr, uploads.map((u) => u.key))
|
|
2115
|
+
: undefined;
|
|
1877
2116
|
// Stage-time binding warning (issue #398/#400): same check `attach
|
|
1878
2117
|
// --branch` runs, now also on the bare-put staging path. Best-effort — see
|
|
1879
2118
|
// resolveStageBindingWarning; never affects exit code or the upload.
|
|
@@ -1881,20 +2120,19 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1881
2120
|
? await resolveStageBindingWarning({ ctx, defaults, repo: stagingTarget.repo })
|
|
1882
2121
|
: undefined;
|
|
1883
2122
|
// Lever 3 (issue #469): tip when a --pr/--issue put lands an image with no
|
|
1884
|
-
// `path` meta. Only relevant on the
|
|
1885
|
-
//
|
|
1886
|
-
// nothing to look up from a page later.
|
|
1887
|
-
const pathHint =
|
|
2123
|
+
// `path` meta. Only relevant on the (explicit or auto) gh target path — the
|
|
2124
|
+
// bare-put staging/dated paths aren't attached to a PR/issue yet, so
|
|
2125
|
+
// there's nothing to look up from a page later.
|
|
2126
|
+
const pathHint = effectiveGhTarget && uploads.length > 0 && !ctx.quiet
|
|
1888
2127
|
? pathMetaHintFor(uploads, sentMetadata)
|
|
1889
2128
|
: undefined;
|
|
1890
|
-
// One JSON `hint` slot, shared
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
//
|
|
1896
|
-
|
|
1897
|
-
const jsonHint = nudge ?? bindingWarning ?? stagingNote ?? pathHint;
|
|
2129
|
+
// One JSON `hint` slot, shared across every advisory this command can
|
|
2130
|
+
// surface. `autoPrNote` and `nudge` are mutually exclusive with each other
|
|
2131
|
+
// and with `stagingNote` (each corresponds to a different destination the
|
|
2132
|
+
// upload landed on); pathHint only ever fires on the gh-target path, so it
|
|
2133
|
+
// never competes with the other three. Same precedence stderr prints,
|
|
2134
|
+
// below.
|
|
2135
|
+
const jsonHint = autoPrNote ?? nudge ?? bindingWarning ?? stagingNote ?? pathHint ?? contextNudge;
|
|
1898
2136
|
const galleriesByKey = new Map();
|
|
1899
2137
|
let galleryHadError = false;
|
|
1900
2138
|
if (galleryId && uploads.length > 0) {
|
|
@@ -1917,9 +2155,9 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1917
2155
|
}
|
|
1918
2156
|
let comment;
|
|
1919
2157
|
let commentError;
|
|
1920
|
-
if (wantComment &&
|
|
2158
|
+
if (wantComment && effectiveGhTarget && !dryRun && uploads.length > 0) {
|
|
1921
2159
|
try {
|
|
1922
|
-
comment = await syncAttachmentsComment(ctx.client,
|
|
2160
|
+
comment = await syncAttachmentsComment(ctx.client, effectiveGhTarget, run, ctx.config.workspace);
|
|
1923
2161
|
if (logHuman)
|
|
1924
2162
|
process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
|
|
1925
2163
|
}
|
|
@@ -1971,6 +2209,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1971
2209
|
for (const failure of failures) {
|
|
1972
2210
|
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
1973
2211
|
}
|
|
2212
|
+
if (autoPrNote)
|
|
2213
|
+
process.stderr.write(`${autoPrNote}\n`);
|
|
1974
2214
|
if (nudge)
|
|
1975
2215
|
process.stderr.write(`${nudge}\n`);
|
|
1976
2216
|
if (stagingNote)
|
|
@@ -1979,6 +2219,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1979
2219
|
process.stderr.write(`${bindingWarning}\n`);
|
|
1980
2220
|
if (pathHint)
|
|
1981
2221
|
process.stderr.write(`${pathHint}\n`);
|
|
2222
|
+
if (contextNudge)
|
|
2223
|
+
process.stderr.write(`${contextNudge}\n`);
|
|
1982
2224
|
}
|
|
1983
2225
|
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
1984
2226
|
}
|
|
@@ -2032,6 +2274,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
2032
2274
|
if (gallery?.error) {
|
|
2033
2275
|
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
2034
2276
|
}
|
|
2277
|
+
if (autoPrNote && format !== "json")
|
|
2278
|
+
process.stderr.write(`${autoPrNote}\n`);
|
|
2035
2279
|
if (nudge && format !== "json")
|
|
2036
2280
|
process.stderr.write(`${nudge}\n`);
|
|
2037
2281
|
if (stagingNote && format !== "json")
|
|
@@ -2040,6 +2284,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
2040
2284
|
process.stderr.write(`${bindingWarning}\n`);
|
|
2041
2285
|
if (pathHint && format !== "json")
|
|
2042
2286
|
process.stderr.write(`${pathHint}\n`);
|
|
2287
|
+
if (contextNudge && format !== "json")
|
|
2288
|
+
process.stderr.write(`${contextNudge}\n`);
|
|
2043
2289
|
return gallery?.error ? 1 : 0;
|
|
2044
2290
|
}
|
|
2045
2291
|
// --- galleries ---
|
|
@@ -2726,7 +2972,9 @@ Scans the PR/issue description and comments for github.com/user-attachments
|
|
|
2726
2972
|
media, mirrors new ones into the workspace (indexed, not added to the managed
|
|
2727
2973
|
comment), and detaches ones no longer referenced. Works on any repo linked to
|
|
2728
2974
|
the workspace; the .uploads.yml ingestGithubAttachments knob only gates the
|
|
2729
|
-
automatic webhook path.
|
|
2975
|
+
automatic webhook path. Bot-authored attachments and images under 200px on
|
|
2976
|
+
either side are always skipped (the .uploads.yml ingestBotAttachments knob
|
|
2977
|
+
re-admits bot media on the webhook path only).
|
|
2730
2978
|
|
|
2731
2979
|
Examples:
|
|
2732
2980
|
uploads ingest --pr 123
|