@buildinternet/uploads 0.6.0 → 0.8.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/README.md +14 -3
- package/dist/cli-args.d.ts +17 -2
- package/dist/cli-args.js +50 -5
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +68 -25
- package/dist/client.d.ts +137 -0
- package/dist/client.js +178 -3
- package/dist/commands/install.js +110 -52
- package/dist/commands/invite.d.ts +10 -0
- package/dist/commands/invite.js +102 -0
- package/dist/commands/login.d.ts +29 -1
- package/dist/commands/login.js +172 -12
- package/dist/commands/setup.js +4 -2
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +211 -17
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +1 -1
- package/dist/github.d.ts +17 -1
- package/dist/github.js +32 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp/args.d.ts +17 -0
- package/dist/mcp/args.js +42 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +121 -21
- package/dist/metadata.d.ts +32 -0
- package/dist/metadata.js +102 -0
- package/dist/public-urls.d.ts +18 -0
- package/dist/public-urls.js +68 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { createUploadsClient } from "./client.js";
|
|
4
|
-
import { parseCommandArgs, flagString, flagBool, flagInt, UsageError, } from "./cli-args.js";
|
|
4
|
+
import { parseCommandArgs, flagString, flagBool, flagInt, flagValues, UsageError, } from "./cli-args.js";
|
|
5
5
|
import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./config.js";
|
|
6
6
|
import { buildMarkdown } from "./embed.js";
|
|
7
|
+
import { urlForGithubEmbed } from "./public-urls.js";
|
|
7
8
|
import { UploadsError } from "./errors.js";
|
|
8
9
|
import { writeJson, writeStdout } from "./io.js";
|
|
9
|
-
import {
|
|
10
|
+
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
11
|
+
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
10
12
|
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
11
13
|
import { resolvePutPrefix } from "./destinations.js";
|
|
12
14
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
13
15
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
14
16
|
import { buildCliProvenance } from "./provenance.js";
|
|
15
17
|
import { packageVersion } from "./package-version.js";
|
|
18
|
+
/** Read a local file (or `-` for stdin). Missing path → FILE_NOT_FOUND (exit 2). */
|
|
19
|
+
export function readFileArg(fileArg) {
|
|
20
|
+
try {
|
|
21
|
+
return new Uint8Array(readFileSync(fileArg === "-" ? 0 : fileArg));
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err?.code === "ENOENT") {
|
|
25
|
+
throw new UploadsError(`file not found: ${fileArg}`, "FILE_NOT_FOUND");
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
// --- put ---
|
|
17
31
|
const PUT_HELP = `uploads put <file> [options]
|
|
18
32
|
|
|
@@ -30,8 +44,12 @@ Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
|
30
44
|
remain public even for private/internal GitHub repositories. Upload only media
|
|
31
45
|
that is safe at a predictable public URL.
|
|
32
46
|
|
|
47
|
+
Human/json output includes durable url and (when dual-host applies) embedUrl.
|
|
48
|
+
MARKDOWN prefers embedUrl for GitHub. Override: UPLOADS_EMBED_PUBLIC_BASE_URL.
|
|
49
|
+
|
|
33
50
|
Options:
|
|
34
51
|
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
52
|
+
--name <leaf> Clean key leaf + default alt (no '/'); keeps --pr/default path. Not with --key
|
|
35
53
|
--destination <id> Typed root: screenshots | gh | f (sets --prefix)
|
|
36
54
|
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
37
55
|
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
@@ -53,13 +71,24 @@ Options:
|
|
|
53
71
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
54
72
|
--comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
|
|
55
73
|
--gallery <id> Add the uploaded object to this public gallery
|
|
74
|
+
--meta <k=v> Queryable custom metadata (repeatable; value may contain "="): key ^[a-z][a-z0-9._-]{0,63}$, value 1-512 printable ASCII, max 24 pairs
|
|
75
|
+
Re-uploading to an existing key WITH --meta replaces that file's
|
|
76
|
+
entire metadata set; without --meta the existing metadata is
|
|
77
|
+
preserved. Use "uploads meta set" to edit individual keys.
|
|
78
|
+
--dry-run Print key + public URL without uploading. Not with --comment/--gallery
|
|
79
|
+
|
|
80
|
+
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
|
|
81
|
+
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
56
82
|
|
|
57
83
|
Examples:
|
|
58
84
|
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
59
85
|
uploads put ./mobile.png --frame phone
|
|
60
86
|
uploads put ./ui.png --frame browser --frame-url "https://app.example/settings"
|
|
61
87
|
uploads put ./shot.png --destination screenshots
|
|
88
|
+
uploads put ./capture-….webp --pr 128 --name hero.webp
|
|
89
|
+
uploads put ./shot.png --pr 128 --name hero.webp --dry-run --format url
|
|
62
90
|
uploads put ./after.png --gallery gal_example
|
|
91
|
+
uploads put ./shot.png --meta app=myapp --meta page=settings
|
|
63
92
|
`;
|
|
64
93
|
/**
|
|
65
94
|
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
@@ -160,7 +189,7 @@ function frameOptionsFromFlags(flags) {
|
|
|
160
189
|
* fatal (`comment` command) or a warning (`put --comment`).
|
|
161
190
|
*/
|
|
162
191
|
export async function syncAttachmentsComment(client, target, run) {
|
|
163
|
-
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
|
|
192
|
+
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url, embedUrl }) => ({ key, url, embedUrl }));
|
|
164
193
|
const galleries = [];
|
|
165
194
|
let cursor;
|
|
166
195
|
do {
|
|
@@ -181,7 +210,12 @@ export async function syncAttachmentsComment(client, target, run) {
|
|
|
181
210
|
previews: detail.items
|
|
182
211
|
.filter((item) => item.status === "available" && item.url && item.contentType?.startsWith("image/"))
|
|
183
212
|
.slice(0, 3)
|
|
184
|
-
.map((item) => ({
|
|
213
|
+
.map((item) => ({
|
|
214
|
+
url: item.url,
|
|
215
|
+
embedUrl: item.embedUrl,
|
|
216
|
+
alt: item.altText ?? item.objectKey,
|
|
217
|
+
itemUrl: item.pageUrl,
|
|
218
|
+
})),
|
|
185
219
|
};
|
|
186
220
|
}
|
|
187
221
|
catch {
|
|
@@ -222,12 +256,19 @@ Options:
|
|
|
222
256
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
223
257
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
224
258
|
--workspace, -w <name> Override workspace
|
|
259
|
+
--meta <k=v> Extra queryable metadata (repeatable; value may contain "=").
|
|
260
|
+
gh.repo/gh.kind/gh.number/gh.ref are always set from the resolved
|
|
261
|
+
target — a --meta pair with the same key is overridden by it.
|
|
262
|
+
Because attach always sends its own gh.* pairs, re-attaching to
|
|
263
|
+
the same key always replaces that file's entire metadata set
|
|
264
|
+
(never preserves) — use "uploads meta set" to add to it instead.
|
|
225
265
|
|
|
226
266
|
Examples:
|
|
227
267
|
uploads attach ./before.png ./after.png
|
|
228
268
|
uploads attach ./mobile.png --frame phone
|
|
229
269
|
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
230
270
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
271
|
+
uploads attach ./shot.png --meta app=myapp --meta page=settings
|
|
231
272
|
`;
|
|
232
273
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
233
274
|
const parsed = parseCommandArgs(args);
|
|
@@ -249,6 +290,15 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
249
290
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
250
291
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
251
292
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
293
|
+
// User-supplied extras first, then the resolved target's gh.* — explicit
|
|
294
|
+
// target pairs always win over a same-named --meta extra (documented above).
|
|
295
|
+
// Validate the merged map (not just the extras) so the 24-key/8KB caps are
|
|
296
|
+
// enforced client-side even when extras alone are under the cap but extras
|
|
297
|
+
// + the 4 gh.* pairs push the merged map over it.
|
|
298
|
+
const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
|
|
299
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
300
|
+
if (Object.keys(metadata).length > 0)
|
|
301
|
+
validateMetaMap(metadata);
|
|
252
302
|
const results = [];
|
|
253
303
|
for (const file of parsed.positionals) {
|
|
254
304
|
if (file === "-")
|
|
@@ -256,7 +306,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
256
306
|
const sourceName = basename(file);
|
|
257
307
|
if (!ctx.quiet && !ctx.json)
|
|
258
308
|
process.stderr.write(`>> uploading ${file}\n`);
|
|
259
|
-
const prepared = await prepareImageForUpload(
|
|
309
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
260
310
|
...frameOpts,
|
|
261
311
|
optimize: optimizeOpts,
|
|
262
312
|
});
|
|
@@ -276,10 +326,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
276
326
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
277
327
|
keepExif: optimizeOpts.keepExif === true,
|
|
278
328
|
}),
|
|
329
|
+
metadata,
|
|
279
330
|
});
|
|
331
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
280
332
|
results.push({
|
|
281
333
|
...result,
|
|
282
|
-
markdown: buildMarkdown(
|
|
334
|
+
markdown: buildMarkdown(embedSrc, { alt: sourceName }),
|
|
283
335
|
optimize: {
|
|
284
336
|
optimized: prepared.optimized,
|
|
285
337
|
skippedReason: prepared.skippedReason,
|
|
@@ -306,7 +358,8 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
306
358
|
}
|
|
307
359
|
else {
|
|
308
360
|
for (const result of results) {
|
|
309
|
-
|
|
361
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
362
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
310
363
|
}
|
|
311
364
|
if (!ctx.quiet && comment)
|
|
312
365
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
@@ -334,20 +387,41 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
334
387
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
335
388
|
const wantComment = parsed.flags.has("--comment");
|
|
336
389
|
const galleryId = flagString(parsed.flags, "--gallery");
|
|
390
|
+
const nameFlag = flagString(parsed.flags, "--name");
|
|
391
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
392
|
+
// Validate --meta up front (fail fast, before reading/optimizing the file).
|
|
393
|
+
const metadata = (() => {
|
|
394
|
+
const pairs = flagValues(parsed.flags, "--meta");
|
|
395
|
+
return pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
396
|
+
})();
|
|
337
397
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
338
398
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
339
399
|
}
|
|
340
400
|
if (wantComment && !ghTarget)
|
|
341
401
|
throw new UsageError("--comment requires --pr or --issue");
|
|
342
402
|
if (ghTarget) {
|
|
343
|
-
if (keyHint)
|
|
344
|
-
throw new UsageError("--key cannot be combined with --pr/--issue");
|
|
403
|
+
if (keyHint) {
|
|
404
|
+
throw new UsageError("--key cannot be combined with --pr/--issue; use --name <leaf> to set a clean filename on the stable path");
|
|
405
|
+
}
|
|
345
406
|
if (flagString(parsed.flags, "--ref")) {
|
|
346
407
|
throw new UsageError("--ref cannot be combined with --pr/--issue");
|
|
347
408
|
}
|
|
348
409
|
if (prefixFlag)
|
|
349
410
|
throw new UsageError("--prefix cannot be combined with --pr/--issue");
|
|
350
411
|
}
|
|
412
|
+
if (nameFlag !== undefined) {
|
|
413
|
+
if (nameFlag === "" || nameFlag.includes("/")) {
|
|
414
|
+
throw new UsageError("--name must be a bare filename with no '/'");
|
|
415
|
+
}
|
|
416
|
+
if (keyHint)
|
|
417
|
+
throw new UsageError("--name cannot be combined with --key");
|
|
418
|
+
}
|
|
419
|
+
if (dryRun) {
|
|
420
|
+
if (wantComment)
|
|
421
|
+
throw new UsageError("--dry-run cannot be combined with --comment");
|
|
422
|
+
if (galleryId)
|
|
423
|
+
throw new UsageError("--dry-run cannot be combined with --gallery");
|
|
424
|
+
}
|
|
351
425
|
let resolvedPrefix;
|
|
352
426
|
try {
|
|
353
427
|
resolvedPrefix = resolvePutPrefix({
|
|
@@ -360,8 +434,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
360
434
|
catch (err) {
|
|
361
435
|
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
362
436
|
}
|
|
363
|
-
const bytes =
|
|
364
|
-
const sourceName = fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg);
|
|
437
|
+
const bytes = readFileArg(fileArg);
|
|
438
|
+
const sourceName = nameFlag ?? (fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg));
|
|
365
439
|
const format = ctx.json
|
|
366
440
|
? "json"
|
|
367
441
|
: (() => {
|
|
@@ -375,6 +449,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
375
449
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
376
450
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
377
451
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
452
|
+
// Optimize even on --dry-run so the preview key extension/hash match a real put.
|
|
378
453
|
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
379
454
|
...frameOpts,
|
|
380
455
|
optimize: optimizeOpts,
|
|
@@ -391,7 +466,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
391
466
|
})()
|
|
392
467
|
: defaults.width;
|
|
393
468
|
if (!ctx.quiet && format === "human") {
|
|
394
|
-
process.stderr.write(`>> uploading ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
469
|
+
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
395
470
|
if (prepared.frame?.framed)
|
|
396
471
|
process.stderr.write(`>> framed with ${prepared.frame.frameId}\n`);
|
|
397
472
|
const note = formatOptimizeNote(prepared);
|
|
@@ -410,14 +485,17 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
410
485
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
411
486
|
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
412
487
|
deriveRepoFromGit: !noGit,
|
|
488
|
+
dryRun,
|
|
413
489
|
provenance: buildCliProvenance({
|
|
414
490
|
sourceName,
|
|
415
491
|
optimized: prepared.optimized,
|
|
416
492
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
417
493
|
keepExif: optimizeOpts.keepExif === true,
|
|
418
494
|
}),
|
|
495
|
+
metadata,
|
|
419
496
|
});
|
|
420
|
-
const
|
|
497
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
498
|
+
const markdown = buildMarkdown(embedSrc, { alt, width });
|
|
421
499
|
let gallery;
|
|
422
500
|
if (galleryId) {
|
|
423
501
|
try {
|
|
@@ -442,7 +520,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
442
520
|
filename: prepared.filename,
|
|
443
521
|
};
|
|
444
522
|
if (!ctx.quiet && format === "human") {
|
|
445
|
-
process.stderr.write(`>> key: ${result.key}\n\n`);
|
|
523
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
|
|
446
524
|
}
|
|
447
525
|
switch (format) {
|
|
448
526
|
case "json":
|
|
@@ -452,6 +530,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
452
530
|
optimize: optimizeMeta,
|
|
453
531
|
frame: prepared.frame,
|
|
454
532
|
gallery,
|
|
533
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
455
534
|
});
|
|
456
535
|
break;
|
|
457
536
|
case "url":
|
|
@@ -460,8 +539,10 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
460
539
|
case "markdown":
|
|
461
540
|
await writeStdout(`${markdown}\n`);
|
|
462
541
|
break;
|
|
463
|
-
default:
|
|
464
|
-
|
|
542
|
+
default: {
|
|
543
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
544
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
545
|
+
}
|
|
465
546
|
}
|
|
466
547
|
if (gallery?.url && format !== "human") {
|
|
467
548
|
process.stderr.write(`gallery: ${gallery.url}\n`);
|
|
@@ -699,21 +780,51 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
699
780
|
}
|
|
700
781
|
}
|
|
701
782
|
// --- list ---
|
|
702
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
783
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--meta <k=v>]... [--workspace <name>]
|
|
703
784
|
|
|
704
785
|
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
705
786
|
|
|
787
|
+
--meta <k=v> (repeatable, ANDed) switches to the metadata filter endpoint —
|
|
788
|
+
returned items include their matched metadata. Combines with --prefix, not
|
|
789
|
+
with --pr/--issue/--all. See also: uploads find (positional-pair alias).
|
|
790
|
+
|
|
706
791
|
Examples:
|
|
707
792
|
uploads list --prefix screenshots/
|
|
708
793
|
uploads list --pr 123
|
|
709
794
|
uploads list --all --json
|
|
795
|
+
uploads list --meta gh.repo=buildinternet/uploads --meta gh.number=123
|
|
710
796
|
`;
|
|
797
|
+
/** `--meta k=v` (repeatable) filter path, shared by `runList` and `runFind`. */
|
|
798
|
+
async function runFindFiles(ctx, filters, flags) {
|
|
799
|
+
if (flagString(flags, "--cursor") !== undefined) {
|
|
800
|
+
throw new UsageError("--cursor is not supported with metadata filters");
|
|
801
|
+
}
|
|
802
|
+
const prefix = flagString(flags, "--prefix");
|
|
803
|
+
const limit = flagInt(flags, "--limit", "--limit");
|
|
804
|
+
const result = await ctx.client.findFiles(filters, { prefix, limit });
|
|
805
|
+
if (ctx.json)
|
|
806
|
+
await writeJson(result);
|
|
807
|
+
else
|
|
808
|
+
for (const item of result.items)
|
|
809
|
+
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}\n`);
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
711
812
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
712
813
|
const parsed = parseCommandArgs(args);
|
|
713
814
|
if (help || parsed.help) {
|
|
714
815
|
process.stderr.write(LIST_HELP);
|
|
715
816
|
return 0;
|
|
716
817
|
}
|
|
818
|
+
const metaPairs = flagValues(parsed.flags, "--meta");
|
|
819
|
+
if (metaPairs.length > 0) {
|
|
820
|
+
if (ghTargetFromFlags(parsed.flags, run)) {
|
|
821
|
+
throw new UsageError("--meta cannot be combined with --pr/--issue");
|
|
822
|
+
}
|
|
823
|
+
if (flagBool(parsed.flags, "--all")) {
|
|
824
|
+
throw new UsageError("--meta cannot be combined with --all");
|
|
825
|
+
}
|
|
826
|
+
return runFindFiles(ctx, parseMetaFlags(metaPairs), parsed.flags);
|
|
827
|
+
}
|
|
717
828
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
718
829
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
719
830
|
let prefix = prefixFlag ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
|
|
@@ -746,6 +857,89 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
746
857
|
}
|
|
747
858
|
return 0;
|
|
748
859
|
}
|
|
860
|
+
// --- find ---
|
|
861
|
+
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
862
|
+
|
|
863
|
+
Human-friendly alias for \`uploads list --meta k=v...\` — same metadata filter
|
|
864
|
+
(ANDed equality), same output; pairs are positional instead of repeated flags.
|
|
865
|
+
|
|
866
|
+
Examples:
|
|
867
|
+
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
868
|
+
uploads find app=myapp page=settings --prefix screenshots/
|
|
869
|
+
`;
|
|
870
|
+
export async function runFind(ctx, args, help = false) {
|
|
871
|
+
const parsed = parseCommandArgs(args);
|
|
872
|
+
if (help || parsed.help) {
|
|
873
|
+
process.stderr.write(FIND_HELP);
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
876
|
+
if (parsed.positionals.length === 0) {
|
|
877
|
+
process.stderr.write(FIND_HELP);
|
|
878
|
+
return 2;
|
|
879
|
+
}
|
|
880
|
+
const filters = parseMetaFlags(parsed.positionals);
|
|
881
|
+
return runFindFiles(ctx, filters, parsed.flags);
|
|
882
|
+
}
|
|
883
|
+
// --- meta ---
|
|
884
|
+
const META_HELP = `uploads meta <command> [args]
|
|
885
|
+
|
|
886
|
+
Read/write an object's queryable custom metadata (D1-backed key-value pairs;
|
|
887
|
+
distinct from the R2 provenance headers put on upload).
|
|
888
|
+
|
|
889
|
+
Commands:
|
|
890
|
+
get <key> Show metadata for an object
|
|
891
|
+
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
892
|
+
|
|
893
|
+
Examples:
|
|
894
|
+
uploads meta get screenshots/myapp/42/shot.png
|
|
895
|
+
uploads meta set screenshots/myapp/42/shot.png app=myapp page=settings
|
|
896
|
+
uploads meta set screenshots/myapp/42/shot.png --delete app --delete page
|
|
897
|
+
`;
|
|
898
|
+
export async function runMeta(ctx, args, help = false) {
|
|
899
|
+
const parsed = parseCommandArgs(args);
|
|
900
|
+
const action = parsed.positionals[0];
|
|
901
|
+
if (help || parsed.help || !action) {
|
|
902
|
+
process.stderr.write(META_HELP);
|
|
903
|
+
return help || parsed.help ? 0 : 2;
|
|
904
|
+
}
|
|
905
|
+
switch (action) {
|
|
906
|
+
case "get": {
|
|
907
|
+
const key = parsed.positionals[1];
|
|
908
|
+
if (!key)
|
|
909
|
+
throw new UsageError("meta get requires an object key");
|
|
910
|
+
const result = await ctx.client.getMetadata(key);
|
|
911
|
+
if (ctx.json)
|
|
912
|
+
await writeJson(result);
|
|
913
|
+
else
|
|
914
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
915
|
+
await writeStdout(`${k}=${v}\n`);
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
case "set": {
|
|
919
|
+
const key = parsed.positionals[1];
|
|
920
|
+
if (!key)
|
|
921
|
+
throw new UsageError("meta set requires an object key");
|
|
922
|
+
const pairs = parsed.positionals.slice(2);
|
|
923
|
+
const del = flagValues(parsed.flags, "--delete");
|
|
924
|
+
if (pairs.length === 0 && del.length === 0) {
|
|
925
|
+
throw new UsageError("meta set requires k=v pairs and/or --delete <key>");
|
|
926
|
+
}
|
|
927
|
+
const set = pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
928
|
+
const result = await ctx.client.patchMetadata(key, {
|
|
929
|
+
set,
|
|
930
|
+
delete: del.length > 0 ? del : undefined,
|
|
931
|
+
});
|
|
932
|
+
if (ctx.json)
|
|
933
|
+
await writeJson(result);
|
|
934
|
+
else
|
|
935
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
936
|
+
await writeStdout(`${k}=${v}\n`);
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
default:
|
|
940
|
+
throw new UsageError(`unknown meta command: ${action}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
749
943
|
// --- delete ---
|
|
750
944
|
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
751
945
|
|
package/dist/config.js
CHANGED
|
@@ -140,11 +140,14 @@ export function resolveConfig(flags) {
|
|
|
140
140
|
}
|
|
141
141
|
function missingTokenMessage(configPath) {
|
|
142
142
|
return [
|
|
143
|
-
"
|
|
144
|
-
"
|
|
143
|
+
"You're not signed in yet — one quick step and you're set:",
|
|
144
|
+
"",
|
|
145
|
+
" uploads login # open a browser and authorize this device",
|
|
146
|
+
"",
|
|
147
|
+
"Already have a token?",
|
|
145
148
|
` uploads setup --token <token> # guided setup → ${configPath}`,
|
|
146
149
|
` uploads config init --token <token> # writes ${configPath}`,
|
|
147
|
-
" or set UPLOADS_TOKEN
|
|
150
|
+
" or set UPLOADS_TOKEN / pass --token / use --env-file",
|
|
148
151
|
].join("\n");
|
|
149
152
|
}
|
|
150
153
|
/** Warn when an explicit workspace override may not match the token's embedded workspace. */
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
1
|
+
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
2
2
|
export declare class UploadsError extends Error {
|
|
3
3
|
readonly code: UploadsErrorCode;
|
|
4
4
|
readonly status?: number;
|
package/dist/github.d.ts
CHANGED
|
@@ -22,21 +22,37 @@ export declare function ghKeyPrefix(target: GhTarget): string;
|
|
|
22
22
|
* (unlike buildScreenshotKey).
|
|
23
23
|
*/
|
|
24
24
|
export declare function ghAttachmentKey(target: GhTarget, filename: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
27
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
28
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
29
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
30
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
31
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
32
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
33
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
34
|
+
* lookup key.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ghMetadataFromTarget(target: GhTarget): Record<string, string>;
|
|
25
37
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
26
38
|
export declare const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
27
39
|
export interface AttachmentItem {
|
|
28
40
|
key: string;
|
|
29
41
|
url: string | null;
|
|
42
|
+
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
43
|
+
embedUrl?: string | null;
|
|
30
44
|
}
|
|
31
45
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
32
46
|
export interface GalleryCommentItem {
|
|
33
47
|
title: string;
|
|
34
48
|
/** Canonical URL returned by the API; callers must not synthesize it. */
|
|
35
49
|
url: string;
|
|
36
|
-
/** A bounded set of available images
|
|
50
|
+
/** A bounded set of available images; each links to its item page when known, else the gallery. */
|
|
37
51
|
previews?: {
|
|
38
52
|
url: string;
|
|
39
53
|
alt: string;
|
|
54
|
+
embedUrl?: string | null;
|
|
55
|
+
itemUrl?: string;
|
|
40
56
|
}[];
|
|
41
57
|
}
|
|
42
58
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
package/dist/github.js
CHANGED
|
@@ -63,6 +63,26 @@ export function ghKeyPrefix(target) {
|
|
|
63
63
|
export function ghAttachmentKey(target, filename) {
|
|
64
64
|
return `${ghKeyPrefix(target)}${sanitizeKeySegment(filename)}`;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
68
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
69
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
70
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
71
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
72
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
73
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
74
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
75
|
+
* lookup key.
|
|
76
|
+
*/
|
|
77
|
+
export function ghMetadataFromTarget(target) {
|
|
78
|
+
const repo = target.repo.toLowerCase();
|
|
79
|
+
return {
|
|
80
|
+
"gh.repo": repo,
|
|
81
|
+
"gh.kind": target.kind === "issues" ? "issue" : "pull",
|
|
82
|
+
"gh.number": String(target.num),
|
|
83
|
+
"gh.ref": `${repo}#${target.num}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
66
86
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
67
87
|
export const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
68
88
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
|
@@ -106,7 +126,9 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
106
126
|
const href = escapeHtmlAttr(gallery.url);
|
|
107
127
|
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
128
|
for (const preview of gallery.previews ?? []) {
|
|
109
|
-
|
|
129
|
+
const previewHref = preview.itemUrl ? escapeHtmlAttr(preview.itemUrl) : href;
|
|
130
|
+
const previewSrc = escapeHtmlAttr(preview.embedUrl ?? preview.url);
|
|
131
|
+
lines.push(`<a href="${previewHref}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${previewSrc}"></a>`);
|
|
110
132
|
}
|
|
111
133
|
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
112
134
|
}
|
|
@@ -116,17 +138,20 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
116
138
|
lines.push("### 📎 Attachments", "");
|
|
117
139
|
for (const item of sorted) {
|
|
118
140
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
119
|
-
|
|
141
|
+
const stable = item.url;
|
|
142
|
+
const src = item.embedUrl ?? item.url;
|
|
143
|
+
if (src && inferContentType(name).startsWith("image/")) {
|
|
120
144
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
121
|
-
//
|
|
145
|
+
// img src uses embed host when available (Camo revalidates); click-through keeps stable URL.
|
|
122
146
|
const w = attachmentImageWidth(name);
|
|
123
147
|
const alt = escapeHtmlAttr(name);
|
|
124
|
-
const href = escapeHtmlAttr(
|
|
125
|
-
|
|
148
|
+
const href = escapeHtmlAttr(stable ?? src);
|
|
149
|
+
const imgSrc = escapeHtmlAttr(src);
|
|
150
|
+
lines.push(`<a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>`);
|
|
126
151
|
lines.push("");
|
|
127
152
|
}
|
|
128
|
-
else if (
|
|
129
|
-
lines.push(`- [${name}](${
|
|
153
|
+
else if (stable) {
|
|
154
|
+
lines.push(`- [${name}](${stable})`);
|
|
130
155
|
}
|
|
131
156
|
else {
|
|
132
157
|
lines.push(`- ${name}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
5
|
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
6
|
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 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
|
+
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, type FindFilesOptions, type FindFilesItem, type FindFilesResult, type GetMetadataResult, type PatchMetadataOptions, } from "./client.js";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
9
|
+
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, } from "./destinations.js";
|
|
4
5
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, } from "./config.js";
|
|
5
6
|
export { UploadsError } from "./errors.js";
|
|
6
7
|
export { createUploadsClient, } from "./client.js";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
9
|
+
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, } from "./github-gh.js";
|
package/dist/mcp/args.d.ts
CHANGED
|
@@ -2,3 +2,20 @@ export type ToolArgs = Record<string, unknown>;
|
|
|
2
2
|
export declare function usage(msg: string): never;
|
|
3
3
|
export declare function optString(args: ToolArgs, name: string): string | undefined;
|
|
4
4
|
export declare function optPosInt(args: ToolArgs, name: string): number | undefined;
|
|
5
|
+
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
6
|
+
export declare function optStringRecord(args: ToolArgs, name: string): Record<string, string> | undefined;
|
|
7
|
+
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
|
8
|
+
export declare function optStringArray(args: ToolArgs, name: string): string[] | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Shared tool-description text for the metadata-shaped `metadata`/`set`/
|
|
11
|
+
* `filters` params across the CLI/local MCP (put/attach/set_metadata/
|
|
12
|
+
* find_files) and the remote MCP worker (set_metadata/find_files).
|
|
13
|
+
*/
|
|
14
|
+
export declare const METADATA_DESCRIPTION = "Queryable custom metadata (key\u2192value), separate from provenance. Omit to leave any metadata already stored for this key untouched; pass an object (even {}) to fully replace it. Keys: lowercase, ^[a-z][a-z0-9._-]{0,63}$. Values: 1-512 printable ASCII characters. Caps: at most 24 keys, at most 8192 total key+value bytes. Suggested keys: app, url, page, device, resolution, commit, branch. `gh.*` is reserved by convention for GitHub PR/issue attachment context (repo/kind/number/ref).";
|
|
15
|
+
export declare const metadataProp: {
|
|
16
|
+
type: string;
|
|
17
|
+
additionalProperties: {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
description: string;
|
|
21
|
+
};
|