@buildinternet/uploads 0.11.0 → 0.12.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.
@@ -0,0 +1,317 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
4
+ import { writeCommandHelp } from "../cli-style.js";
5
+ import { frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, syncAttachmentsComment, uploadPreparedImage, } from "../commands.js";
6
+ import { resolvePutDefaults } from "../config.js";
7
+ import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
8
+ import { resolvePutPrefix } from "../destinations.js";
9
+ import { ghMetadataFromTarget } from "../github.js";
10
+ import { execRunner } from "../github-gh.js";
11
+ import { parseMetaFlags, validateMetaMap } from "../metadata.js";
12
+ import { writeJson, writeStdout } from "../io.js";
13
+ import { captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
14
+ const SCREENSHOT_HELP = `uploads screenshot <target> [options]
15
+
16
+ Capture a URL or a local .html file and host it — a hosted, PR-embeddable
17
+ image in one step. target is an http(s) URL or a path to an .html file.
18
+
19
+ Two capture backends: "local" drives an already-installed Chrome/Chromium via
20
+ playwright-core (no browser download); "remote" renders server-side via the
21
+ uploads.sh render endpoint (no local browser needed, counts against the
22
+ workspace's monthly upload budget). Default --via auto prefers local when a
23
+ browser is found, else remote.
24
+
25
+ .html files work on both backends (sent inline to remote, ≤ 2 MiB; anything
26
+ they reference via file:// or relative paths only resolves with --via local).
27
+ localhost/private-network URLs are reachable only by the
28
+ local backend — with --via remote (or auto falling back to remote) these
29
+ fail fast with a clear error instead of sending a doomed request.
30
+
31
+ After capture, screenshots share the put upload pipeline: optional --frame,
32
+ optimize-by-default, --pr/--issue attachment + --comment, --gallery, --meta.
33
+
34
+ Options:
35
+ --via auto|local|remote Capture backend (default: auto, or UPLOADS_SCREENSHOT_VIA)
36
+ --browser <path> Explicit local browser executable (or UPLOADS_CHROME_PATH / CHROME_PATH)
37
+ --cdp <endpoint> Attach to a running Chrome via CDP (http://host:port or ws://…)
38
+ --viewport <WxH[@Sx]> Size + device scale factor (default: 1280x800@2)
39
+ --selector <css> Capture one element instead of the viewport
40
+ --full-page Capture the full scrollable page
41
+ --dark / --light Emulate prefers-color-scheme (full media-query emulation on --via local
42
+ only; --via remote just sets the CSS color-scheme property, so a page's
43
+ own prefers-color-scheme queries won't flip)
44
+ --wait <load|domcontentloaded|networkidle|ms> Settle strategy (default: load); a millisecond
45
+ count is local-only — use --via local
46
+ --out <file> Also write the PNG to a local file
47
+ --no-upload Skip hosting; requires --out (local file only)
48
+ --destination <id> Typed root: screenshots | gh | f
49
+ --prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
50
+ --repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
51
+ --ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
52
+ --key <key> Explicit object key; cannot combine with --pr/--issue
53
+ --alt <text> Alt text (default: derived filename)
54
+ --width <px> <img width=…> markdown
55
+ --frame <id> Device/browser frame before optimize (phone|browser|iphone-16-pro)
56
+ --frame-url <url> Address bar text for --frame browser
57
+ --frame-fit cover|contain How the shot fills the screen (default: cover)
58
+ --no-optimize Skip client-side image optimization
59
+ --optimize-max-edge <px> Max long edge when optimizing (default: 2400)
60
+ --optimize-quality <1-100> WebP quality (default: 85)
61
+ --keep-exif Keep EXIF/XMP/ICC when optimizing
62
+ --pr <num> Attach to a pull request (stable URL, no hash)
63
+ --issue <num> Attach to an issue
64
+ --comment With --pr/--issue: update the managed attachments comment
65
+ --gallery <id> Add the uploaded object to this public gallery
66
+ --meta <k=v> Queryable custom metadata (repeatable)
67
+ --workspace, -w <name> Override workspace
68
+ --dry-run Capture + resolve key/URL without uploading
69
+ --format human|url|markdown|json
70
+
71
+ Exit codes: 0 ok · 2 usage/no browser found/file · 3 auth/policy/budget · 4 network · 1 other.
72
+
73
+ Examples:
74
+ uploads screenshot https://uploads.sh
75
+ uploads screenshot ./card.html --out ./card.png
76
+ uploads screenshot https://app.example/settings --selector main --dark
77
+ uploads screenshot http://localhost:3000 --via local --full-page
78
+ uploads screenshot https://uploads.sh --pr 128 --comment
79
+ uploads screenshot ./card.html --no-upload --out ./card.png
80
+ `;
81
+ function colorSchemeFromFlags(flags) {
82
+ const dark = flagBool(flags, "--dark");
83
+ const light = flagBool(flags, "--light");
84
+ if (dark && light)
85
+ throw new UsageError("--dark and --light are mutually exclusive");
86
+ if (dark)
87
+ return "dark";
88
+ if (light)
89
+ return "light";
90
+ return undefined;
91
+ }
92
+ function viaFromFlags(flags, fallback) {
93
+ const raw = flagString(flags, "--via");
94
+ if (!raw)
95
+ return fallback;
96
+ if (raw === "auto" || raw === "local" || raw === "remote")
97
+ return raw;
98
+ throw new UsageError(`invalid --via: ${raw} (use auto, local, or remote)`);
99
+ }
100
+ export async function runScreenshot(ctx, args, help = false, run = execRunner,
101
+ /** Injectable for tests — avoids launching a real browser or hitting the network. */
102
+ captureImpl = captureScreenshot) {
103
+ if (help) {
104
+ writeCommandHelp(SCREENSHOT_HELP);
105
+ return 0;
106
+ }
107
+ const parsed = parseCommandArgs(args);
108
+ if (parsed.help) {
109
+ writeCommandHelp(SCREENSHOT_HELP);
110
+ return 0;
111
+ }
112
+ const target = parsed.positionals[0];
113
+ if (!target) {
114
+ writeCommandHelp(SCREENSHOT_HELP);
115
+ return 2;
116
+ }
117
+ if (parsed.positionals.length > 1) {
118
+ throw new UsageError("screenshot takes exactly one target");
119
+ }
120
+ // Read the on-disk config once and share it between the screenshot and
121
+ // put-style default resolvers (both would otherwise read the same file).
122
+ const rawDefaults = loadDefaultsRaw({ envFile: ctx.envFile });
123
+ const screenshotDefaults = resolveScreenshotDefaults({ envFile: ctx.envFile }, rawDefaults);
124
+ const via = viaFromFlags(parsed.flags, screenshotDefaults.via ?? "auto");
125
+ const browserPath = flagString(parsed.flags, "--browser");
126
+ const cdp = flagString(parsed.flags, "--cdp");
127
+ const viewport = parseViewport(flagString(parsed.flags, "--viewport"));
128
+ const selector = flagString(parsed.flags, "--selector");
129
+ const fullPage = flagBool(parsed.flags, "--full-page");
130
+ const colorScheme = colorSchemeFromFlags(parsed.flags);
131
+ const waitUntil = parseWaitUntil(flagString(parsed.flags, "--wait"));
132
+ const outFile = flagString(parsed.flags, "--out");
133
+ const noUpload = flagBool(parsed.flags, "--no-upload");
134
+ if (noUpload && !outFile)
135
+ throw new UsageError("--no-upload requires --out");
136
+ const keyHint = flagString(parsed.flags, "--key");
137
+ const destFlag = flagString(parsed.flags, "--destination");
138
+ const prefixFlag = flagString(parsed.flags, "--prefix");
139
+ const ghTarget = ghTargetFromFlags(parsed.flags, run);
140
+ const wantComment = parsed.flags.has("--comment");
141
+ const galleryId = flagString(parsed.flags, "--gallery");
142
+ const dryRun = flagBool(parsed.flags, "--dry-run");
143
+ if (wantComment && !ghTarget)
144
+ throw new UsageError("--comment requires --pr or --issue");
145
+ if (ghTarget) {
146
+ if (keyHint)
147
+ throw new UsageError("--key cannot be combined with --pr/--issue");
148
+ if (flagString(parsed.flags, "--ref"))
149
+ throw new UsageError("--ref cannot be combined with --pr/--issue");
150
+ if (prefixFlag)
151
+ throw new UsageError("--prefix cannot be combined with --pr/--issue");
152
+ }
153
+ if (dryRun) {
154
+ if (wantComment)
155
+ throw new UsageError("--dry-run cannot be combined with --comment");
156
+ if (galleryId)
157
+ throw new UsageError("--dry-run cannot be combined with --gallery");
158
+ if (noUpload)
159
+ throw new UsageError("--dry-run cannot be combined with --no-upload");
160
+ }
161
+ let resolvedPrefix;
162
+ try {
163
+ resolvedPrefix = resolvePutPrefix({
164
+ destination: destFlag,
165
+ prefix: prefixFlag,
166
+ key: keyHint,
167
+ ghAttachment: Boolean(ghTarget),
168
+ });
169
+ }
170
+ catch (err) {
171
+ throw new UsageError(err instanceof Error ? err.message : String(err));
172
+ }
173
+ const format = ctx.json
174
+ ? "json"
175
+ : (() => {
176
+ const raw = flagString(parsed.flags, "--format");
177
+ if (!raw || raw === "human")
178
+ return "human";
179
+ if (raw === "url" || raw === "markdown" || raw === "json")
180
+ return raw;
181
+ throw new UsageError(`invalid --format: ${raw}`);
182
+ })();
183
+ const putDefaults = resolvePutDefaults({ envFile: ctx.envFile }, rawDefaults);
184
+ const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, putDefaults);
185
+ const frameOpts = frameOptionsFromFlags(parsed.flags);
186
+ const altFlag = flagString(parsed.flags, "--alt");
187
+ const width = flagInt(parsed.flags, "--width", "--width") ?? putDefaults.width;
188
+ const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
189
+ let metadata = metaExtras;
190
+ if (ghTarget) {
191
+ metadata = { ...metaExtras, ...ghMetadataFromTarget(ghTarget) };
192
+ validateMetaMap(metadata);
193
+ }
194
+ else if (Object.keys(metaExtras).length > 0) {
195
+ validateMetaMap(metaExtras);
196
+ }
197
+ const logHuman = !ctx.quiet && format === "human";
198
+ if (logHuman)
199
+ process.stderr.write(`>> capturing ${target}\n`);
200
+ const captured = await captureImpl({
201
+ target,
202
+ via,
203
+ browserPath,
204
+ cdp,
205
+ viewport,
206
+ selector,
207
+ fullPage,
208
+ colorScheme,
209
+ waitUntil,
210
+ apiUrl: ctx.config.apiUrl,
211
+ token: ctx.config.token,
212
+ });
213
+ if (logHuman)
214
+ process.stderr.write(`>> captured via ${captured.backend} backend\n`);
215
+ if (outFile) {
216
+ writeFileSync(outFile, captured.png);
217
+ if (logHuman)
218
+ process.stderr.write(`>> wrote ${outFile}\n`);
219
+ }
220
+ if (noUpload) {
221
+ if (ctx.json) {
222
+ await writeJson({ file: outFile, backend: captured.backend, size: captured.png.byteLength });
223
+ }
224
+ else {
225
+ await writeStdout(`FILE: ${outFile}\n`);
226
+ }
227
+ return 0;
228
+ }
229
+ const repo = flagString(parsed.flags, "--repo") ?? putDefaults.repo;
230
+ const ref = flagString(parsed.flags, "--ref") ?? putDefaults.ref;
231
+ const alt = altFlag ?? basename(captured.filename);
232
+ const { result, prepared, markdown } = await uploadPreparedImage(ctx.client, captured.png, captured.filename, {
233
+ frame: frameOpts,
234
+ optimize: optimizeOpts,
235
+ ghTarget,
236
+ key: keyHint,
237
+ prefix: resolvedPrefix ?? putDefaults.prefix,
238
+ repo,
239
+ ref,
240
+ deriveRepoFromGit: !(flagBool(parsed.flags, "--no-git") || putDefaults.noGit === true),
241
+ dryRun,
242
+ metadata,
243
+ provenanceClient: "uploads-cli-screenshot",
244
+ alt: () => alt,
245
+ width,
246
+ });
247
+ let gallery;
248
+ if (galleryId) {
249
+ try {
250
+ const current = await ctx.client.getGallery(galleryId);
251
+ const item = await ctx.client.addGalleryItem(galleryId, result.key, {
252
+ expectedVersion: current.version,
253
+ altText: alt,
254
+ });
255
+ gallery = { id: galleryId, url: current.url };
256
+ void item;
257
+ }
258
+ catch (err) {
259
+ gallery = { id: galleryId, error: err instanceof Error ? err.message : String(err) };
260
+ }
261
+ }
262
+ let comment;
263
+ let commentError;
264
+ if (wantComment && ghTarget) {
265
+ try {
266
+ comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
267
+ if (logHuman)
268
+ process.stderr.write(`>> attachments comment ${comment.action}\n`);
269
+ }
270
+ catch (err) {
271
+ commentError = err instanceof Error ? err.message : String(err);
272
+ process.stderr.write(`warning: upload succeeded but the GitHub comment failed (is gh installed and authenticated?): ${commentError}\n`);
273
+ }
274
+ }
275
+ if (logHuman) {
276
+ if (prepared.frame?.framed)
277
+ process.stderr.write(`>> framed with ${prepared.frame.frameId}\n`);
278
+ if (prepared.optimized) {
279
+ process.stderr.write(`>> optimized ${prepared.originalBytes} → ${prepared.outputBytes} bytes\n`);
280
+ }
281
+ process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
282
+ }
283
+ switch (format) {
284
+ case "json":
285
+ await writeJson({
286
+ workspace: result.workspace,
287
+ key: result.key,
288
+ url: result.url,
289
+ embedUrl: result.embedUrl,
290
+ size: result.size,
291
+ contentType: result.contentType,
292
+ replaced: result.replaced,
293
+ markdown,
294
+ backend: captured.backend,
295
+ gallery,
296
+ ...(dryRun ? { dryRun: true } : {}),
297
+ });
298
+ break;
299
+ case "url":
300
+ await writeStdout(`${result.url}\n`);
301
+ break;
302
+ case "markdown":
303
+ await writeStdout(`${markdown}\n`);
304
+ break;
305
+ default: {
306
+ const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
307
+ await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
308
+ }
309
+ }
310
+ if (gallery?.error) {
311
+ process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error}\n`);
312
+ }
313
+ if (commentError && ctx.json) {
314
+ // already reported to stderr above; json output stays upload-focused.
315
+ }
316
+ return gallery?.error ? 1 : 0;
317
+ }
@@ -1,4 +1,4 @@
1
- import { type UploadsClient } from "./client.js";
1
+ import { type PutResult, type UploadsClient } from "./client.js";
2
2
  import { type CommandFlags } from "./cli-args.js";
3
3
  import { type ResolvedConfig } from "./config.js";
4
4
  import { type GhTarget } from "./github.js";
@@ -6,6 +6,11 @@ import { type CommandRunner } from "./github-gh.js";
6
6
  import { type OptimizeImageOptions, type OptimizeImageResult } from "./optimize.js";
7
7
  import { type FrameResult } from "./frame.js";
8
8
  import type { PutDefaults } from "./config-file.js";
9
+ import type { DetectRoots } from "./screenshot-local.js";
10
+ /** Parallel fan-out for multi-file put/attach (matches files-sdk bulk default). */
11
+ export declare const UPLOAD_BATCH_CONCURRENCY = 8;
12
+ /** @deprecated Use UPLOAD_BATCH_CONCURRENCY. */
13
+ export declare const ATTACH_CONCURRENCY = 8;
9
14
  export { formatUsageHuman } from "./format-usage.js";
10
15
  export interface CliContext {
11
16
  config: ResolvedConfig;
@@ -21,6 +26,8 @@ export declare function readFileArg(fileArg: string): Uint8Array;
21
26
  * neither is present. Shared by the CLI flags and the MCP tool arguments.
22
27
  */
23
28
  export declare function makeGhTarget(pr: number | undefined, issue: number | undefined, repoArg: string | undefined, run: CommandRunner): GhTarget | undefined;
29
+ /** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
30
+ export declare function ghTargetFromFlags(flags: CommandFlags["flags"], run: CommandRunner): GhTarget | undefined;
24
31
  /** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
25
32
  export declare function optimizeOptionsFromFlags(flags: CommandFlags["flags"], defaults: PutDefaults): OptimizeImageOptions;
26
33
  export type PreparedUpload = OptimizeImageResult & {
@@ -33,6 +40,53 @@ export declare function prepareImageForUpload(bytes: Uint8Array, filename: strin
33
40
  frameFit?: "cover" | "contain";
34
41
  optimize: OptimizeImageOptions;
35
42
  }): Promise<PreparedUpload>;
43
+ export interface UploadPreparedImageOptions {
44
+ frame: {
45
+ frameId?: string;
46
+ frameUrl?: string;
47
+ frameFit?: "cover" | "contain";
48
+ };
49
+ optimize: OptimizeImageOptions;
50
+ /** gh attachment key wins over `key` when both are set (matches every call site). */
51
+ ghTarget?: GhTarget;
52
+ key?: string;
53
+ prefix?: string;
54
+ repo?: string;
55
+ ref?: string;
56
+ deriveRepoFromGit?: boolean;
57
+ contentType?: string;
58
+ dryRun?: boolean;
59
+ metadata?: Record<string, string>;
60
+ provenanceClient?: string;
61
+ /**
62
+ * Alt text for the markdown. Takes the prepared result so callers whose
63
+ * default depends on the post-frame/optimize filename can use it — each
64
+ * call site's existing default is preserved verbatim (see
65
+ * .context/2026-07-16-screenshot-command-RESULT.md, "Simplify pass").
66
+ */
67
+ alt: (prepared: PreparedUpload) => string;
68
+ width?: number;
69
+ }
70
+ export interface UploadPreparedImageResult {
71
+ result: PutResult;
72
+ prepared: PreparedUpload;
73
+ markdown: string;
74
+ }
75
+ /**
76
+ * Shared bytes-oriented upload tail: frame + optimize the bytes, resolve the
77
+ * object key (gh attachment key wins over an explicit key; extension
78
+ * rewritten post-optimize), put, and build the GitHub embed markdown. Used by
79
+ * the screenshot CLI command, the MCP screenshot tool, and the MCP put
80
+ * tool's contentBase64 path — the three in-memory-bytes call sites.
81
+ * uploadPuts/uploadAttachments loop over file paths with their own bounded
82
+ * concurrency and delegate here per item.
83
+ */
84
+ export declare function uploadPreparedImage(client: UploadsClient, bytes: Uint8Array, sourceName: string, opts: UploadPreparedImageOptions): Promise<UploadPreparedImageResult>;
85
+ export declare function frameOptionsFromFlags(flags: CommandFlags["flags"]): {
86
+ frameId?: string;
87
+ frameUrl?: string;
88
+ frameFit?: "cover" | "contain";
89
+ };
36
90
  /**
37
91
  * List every attachment under the target's prefix and create/update the
38
92
  * managed comment. Throws on gh failure — callers decide whether that is
@@ -42,6 +96,92 @@ export declare function syncAttachmentsComment(client: UploadsClient, target: Gh
42
96
  action: "created" | "updated" | "skipped";
43
97
  count: number;
44
98
  }>;
99
+ export type AttachUploadItem = PutResult & {
100
+ file: string;
101
+ markdown: string;
102
+ optimize: {
103
+ optimized: boolean;
104
+ skippedReason?: OptimizeImageResult["skippedReason"];
105
+ originalBytes: number;
106
+ outputBytes: number;
107
+ filename: string;
108
+ };
109
+ frame?: PreparedUpload["frame"];
110
+ };
111
+ export type AttachFailure = {
112
+ file: string;
113
+ error: {
114
+ message: string;
115
+ code?: string;
116
+ status?: number;
117
+ };
118
+ };
119
+ /**
120
+ * Prepare + put each path as a PR/issue attachment with bounded concurrency.
121
+ * Per-file errors collect in `failures` (does not throw). `firstError` is the
122
+ * original cause of the first failure — for rethrowing single-file CLI paths.
123
+ */
124
+ export declare function uploadAttachments(opts: {
125
+ client: UploadsClient;
126
+ target: GhTarget;
127
+ files: readonly string[];
128
+ contentType?: string;
129
+ optimize: OptimizeImageOptions;
130
+ frame: {
131
+ frameId?: string;
132
+ frameUrl?: string;
133
+ frameFit?: "cover" | "contain";
134
+ };
135
+ metadata?: Record<string, string>;
136
+ /** Provenance `client` field (default uploads-cli). */
137
+ provenanceClient?: string;
138
+ concurrency?: number;
139
+ }): Promise<{
140
+ uploads: AttachUploadItem[];
141
+ failures: AttachFailure[];
142
+ firstError?: unknown;
143
+ }>;
144
+ export type PutUploadItem = PutResult & {
145
+ file: string;
146
+ markdown: string;
147
+ optimize: AttachUploadItem["optimize"];
148
+ frame?: PreparedUpload["frame"];
149
+ };
150
+ /**
151
+ * Prepare + put each path with put-style key resolution and bounded concurrency.
152
+ * Same partial-failure shape as uploadAttachments.
153
+ */
154
+ export declare function uploadPuts(opts: {
155
+ client: UploadsClient;
156
+ files: readonly string[];
157
+ /** Single-file --name leaf override. */
158
+ nameOverride?: string;
159
+ /** Single-file --key. */
160
+ explicitKey?: string;
161
+ ghTarget?: GhTarget;
162
+ prefix?: string;
163
+ repo?: string;
164
+ ref?: string;
165
+ deriveRepoFromGit?: boolean;
166
+ contentType?: string;
167
+ dryRun?: boolean;
168
+ optimize: OptimizeImageOptions;
169
+ frame: {
170
+ frameId?: string;
171
+ frameUrl?: string;
172
+ frameFit?: "cover" | "contain";
173
+ };
174
+ metadata?: Record<string, string>;
175
+ provenanceClient?: string;
176
+ /** When set, used as alt for every file; else each file's basename. */
177
+ alt?: string;
178
+ width?: number;
179
+ concurrency?: number;
180
+ }): Promise<{
181
+ uploads: PutUploadItem[];
182
+ failures: AttachFailure[];
183
+ firstError?: unknown;
184
+ }>;
45
185
  export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
46
186
  export declare function runPut(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
47
187
  export declare function runGallery(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
@@ -84,7 +224,27 @@ export interface DoctorReport {
84
224
  /** Workspace/token mismatch warning (also present in hints). */
85
225
  warning?: string;
86
226
  hints: string[];
227
+ /** `screenshot`'s local-browser detection (fs scans only — never launches a browser). */
228
+ browser: {
229
+ /** false when this runtime has no Node fs/process (e.g. the apps/mcp Worker). */
230
+ supported: boolean;
231
+ found: boolean;
232
+ /** Which backend `uploads screenshot --via auto` would pick right now. */
233
+ autoBackend: "local" | "remote";
234
+ candidates: {
235
+ source: string;
236
+ kind: string;
237
+ executablePath: string;
238
+ }[];
239
+ /** The best candidate by rank (may differ from candidates[0], which is scan order). */
240
+ winner?: {
241
+ source: string;
242
+ kind: string;
243
+ executablePath: string;
244
+ };
245
+ note?: string;
246
+ };
87
247
  }
88
248
  /** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
89
- export declare function buildDoctorReport(config: ResolvedConfig, client: UploadsClient): Promise<DoctorReport>;
249
+ export declare function buildDoctorReport(config: ResolvedConfig, client: UploadsClient, detectRoots?: DetectRoots): Promise<DoctorReport>;
90
250
  export declare function runDoctor(ctx: CliContext, args: string[], help?: boolean): Promise<number>;