@buildinternet/uploads 0.26.0 → 0.27.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/LICENSE +202 -21
- package/README.md +5 -0
- package/dist/cli-catalog.js +1 -0
- package/dist/cli.js +5 -0
- package/dist/client.d.ts +5 -0
- package/dist/commands/config.js +6 -1
- package/dist/commands/login.js +25 -13
- package/dist/commands/screenshot.js +85 -12
- package/dist/commands/session.js +7 -5
- package/dist/commands.d.ts +23 -0
- package/dist/commands.js +112 -16
- package/dist/config-file.d.ts +1 -1
- package/dist/config-file.js +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -0
- package/dist/format-bytes.d.ts +6 -2
- package/dist/format-bytes.js +19 -5
- package/dist/format-usage.d.ts +9 -0
- package/dist/format-usage.js +34 -5
- package/dist/github.d.ts +6 -0
- package/dist/github.js +12 -0
- package/dist/mcp/tools.js +38 -22
- package/dist/session-cli-version.d.ts +15 -0
- package/dist/session-cli-version.js +85 -0
- package/dist/sidecar.d.ts +31 -0
- package/dist/sidecar.js +111 -0
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -11,7 +11,8 @@ import { writeJson, writeStdout } from "./io.js";
|
|
|
11
11
|
import { imageFactsFromBytes } from "./image-facts.js";
|
|
12
12
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
13
13
|
import { mergeDerivedMeta, nearMissMetaWarnings, validateStateValue } from "./metadata-vocab.js";
|
|
14
|
-
import {
|
|
14
|
+
import { mergeSidecarMeta } from "./sidecar.js";
|
|
15
|
+
import { ghAttachmentKey, ghBranchAttachmentKey, ghBranchKeyPrefix, ghKeyPrefix, ghMetadataFromTarget, parseGhKey, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, normalizeGithubCoordinate, } from "./github.js";
|
|
15
16
|
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
16
17
|
import { deriveRepoFromGit } from "./keys.js";
|
|
17
18
|
import { resolvePutPrefix } from "./destinations.js";
|
|
@@ -56,6 +57,12 @@ upload as-is, or --keep-exif when image metadata matters for the discussion.
|
|
|
56
57
|
Optional --frame wraps the image in a device/browser chrome before optimize
|
|
57
58
|
(default off). See: uploads put --help frames
|
|
58
59
|
|
|
60
|
+
If the file has a sidecar manifest (<file>.uploads.json, written by
|
|
61
|
+
"screenshot --out") and its content hash still matches this file, that
|
|
62
|
+
capture's derived metadata (path/url/env/viewport/state) is merged in
|
|
63
|
+
automatically — explicit --meta/--state always win. A regenerated or edited
|
|
64
|
+
file loses its sidecar silently (hash no longer matches).
|
|
65
|
+
|
|
59
66
|
Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
60
67
|
remain public even for private/internal GitHub repositories. Upload only media
|
|
61
68
|
that is safe at a predictable public URL.
|
|
@@ -558,6 +565,12 @@ URL and every embed hot-swap. Human mode prints ">> replaced existing object
|
|
|
558
565
|
Still images are optimized to WebP by default (same as put). Use --no-optimize
|
|
559
566
|
to upload originals. Optional --frame wraps images in device/browser chrome.
|
|
560
567
|
|
|
568
|
+
If a file has a sidecar manifest (<file>.uploads.json, written by
|
|
569
|
+
"screenshot --out") and its content hash still matches, that capture's
|
|
570
|
+
derived metadata (path/url/env/viewport/state) is merged in automatically —
|
|
571
|
+
explicit --meta/--state always win. A regenerated or edited file loses its
|
|
572
|
+
sidecar silently (hash no longer matches).
|
|
573
|
+
|
|
561
574
|
Branch staging (pre-PR): --branch [name] stages files against a git branch
|
|
562
575
|
before a pull request exists, e.g. for a coding agent working a branch that
|
|
563
576
|
hasn't opened a PR yet. Key: gh/<owner>/<repo>/branch/<branch>/<filename>
|
|
@@ -620,6 +633,21 @@ Examples:
|
|
|
620
633
|
uploads attach ./shot.png --branch feature/new-settings
|
|
621
634
|
uploads attach --promote
|
|
622
635
|
`;
|
|
636
|
+
/**
|
|
637
|
+
* Lever 3 (issue #469): a nudge for when an image lands on a PR/issue with
|
|
638
|
+
* no `path` metadata — `path` is one of the highest-value queryable tags
|
|
639
|
+
* (same tier as `state=`), and unlike `uploads screenshot` (which derives it
|
|
640
|
+
* from the captured URL), a plain `attach`/`put --pr`/`put --issue` of an
|
|
641
|
+
* already-existing image has nothing to derive it from, so it's easy to
|
|
642
|
+
* forget. Fires once per batch (not per file) — checks the metadata the
|
|
643
|
+
* server actually stored (`PutResult.metadata`), not what was requested, so
|
|
644
|
+
* a merge/validation drop still surfaces the gap. Non-image uploads (zips,
|
|
645
|
+
* PDFs, etc.) are exempt — "findable by page" doesn't apply to them.
|
|
646
|
+
*/
|
|
647
|
+
export function pathMetaHintFor(uploads) {
|
|
648
|
+
const missingPath = uploads.some((u) => u.contentType.startsWith("image/") && !u.metadata?.path);
|
|
649
|
+
return missingPath ? "tip: add --meta path=/route so this shot is findable by page" : undefined;
|
|
650
|
+
}
|
|
623
651
|
/**
|
|
624
652
|
* Shared prepare + put loop for both PR/issue attach (`uploadAttachments`)
|
|
625
653
|
* and branch-staged attach (`uploadBranchAttachments`) — bounded concurrency,
|
|
@@ -634,11 +662,14 @@ async function uploadAttachmentBatch(opts) {
|
|
|
634
662
|
try {
|
|
635
663
|
const sourceName = basename(file);
|
|
636
664
|
const bytes = readFileArg(file);
|
|
665
|
+
// Sidecar manifest from a prior `screenshot --out` of this exact file
|
|
666
|
+
// (issue #469 lever 2) — see mergeSidecarMeta.
|
|
667
|
+
const baseMetadata = mergeSidecarMeta(file, bytes, opts.metadata);
|
|
637
668
|
// Same EXIF promotion uploadPreparedImage does; attach keeps its own
|
|
638
669
|
// per-file tail (it builds keys differently), so it opts in here too.
|
|
639
670
|
const metadata = opts.deriveImageFacts
|
|
640
|
-
? await mergeImageFacts(bytes,
|
|
641
|
-
:
|
|
671
|
+
? await mergeImageFacts(bytes, baseMetadata)
|
|
672
|
+
: baseMetadata;
|
|
642
673
|
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
643
674
|
...opts.frame,
|
|
644
675
|
optimize: opts.optimize,
|
|
@@ -743,7 +774,11 @@ export async function uploadPuts(opts) {
|
|
|
743
774
|
? basename(opts.explicitKey)
|
|
744
775
|
: "stdin.bin"
|
|
745
776
|
: basename(file));
|
|
746
|
-
const
|
|
777
|
+
const bytes = readFileArg(file);
|
|
778
|
+
// Sidecar manifest from a prior `screenshot --out` of this exact file
|
|
779
|
+
// (issue #469 lever 2) — see mergeSidecarMeta. Not applicable to stdin.
|
|
780
|
+
const metadata = file !== "-" ? mergeSidecarMeta(file, bytes, opts.metadata) : opts.metadata;
|
|
781
|
+
const { result, prepared, markdown } = await uploadPreparedImage(opts.client, bytes, sourceName, {
|
|
747
782
|
frame: opts.frame,
|
|
748
783
|
optimize: opts.optimize,
|
|
749
784
|
ghTarget: opts.ghTarget,
|
|
@@ -756,7 +791,7 @@ export async function uploadPuts(opts) {
|
|
|
756
791
|
contentType: opts.contentType,
|
|
757
792
|
dryRun: opts.dryRun,
|
|
758
793
|
replace: opts.replace,
|
|
759
|
-
metadata
|
|
794
|
+
metadata,
|
|
760
795
|
deriveImageFacts: opts.deriveImageFacts,
|
|
761
796
|
provenanceClient: opts.provenanceClient,
|
|
762
797
|
alt: () => opts.alt ?? basename(sourceName),
|
|
@@ -935,6 +970,8 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
935
970
|
process.stderr.write(`warning: uploads succeeded but the GitHub comment failed (is gh installed and authenticated?): ${commentError}\n`);
|
|
936
971
|
}
|
|
937
972
|
}
|
|
973
|
+
// Lever 3 (issue #469): tip when an image lands here with no `path` meta.
|
|
974
|
+
const pathHint = uploads.length > 0 && !ctx.quiet ? pathMetaHintFor(uploads) : undefined;
|
|
938
975
|
if (ctx.json) {
|
|
939
976
|
await writeJson({
|
|
940
977
|
target,
|
|
@@ -943,6 +980,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
943
980
|
comment,
|
|
944
981
|
commentError,
|
|
945
982
|
promotion: promotion ?? null,
|
|
983
|
+
...(pathHint ? { hint: pathHint } : {}),
|
|
946
984
|
});
|
|
947
985
|
}
|
|
948
986
|
else {
|
|
@@ -971,6 +1009,8 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
971
1009
|
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
972
1010
|
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
973
1011
|
}
|
|
1012
|
+
if (pathHint)
|
|
1013
|
+
process.stderr.write(`${pathHint}\n`);
|
|
974
1014
|
}
|
|
975
1015
|
return failures.length === 0 ? 0 : 1;
|
|
976
1016
|
}
|
|
@@ -1269,6 +1309,18 @@ export function resolvePutStagingTarget(opts) {
|
|
|
1269
1309
|
return undefined; // gh/git unavailable, or repo unresolvable — dated layout
|
|
1270
1310
|
}
|
|
1271
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Merges a staging target's `gh.*` branch metadata over `base` and validates
|
|
1314
|
+
* the result (same builder, same contract as `attach --branch`) — the one
|
|
1315
|
+
* merge+validate step shared by every staging call site: `runPut`,
|
|
1316
|
+
* `runScreenshot`, and both the local stdio MCP `put` and `screenshot`
|
|
1317
|
+
* tools.
|
|
1318
|
+
*/
|
|
1319
|
+
export function mergeStagingMeta(base, target) {
|
|
1320
|
+
const merged = { ...base, ...ghMetadataForBranch(target.repo, target.branch) };
|
|
1321
|
+
validateMetaMap(merged);
|
|
1322
|
+
return merged;
|
|
1323
|
+
}
|
|
1272
1324
|
/**
|
|
1273
1325
|
* The bare-put staging note's wording (issue #403): replaces the #393 nudge
|
|
1274
1326
|
* for the (now default) case where a bare put on a non-default branch stages
|
|
@@ -1553,12 +1605,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1553
1605
|
attachedRef = merged["gh.ref"];
|
|
1554
1606
|
}
|
|
1555
1607
|
else if (stagingTarget) {
|
|
1556
|
-
|
|
1557
|
-
...userMeta,
|
|
1558
|
-
...ghMetadataForBranch(stagingTarget.repo, stagingTarget.branch),
|
|
1559
|
-
};
|
|
1560
|
-
validateMetaMap(merged); // matches attach --branch's unwrapped call — same builder, same contract
|
|
1561
|
-
metadata = merged;
|
|
1608
|
+
metadata = mergeStagingMeta(userMeta, stagingTarget);
|
|
1562
1609
|
}
|
|
1563
1610
|
else {
|
|
1564
1611
|
// gh.* additionally needs git, which the shared derived gate ignores.
|
|
@@ -1648,13 +1695,19 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1648
1695
|
const bindingWarning = stagingTarget && uploads.length > 0
|
|
1649
1696
|
? await resolveStageBindingWarning({ ctx, defaults, repo: stagingTarget.repo })
|
|
1650
1697
|
: undefined;
|
|
1698
|
+
// Lever 3 (issue #469): tip when a --pr/--issue put lands an image with no
|
|
1699
|
+
// `path` meta. Only relevant on the ghTarget path — the bare-put paths
|
|
1700
|
+
// above (staging/auto/dated) aren't attached to a PR/issue yet, so there's
|
|
1701
|
+
// nothing to look up from a page later.
|
|
1702
|
+
const pathHint = ghTarget && uploads.length > 0 && !ctx.quiet ? pathMetaHintFor(uploads) : undefined;
|
|
1651
1703
|
// One JSON `hint` slot, shared with the #393 nudge (mutually exclusive with
|
|
1652
1704
|
// it — nudge is undefined whenever staging took over). When staging fires,
|
|
1653
1705
|
// prefer the more actionable binding warning over the generic staging note
|
|
1654
1706
|
// (mirrors attach --branch, whose only JSON hint content IS the binding
|
|
1655
1707
|
// warning); stderr prints the nudge/staging-note and binding-warning lines
|
|
1656
|
-
// independently, below.
|
|
1657
|
-
|
|
1708
|
+
// independently, below. pathHint only ever fires on the ghTarget path, so
|
|
1709
|
+
// it never competes with the other three.
|
|
1710
|
+
const jsonHint = nudge ?? bindingWarning ?? stagingNote ?? pathHint;
|
|
1658
1711
|
const galleriesByKey = new Map();
|
|
1659
1712
|
let galleryHadError = false;
|
|
1660
1713
|
if (galleryId && uploads.length > 0) {
|
|
@@ -1737,6 +1790,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1737
1790
|
process.stderr.write(`${stagingNote}\n`);
|
|
1738
1791
|
if (bindingWarning)
|
|
1739
1792
|
process.stderr.write(`${bindingWarning}\n`);
|
|
1793
|
+
if (pathHint)
|
|
1794
|
+
process.stderr.write(`${pathHint}\n`);
|
|
1740
1795
|
}
|
|
1741
1796
|
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
1742
1797
|
}
|
|
@@ -1794,6 +1849,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1794
1849
|
process.stderr.write(`${stagingNote}\n`);
|
|
1795
1850
|
if (bindingWarning && format !== "json")
|
|
1796
1851
|
process.stderr.write(`${bindingWarning}\n`);
|
|
1852
|
+
if (pathHint && format !== "json")
|
|
1853
|
+
process.stderr.write(`${pathHint}\n`);
|
|
1797
1854
|
return gallery?.error ? 1 : 0;
|
|
1798
1855
|
}
|
|
1799
1856
|
// --- galleries ---
|
|
@@ -2173,12 +2230,50 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2173
2230
|
else
|
|
2174
2231
|
for (const [k, v] of Object.entries(result.metadata))
|
|
2175
2232
|
await writeStdout(`${k}=${v}\n`);
|
|
2233
|
+
await resyncCommentAfterMetaSet(ctx, key, [...Object.keys(set ?? {}), ...del]);
|
|
2176
2234
|
return 0;
|
|
2177
2235
|
}
|
|
2178
2236
|
default:
|
|
2179
2237
|
throw new UsageError(`unknown meta command: ${action}`);
|
|
2180
2238
|
}
|
|
2181
2239
|
}
|
|
2240
|
+
/** The metadata keys the managed comment renders (path/state, PR #370). */
|
|
2241
|
+
const COMMENT_RENDERED_META_KEYS = ["path", "state"];
|
|
2242
|
+
/**
|
|
2243
|
+
* Best-effort managed-comment refresh after `meta set` touches a
|
|
2244
|
+
* display-relevant key on a PR/issue-keyed object (issue #470) — without
|
|
2245
|
+
* this, backfilled `path=`/`state=` never reaches the rendered comment until
|
|
2246
|
+
* an unrelated attach fires. Bot endpoint only (no gh fallback — this is a
|
|
2247
|
+
* metadata tweak, not an explicit comment command); any failure degrades to
|
|
2248
|
+
* a stderr hint instead of failing the metadata write that already landed.
|
|
2249
|
+
*/
|
|
2250
|
+
async function resyncCommentAfterMetaSet(ctx, key, touchedKeys) {
|
|
2251
|
+
if (!touchedKeys.some((k) => COMMENT_RENDERED_META_KEYS.includes(k)))
|
|
2252
|
+
return;
|
|
2253
|
+
const target = parseGhKey(key);
|
|
2254
|
+
if (!target)
|
|
2255
|
+
return;
|
|
2256
|
+
try {
|
|
2257
|
+
const bot = await ctx.client.upsertGithubComment({
|
|
2258
|
+
repo: target.repo,
|
|
2259
|
+
num: target.num,
|
|
2260
|
+
kind: target.kind,
|
|
2261
|
+
});
|
|
2262
|
+
if (bot.posted) {
|
|
2263
|
+
if (!ctx.quiet && !ctx.json) {
|
|
2264
|
+
process.stderr.write(`refreshed the managed comment on ${target.repo}#${target.num}\n`);
|
|
2265
|
+
}
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
catch {
|
|
2270
|
+
// Fall through to the hint.
|
|
2271
|
+
}
|
|
2272
|
+
if (!ctx.quiet && !ctx.json) {
|
|
2273
|
+
const flag = target.kind === "pull" ? "--pr" : "--issue";
|
|
2274
|
+
process.stderr.write(`tip: run \`uploads comment ${flag} ${target.num}\` to refresh the PR comment\n`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2182
2277
|
// --- delete ---
|
|
2183
2278
|
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
2184
2279
|
|
|
@@ -2424,9 +2519,10 @@ const USAGE_HELP = `uploads usage [--workspace <name>]
|
|
|
2424
2519
|
Show workspace storage and monthly upload counters.
|
|
2425
2520
|
|
|
2426
2521
|
When the API reports workspace quotas (typical on uploads.sh cloud /
|
|
2427
|
-
self-serve
|
|
2428
|
-
|
|
2429
|
-
|
|
2522
|
+
self-serve Free and Pro), human output includes the plan name and progress
|
|
2523
|
+
bars toward those caps. Free is not unlimited — storage and monthly upload
|
|
2524
|
+
limits show on the meters. Self-hosted or unlimited operator workspaces get
|
|
2525
|
+
usage totals only, plus a short unmetered note — no invented limits.
|
|
2430
2526
|
|
|
2431
2527
|
Examples:
|
|
2432
2528
|
uploads --env-file .env usage
|
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", "UPLOADS_NO_AUTO_META", "UPLOADS_SCREENSHOT_VIA", "UPLOADS_NO_NUDGE"];
|
|
2
|
+
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_SESSION_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", "UPLOADS_SCREENSHOT_VIA", "UPLOADS_NO_NUDGE"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
package/dist/config-file.js
CHANGED
|
@@ -5,6 +5,8 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
5
5
|
"UPLOADS_API_URL",
|
|
6
6
|
"UPLOADS_WORKSPACE",
|
|
7
7
|
"UPLOADS_TOKEN",
|
|
8
|
+
/** Better Auth device-flow session bearer — keeps session.cliVersion fresh. */
|
|
9
|
+
"UPLOADS_SESSION_TOKEN",
|
|
8
10
|
"UPLOADS_DEFAULT_PREFIX",
|
|
9
11
|
"UPLOADS_DEFAULT_REPO",
|
|
10
12
|
"UPLOADS_DEFAULT_REF",
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface UploadsClientConfig {
|
|
|
4
4
|
workspace: string;
|
|
5
5
|
token: string;
|
|
6
6
|
}
|
|
7
|
+
/** Derive auth origin from an API base (`api.` → `auth.`), else production default. */
|
|
8
|
+
export declare function authUrlFromApi(apiUrl: string): string;
|
|
7
9
|
export declare const DEFAULT_API_URL = "https://api.uploads.sh";
|
|
8
10
|
export declare const DEFAULT_WORKSPACE = "default";
|
|
9
11
|
/** Workspace encoded in minted tokens: `up_<workspace>_…` */
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,20 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { loadConfigFile, resolveConfigPath } from "./config-file.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
4
|
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, removeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, resolveScreenshotDefaults, UPLOADS_CONFIG_KEYS, } from "./config-file.js";
|
|
5
|
+
/** Derive auth origin from an API base (`api.` → `auth.`), else production default. */
|
|
6
|
+
export function authUrlFromApi(apiUrl) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(apiUrl);
|
|
9
|
+
if (url.hostname.startsWith("api.")) {
|
|
10
|
+
url.hostname = `auth.${url.hostname.slice(4)}`;
|
|
11
|
+
return url.origin;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// fall through
|
|
16
|
+
}
|
|
17
|
+
return "https://auth.uploads.sh";
|
|
18
|
+
}
|
|
5
19
|
export const DEFAULT_API_URL = "https://api.uploads.sh";
|
|
6
20
|
export const DEFAULT_WORKSPACE = "default";
|
|
7
21
|
const TOKEN_WORKSPACE_RE = /^up_([a-z0-9][a-z0-9-]{1,62})_/;
|
package/dist/format-bytes.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Decimal (SI) human sizes for CLI output. Plan catalog caps are round decimal
|
|
3
|
+
* numbers (250 MB free, 10 GB pro); binary units made Free look like 238.4 MB.
|
|
4
|
+
* Used for usage meters, list sizes, optimize notes, and doctor — same base
|
|
5
|
+
* as apps/web `formatBytes` / `formatMarketedBytes`.
|
|
4
6
|
*/
|
|
5
7
|
export declare function formatByteSize(bytes: number): string;
|
|
8
|
+
/** Alias for call sites that want plan-cap wording; same SI formatter. */
|
|
9
|
+
export declare function formatMarketedBytes(bytes: number): string;
|
package/dist/format-bytes.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Decimal (SI) human sizes for CLI output. Plan catalog caps are round decimal
|
|
3
|
+
* numbers (250 MB free, 10 GB pro); binary units made Free look like 238.4 MB.
|
|
4
|
+
* Used for usage meters, list sizes, optimize notes, and doctor — same base
|
|
5
|
+
* as apps/web `formatBytes` / `formatMarketedBytes`.
|
|
4
6
|
*/
|
|
5
7
|
export function formatByteSize(bytes) {
|
|
6
8
|
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
7
9
|
return "0 B";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
if (bytes < 1000)
|
|
11
|
+
return `${Math.round(bytes)} B`;
|
|
12
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
13
|
+
let value = bytes / 1000;
|
|
14
|
+
let unit = 0;
|
|
15
|
+
while (value >= 1000 && unit < units.length - 1) {
|
|
16
|
+
value /= 1000;
|
|
17
|
+
unit += 1;
|
|
18
|
+
}
|
|
19
|
+
const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
20
|
+
return `${rounded} ${units[unit]}`;
|
|
21
|
+
}
|
|
22
|
+
/** Alias for call sites that want plan-cap wording; same SI formatter. */
|
|
23
|
+
export function formatMarketedBytes(bytes) {
|
|
24
|
+
return formatByteSize(bytes);
|
|
11
25
|
}
|
package/dist/format-usage.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type UsageSnapshotLike = {
|
|
|
9
9
|
storageRemainingBytes?: number;
|
|
10
10
|
maxUploadsPerPeriod?: number;
|
|
11
11
|
uploadsRemaining?: number;
|
|
12
|
+
/** Catalog plan id when the API reports it (`free` | `pro`). */
|
|
13
|
+
plan?: string;
|
|
12
14
|
};
|
|
13
15
|
export type FormatUsageOptions = {
|
|
14
16
|
/** IANA zone or undefined for the host local zone. */
|
|
@@ -34,5 +36,12 @@ export declare function formatProgressBar(pct: number, opts?: {
|
|
|
34
36
|
}): string;
|
|
35
37
|
/** Host-local time by default; pass `timeZone` for stable tests. */
|
|
36
38
|
export declare function formatUsageTimestamp(iso: string, timeZone?: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Display label for a plan catalog id. Free is shown too — free workspaces
|
|
41
|
+
* still have quotas (storage / monthly uploads) that usage meters report.
|
|
42
|
+
*/
|
|
43
|
+
export declare function planLabel(plan: string | undefined): string | null;
|
|
44
|
+
/** @deprecated use planLabel — kept as an alias for any external importers. */
|
|
45
|
+
export declare const paidPlanLabel: typeof planLabel;
|
|
37
46
|
/** Human-readable lines for `uploads usage` (not JSON). */
|
|
38
47
|
export declare function formatUsageHuman(result: UsageSnapshotLike, opts?: FormatUsageOptions): string[];
|
package/dist/format-usage.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and operator workspaces usually omit them (unlimited). Progress bars only
|
|
8
8
|
* appear for fields that have a positive cap — never invent a budget.
|
|
9
9
|
*/
|
|
10
|
-
import { formatByteSize } from "./format-bytes.js";
|
|
10
|
+
import { formatByteSize, formatMarketedBytes } from "./format-bytes.js";
|
|
11
11
|
import { BRAND } from "./cli-brand.js";
|
|
12
12
|
/** True when the API reported any cumulative workspace quota. */
|
|
13
13
|
export function isUsageMetered(result) {
|
|
@@ -87,17 +87,39 @@ export function formatUsageTimestamp(iso, timeZone) {
|
|
|
87
87
|
return iso;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Display label for a plan catalog id. Free is shown too — free workspaces
|
|
92
|
+
* still have quotas (storage / monthly uploads) that usage meters report.
|
|
93
|
+
*/
|
|
94
|
+
export function planLabel(plan) {
|
|
95
|
+
if (!plan)
|
|
96
|
+
return null;
|
|
97
|
+
if (plan === "free")
|
|
98
|
+
return "Free";
|
|
99
|
+
if (plan === "pro")
|
|
100
|
+
return "Pro";
|
|
101
|
+
// Future tiers: title-case the id rather than hiding them.
|
|
102
|
+
return plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
103
|
+
}
|
|
104
|
+
/** @deprecated use planLabel — kept as an alias for any external importers. */
|
|
105
|
+
export const paidPlanLabel = planLabel;
|
|
90
106
|
/** Human-readable lines for `uploads usage` (not JSON). */
|
|
91
107
|
export function formatUsageHuman(result, opts = {}) {
|
|
92
108
|
const width = opts.barWidth ?? 20;
|
|
93
109
|
const color = opts.color === true;
|
|
94
110
|
const metered = isUsageMetered(result);
|
|
95
111
|
const lines = [`workspace: ${result.workspace}`];
|
|
112
|
+
const label = planLabel(result.plan);
|
|
113
|
+
if (label)
|
|
114
|
+
lines.push(`plan: ${label}`);
|
|
96
115
|
const storagePct = usagePct(result.bytes, result.maxStorageBytes);
|
|
97
116
|
if (storagePct !== null && result.maxStorageBytes != null) {
|
|
98
|
-
|
|
117
|
+
// Caps (and remaining-against-cap) use SI marketed formatting so Free's
|
|
118
|
+
// 250_000_000 reads as "250 MB", not binary "238.4 MB". Used bytes share
|
|
119
|
+
// the same base on this line so the three numbers stay coherent.
|
|
120
|
+
const detail = `${formatMarketedBytes(result.bytes)} / ${formatMarketedBytes(result.maxStorageBytes)}` +
|
|
99
121
|
(result.storageRemainingBytes != null
|
|
100
|
-
? ` (${
|
|
122
|
+
? ` (${formatMarketedBytes(result.storageRemainingBytes)} free)`
|
|
101
123
|
: "");
|
|
102
124
|
const bar = formatProgressBar(storagePct, { width, color });
|
|
103
125
|
lines.push(`storage: ${bar} ${detail}`);
|
|
@@ -118,8 +140,15 @@ export function formatUsageHuman(result, opts = {}) {
|
|
|
118
140
|
}
|
|
119
141
|
lines.push(`updated: ${formatUsageTimestamp(result.updatedAt, opts.timeZone)}`);
|
|
120
142
|
if (!metered) {
|
|
121
|
-
// Self-host / operator unlimited
|
|
122
|
-
|
|
143
|
+
// Self-host / operator unlimited, or a plan without reported caps.
|
|
144
|
+
// Free self-serve normally reports maxStorageBytes / maxUploadsPerPeriod;
|
|
145
|
+
// when those are missing, say so explicitly rather than implying unlimited free.
|
|
146
|
+
if (result.plan === "free") {
|
|
147
|
+
lines.push("note: free plan — no quotas reported for this workspace (limits may still apply server-side)");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
lines.push("note: unmetered — no storage or upload quotas on this workspace");
|
|
151
|
+
}
|
|
123
152
|
}
|
|
124
153
|
return lines;
|
|
125
154
|
}
|
package/dist/github.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export declare function isValidRepo(repo: string): boolean;
|
|
|
15
15
|
export declare function parseRepoFromRemoteUrl(url: string): string | undefined;
|
|
16
16
|
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
17
17
|
export declare function normalizeGithubCoordinate(value: string): GithubCoordinate | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Inverse of `ghKeyPrefix`: parse the PR/issue coordinate back out of a
|
|
20
|
+
* stable attachment key (`gh/<owner>/<name>/<kind>/<num>/<filename>`), or
|
|
21
|
+
* undefined for any other key shape.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseGhKey(key: string): GhTarget | undefined;
|
|
18
24
|
export declare function ghKeyPrefix(target: GhTarget): string;
|
|
19
25
|
/**
|
|
20
26
|
* Stable attachment key: same filename → same key → same public URL, so
|
package/dist/github.js
CHANGED
|
@@ -53,6 +53,18 @@ export function normalizeGithubCoordinate(value) {
|
|
|
53
53
|
number,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Inverse of `ghKeyPrefix`: parse the PR/issue coordinate back out of a
|
|
58
|
+
* stable attachment key (`gh/<owner>/<name>/<kind>/<num>/<filename>`), or
|
|
59
|
+
* undefined for any other key shape.
|
|
60
|
+
*/
|
|
61
|
+
export function parseGhKey(key) {
|
|
62
|
+
const match = /^gh\/([^/]+)\/([^/]+)\/(pull|issues)\/([1-9][0-9]*)\/./.exec(key);
|
|
63
|
+
if (!match)
|
|
64
|
+
return undefined;
|
|
65
|
+
const [, owner, name, kind, num] = match;
|
|
66
|
+
return { repo: `${owner}/${name}`, kind: kind, num: Number(num) };
|
|
67
|
+
}
|
|
56
68
|
export function ghKeyPrefix(target) {
|
|
57
69
|
const [owner, name] = target.repo.split("/");
|
|
58
70
|
return `gh/${sanitizeKeySegment(owner)}/${sanitizeKeySegment(name)}/${target.kind}/${target.num}/`;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createUploadsClient } from "../client.js";
|
|
2
|
-
import { buildDoctorReport, makeGhTarget, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
2
|
+
import { buildDoctorReport, makeGhTarget, mergeStagingMeta, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
3
3
|
import { resolveFrameId } from "../frame.js";
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ghBranchAttachmentKey, ghKeyPrefix } from "../github.js";
|
|
7
7
|
import { safeCaptureFacts } from "../capture-facts.js";
|
|
8
8
|
import { validateMetaMap } from "../metadata.js";
|
|
9
9
|
import { mergeDerivedMeta } from "../metadata-vocab.js";
|
|
@@ -430,16 +430,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
430
430
|
repoArg: optString(args, "repo") ?? defaults.repo,
|
|
431
431
|
run,
|
|
432
432
|
});
|
|
433
|
-
const putMetadata = stagingTarget
|
|
434
|
-
? (() => {
|
|
435
|
-
const merged = {
|
|
436
|
-
...metadata,
|
|
437
|
-
...ghMetadataForBranch(stagingTarget.repo, stagingTarget.branch),
|
|
438
|
-
};
|
|
439
|
-
validateMetaMap(merged); // same builder, same contract as attach --branch
|
|
440
|
-
return merged;
|
|
441
|
-
})()
|
|
442
|
-
: metadata;
|
|
433
|
+
const putMetadata = stagingTarget ? mergeStagingMeta(metadata, stagingTarget) : metadata;
|
|
443
434
|
const putShared = {
|
|
444
435
|
client,
|
|
445
436
|
ghTarget: target,
|
|
@@ -695,25 +686,41 @@ export function createUploadsMcpTools(opts) {
|
|
|
695
686
|
const metadata = metadataArgWithCanonical(args);
|
|
696
687
|
if (metadata)
|
|
697
688
|
validateMetaMap(metadata);
|
|
689
|
+
const { config, client } = clientFor(args);
|
|
690
|
+
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
691
|
+
const frameOpts = mcpFrameOptions(args);
|
|
692
|
+
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
693
|
+
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
694
|
+
const alt = optString(args, "alt");
|
|
695
|
+
const width = optPosInt(args, "width") ?? defaults.width;
|
|
696
|
+
// Auto branch staging (issue #469 lever 1): mirrors the CLI screenshot
|
|
697
|
+
// command and the put tool above (issue #403) — no pr/issue/key/ref/
|
|
698
|
+
// prefix/destination, not noGit, on a non-default git branch stages
|
|
699
|
+
// to the branch prefix (identical key/metadata to `attach --branch`)
|
|
700
|
+
// instead of the dated `screenshots/<repo>/<date>/...` layout. Never
|
|
701
|
+
// throws — see resolvePutStagingTarget.
|
|
702
|
+
const stagingTarget = resolvePutStagingTarget({
|
|
703
|
+
ghTarget: target,
|
|
704
|
+
keyHint: keyArg,
|
|
705
|
+
refArg,
|
|
706
|
+
prefixArg,
|
|
707
|
+
destinationArg: destArg,
|
|
708
|
+
noGit,
|
|
709
|
+
repoArg: optString(args, "repo") ?? defaults.repo,
|
|
710
|
+
run,
|
|
711
|
+
});
|
|
698
712
|
let resolvedPrefix;
|
|
699
713
|
try {
|
|
700
714
|
resolvedPrefix = resolvePutPrefix({
|
|
701
715
|
destination: destArg,
|
|
702
716
|
prefix: prefixArg,
|
|
703
717
|
key: keyArg,
|
|
704
|
-
ghAttachment: Boolean(target),
|
|
718
|
+
ghAttachment: Boolean(target) || stagingTarget !== undefined,
|
|
705
719
|
});
|
|
706
720
|
}
|
|
707
721
|
catch (err) {
|
|
708
722
|
usage(err instanceof Error ? err.message : String(err));
|
|
709
723
|
}
|
|
710
|
-
const { config, client } = clientFor(args);
|
|
711
|
-
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
712
|
-
const frameOpts = mcpFrameOptions(args);
|
|
713
|
-
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
714
|
-
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
715
|
-
const alt = optString(args, "alt");
|
|
716
|
-
const width = optPosInt(args, "width") ?? defaults.width;
|
|
717
724
|
// Dynamic import only: keeps mcp/tools.ts (and therefore anything
|
|
718
725
|
// that statically imports it) free of a static reference to the
|
|
719
726
|
// local-backend chain. If this fails, the runtime can't do Node-side
|
|
@@ -730,9 +737,15 @@ export function createUploadsMcpTools(opts) {
|
|
|
730
737
|
// Keep undefined when nothing at all was supplied or derived, so the
|
|
731
738
|
// "omit to leave stored metadata untouched" contract still holds.
|
|
732
739
|
const captureDerived = safeCaptureFacts(targetArg, viewport, colorSchemeArg);
|
|
733
|
-
const
|
|
740
|
+
const metadataBase = metadata === undefined && Object.keys(captureDerived).length === 0
|
|
734
741
|
? undefined
|
|
735
742
|
: mergeDerivedMeta(metadata ?? {}, captureDerived);
|
|
743
|
+
// gh.* metadata: explicit pr/issue target wins; staging wins the same
|
|
744
|
+
// way (matches attach --branch/bare put); otherwise capture-derived +
|
|
745
|
+
// explicit only.
|
|
746
|
+
const metadataWithCaptureFacts = stagingTarget
|
|
747
|
+
? mergeStagingMeta(metadataBase, stagingTarget)
|
|
748
|
+
: metadataBase;
|
|
736
749
|
let captured;
|
|
737
750
|
try {
|
|
738
751
|
captured = await screenshotModule.captureScreenshot({
|
|
@@ -761,11 +774,14 @@ export function createUploadsMcpTools(opts) {
|
|
|
761
774
|
}
|
|
762
775
|
throw err;
|
|
763
776
|
}
|
|
777
|
+
const branchKey = stagingTarget
|
|
778
|
+
? ghBranchAttachmentKey(stagingTarget.repo, stagingTarget.branch, captured.filename)
|
|
779
|
+
: undefined;
|
|
764
780
|
const { result, prepared, markdown } = await uploadPreparedImage(client, captured.png, captured.filename, {
|
|
765
781
|
frame: frameOpts,
|
|
766
782
|
optimize: optimizeOpts,
|
|
767
783
|
ghTarget: target,
|
|
768
|
-
key: keyArg,
|
|
784
|
+
key: keyArg ?? branchKey,
|
|
769
785
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
770
786
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
771
787
|
ref: refArg ?? defaults.ref,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SyncCliVersionOptions {
|
|
2
|
+
sessionToken?: string;
|
|
3
|
+
authUrl?: string;
|
|
4
|
+
apiUrl?: string;
|
|
5
|
+
envFile?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
/** Skip the local "already synced this version" short-circuit. */
|
|
8
|
+
force?: boolean;
|
|
9
|
+
cachePath?: string;
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/** Best-effort POST; never throws. */
|
|
13
|
+
export declare function syncSessionCliVersion(opts?: SyncCliVersionOptions): Promise<boolean>;
|
|
14
|
+
/** Fire-and-forget for CLI command starts. */
|
|
15
|
+
export declare function maybeSyncSessionCliVersion(opts?: SyncCliVersionOptions): void;
|