@buildinternet/uploads 0.7.0 → 0.9.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 +15 -4
- 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 +67 -24
- package/dist/client.d.ts +69 -0
- package/dist/client.js +99 -5
- 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 +15 -0
- package/dist/commands/login.js +17 -9
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +287 -17
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +10 -0
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +1 -1
- package/dist/github-gh.d.ts +7 -0
- package/dist/github-gh.js +24 -0
- package/dist/github.d.ts +15 -0
- package/dist/github.js +31 -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 {
|
|
10
|
+
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
11
|
+
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
12
|
+
import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, 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)
|
|
@@ -47,19 +65,32 @@ Options:
|
|
|
47
65
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
48
66
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
49
67
|
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
68
|
+
--auto Resolve current PR/issue and stamp gh.* metadata (default on)
|
|
69
|
+
--no-auto Skip gh.* auto-resolution (also skipped by --no-git or UPLOADS_NO_AUTO_META=1)
|
|
50
70
|
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
51
71
|
--format human|url|markdown|json
|
|
52
72
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
53
73
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
54
74
|
--comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
|
|
55
75
|
--gallery <id> Add the uploaded object to this public gallery
|
|
76
|
+
--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
|
|
77
|
+
Re-uploading to an existing key WITH --meta replaces that file's
|
|
78
|
+
entire metadata set; without --meta the existing metadata is
|
|
79
|
+
preserved. Use "uploads meta set" to edit individual keys.
|
|
80
|
+
--dry-run Print key + public URL without uploading. Not with --comment/--gallery
|
|
81
|
+
|
|
82
|
+
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
|
|
83
|
+
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
56
84
|
|
|
57
85
|
Examples:
|
|
58
86
|
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
59
87
|
uploads put ./mobile.png --frame phone
|
|
60
88
|
uploads put ./ui.png --frame browser --frame-url "https://app.example/settings"
|
|
61
89
|
uploads put ./shot.png --destination screenshots
|
|
90
|
+
uploads put ./capture-….webp --pr 128 --name hero.webp
|
|
91
|
+
uploads put ./shot.png --pr 128 --name hero.webp --dry-run --format url
|
|
62
92
|
uploads put ./after.png --gallery gal_example
|
|
93
|
+
uploads put ./shot.png --meta app=myapp --meta page=settings
|
|
63
94
|
`;
|
|
64
95
|
/**
|
|
65
96
|
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
@@ -78,6 +109,24 @@ export function makeGhTarget(pr, issue, repoArg, run) {
|
|
|
78
109
|
function ghTargetFromFlags(flags, run) {
|
|
79
110
|
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
80
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Best-effort GitHub target for the default put path (no --pr/--issue). A
|
|
114
|
+
* numeric --ref is classified as pull vs issue; otherwise the current branch's
|
|
115
|
+
* PR is resolved. Never throws — any failure yields undefined so the upload
|
|
116
|
+
* proceeds without gh metadata.
|
|
117
|
+
*/
|
|
118
|
+
function resolveAutoGhTarget(repoArg, ref, run) {
|
|
119
|
+
try {
|
|
120
|
+
const repo = resolveRepo(repoArg, run);
|
|
121
|
+
if (ref !== undefined && /^\d+$/.test(ref) && Number(ref) > 0) {
|
|
122
|
+
return classifyGhNumber(repo, Number.parseInt(ref, 10), run);
|
|
123
|
+
}
|
|
124
|
+
return resolveCurrentPullRequest(repo, run);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
81
130
|
/** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
|
|
82
131
|
export function optimizeOptionsFromFlags(flags, defaults) {
|
|
83
132
|
if (flags.has("--no-optimize") && typeof flags.get("--no-optimize") === "string") {
|
|
@@ -160,7 +209,7 @@ function frameOptionsFromFlags(flags) {
|
|
|
160
209
|
* fatal (`comment` command) or a warning (`put --comment`).
|
|
161
210
|
*/
|
|
162
211
|
export async function syncAttachmentsComment(client, target, run) {
|
|
163
|
-
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
|
|
212
|
+
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url, embedUrl }) => ({ key, url, embedUrl }));
|
|
164
213
|
const galleries = [];
|
|
165
214
|
let cursor;
|
|
166
215
|
do {
|
|
@@ -183,6 +232,7 @@ export async function syncAttachmentsComment(client, target, run) {
|
|
|
183
232
|
.slice(0, 3)
|
|
184
233
|
.map((item) => ({
|
|
185
234
|
url: item.url,
|
|
235
|
+
embedUrl: item.embedUrl,
|
|
186
236
|
alt: item.altText ?? item.objectKey,
|
|
187
237
|
itemUrl: item.pageUrl,
|
|
188
238
|
})),
|
|
@@ -226,12 +276,19 @@ Options:
|
|
|
226
276
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
227
277
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
228
278
|
--workspace, -w <name> Override workspace
|
|
279
|
+
--meta <k=v> Extra queryable metadata (repeatable; value may contain "=").
|
|
280
|
+
gh.repo/gh.kind/gh.number/gh.ref are always set from the resolved
|
|
281
|
+
target — a --meta pair with the same key is overridden by it.
|
|
282
|
+
Because attach always sends its own gh.* pairs, re-attaching to
|
|
283
|
+
the same key always replaces that file's entire metadata set
|
|
284
|
+
(never preserves) — use "uploads meta set" to add to it instead.
|
|
229
285
|
|
|
230
286
|
Examples:
|
|
231
287
|
uploads attach ./before.png ./after.png
|
|
232
288
|
uploads attach ./mobile.png --frame phone
|
|
233
289
|
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
234
290
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
291
|
+
uploads attach ./shot.png --meta app=myapp --meta page=settings
|
|
235
292
|
`;
|
|
236
293
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
237
294
|
const parsed = parseCommandArgs(args);
|
|
@@ -253,6 +310,15 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
253
310
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
254
311
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
255
312
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
313
|
+
// User-supplied extras first, then the resolved target's gh.* — explicit
|
|
314
|
+
// target pairs always win over a same-named --meta extra (documented above).
|
|
315
|
+
// Validate the merged map (not just the extras) so the 24-key/8KB caps are
|
|
316
|
+
// enforced client-side even when extras alone are under the cap but extras
|
|
317
|
+
// + the 4 gh.* pairs push the merged map over it.
|
|
318
|
+
const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
|
|
319
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
320
|
+
if (Object.keys(metadata).length > 0)
|
|
321
|
+
validateMetaMap(metadata);
|
|
256
322
|
const results = [];
|
|
257
323
|
for (const file of parsed.positionals) {
|
|
258
324
|
if (file === "-")
|
|
@@ -260,7 +326,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
260
326
|
const sourceName = basename(file);
|
|
261
327
|
if (!ctx.quiet && !ctx.json)
|
|
262
328
|
process.stderr.write(`>> uploading ${file}\n`);
|
|
263
|
-
const prepared = await prepareImageForUpload(
|
|
329
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
264
330
|
...frameOpts,
|
|
265
331
|
optimize: optimizeOpts,
|
|
266
332
|
});
|
|
@@ -280,10 +346,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
280
346
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
281
347
|
keepExif: optimizeOpts.keepExif === true,
|
|
282
348
|
}),
|
|
349
|
+
metadata,
|
|
283
350
|
});
|
|
351
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
284
352
|
results.push({
|
|
285
353
|
...result,
|
|
286
|
-
markdown: buildMarkdown(
|
|
354
|
+
markdown: buildMarkdown(embedSrc, { alt: sourceName }),
|
|
287
355
|
optimize: {
|
|
288
356
|
optimized: prepared.optimized,
|
|
289
357
|
skippedReason: prepared.skippedReason,
|
|
@@ -310,10 +378,16 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
310
378
|
}
|
|
311
379
|
else {
|
|
312
380
|
for (const result of results) {
|
|
313
|
-
|
|
381
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
382
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
314
383
|
}
|
|
315
384
|
if (!ctx.quiet && comment)
|
|
316
385
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
386
|
+
// attach auto-writes gh.* metadata; point the user at how to find it later.
|
|
387
|
+
if (!ctx.quiet) {
|
|
388
|
+
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
389
|
+
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
390
|
+
}
|
|
317
391
|
}
|
|
318
392
|
return 0;
|
|
319
393
|
}
|
|
@@ -338,20 +412,47 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
338
412
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
339
413
|
const wantComment = parsed.flags.has("--comment");
|
|
340
414
|
const galleryId = flagString(parsed.flags, "--gallery");
|
|
415
|
+
const nameFlag = flagString(parsed.flags, "--name");
|
|
416
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
417
|
+
// Validate --meta up front (fail fast, before reading/optimizing the file).
|
|
418
|
+
const userMeta = (() => {
|
|
419
|
+
const pairs = flagValues(parsed.flags, "--meta");
|
|
420
|
+
return pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
421
|
+
})();
|
|
341
422
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
342
423
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
343
424
|
}
|
|
425
|
+
if (parsed.flags.has("--auto") && typeof parsed.flags.get("--auto") === "string") {
|
|
426
|
+
throw new UsageError("--auto takes no value");
|
|
427
|
+
}
|
|
428
|
+
if (parsed.flags.has("--no-auto") && typeof parsed.flags.get("--no-auto") === "string") {
|
|
429
|
+
throw new UsageError("--no-auto takes no value");
|
|
430
|
+
}
|
|
344
431
|
if (wantComment && !ghTarget)
|
|
345
432
|
throw new UsageError("--comment requires --pr or --issue");
|
|
346
433
|
if (ghTarget) {
|
|
347
|
-
if (keyHint)
|
|
348
|
-
throw new UsageError("--key cannot be combined with --pr/--issue");
|
|
434
|
+
if (keyHint) {
|
|
435
|
+
throw new UsageError("--key cannot be combined with --pr/--issue; use --name <leaf> to set a clean filename on the stable path");
|
|
436
|
+
}
|
|
349
437
|
if (flagString(parsed.flags, "--ref")) {
|
|
350
438
|
throw new UsageError("--ref cannot be combined with --pr/--issue");
|
|
351
439
|
}
|
|
352
440
|
if (prefixFlag)
|
|
353
441
|
throw new UsageError("--prefix cannot be combined with --pr/--issue");
|
|
354
442
|
}
|
|
443
|
+
if (nameFlag !== undefined) {
|
|
444
|
+
if (nameFlag === "" || nameFlag.includes("/")) {
|
|
445
|
+
throw new UsageError("--name must be a bare filename with no '/'");
|
|
446
|
+
}
|
|
447
|
+
if (keyHint)
|
|
448
|
+
throw new UsageError("--name cannot be combined with --key");
|
|
449
|
+
}
|
|
450
|
+
if (dryRun) {
|
|
451
|
+
if (wantComment)
|
|
452
|
+
throw new UsageError("--dry-run cannot be combined with --comment");
|
|
453
|
+
if (galleryId)
|
|
454
|
+
throw new UsageError("--dry-run cannot be combined with --gallery");
|
|
455
|
+
}
|
|
355
456
|
let resolvedPrefix;
|
|
356
457
|
try {
|
|
357
458
|
resolvedPrefix = resolvePutPrefix({
|
|
@@ -364,8 +465,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
364
465
|
catch (err) {
|
|
365
466
|
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
366
467
|
}
|
|
367
|
-
const bytes =
|
|
368
|
-
const sourceName = fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg);
|
|
468
|
+
const bytes = readFileArg(fileArg);
|
|
469
|
+
const sourceName = nameFlag ?? (fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg));
|
|
369
470
|
const format = ctx.json
|
|
370
471
|
? "json"
|
|
371
472
|
: (() => {
|
|
@@ -379,6 +480,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
379
480
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
380
481
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
381
482
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
483
|
+
// Optimize even on --dry-run so the preview key extension/hash match a real put.
|
|
382
484
|
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
383
485
|
...frameOpts,
|
|
384
486
|
optimize: optimizeOpts,
|
|
@@ -395,7 +497,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
395
497
|
})()
|
|
396
498
|
: defaults.width;
|
|
397
499
|
if (!ctx.quiet && format === "human") {
|
|
398
|
-
process.stderr.write(`>> uploading ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
500
|
+
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
399
501
|
if (prepared.frame?.framed)
|
|
400
502
|
process.stderr.write(`>> framed with ${prepared.frame.frameId}\n`);
|
|
401
503
|
const note = formatOptimizeNote(prepared);
|
|
@@ -403,6 +505,43 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
403
505
|
process.stderr.write(`>> ${note}\n`);
|
|
404
506
|
}
|
|
405
507
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
508
|
+
// gh.* metadata: explicit --pr/--issue target wins over --meta; otherwise
|
|
509
|
+
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
510
|
+
// --no-auto, or UPLOADS_NO_AUTO_META disable auto; --auto forces past the
|
|
511
|
+
// config default but never past --no-git (no repo to resolve).
|
|
512
|
+
let metadata = userMeta;
|
|
513
|
+
let attachedRef;
|
|
514
|
+
if (ghTarget) {
|
|
515
|
+
const merged = { ...userMeta, ...ghMetadataFromTarget(ghTarget) };
|
|
516
|
+
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
517
|
+
metadata = merged;
|
|
518
|
+
attachedRef = merged["gh.ref"];
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
const autoEnabled = !noGit &&
|
|
522
|
+
!flagBool(parsed.flags, "--no-auto") &&
|
|
523
|
+
(flagBool(parsed.flags, "--auto") || defaults.noAutoMeta !== true);
|
|
524
|
+
if (autoEnabled) {
|
|
525
|
+
const autoTarget = resolveAutoGhTarget(flagString(parsed.flags, "--repo") ?? defaults.repo, flagString(parsed.flags, "--ref") ?? defaults.ref, run);
|
|
526
|
+
if (autoTarget) {
|
|
527
|
+
const autoMeta = ghMetadataFromTarget(autoTarget);
|
|
528
|
+
const merged = { ...autoMeta, ...userMeta };
|
|
529
|
+
// Auto resolution must never fail the upload: if merging the gh.* pairs
|
|
530
|
+
// would exceed the metadata caps, drop them and upload with --meta only.
|
|
531
|
+
try {
|
|
532
|
+
validateMetaMap(merged);
|
|
533
|
+
metadata = merged;
|
|
534
|
+
attachedRef = merged["gh.ref"];
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// keep metadata = userMeta (already validated); skip auto gh.*
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (attachedRef && !ctx.quiet && format === "human") {
|
|
543
|
+
process.stderr.write(`>> attached to ${attachedRef}\n`);
|
|
544
|
+
}
|
|
406
545
|
let key = ghTarget ? ghAttachmentKey(ghTarget, filename) : keyHint;
|
|
407
546
|
if (key && prepared.optimized)
|
|
408
547
|
key = rewriteKeyExtension(key, filename);
|
|
@@ -414,14 +553,17 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
414
553
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
415
554
|
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
416
555
|
deriveRepoFromGit: !noGit,
|
|
556
|
+
dryRun,
|
|
417
557
|
provenance: buildCliProvenance({
|
|
418
558
|
sourceName,
|
|
419
559
|
optimized: prepared.optimized,
|
|
420
560
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
421
561
|
keepExif: optimizeOpts.keepExif === true,
|
|
422
562
|
}),
|
|
563
|
+
metadata,
|
|
423
564
|
});
|
|
424
|
-
const
|
|
565
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
566
|
+
const markdown = buildMarkdown(embedSrc, { alt, width });
|
|
425
567
|
let gallery;
|
|
426
568
|
if (galleryId) {
|
|
427
569
|
try {
|
|
@@ -446,7 +588,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
446
588
|
filename: prepared.filename,
|
|
447
589
|
};
|
|
448
590
|
if (!ctx.quiet && format === "human") {
|
|
449
|
-
process.stderr.write(`>> key: ${result.key}\n\n`);
|
|
591
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
|
|
450
592
|
}
|
|
451
593
|
switch (format) {
|
|
452
594
|
case "json":
|
|
@@ -456,6 +598,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
456
598
|
optimize: optimizeMeta,
|
|
457
599
|
frame: prepared.frame,
|
|
458
600
|
gallery,
|
|
601
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
459
602
|
});
|
|
460
603
|
break;
|
|
461
604
|
case "url":
|
|
@@ -464,8 +607,10 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
464
607
|
case "markdown":
|
|
465
608
|
await writeStdout(`${markdown}\n`);
|
|
466
609
|
break;
|
|
467
|
-
default:
|
|
468
|
-
|
|
610
|
+
default: {
|
|
611
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
612
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
613
|
+
}
|
|
469
614
|
}
|
|
470
615
|
if (gallery?.url && format !== "human") {
|
|
471
616
|
process.stderr.write(`gallery: ${gallery.url}\n`);
|
|
@@ -703,21 +848,58 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
703
848
|
}
|
|
704
849
|
}
|
|
705
850
|
// --- list ---
|
|
706
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
851
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--meta <k=v>]... [--workspace <name>]
|
|
707
852
|
|
|
708
853
|
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
709
854
|
|
|
855
|
+
--meta <k=v> (repeatable, ANDed) switches to the metadata filter endpoint —
|
|
856
|
+
returned items include their matched metadata. Combines with --prefix, not
|
|
857
|
+
with --pr/--issue/--all. See also: uploads find (positional-pair alias).
|
|
858
|
+
|
|
710
859
|
Examples:
|
|
711
860
|
uploads list --prefix screenshots/
|
|
712
861
|
uploads list --pr 123
|
|
713
862
|
uploads list --all --json
|
|
863
|
+
uploads list --meta gh.repo=buildinternet/uploads --meta gh.number=123
|
|
714
864
|
`;
|
|
865
|
+
/** `--meta k=v` (repeatable) filter path, shared by `runList` and `runFind`. */
|
|
866
|
+
async function runFindFiles(ctx, filters, flags) {
|
|
867
|
+
if (flagString(flags, "--cursor") !== undefined) {
|
|
868
|
+
throw new UsageError("--cursor is not supported with metadata filters");
|
|
869
|
+
}
|
|
870
|
+
const prefix = flagString(flags, "--prefix");
|
|
871
|
+
const limit = flagInt(flags, "--limit", "--limit");
|
|
872
|
+
const result = await ctx.client.findFiles(filters, { prefix, limit });
|
|
873
|
+
if (ctx.json)
|
|
874
|
+
await writeJson(result);
|
|
875
|
+
else
|
|
876
|
+
for (const item of result.items) {
|
|
877
|
+
// LIST_HELP promises matched metadata in the output; render it inline
|
|
878
|
+
// (sorted for stable output) so human mode honors that, not just --json.
|
|
879
|
+
const meta = Object.entries(item.metadata)
|
|
880
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
881
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
882
|
+
.join(" ");
|
|
883
|
+
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
|
|
884
|
+
}
|
|
885
|
+
return 0;
|
|
886
|
+
}
|
|
715
887
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
716
888
|
const parsed = parseCommandArgs(args);
|
|
717
889
|
if (help || parsed.help) {
|
|
718
890
|
process.stderr.write(LIST_HELP);
|
|
719
891
|
return 0;
|
|
720
892
|
}
|
|
893
|
+
const metaPairs = flagValues(parsed.flags, "--meta");
|
|
894
|
+
if (metaPairs.length > 0) {
|
|
895
|
+
if (ghTargetFromFlags(parsed.flags, run)) {
|
|
896
|
+
throw new UsageError("--meta cannot be combined with --pr/--issue");
|
|
897
|
+
}
|
|
898
|
+
if (flagBool(parsed.flags, "--all")) {
|
|
899
|
+
throw new UsageError("--meta cannot be combined with --all");
|
|
900
|
+
}
|
|
901
|
+
return runFindFiles(ctx, parseMetaFlags(metaPairs), parsed.flags);
|
|
902
|
+
}
|
|
721
903
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
722
904
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
723
905
|
let prefix = prefixFlag ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
|
|
@@ -750,6 +932,94 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
750
932
|
}
|
|
751
933
|
return 0;
|
|
752
934
|
}
|
|
935
|
+
// --- find ---
|
|
936
|
+
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
937
|
+
|
|
938
|
+
Human-friendly alias for \`uploads list --meta k=v...\` — same metadata filter
|
|
939
|
+
(ANDed equality), same output; pairs are positional instead of repeated flags.
|
|
940
|
+
|
|
941
|
+
Examples:
|
|
942
|
+
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
943
|
+
uploads find app=myapp page=settings --prefix screenshots/
|
|
944
|
+
`;
|
|
945
|
+
export async function runFind(ctx, args, help = false) {
|
|
946
|
+
const parsed = parseCommandArgs(args);
|
|
947
|
+
if (help || parsed.help) {
|
|
948
|
+
process.stderr.write(FIND_HELP);
|
|
949
|
+
return 0;
|
|
950
|
+
}
|
|
951
|
+
if (parsed.positionals.length === 0) {
|
|
952
|
+
process.stderr.write(FIND_HELP);
|
|
953
|
+
return 2;
|
|
954
|
+
}
|
|
955
|
+
const filters = parseMetaFlags(parsed.positionals);
|
|
956
|
+
return runFindFiles(ctx, filters, parsed.flags);
|
|
957
|
+
}
|
|
958
|
+
// --- meta ---
|
|
959
|
+
const META_HELP = `uploads meta <command> [args]
|
|
960
|
+
|
|
961
|
+
Read/write an object's queryable custom metadata (D1-backed key-value pairs;
|
|
962
|
+
distinct from the R2 provenance headers put on upload).
|
|
963
|
+
|
|
964
|
+
Commands:
|
|
965
|
+
get <key> Show metadata for an object
|
|
966
|
+
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
967
|
+
|
|
968
|
+
Examples:
|
|
969
|
+
uploads meta get screenshots/myapp/42/shot.png
|
|
970
|
+
uploads meta set screenshots/myapp/42/shot.png app=myapp page=settings
|
|
971
|
+
uploads meta set screenshots/myapp/42/shot.png --delete app --delete page
|
|
972
|
+
`;
|
|
973
|
+
export async function runMeta(ctx, args, help = false) {
|
|
974
|
+
const parsed = parseCommandArgs(args);
|
|
975
|
+
const action = parsed.positionals[0];
|
|
976
|
+
if (help || parsed.help || !action) {
|
|
977
|
+
process.stderr.write(META_HELP);
|
|
978
|
+
return help || parsed.help ? 0 : 2;
|
|
979
|
+
}
|
|
980
|
+
switch (action) {
|
|
981
|
+
case "get": {
|
|
982
|
+
const key = parsed.positionals[1];
|
|
983
|
+
if (!key)
|
|
984
|
+
throw new UsageError("meta get requires an object key");
|
|
985
|
+
const result = await ctx.client.getMetadata(key);
|
|
986
|
+
if (ctx.json)
|
|
987
|
+
await writeJson(result);
|
|
988
|
+
else if (Object.keys(result.metadata).length === 0) {
|
|
989
|
+
// Empty stdout reads as failure; a stderr note keeps stdout parseable.
|
|
990
|
+
if (!ctx.quiet)
|
|
991
|
+
process.stderr.write("(no metadata)\n");
|
|
992
|
+
}
|
|
993
|
+
else
|
|
994
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
995
|
+
await writeStdout(`${k}=${v}\n`);
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
case "set": {
|
|
999
|
+
const key = parsed.positionals[1];
|
|
1000
|
+
if (!key)
|
|
1001
|
+
throw new UsageError("meta set requires an object key");
|
|
1002
|
+
const pairs = parsed.positionals.slice(2);
|
|
1003
|
+
const del = flagValues(parsed.flags, "--delete");
|
|
1004
|
+
if (pairs.length === 0 && del.length === 0) {
|
|
1005
|
+
throw new UsageError("meta set requires k=v pairs and/or --delete <key>");
|
|
1006
|
+
}
|
|
1007
|
+
const set = pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
1008
|
+
const result = await ctx.client.patchMetadata(key, {
|
|
1009
|
+
set,
|
|
1010
|
+
delete: del.length > 0 ? del : undefined,
|
|
1011
|
+
});
|
|
1012
|
+
if (ctx.json)
|
|
1013
|
+
await writeJson(result);
|
|
1014
|
+
else
|
|
1015
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
1016
|
+
await writeStdout(`${k}=${v}\n`);
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
1019
|
+
default:
|
|
1020
|
+
throw new UsageError(`unknown meta command: ${action}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
753
1023
|
// --- delete ---
|
|
754
1024
|
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
755
1025
|
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
-
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF"];
|
|
2
|
+
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
|
@@ -12,6 +12,8 @@ export interface PutDefaults {
|
|
|
12
12
|
noOptimize?: boolean;
|
|
13
13
|
/** When true, optimize keeps EXIF/XMP/ICC (default strips). */
|
|
14
14
|
keepExif?: boolean;
|
|
15
|
+
/** When true, `put` does NOT auto-resolve/stamp gh.* on the default path. */
|
|
16
|
+
noAutoMeta?: boolean;
|
|
15
17
|
}
|
|
16
18
|
declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
|
|
17
19
|
export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
|
package/dist/config-file.js
CHANGED
|
@@ -12,6 +12,7 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
12
12
|
"UPLOADS_NO_GIT",
|
|
13
13
|
"UPLOADS_NO_OPTIMIZE",
|
|
14
14
|
"UPLOADS_KEEP_EXIF",
|
|
15
|
+
"UPLOADS_NO_AUTO_META",
|
|
15
16
|
];
|
|
16
17
|
const PUT_DEFAULT_KEY_MAP = {
|
|
17
18
|
prefix: "UPLOADS_DEFAULT_PREFIX",
|
|
@@ -21,6 +22,7 @@ const PUT_DEFAULT_KEY_MAP = {
|
|
|
21
22
|
noGit: "UPLOADS_NO_GIT",
|
|
22
23
|
noOptimize: "UPLOADS_NO_OPTIMIZE",
|
|
23
24
|
keepExif: "UPLOADS_KEEP_EXIF",
|
|
25
|
+
noAutoMeta: "UPLOADS_NO_AUTO_META",
|
|
24
26
|
};
|
|
25
27
|
function isTruthyConfigFlag(value) {
|
|
26
28
|
if (!value)
|
|
@@ -44,6 +46,8 @@ export function putDefaultsToConfigValues(defaults) {
|
|
|
44
46
|
out.UPLOADS_NO_OPTIMIZE = "1";
|
|
45
47
|
if (defaults.keepExif)
|
|
46
48
|
out.UPLOADS_KEEP_EXIF = "1";
|
|
49
|
+
if (defaults.noAutoMeta)
|
|
50
|
+
out.UPLOADS_NO_AUTO_META = "1";
|
|
47
51
|
return out;
|
|
48
52
|
}
|
|
49
53
|
function parsePutDefaultsFromRaw(raw) {
|
|
@@ -65,6 +69,8 @@ function parsePutDefaultsFromRaw(raw) {
|
|
|
65
69
|
out.noOptimize = true;
|
|
66
70
|
if (isTruthyConfigFlag(raw.UPLOADS_KEEP_EXIF))
|
|
67
71
|
out.keepExif = true;
|
|
72
|
+
if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_META))
|
|
73
|
+
out.noAutoMeta = true;
|
|
68
74
|
return out;
|
|
69
75
|
}
|
|
70
76
|
function parsePutDefaultsFromEnv() {
|
|
@@ -83,6 +89,8 @@ function parsePutDefaultsFromEnv() {
|
|
|
83
89
|
raw.UPLOADS_NO_OPTIMIZE = process.env.UPLOADS_NO_OPTIMIZE;
|
|
84
90
|
if (process.env.UPLOADS_KEEP_EXIF)
|
|
85
91
|
raw.UPLOADS_KEEP_EXIF = process.env.UPLOADS_KEEP_EXIF;
|
|
92
|
+
if (process.env.UPLOADS_NO_AUTO_META)
|
|
93
|
+
raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
|
|
86
94
|
return parsePutDefaultsFromRaw(raw);
|
|
87
95
|
}
|
|
88
96
|
/** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
|
|
@@ -152,6 +160,8 @@ export function mergePutDefaults(...layers) {
|
|
|
152
160
|
out.noOptimize = layer.noOptimize;
|
|
153
161
|
if (layer.keepExif != null)
|
|
154
162
|
out.keepExif = layer.keepExif;
|
|
163
|
+
if (layer.noAutoMeta != null)
|
|
164
|
+
out.noAutoMeta = layer.noAutoMeta;
|
|
155
165
|
}
|
|
156
166
|
return out;
|
|
157
167
|
}
|
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-gh.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare const execRunner: CommandRunner;
|
|
|
9
9
|
export declare function resolveRepo(explicit: string | undefined, run?: CommandRunner): string;
|
|
10
10
|
/** Resolve the pull request associated with the current branch. */
|
|
11
11
|
export declare function resolveCurrentPullRequest(repo: string, run?: CommandRunner): GhTarget;
|
|
12
|
+
/**
|
|
13
|
+
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
14
|
+
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
|
15
|
+
* missing, 404, network) — the caller treats that as "no gh context" and
|
|
16
|
+
* uploads without metadata.
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyGhNumber(repo: string, num: number, run?: CommandRunner): GhTarget | undefined;
|
|
12
19
|
/**
|
|
13
20
|
* Create the managed attachments comment, or edit it in place if it already
|
|
14
21
|
* exists. Never touches any other comment. Body is passed via stdin
|
package/dist/github-gh.js
CHANGED
|
@@ -61,6 +61,30 @@ export function resolveCurrentPullRequest(repo, run = execRunner) {
|
|
|
61
61
|
}
|
|
62
62
|
throw new UsageError("could not infer a pull request for the current branch — pass --pr <num> or --issue <num>");
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
66
|
+
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
|
67
|
+
* missing, 404, network) — the caller treats that as "no gh context" and
|
|
68
|
+
* uploads without metadata.
|
|
69
|
+
*/
|
|
70
|
+
export function classifyGhNumber(repo, num, run = execRunner) {
|
|
71
|
+
try {
|
|
72
|
+
const out = run("gh", [
|
|
73
|
+
"api",
|
|
74
|
+
`repos/${repo}/issues/${num}`,
|
|
75
|
+
"--jq",
|
|
76
|
+
'if .pull_request then "pull" else "issue" end',
|
|
77
|
+
]).trim();
|
|
78
|
+
if (out === "pull")
|
|
79
|
+
return { repo, kind: "pull", num };
|
|
80
|
+
if (out === "issue")
|
|
81
|
+
return { repo, kind: "issues", num };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// gh missing / not found / network — caller skips
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
64
88
|
/**
|
|
65
89
|
* PR comments live on the issues endpoint, so one path covers PRs and issues.
|
|
66
90
|
* Only the first 100 comments are searched (accepted v1 limitation).
|