@buildinternet/uploads 0.22.1 → 0.24.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 +8 -0
- package/dist/cli-catalog.js +4 -0
- package/dist/cli.js +5 -1
- package/dist/client.d.ts +18 -0
- package/dist/client.js +10 -0
- package/dist/commands.d.ts +109 -0
- package/dist/commands.js +409 -5
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +10 -0
- package/dist/github-gh.d.ts +16 -0
- package/dist/github-gh.js +33 -0
- package/dist/github.d.ts +12 -0
- package/dist/github.js +46 -2
- package/dist/keys.d.ts +8 -1
- package/dist/keys.js +11 -2
- package/dist/mcp/tools.js +57 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,14 @@ attachments when one opens: automatically via the
|
|
|
94
94
|
workspace↔repo binding the webhook path uses. Promoted staging is cleaned up
|
|
95
95
|
server-side after ~7 days (~30 for branches that never got a PR).
|
|
96
96
|
|
|
97
|
+
**Bare `put` stages too, by default (issue #403):** on a non-default git
|
|
98
|
+
branch, a `put` with none of
|
|
99
|
+
`--pr`/`--issue`/`--key`/`--ref`/`--prefix`/`--destination` set
|
|
100
|
+
(and not `--no-git`) stages exactly like `attach --branch` — same key,
|
|
101
|
+
same `gh.*` metadata. The classic dated layout
|
|
102
|
+
(`<prefix>/<repo>/<ref-or-date>/<name>`) remains the default branch/detached
|
|
103
|
+
HEAD/non-repo/`--no-git` behavior, and the explicit-flags opt-out.
|
|
104
|
+
|
|
97
105
|
**Screenshot capture:** `uploads screenshot <url|file.html>` renders a page to a
|
|
98
106
|
hosted image in one step — no separate browser tooling needed. `--via auto`
|
|
99
107
|
(default) drives a Chrome/Chromium already on the machine (`playwright-core`
|
package/dist/cli-catalog.js
CHANGED
|
@@ -114,6 +114,10 @@ export const ROOT_COMMANDS = [
|
|
|
114
114
|
summary: "Upload (+ URL + markdown for GitHub)",
|
|
115
115
|
essential: true,
|
|
116
116
|
},
|
|
117
|
+
{
|
|
118
|
+
name: "staged",
|
|
119
|
+
summary: "Show what's staged for a branch, and whether it will auto-attach",
|
|
120
|
+
},
|
|
117
121
|
{
|
|
118
122
|
name: "screenshot",
|
|
119
123
|
usage: "screenshot <target>",
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { UploadsError } from "./errors.js";
|
|
|
4
4
|
import { commandWorkspace, flagString, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
5
|
import { formatRootHelp, wantsFullHelp } from "./cli-help.js";
|
|
6
6
|
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
7
|
-
import { runPut, runAttach, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
7
|
+
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
8
8
|
import { runConfig } from "./commands/config.js";
|
|
9
9
|
import { runSetup } from "./commands/setup.js";
|
|
10
10
|
import { runLogin } from "./commands/login.js";
|
|
@@ -269,6 +269,7 @@ export async function runCli(argv) {
|
|
|
269
269
|
break;
|
|
270
270
|
case "attach":
|
|
271
271
|
case "put":
|
|
272
|
+
case "staged":
|
|
272
273
|
case "screenshot":
|
|
273
274
|
case "gallery":
|
|
274
275
|
case "list":
|
|
@@ -289,6 +290,9 @@ export async function runCli(argv) {
|
|
|
289
290
|
case "put":
|
|
290
291
|
code = await runPut(ctx, cmdArgs, showHelp);
|
|
291
292
|
break;
|
|
293
|
+
case "staged":
|
|
294
|
+
code = await runStaged(ctx, cmdArgs, showHelp);
|
|
295
|
+
break;
|
|
292
296
|
case "screenshot":
|
|
293
297
|
code = await runScreenshot(ctx, cmdArgs, showHelp);
|
|
294
298
|
break;
|
package/dist/client.d.ts
CHANGED
|
@@ -249,6 +249,16 @@ export interface GithubLinkUnlinkResult {
|
|
|
249
249
|
unlinked: boolean;
|
|
250
250
|
reason?: "not_linked";
|
|
251
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* `GET /v1/:workspace/github/repo-link` result (issue #398). Deliberately
|
|
254
|
+
* minimal relative to `GithubLinkResult`: never names the owning workspace
|
|
255
|
+
* when it isn't this one — "self"/"other"/"none" is all the stage-time
|
|
256
|
+
* warning needs, and anything richer would leak cross-tenant info to a
|
|
257
|
+
* caller that's just probing a repo it detected from local git context.
|
|
258
|
+
*/
|
|
259
|
+
export interface GithubRepoLinkResult {
|
|
260
|
+
binding: "self" | "other" | "none";
|
|
261
|
+
}
|
|
252
262
|
export interface HealthResult {
|
|
253
263
|
ok: boolean;
|
|
254
264
|
}
|
|
@@ -528,6 +538,14 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
528
538
|
* server without this route.
|
|
529
539
|
*/
|
|
530
540
|
githubLinkClaim(repo: string): Promise<GithubLinkClaimResult>;
|
|
541
|
+
/**
|
|
542
|
+
* Tri-state binding status for `repo` relative to this workspace (issue
|
|
543
|
+
* #398, `attach --branch`'s stage-time warning) — never names another
|
|
544
|
+
* workspace, unlike `githubLinkStatus` above. Throws `UploadsError`
|
|
545
|
+
* (status 404) on an older/self-hosted server without this route; the
|
|
546
|
+
* caller (the stage warning) treats ANY failure here as "stay silent".
|
|
547
|
+
*/
|
|
548
|
+
githubRepoLinkStatus(repo: string): Promise<GithubRepoLinkResult>;
|
|
531
549
|
/**
|
|
532
550
|
* GitHub App configuration + webhook event subscription check. Throws
|
|
533
551
|
* `UploadsError` (status 404) on an older/self-hosted server without
|
package/dist/client.js
CHANGED
|
@@ -516,6 +516,16 @@ export function createUploadsClient(config) {
|
|
|
516
516
|
headers: { "Content-Type": "application/json" },
|
|
517
517
|
});
|
|
518
518
|
},
|
|
519
|
+
/**
|
|
520
|
+
* Tri-state binding status for `repo` relative to this workspace (issue
|
|
521
|
+
* #398, `attach --branch`'s stage-time warning) — never names another
|
|
522
|
+
* workspace, unlike `githubLinkStatus` above. Throws `UploadsError`
|
|
523
|
+
* (status 404) on an older/self-hosted server without this route; the
|
|
524
|
+
* caller (the stage warning) treats ANY failure here as "stay silent".
|
|
525
|
+
*/
|
|
526
|
+
async githubRepoLinkStatus(repo) {
|
|
527
|
+
return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/repo-link?repo=${encodeURIComponent(repo)}`);
|
|
528
|
+
},
|
|
519
529
|
/**
|
|
520
530
|
* GitHub App configuration + webhook event subscription check. Throws
|
|
521
531
|
* `UploadsError` (status 404) on an older/self-hosted server without
|
package/dist/commands.d.ts
CHANGED
|
@@ -82,6 +82,13 @@ export interface UploadPreparedImageOptions {
|
|
|
82
82
|
optimize: OptimizeImageOptions;
|
|
83
83
|
/** gh attachment key wins over `key` when both are set (matches every call site). */
|
|
84
84
|
ghTarget?: GhTarget;
|
|
85
|
+
/**
|
|
86
|
+
* Branch-staged key (issue #403): wins over `key`, loses to `ghTarget` (the
|
|
87
|
+
* two are mutually exclusive at every call site — a PR/issue target always
|
|
88
|
+
* implies staging is moot). Produces the exact same key as `attach
|
|
89
|
+
* --branch` for the same filename via `ghBranchAttachmentKey`.
|
|
90
|
+
*/
|
|
91
|
+
ghBranchTarget?: BranchTarget;
|
|
85
92
|
key?: string;
|
|
86
93
|
prefix?: string;
|
|
87
94
|
repo?: string;
|
|
@@ -258,6 +265,8 @@ export declare function uploadPuts(opts: {
|
|
|
258
265
|
/** Single-file --key. */
|
|
259
266
|
explicitKey?: string;
|
|
260
267
|
ghTarget?: GhTarget;
|
|
268
|
+
/** Branch-staged key (issue #403) — see UploadPreparedImageOptions.ghBranchTarget. */
|
|
269
|
+
ghBranchTarget?: BranchTarget;
|
|
261
270
|
prefix?: string;
|
|
262
271
|
repo?: string;
|
|
263
272
|
ref?: string;
|
|
@@ -290,6 +299,106 @@ export declare function uploadPuts(opts: {
|
|
|
290
299
|
firstError?: unknown;
|
|
291
300
|
}>;
|
|
292
301
|
export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
302
|
+
/**
|
|
303
|
+
* One source of truth for the "staged, but not going to auto-attach" advisory
|
|
304
|
+
* text (issue #398), shared by the `attach --branch`/bare-`put` stage-time
|
|
305
|
+
* warning below and the `uploads staged` view (issue #405) — both surfaces
|
|
306
|
+
* must say the exact same thing for the same binding state, verified by
|
|
307
|
+
* tests on both call sites. Returns undefined for `"self"` (the happy path —
|
|
308
|
+
* callers each phrase that themselves) and any unrecognized value.
|
|
309
|
+
*/
|
|
310
|
+
export declare function stagingBindingAdvisory(binding: string, repo: string): string | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* Best-effort stage-time binding warning (issue #398): after `attach
|
|
313
|
+
* --branch` stages files, checks whether `repo` is bound to THIS workspace —
|
|
314
|
+
* webhook auto-promotion at PR open only fires for a repo already bound
|
|
315
|
+
* (#297), and staging alone never binds one. Fires only for `binding: "none"`
|
|
316
|
+
* (unbound) or `"other"` (bound elsewhere); `"self"` and any failure
|
|
317
|
+
* (network, non-200, older server without the route, `binding: "unknown"`)
|
|
318
|
+
* are silent — this is advisory only and must never make staging look like
|
|
319
|
+
* it failed. Same suppression as the #393 put nudge: `--quiet`,
|
|
320
|
+
* `UPLOADS_NO_NUDGE=1` (env or config).
|
|
321
|
+
*/
|
|
322
|
+
export declare function resolveStageBindingWarning(opts: {
|
|
323
|
+
ctx: CliContext;
|
|
324
|
+
defaults: PutDefaults;
|
|
325
|
+
repo: string;
|
|
326
|
+
}): Promise<string | undefined>;
|
|
327
|
+
/**
|
|
328
|
+
* Bare-put branch-staging trigger (issue #403): put on a non-default git
|
|
329
|
+
* branch stages to the branch prefix by default — the branch becomes the
|
|
330
|
+
* organizing unit instead of the date, superseding the #393 CLI nudge for
|
|
331
|
+
* this case (see `putStagingNoteText`). Reuses the same detection stack as
|
|
332
|
+
* `resolvePutNudge` (`deriveRepoFromGit` / `resolveCurrentBranch` /
|
|
333
|
+
* `resolveDefaultBranch` / main-master fallback) plus a `resolveRepo` lookup
|
|
334
|
+
* (needed for the "owner/name" staging key) and an explicit-flag opt-out:
|
|
335
|
+
* `ghTarget`/`keyHint`/`refArg`/`prefixArg`/`destinationArg` set, or `noGit`,
|
|
336
|
+
* forces the classic dated (or typed-destination) layout. Never throws — any
|
|
337
|
+
* failure (not a repo, detached HEAD, gh missing/unauthenticated/timed out,
|
|
338
|
+
* unresolvable repo) degrades to "no staging", leaving the caller to fall
|
|
339
|
+
* back to the dated path.
|
|
340
|
+
*
|
|
341
|
+
* Plain-params (not CLI `flags`) so both `runPut` and the local stdio MCP
|
|
342
|
+
* `put` tool — same staging default, issue #403's scope — can call this
|
|
343
|
+
* without either depending on the other's argument shape.
|
|
344
|
+
*/
|
|
345
|
+
export declare function resolvePutStagingTarget(opts: {
|
|
346
|
+
ghTarget: GhTarget | undefined;
|
|
347
|
+
keyHint: string | undefined;
|
|
348
|
+
refArg: string | undefined;
|
|
349
|
+
prefixArg: string | undefined;
|
|
350
|
+
/** Explicit `--destination` (CLI) / `destination` (MCP) also opts out — it
|
|
351
|
+
* resolves to its own prefix via `resolvePutPrefix`, which staging would
|
|
352
|
+
* otherwise silently override. */
|
|
353
|
+
destinationArg: string | undefined;
|
|
354
|
+
noGit: boolean;
|
|
355
|
+
repoArg: string | undefined;
|
|
356
|
+
run: CommandRunner;
|
|
357
|
+
}): BranchTarget | undefined;
|
|
358
|
+
/**
|
|
359
|
+
* The bare-put staging note's wording (issue #403): replaces the #393 nudge
|
|
360
|
+
* for the (now default) case where a bare put on a non-default branch stages
|
|
361
|
+
* to the branch prefix instead of landing on the dated layout. Used verbatim
|
|
362
|
+
* for both the human-mode stderr line and the JSON `hint` field.
|
|
363
|
+
*/
|
|
364
|
+
export declare function putStagingNoteText(branch: string): string;
|
|
365
|
+
/** One file currently staged for a branch (issue #405). */
|
|
366
|
+
export interface StagedFile {
|
|
367
|
+
/** Full object key (`gh/<owner>/<repo>/branch/<branch>/<filename>`). */
|
|
368
|
+
key: string;
|
|
369
|
+
/** `key` with the staging prefix stripped. */
|
|
370
|
+
filename: string;
|
|
371
|
+
size?: number;
|
|
372
|
+
/** `gh.staged-at` metadata (ISO 8601 UTC), when present. */
|
|
373
|
+
stagedAt?: string;
|
|
374
|
+
url: string | null;
|
|
375
|
+
}
|
|
376
|
+
/** Tri-state binding, folded into a ready-to-render advisory (issue #405/#398). */
|
|
377
|
+
export interface StagedBinding {
|
|
378
|
+
state: "self" | "other" | "none" | "unknown";
|
|
379
|
+
/** True only for "self" — the only state where staged files actually auto-attach. */
|
|
380
|
+
autoAttach: boolean;
|
|
381
|
+
message: string;
|
|
382
|
+
}
|
|
383
|
+
export interface StagedResult {
|
|
384
|
+
repo: string;
|
|
385
|
+
branch: string;
|
|
386
|
+
files: StagedFile[];
|
|
387
|
+
binding: StagedBinding;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Shared core for `uploads staged` (CLI) and the `staged` MCP tool (issue
|
|
391
|
+
* #405): one `list` call against the branch staging prefix
|
|
392
|
+
* (`ghBranchKeyPrefix` — never hand-built) plus the #398 binding check. Never
|
|
393
|
+
* throws on the binding check (see `resolveStagedBinding`); a failed `list`
|
|
394
|
+
* call still propagates, same as every other read command.
|
|
395
|
+
*/
|
|
396
|
+
export declare function resolveStaged(opts: {
|
|
397
|
+
client: UploadsClient;
|
|
398
|
+
repo: string;
|
|
399
|
+
branch: string;
|
|
400
|
+
}): Promise<StagedResult>;
|
|
401
|
+
export declare function runStaged(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
293
402
|
export declare function runPut(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
294
403
|
export declare function runGallery(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
295
404
|
export declare function runList(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
package/dist/commands.js
CHANGED
|
@@ -11,8 +11,9 @@ 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 { ghAttachmentKey, ghBranchAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, normalizeGithubCoordinate, } from "./github.js";
|
|
15
|
-
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, classifyGhNumber, execRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
14
|
+
import { ghAttachmentKey, ghBranchAttachmentKey, ghBranchKeyPrefix, ghKeyPrefix, ghMetadataFromTarget, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, normalizeGithubCoordinate, } from "./github.js";
|
|
15
|
+
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
16
|
+
import { deriveRepoFromGit } from "./keys.js";
|
|
16
17
|
import { resolvePutPrefix } from "./destinations.js";
|
|
17
18
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
18
19
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
@@ -111,6 +112,10 @@ Options:
|
|
|
111
112
|
--dry-run Print key + public URL without uploading; reports if the key would replace
|
|
112
113
|
(or, on a strict key, be refused). Not with --comment/--gallery
|
|
113
114
|
|
|
115
|
+
A bare put (no --pr/--issue/--key) on a non-default git branch prints a one-line
|
|
116
|
+
nudge toward --pr/attach --branch (stderr in human mode, a "hint" field in
|
|
117
|
+
--format json). Suppress with --quiet, UPLOADS_NO_NUDGE=1, or config UPLOADS_NO_NUDGE=1.
|
|
118
|
+
|
|
114
119
|
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other (incl. partial multi-file failure).
|
|
115
120
|
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
116
121
|
|
|
@@ -368,7 +373,11 @@ export async function uploadPreparedImage(client, bytes, sourceName, opts) {
|
|
|
368
373
|
frameFit: opts.frame.frameFit,
|
|
369
374
|
optimize: opts.optimize,
|
|
370
375
|
});
|
|
371
|
-
let key = opts.ghTarget
|
|
376
|
+
let key = opts.ghTarget
|
|
377
|
+
? ghAttachmentKey(opts.ghTarget, prepared.filename)
|
|
378
|
+
: opts.ghBranchTarget
|
|
379
|
+
? ghBranchAttachmentKey(opts.ghBranchTarget.repo, opts.ghBranchTarget.branch, prepared.filename)
|
|
380
|
+
: opts.key;
|
|
372
381
|
if (key && prepared.optimized)
|
|
373
382
|
key = rewriteKeyExtension(key, prepared.filename);
|
|
374
383
|
const result = await client.put(prepared.bytes, {
|
|
@@ -738,6 +747,7 @@ export async function uploadPuts(opts) {
|
|
|
738
747
|
frame: opts.frame,
|
|
739
748
|
optimize: opts.optimize,
|
|
740
749
|
ghTarget: opts.ghTarget,
|
|
750
|
+
ghBranchTarget: opts.ghBranchTarget,
|
|
741
751
|
key: opts.explicitKey,
|
|
742
752
|
prefix: opts.prefix,
|
|
743
753
|
repo: opts.repo,
|
|
@@ -1004,8 +1014,17 @@ async function runAttachBranch(ctx, parsed, branch, run) {
|
|
|
1004
1014
|
if (uploads.length === 0 && failures.length === 1 && parsed.positionals.length === 1) {
|
|
1005
1015
|
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
1006
1016
|
}
|
|
1017
|
+
// Stage-time binding warning (issue #398): only worth checking once staging
|
|
1018
|
+
// actually produced something to warn about. Best-effort — see
|
|
1019
|
+
// resolveStageBindingWarning; never affects exit code or the upload itself.
|
|
1020
|
+
const bindingWarning = uploads.length > 0 ? await resolveStageBindingWarning({ ctx, defaults, repo }) : undefined;
|
|
1007
1021
|
if (ctx.json) {
|
|
1008
|
-
await writeJson({
|
|
1022
|
+
await writeJson({
|
|
1023
|
+
target,
|
|
1024
|
+
uploads,
|
|
1025
|
+
failures,
|
|
1026
|
+
...(bindingWarning ? { hint: bindingWarning } : {}),
|
|
1027
|
+
});
|
|
1009
1028
|
}
|
|
1010
1029
|
else {
|
|
1011
1030
|
for (const result of uploads) {
|
|
@@ -1029,9 +1048,58 @@ async function runAttachBranch(ctx, parsed, branch, run) {
|
|
|
1029
1048
|
process.stderr.write(`>> staged: these auto-attach to this branch's PR when it opens ` +
|
|
1030
1049
|
`(or run \`uploads attach --promote\` after opening)\n`);
|
|
1031
1050
|
}
|
|
1051
|
+
if (bindingWarning)
|
|
1052
|
+
process.stderr.write(`${bindingWarning}\n`);
|
|
1032
1053
|
}
|
|
1033
1054
|
return failures.length === 0 ? 0 : 1;
|
|
1034
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* One source of truth for the "staged, but not going to auto-attach" advisory
|
|
1058
|
+
* text (issue #398), shared by the `attach --branch`/bare-`put` stage-time
|
|
1059
|
+
* warning below and the `uploads staged` view (issue #405) — both surfaces
|
|
1060
|
+
* must say the exact same thing for the same binding state, verified by
|
|
1061
|
+
* tests on both call sites. Returns undefined for `"self"` (the happy path —
|
|
1062
|
+
* callers each phrase that themselves) and any unrecognized value.
|
|
1063
|
+
*/
|
|
1064
|
+
export function stagingBindingAdvisory(binding, repo) {
|
|
1065
|
+
switch (binding) {
|
|
1066
|
+
case "none":
|
|
1067
|
+
return (`staged, but ${repo} isn't linked to your workspace yet — staged files only ` +
|
|
1068
|
+
`auto-attach on PR open for linked repos. Link it once with: uploads attach <file> ` +
|
|
1069
|
+
`(on any PR) or uploads github link. After the PR opens: uploads attach --promote`);
|
|
1070
|
+
case "other":
|
|
1071
|
+
return (`staged, but ${repo} is linked to a different workspace — these files won't ` +
|
|
1072
|
+
`auto-attach from here.`);
|
|
1073
|
+
default:
|
|
1074
|
+
return undefined; // "self", or any unrecognized value
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Best-effort stage-time binding warning (issue #398): after `attach
|
|
1079
|
+
* --branch` stages files, checks whether `repo` is bound to THIS workspace —
|
|
1080
|
+
* webhook auto-promotion at PR open only fires for a repo already bound
|
|
1081
|
+
* (#297), and staging alone never binds one. Fires only for `binding: "none"`
|
|
1082
|
+
* (unbound) or `"other"` (bound elsewhere); `"self"` and any failure
|
|
1083
|
+
* (network, non-200, older server without the route, `binding: "unknown"`)
|
|
1084
|
+
* are silent — this is advisory only and must never make staging look like
|
|
1085
|
+
* it failed. Same suppression as the #393 put nudge: `--quiet`,
|
|
1086
|
+
* `UPLOADS_NO_NUDGE=1` (env or config).
|
|
1087
|
+
*/
|
|
1088
|
+
export async function resolveStageBindingWarning(opts) {
|
|
1089
|
+
const { ctx, defaults, repo } = opts;
|
|
1090
|
+
if (ctx.quiet)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
if (defaults.noNudge)
|
|
1093
|
+
return undefined;
|
|
1094
|
+
try {
|
|
1095
|
+
const { binding } = await ctx.client.githubRepoLinkStatus(repo);
|
|
1096
|
+
const advisory = stagingBindingAdvisory(binding, repo);
|
|
1097
|
+
return advisory ? `note: ${advisory}` : undefined;
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
return undefined; // any failure (network, non-200, older server) — stay silent
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1035
1103
|
/**
|
|
1036
1104
|
* `attach --promote` with zero file arguments: resolve the PR target (same
|
|
1037
1105
|
* resolution as the default `runAttach` path), promote this workspace's
|
|
@@ -1079,6 +1147,263 @@ async function runAttachPromoteOnly(ctx, parsed, run) {
|
|
|
1079
1147
|
}
|
|
1080
1148
|
return 0;
|
|
1081
1149
|
}
|
|
1150
|
+
/** Bounds the best-effort `gh pr view` lookup the put nudge (issue #393) makes
|
|
1151
|
+
* on top of the normal put flow — long enough for a real gh call, short
|
|
1152
|
+
* enough to never be felt as a hang. */
|
|
1153
|
+
const PUT_NUDGE_GH_TIMEOUT_MS = 3000;
|
|
1154
|
+
/**
|
|
1155
|
+
* The bare-put nudge's wording (issue #393): teaches `--pr`/`attach --branch`
|
|
1156
|
+
* as an upgrade from a targetless `put`. `pr` present → names the PR;
|
|
1157
|
+
* otherwise a generic variant that still points at `--pr <num>`. Used
|
|
1158
|
+
* verbatim for both the human-mode stderr line and the JSON `hint` field.
|
|
1159
|
+
*/
|
|
1160
|
+
function putNudgeText(branch, pr) {
|
|
1161
|
+
const prClause = pr !== undefined ? ` (PR #${pr} open) — rerun with --pr ${pr}` : ` — rerun with --pr <num>`;
|
|
1162
|
+
return (`note: on branch ${branch}${prClause} for a stable key plus a managed comment ` +
|
|
1163
|
+
`that collects this PR's media, or stage pre-PR files with: uploads attach <file> --branch`);
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Best-effort bare-put nudge (issue #393): fires only when `put` has no
|
|
1167
|
+
* targeting flag at all (`--pr`/`--issue`/`--key`; `--branch` too, though
|
|
1168
|
+
* `put` doesn't currently accept it — defensive parity with `attach`), is
|
|
1169
|
+
* inside a git repo (reusing `deriveRepoFromGit`, the same detection the
|
|
1170
|
+
* default screenshot key's repo segment uses), and the current branch isn't
|
|
1171
|
+
* the default one. Never throws — any failure (not a repo, detached HEAD,
|
|
1172
|
+
* `gh` missing/unauthenticated/timed out) degrades to "no nudge" or, once a
|
|
1173
|
+
* branch is already known, to the generic no-PR wording. Must never affect
|
|
1174
|
+
* put's exit code, stdout, or upload behavior.
|
|
1175
|
+
*/
|
|
1176
|
+
function resolvePutNudge(opts) {
|
|
1177
|
+
const { ctx, flags, ghTarget, keyHint, noGit, defaults, run } = opts;
|
|
1178
|
+
if (ctx.quiet)
|
|
1179
|
+
return undefined;
|
|
1180
|
+
if (defaults.noNudge)
|
|
1181
|
+
return undefined;
|
|
1182
|
+
if (ghTarget || keyHint || noGit)
|
|
1183
|
+
return undefined;
|
|
1184
|
+
if (flags.has("--branch"))
|
|
1185
|
+
return undefined; // not a real put flag today; defensive only
|
|
1186
|
+
try {
|
|
1187
|
+
if (deriveRepoFromGit(run) === undefined)
|
|
1188
|
+
return undefined; // not a (usable) git repo
|
|
1189
|
+
let branch;
|
|
1190
|
+
try {
|
|
1191
|
+
branch = resolveCurrentBranch(run);
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
return undefined; // detached HEAD, or git unavailable
|
|
1195
|
+
}
|
|
1196
|
+
const defaultBranch = resolveDefaultBranch(run);
|
|
1197
|
+
const onDefaultBranch = defaultBranch
|
|
1198
|
+
? branch === defaultBranch
|
|
1199
|
+
: branch === "main" || branch === "master"; // undetermined: err toward not nudging
|
|
1200
|
+
if (onDefaultBranch)
|
|
1201
|
+
return undefined;
|
|
1202
|
+
let pr;
|
|
1203
|
+
try {
|
|
1204
|
+
// Only swap in the bounded runner for the real subprocess path — an
|
|
1205
|
+
// injected `run` (tests, or a future caller) is trusted to already be
|
|
1206
|
+
// fast/fake, and execFileSync's `timeout` option is meaningless
|
|
1207
|
+
// against anything that isn't actually shelling out.
|
|
1208
|
+
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1209
|
+
const repoArg = flagString(flags, "--repo") ?? defaults.repo;
|
|
1210
|
+
const repo = resolveRepo(repoArg, timed);
|
|
1211
|
+
pr = resolveCurrentPullRequest(repo, timed).num;
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
pr = undefined; // gh missing/unauthenticated/timed out/no open PR — generic wording
|
|
1215
|
+
}
|
|
1216
|
+
return putNudgeText(branch, pr);
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
return undefined;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Bare-put branch-staging trigger (issue #403): put on a non-default git
|
|
1224
|
+
* branch stages to the branch prefix by default — the branch becomes the
|
|
1225
|
+
* organizing unit instead of the date, superseding the #393 CLI nudge for
|
|
1226
|
+
* this case (see `putStagingNoteText`). Reuses the same detection stack as
|
|
1227
|
+
* `resolvePutNudge` (`deriveRepoFromGit` / `resolveCurrentBranch` /
|
|
1228
|
+
* `resolveDefaultBranch` / main-master fallback) plus a `resolveRepo` lookup
|
|
1229
|
+
* (needed for the "owner/name" staging key) and an explicit-flag opt-out:
|
|
1230
|
+
* `ghTarget`/`keyHint`/`refArg`/`prefixArg`/`destinationArg` set, or `noGit`,
|
|
1231
|
+
* forces the classic dated (or typed-destination) layout. Never throws — any
|
|
1232
|
+
* failure (not a repo, detached HEAD, gh missing/unauthenticated/timed out,
|
|
1233
|
+
* unresolvable repo) degrades to "no staging", leaving the caller to fall
|
|
1234
|
+
* back to the dated path.
|
|
1235
|
+
*
|
|
1236
|
+
* Plain-params (not CLI `flags`) so both `runPut` and the local stdio MCP
|
|
1237
|
+
* `put` tool — same staging default, issue #403's scope — can call this
|
|
1238
|
+
* without either depending on the other's argument shape.
|
|
1239
|
+
*/
|
|
1240
|
+
export function resolvePutStagingTarget(opts) {
|
|
1241
|
+
const { ghTarget, keyHint, refArg, prefixArg, destinationArg, noGit, repoArg, run } = opts;
|
|
1242
|
+
if (ghTarget || keyHint || noGit)
|
|
1243
|
+
return undefined;
|
|
1244
|
+
if (refArg || prefixArg || destinationArg)
|
|
1245
|
+
return undefined;
|
|
1246
|
+
try {
|
|
1247
|
+
if (deriveRepoFromGit(run) === undefined)
|
|
1248
|
+
return undefined; // not a (usable) git repo
|
|
1249
|
+
let branch;
|
|
1250
|
+
try {
|
|
1251
|
+
branch = resolveCurrentBranch(run);
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
return undefined; // detached HEAD, or git unavailable
|
|
1255
|
+
}
|
|
1256
|
+
const defaultBranch = resolveDefaultBranch(run);
|
|
1257
|
+
const onDefaultBranch = defaultBranch
|
|
1258
|
+
? branch === defaultBranch
|
|
1259
|
+
: branch === "main" || branch === "master"; // undetermined: err toward the dated layout
|
|
1260
|
+
if (onDefaultBranch)
|
|
1261
|
+
return undefined;
|
|
1262
|
+
// Same bounded-timeout treatment as the #393 nudge's `gh pr view` call —
|
|
1263
|
+
// this is best-effort, and must never be felt as a hang.
|
|
1264
|
+
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1265
|
+
const repo = resolveRepo(repoArg, timed);
|
|
1266
|
+
return { repo, branch };
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
return undefined; // gh/git unavailable, or repo unresolvable — dated layout
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* The bare-put staging note's wording (issue #403): replaces the #393 nudge
|
|
1274
|
+
* for the (now default) case where a bare put on a non-default branch stages
|
|
1275
|
+
* to the branch prefix instead of landing on the dated layout. Used verbatim
|
|
1276
|
+
* for both the human-mode stderr line and the JSON `hint` field.
|
|
1277
|
+
*/
|
|
1278
|
+
export function putStagingNoteText(branch) {
|
|
1279
|
+
return (`note: staged for branch ${branch} — auto-attaches to this branch's PR when it opens ` +
|
|
1280
|
+
`(or run: uploads attach --promote once it exists). Use --ref/--prefix for a plain dated upload.`);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Binding lookup for `uploads staged`, folded into a renderable `StagedBinding`.
|
|
1284
|
+
* `"none"`/`"other"` reuse `stagingBindingAdvisory` — the exact #398 wording,
|
|
1285
|
+
* one source of truth. `"self"` gets its own message (the #398 warning stays
|
|
1286
|
+
* silent on "self"; this view is the one place that names the happy path
|
|
1287
|
+
* explicitly). Any failure (network, non-200, older server without the
|
|
1288
|
+
* route) degrades to `"unknown"` rather than throwing — this is a read-only
|
|
1289
|
+
* view and a binding check failing must never make it fail outright.
|
|
1290
|
+
*/
|
|
1291
|
+
async function resolveStagedBinding(client, repo) {
|
|
1292
|
+
try {
|
|
1293
|
+
const { binding } = await client.githubRepoLinkStatus(repo);
|
|
1294
|
+
switch (binding) {
|
|
1295
|
+
case "self":
|
|
1296
|
+
return {
|
|
1297
|
+
state: "self",
|
|
1298
|
+
autoAttach: true,
|
|
1299
|
+
message: "these auto-attach when this branch's PR opens",
|
|
1300
|
+
};
|
|
1301
|
+
case "none":
|
|
1302
|
+
case "other":
|
|
1303
|
+
return {
|
|
1304
|
+
state: binding,
|
|
1305
|
+
autoAttach: false,
|
|
1306
|
+
// stagingBindingAdvisory is total for "none"/"other" — never undefined here.
|
|
1307
|
+
message: stagingBindingAdvisory(binding, repo),
|
|
1308
|
+
};
|
|
1309
|
+
default:
|
|
1310
|
+
return { state: "unknown", autoAttach: false, message: "binding status unrecognized" };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
catch {
|
|
1314
|
+
return {
|
|
1315
|
+
state: "unknown",
|
|
1316
|
+
autoAttach: false,
|
|
1317
|
+
message: "could not check binding status (offline, or an older server without this route)",
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Shared core for `uploads staged` (CLI) and the `staged` MCP tool (issue
|
|
1323
|
+
* #405): one `list` call against the branch staging prefix
|
|
1324
|
+
* (`ghBranchKeyPrefix` — never hand-built) plus the #398 binding check. Never
|
|
1325
|
+
* throws on the binding check (see `resolveStagedBinding`); a failed `list`
|
|
1326
|
+
* call still propagates, same as every other read command.
|
|
1327
|
+
*/
|
|
1328
|
+
export async function resolveStaged(opts) {
|
|
1329
|
+
const { client, repo, branch } = opts;
|
|
1330
|
+
const prefix = ghBranchKeyPrefix(repo, branch);
|
|
1331
|
+
const [list, binding] = await Promise.all([
|
|
1332
|
+
client.list({ prefix, metadata: true }),
|
|
1333
|
+
resolveStagedBinding(client, repo),
|
|
1334
|
+
]);
|
|
1335
|
+
const files = list.items.map((item) => ({
|
|
1336
|
+
key: item.key,
|
|
1337
|
+
filename: item.key.slice(prefix.length),
|
|
1338
|
+
size: item.size,
|
|
1339
|
+
stagedAt: item.metadata?.["gh.staged-at"],
|
|
1340
|
+
url: item.url,
|
|
1341
|
+
}));
|
|
1342
|
+
return { repo, branch, files, binding };
|
|
1343
|
+
}
|
|
1344
|
+
const STAGED_HELP = `uploads staged [--branch <name>] [--repo <owner/name>] [--format json] [--workspace <name>]
|
|
1345
|
+
|
|
1346
|
+
Read-only view of what's staged for a branch (\`attach --branch\` / bare
|
|
1347
|
+
\`put\` on a non-default branch, issue #403) and whether it will auto-attach
|
|
1348
|
+
once a PR opens. One \`list\` call against the branch staging prefix
|
|
1349
|
+
(gh/<owner>/<repo>/branch/<branch>/) plus a binding check — files:read only,
|
|
1350
|
+
no new server surface.
|
|
1351
|
+
|
|
1352
|
+
Defaults: current git branch (same resolution as \`attach --branch\`, worktree-
|
|
1353
|
+
safe), repo from --repo / gh / git remote (same as every other command).
|
|
1354
|
+
|
|
1355
|
+
Binding: self means these files auto-attach when this branch's PR opens; none
|
|
1356
|
+
or other means they won't (repo unlinked, or linked to a different
|
|
1357
|
+
workspace) — same advisory as the attach --branch stage-time warning.
|
|
1358
|
+
|
|
1359
|
+
Examples:
|
|
1360
|
+
uploads staged
|
|
1361
|
+
uploads staged --branch feature/thing --repo owner/name
|
|
1362
|
+
uploads staged --format json
|
|
1363
|
+
`;
|
|
1364
|
+
export async function runStaged(ctx, args, help = false, run = execRunner) {
|
|
1365
|
+
const parsed = parseCommandArgs(args);
|
|
1366
|
+
if (help || parsed.help) {
|
|
1367
|
+
writeCommandHelp(STAGED_HELP);
|
|
1368
|
+
return 0;
|
|
1369
|
+
}
|
|
1370
|
+
const format = ctx.json
|
|
1371
|
+
? "json"
|
|
1372
|
+
: (() => {
|
|
1373
|
+
const raw = flagString(parsed.flags, "--format");
|
|
1374
|
+
if (!raw || raw === "human")
|
|
1375
|
+
return "human";
|
|
1376
|
+
if (raw === "json")
|
|
1377
|
+
return "json";
|
|
1378
|
+
throw new UsageError(`invalid --format: ${raw} (expected: json)`);
|
|
1379
|
+
})();
|
|
1380
|
+
const repo = resolveRepo(flagString(parsed.flags, "--repo"), run);
|
|
1381
|
+
const branch = flagString(parsed.flags, "--branch") ?? resolveCurrentBranch(run);
|
|
1382
|
+
const result = await resolveStaged({ client: ctx.client, repo, branch });
|
|
1383
|
+
if (format === "json") {
|
|
1384
|
+
// Always a valid JSON document, even with zero files — never empty
|
|
1385
|
+
// stdout (issue #405 explicitly calls out find --format json's empty-
|
|
1386
|
+
// stdout-on-no-matches wart as a wrong pattern to avoid here).
|
|
1387
|
+
await writeJson(result);
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
if (result.files.length === 0) {
|
|
1391
|
+
await writeStdout(`nothing staged for ${branch} in ${repo}\n`);
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
for (const file of result.files) {
|
|
1395
|
+
const size = file.size !== undefined ? formatByteSize(file.size) : "? B";
|
|
1396
|
+
const staged = file.stagedAt ? ` staged ${file.stagedAt}` : "";
|
|
1397
|
+
await writeStdout(`${file.filename} ${size}${staged} ${file.url ?? "(no url)"}\n`);
|
|
1398
|
+
}
|
|
1399
|
+
process.stderr.write(`binding: ${result.binding.state} — ${result.binding.message}\n`);
|
|
1400
|
+
// Promote is pointless advice when the repo belongs to another workspace —
|
|
1401
|
+
// the cross-tenant gate (#297) would reject it from here.
|
|
1402
|
+
if (result.binding.state !== "other") {
|
|
1403
|
+
process.stderr.write(`once the PR exists: uploads attach --promote\n`);
|
|
1404
|
+
}
|
|
1405
|
+
return 0;
|
|
1406
|
+
}
|
|
1082
1407
|
export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
1083
1408
|
if (help) {
|
|
1084
1409
|
writeCommandHelp(PUT_HELP);
|
|
@@ -1197,7 +1522,25 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1197
1522
|
})()
|
|
1198
1523
|
: defaults.width;
|
|
1199
1524
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
1200
|
-
//
|
|
1525
|
+
// Bare-put branch staging (issue #403): a bare put (no --pr/--issue/--key/
|
|
1526
|
+
// --ref/--prefix/--destination, not --no-git) on a non-default git branch
|
|
1527
|
+
// stages to the branch prefix — identical key/metadata to `attach
|
|
1528
|
+
// --branch` — instead of the dated layout. Computed before gh.* metadata
|
|
1529
|
+
// resolution below since it takes over that resolution entirely (branch
|
|
1530
|
+
// metadata, not PR/issue metadata) and supersedes the #393 nudge for this
|
|
1531
|
+
// case.
|
|
1532
|
+
const stagingTarget = resolvePutStagingTarget({
|
|
1533
|
+
ghTarget,
|
|
1534
|
+
keyHint,
|
|
1535
|
+
refArg: flagString(parsed.flags, "--ref"),
|
|
1536
|
+
prefixArg: prefixFlag,
|
|
1537
|
+
destinationArg: destFlag,
|
|
1538
|
+
noGit,
|
|
1539
|
+
repoArg: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1540
|
+
run,
|
|
1541
|
+
});
|
|
1542
|
+
// gh.* metadata: explicit --pr/--issue target wins over --meta; staging
|
|
1543
|
+
// wins over --meta the same way (matches attach --branch); otherwise
|
|
1201
1544
|
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
1202
1545
|
// --no-auto, or UPLOADS_NO_AUTO_META disable auto; --auto forces past the
|
|
1203
1546
|
// config default but never past --no-git (no repo to resolve).
|
|
@@ -1209,6 +1552,14 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1209
1552
|
metadata = merged;
|
|
1210
1553
|
attachedRef = merged["gh.ref"];
|
|
1211
1554
|
}
|
|
1555
|
+
else if (stagingTarget) {
|
|
1556
|
+
const merged = {
|
|
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;
|
|
1562
|
+
}
|
|
1212
1563
|
else {
|
|
1213
1564
|
// gh.* additionally needs git, which the shared derived gate ignores.
|
|
1214
1565
|
if (!noGit && derivedMetaEnabled(parsed.flags, defaults)) {
|
|
@@ -1229,6 +1580,31 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1229
1580
|
}
|
|
1230
1581
|
}
|
|
1231
1582
|
}
|
|
1583
|
+
// Bare-put nudge (issue #393): only relevant when staging didn't take over
|
|
1584
|
+
// — once `stagingTarget` resolves, staging IS the upgrade the nudge used to
|
|
1585
|
+
// point at, so this is skipped entirely rather than firing redundantly.
|
|
1586
|
+
// Still fires as before for a bare put that lands on the dated layout with
|
|
1587
|
+
// a detectable PR (e.g. an explicit --ref/--prefix opts out of staging).
|
|
1588
|
+
// Computed once, used for both the trailing stderr line (human mode) and
|
|
1589
|
+
// the JSON `hint` field below. Best-effort — see resolvePutNudge; never
|
|
1590
|
+
// affects exit code, stdout, or the upload.
|
|
1591
|
+
const nudge = stagingTarget
|
|
1592
|
+
? undefined
|
|
1593
|
+
: resolvePutNudge({
|
|
1594
|
+
ctx,
|
|
1595
|
+
flags: parsed.flags,
|
|
1596
|
+
ghTarget,
|
|
1597
|
+
keyHint,
|
|
1598
|
+
noGit,
|
|
1599
|
+
defaults,
|
|
1600
|
+
run,
|
|
1601
|
+
});
|
|
1602
|
+
// Staging note (issue #403): same suppression as the #393 nudge
|
|
1603
|
+
// (--quiet, UPLOADS_NO_NUDGE=1 env/config); staging itself is NOT gated by
|
|
1604
|
+
// either — only whether the note is printed/hinted.
|
|
1605
|
+
const stagingNote = stagingTarget && !ctx.quiet && !defaults.noNudge
|
|
1606
|
+
? putStagingNoteText(stagingTarget.branch)
|
|
1607
|
+
: undefined;
|
|
1232
1608
|
const logHuman = !ctx.quiet && format === "human";
|
|
1233
1609
|
if (logHuman) {
|
|
1234
1610
|
if (multi) {
|
|
@@ -1247,6 +1623,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1247
1623
|
nameOverride: nameFlag,
|
|
1248
1624
|
explicitKey: keyHint,
|
|
1249
1625
|
ghTarget,
|
|
1626
|
+
ghBranchTarget: stagingTarget,
|
|
1250
1627
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
1251
1628
|
repo: flagString(parsed.flags, "--repo") ?? defaults.repo,
|
|
1252
1629
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
@@ -1265,6 +1642,19 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1265
1642
|
if (uploads.length === 0 && failures.length > 0 && !multi) {
|
|
1266
1643
|
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
1267
1644
|
}
|
|
1645
|
+
// Stage-time binding warning (issue #398/#400): same check `attach
|
|
1646
|
+
// --branch` runs, now also on the bare-put staging path. Best-effort — see
|
|
1647
|
+
// resolveStageBindingWarning; never affects exit code or the upload.
|
|
1648
|
+
const bindingWarning = stagingTarget && uploads.length > 0
|
|
1649
|
+
? await resolveStageBindingWarning({ ctx, defaults, repo: stagingTarget.repo })
|
|
1650
|
+
: undefined;
|
|
1651
|
+
// One JSON `hint` slot, shared with the #393 nudge (mutually exclusive with
|
|
1652
|
+
// it — nudge is undefined whenever staging took over). When staging fires,
|
|
1653
|
+
// prefer the more actionable binding warning over the generic staging note
|
|
1654
|
+
// (mirrors attach --branch, whose only JSON hint content IS the binding
|
|
1655
|
+
// warning); stderr prints the nudge/staging-note and binding-warning lines
|
|
1656
|
+
// independently, below.
|
|
1657
|
+
const jsonHint = nudge ?? bindingWarning ?? stagingNote;
|
|
1268
1658
|
const galleriesByKey = new Map();
|
|
1269
1659
|
let galleryHadError = false;
|
|
1270
1660
|
if (galleryId && uploads.length > 0) {
|
|
@@ -1310,6 +1700,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1310
1700
|
failures,
|
|
1311
1701
|
comment,
|
|
1312
1702
|
commentError,
|
|
1703
|
+
...(jsonHint ? { hint: jsonHint } : {}),
|
|
1313
1704
|
});
|
|
1314
1705
|
}
|
|
1315
1706
|
else {
|
|
@@ -1340,6 +1731,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1340
1731
|
for (const failure of failures) {
|
|
1341
1732
|
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
1342
1733
|
}
|
|
1734
|
+
if (nudge)
|
|
1735
|
+
process.stderr.write(`${nudge}\n`);
|
|
1736
|
+
if (stagingNote)
|
|
1737
|
+
process.stderr.write(`${stagingNote}\n`);
|
|
1738
|
+
if (bindingWarning)
|
|
1739
|
+
process.stderr.write(`${bindingWarning}\n`);
|
|
1343
1740
|
}
|
|
1344
1741
|
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
1345
1742
|
}
|
|
@@ -1371,6 +1768,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1371
1768
|
frame: result.frame,
|
|
1372
1769
|
gallery,
|
|
1373
1770
|
...(dryRun ? { dryRun: true } : {}),
|
|
1771
|
+
...(jsonHint ? { hint: jsonHint } : {}),
|
|
1374
1772
|
});
|
|
1375
1773
|
break;
|
|
1376
1774
|
case "url":
|
|
@@ -1390,6 +1788,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1390
1788
|
if (gallery?.error) {
|
|
1391
1789
|
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
1392
1790
|
}
|
|
1791
|
+
if (nudge && format !== "json")
|
|
1792
|
+
process.stderr.write(`${nudge}\n`);
|
|
1793
|
+
if (stagingNote && format !== "json")
|
|
1794
|
+
process.stderr.write(`${stagingNote}\n`);
|
|
1795
|
+
if (bindingWarning && format !== "json")
|
|
1796
|
+
process.stderr.write(`${bindingWarning}\n`);
|
|
1393
1797
|
return gallery?.error ? 1 : 0;
|
|
1394
1798
|
}
|
|
1395
1799
|
// --- galleries ---
|
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"];
|
|
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"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
|
@@ -14,6 +14,8 @@ export interface PutDefaults {
|
|
|
14
14
|
keepExif?: boolean;
|
|
15
15
|
/** When true, `put` does NOT auto-resolve/stamp gh.* on the default path. */
|
|
16
16
|
noAutoMeta?: boolean;
|
|
17
|
+
/** When true, `put` never prints the bare-put --pr/attach nudge (issue #393). */
|
|
18
|
+
noNudge?: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
|
|
19
21
|
export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
|
package/dist/config-file.js
CHANGED
|
@@ -14,6 +14,7 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
14
14
|
"UPLOADS_KEEP_EXIF",
|
|
15
15
|
"UPLOADS_NO_AUTO_META",
|
|
16
16
|
"UPLOADS_SCREENSHOT_VIA",
|
|
17
|
+
"UPLOADS_NO_NUDGE",
|
|
17
18
|
];
|
|
18
19
|
const PUT_DEFAULT_KEY_MAP = {
|
|
19
20
|
prefix: "UPLOADS_DEFAULT_PREFIX",
|
|
@@ -24,6 +25,7 @@ const PUT_DEFAULT_KEY_MAP = {
|
|
|
24
25
|
noOptimize: "UPLOADS_NO_OPTIMIZE",
|
|
25
26
|
keepExif: "UPLOADS_KEEP_EXIF",
|
|
26
27
|
noAutoMeta: "UPLOADS_NO_AUTO_META",
|
|
28
|
+
noNudge: "UPLOADS_NO_NUDGE",
|
|
27
29
|
};
|
|
28
30
|
function isTruthyConfigFlag(value) {
|
|
29
31
|
if (!value)
|
|
@@ -49,6 +51,8 @@ export function putDefaultsToConfigValues(defaults) {
|
|
|
49
51
|
out.UPLOADS_KEEP_EXIF = "1";
|
|
50
52
|
if (defaults.noAutoMeta)
|
|
51
53
|
out.UPLOADS_NO_AUTO_META = "1";
|
|
54
|
+
if (defaults.noNudge)
|
|
55
|
+
out.UPLOADS_NO_NUDGE = "1";
|
|
52
56
|
return out;
|
|
53
57
|
}
|
|
54
58
|
function parsePutDefaultsFromRaw(raw) {
|
|
@@ -72,6 +76,8 @@ function parsePutDefaultsFromRaw(raw) {
|
|
|
72
76
|
out.keepExif = true;
|
|
73
77
|
if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_META))
|
|
74
78
|
out.noAutoMeta = true;
|
|
79
|
+
if (isTruthyConfigFlag(raw.UPLOADS_NO_NUDGE))
|
|
80
|
+
out.noNudge = true;
|
|
75
81
|
return out;
|
|
76
82
|
}
|
|
77
83
|
function parsePutDefaultsFromEnv() {
|
|
@@ -92,6 +98,8 @@ function parsePutDefaultsFromEnv() {
|
|
|
92
98
|
raw.UPLOADS_KEEP_EXIF = process.env.UPLOADS_KEEP_EXIF;
|
|
93
99
|
if (process.env.UPLOADS_NO_AUTO_META)
|
|
94
100
|
raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
|
|
101
|
+
if (process.env.UPLOADS_NO_NUDGE)
|
|
102
|
+
raw.UPLOADS_NO_NUDGE = process.env.UPLOADS_NO_NUDGE;
|
|
95
103
|
return parsePutDefaultsFromRaw(raw);
|
|
96
104
|
}
|
|
97
105
|
/** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
|
|
@@ -163,6 +171,8 @@ export function mergePutDefaults(...layers) {
|
|
|
163
171
|
out.keepExif = layer.keepExif;
|
|
164
172
|
if (layer.noAutoMeta != null)
|
|
165
173
|
out.noAutoMeta = layer.noAutoMeta;
|
|
174
|
+
if (layer.noNudge != null)
|
|
175
|
+
out.noNudge = layer.noNudge;
|
|
166
176
|
}
|
|
167
177
|
return out;
|
|
168
178
|
}
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { type GhTarget } from "./github.js";
|
|
|
2
2
|
/** Runs a command and returns stdout; throws on non-zero exit. Injectable for tests. */
|
|
3
3
|
export type CommandRunner = (cmd: string, args: string[], input?: string) => string;
|
|
4
4
|
export declare const execRunner: CommandRunner;
|
|
5
|
+
/**
|
|
6
|
+
* A `CommandRunner` bounded by `timeoutMs` (node's native `execFileSync`
|
|
7
|
+
* `timeout` option). There is no other subprocess-timeout wrapper in this
|
|
8
|
+
* codebase to reuse, so this is the minimal one: for a best-effort lookup
|
|
9
|
+
* that must never block its caller for long (e.g. the bare-`put` nudge's `gh
|
|
10
|
+
* pr view` check, issue #393), pass this instead of the default `execRunner`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const timedExecRunner: (timeoutMs: number) => CommandRunner;
|
|
5
13
|
/**
|
|
6
14
|
* Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
|
|
7
15
|
* (fork-aware) → parse the origin remote → UsageError.
|
|
@@ -11,6 +19,14 @@ export declare function resolveRepo(explicit: string | undefined, run?: CommandR
|
|
|
11
19
|
export declare function resolveCurrentPullRequest(repo: string, run?: CommandRunner): GhTarget;
|
|
12
20
|
/** Resolve the current git branch (`--branch` with no value). Throws UsageError on detached HEAD or outside a git repo. */
|
|
13
21
|
export declare function resolveCurrentBranch(run?: CommandRunner): string;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
24
|
+
* network call — just reads the ref git already cached from the last
|
|
25
|
+
* fetch/clone). Returns undefined when it can't be determined (no origin,
|
|
26
|
+
* `origin/HEAD` never set, not a git repo) — callers should treat that as
|
|
27
|
+
* "unknown", not "no default branch exists".
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveDefaultBranch(run?: CommandRunner): string | undefined;
|
|
14
30
|
/**
|
|
15
31
|
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
16
32
|
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
package/dist/github-gh.js
CHANGED
|
@@ -3,6 +3,19 @@ import { UsageError } from "./cli-args.js";
|
|
|
3
3
|
import { ATTACHMENTS_MARKER, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
4
4
|
import { META_VALUE_MAX, isMetaValueSafe } from "./metadata.js";
|
|
5
5
|
export const execRunner = (cmd, args, input) => execFileSync(cmd, args, { encoding: "utf8", input, stdio: ["pipe", "pipe", "pipe"] });
|
|
6
|
+
/**
|
|
7
|
+
* A `CommandRunner` bounded by `timeoutMs` (node's native `execFileSync`
|
|
8
|
+
* `timeout` option). There is no other subprocess-timeout wrapper in this
|
|
9
|
+
* codebase to reuse, so this is the minimal one: for a best-effort lookup
|
|
10
|
+
* that must never block its caller for long (e.g. the bare-`put` nudge's `gh
|
|
11
|
+
* pr view` check, issue #393), pass this instead of the default `execRunner`.
|
|
12
|
+
*/
|
|
13
|
+
export const timedExecRunner = (timeoutMs) => (cmd, args, input) => execFileSync(cmd, args, {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
input,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
});
|
|
6
19
|
/**
|
|
7
20
|
* Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
|
|
8
21
|
* (fork-aware) → parse the origin remote → UsageError.
|
|
@@ -82,6 +95,26 @@ export function resolveCurrentBranch(run = execRunner) {
|
|
|
82
95
|
}
|
|
83
96
|
return branch;
|
|
84
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
100
|
+
* network call — just reads the ref git already cached from the last
|
|
101
|
+
* fetch/clone). Returns undefined when it can't be determined (no origin,
|
|
102
|
+
* `origin/HEAD` never set, not a git repo) — callers should treat that as
|
|
103
|
+
* "unknown", not "no default branch exists".
|
|
104
|
+
*/
|
|
105
|
+
export function resolveDefaultBranch(run = execRunner) {
|
|
106
|
+
try {
|
|
107
|
+
const out = run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).trim();
|
|
108
|
+
if (!out)
|
|
109
|
+
return undefined;
|
|
110
|
+
const slash = out.indexOf("/");
|
|
111
|
+
const branch = slash === -1 ? out : out.slice(slash + 1);
|
|
112
|
+
return branch || undefined;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
85
118
|
/**
|
|
86
119
|
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
87
120
|
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
package/dist/github.d.ts
CHANGED
|
@@ -86,6 +86,18 @@ export interface AttachmentItem {
|
|
|
86
86
|
path?: string;
|
|
87
87
|
state?: string;
|
|
88
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Poster frame for a video (issue #299), server-computed like `embedUrl` —
|
|
91
|
+
* never taken from client-settable metadata. Absent means "no poster", and
|
|
92
|
+
* the renderer falls back to the bullet link.
|
|
93
|
+
*/
|
|
94
|
+
posterUrl?: string | null;
|
|
95
|
+
/** Derived video facts used for the caption and display width. */
|
|
96
|
+
videoMeta?: {
|
|
97
|
+
durationSeconds?: number;
|
|
98
|
+
width?: number;
|
|
99
|
+
height?: number;
|
|
100
|
+
};
|
|
89
101
|
}
|
|
90
102
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
91
103
|
export interface GalleryCommentItem {
|
package/dist/github.js
CHANGED
|
@@ -171,6 +171,34 @@ export function attachmentImageWidth(filename) {
|
|
|
171
171
|
}
|
|
172
172
|
return ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
173
173
|
}
|
|
174
|
+
/** `m:ss` under an hour, `h:mm:ss` at or above one. */
|
|
175
|
+
function formatDuration(seconds) {
|
|
176
|
+
const total = Math.floor(seconds);
|
|
177
|
+
const s = total % 60;
|
|
178
|
+
const m = Math.floor(total / 60) % 60;
|
|
179
|
+
const h = Math.floor(total / 3600);
|
|
180
|
+
const ss = String(s).padStart(2, "0");
|
|
181
|
+
if (h === 0)
|
|
182
|
+
return `${m}:${ss}`;
|
|
183
|
+
return `${h}:${String(m).padStart(2, "0")}:${ss}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Display width for a video poster. Real dimensions only *select* among the
|
|
187
|
+
* width constants — a raw 1920 would blow out the comment column — and the
|
|
188
|
+
* result is capped at the real width so a small clip is never upscaled.
|
|
189
|
+
*/
|
|
190
|
+
function posterImageWidth(videoMeta, filename) {
|
|
191
|
+
const w = videoMeta?.width ?? 0;
|
|
192
|
+
const h = videoMeta?.height ?? 0;
|
|
193
|
+
if (w <= 0 || h <= 0)
|
|
194
|
+
return attachmentImageWidth(filename);
|
|
195
|
+
const chosen = h > w
|
|
196
|
+
? ATTACHMENT_IMAGE_WIDTH_PORTRAIT
|
|
197
|
+
: w / h >= 16 / 9
|
|
198
|
+
? ATTACHMENT_IMAGE_WIDTH_WIDE
|
|
199
|
+
: ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
200
|
+
return Math.min(chosen, w);
|
|
201
|
+
}
|
|
174
202
|
function escapeHtmlAttr(s) {
|
|
175
203
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
176
204
|
}
|
|
@@ -259,13 +287,29 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
259
287
|
const src = item.embedUrl ?? item.url;
|
|
260
288
|
const link = item.pageUrl ?? stable; // click-through: file page when known, else raw
|
|
261
289
|
const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
|
|
262
|
-
|
|
290
|
+
const isPosterVideo = Boolean(item.posterUrl) && inferContentType(name).startsWith("video/");
|
|
291
|
+
const inlines = isImage || isPosterVideo;
|
|
292
|
+
if (inlines && inlinedImages >= MAX_INLINE_ATTACHMENT_IMAGES) {
|
|
263
293
|
// Cap hit — defer to the collapsed overflow list below rather than
|
|
264
294
|
// embedding every remaining image inline.
|
|
265
295
|
overflowImages.push(item);
|
|
266
296
|
continue;
|
|
267
297
|
}
|
|
268
|
-
if (
|
|
298
|
+
if (isPosterVideo) {
|
|
299
|
+
inlinedImages++;
|
|
300
|
+
const w = posterImageWidth(item.videoMeta, name);
|
|
301
|
+
const href = escapeHtmlAttr(link ?? item.posterUrl);
|
|
302
|
+
lines.push(`<a href="${href}"><img width="${w}" alt="${escapeHtmlAttr(name)}" src="${escapeHtmlAttr(item.posterUrl)}"></a>`);
|
|
303
|
+
// GitHub strips <video>, so a still frame needs an explicit affordance
|
|
304
|
+
// or it reads as a screenshot.
|
|
305
|
+
const parts = ["▶ Play video"];
|
|
306
|
+
if (item.videoMeta?.durationSeconds != null) {
|
|
307
|
+
parts.push(formatDuration(item.videoMeta.durationSeconds));
|
|
308
|
+
}
|
|
309
|
+
parts.push(...metaCaptionParts(item.meta).map(escapeHtmlText));
|
|
310
|
+
lines.push(`<sub>${parts.join(" · ")}</sub>`, "");
|
|
311
|
+
}
|
|
312
|
+
else if (isImage) {
|
|
269
313
|
inlinedImages++;
|
|
270
314
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
271
315
|
// img src uses embed host when available (Camo revalidates); click-through prefers the file page.
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export declare function sanitizeKeySegment(s: string): string;
|
|
2
2
|
export declare function sha256Short(bytes: Uint8Array): Promise<string>;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `run` is optional and structurally matches `CommandRunner` (github-gh.ts)
|
|
5
|
+
* without importing it — callers that already have an injected runner (e.g.
|
|
6
|
+
* the put nudge, issue #393) can pass it through for testability; the
|
|
7
|
+
* default (no `run`) preserves the original direct-`execSync` behavior for
|
|
8
|
+
* every existing caller.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveRepoFromGit(run?: (cmd: string, args: string[], input?: string) => string): string | undefined;
|
|
4
11
|
export declare function buildScreenshotKey(opts: {
|
|
5
12
|
filename: string;
|
|
6
13
|
fileBytes: Uint8Array;
|
package/dist/keys.js
CHANGED
|
@@ -9,9 +9,18 @@ export async function sha256Short(bytes) {
|
|
|
9
9
|
.join("")
|
|
10
10
|
.slice(0, 6);
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* `run` is optional and structurally matches `CommandRunner` (github-gh.ts)
|
|
14
|
+
* without importing it — callers that already have an injected runner (e.g.
|
|
15
|
+
* the put nudge, issue #393) can pass it through for testability; the
|
|
16
|
+
* default (no `run`) preserves the original direct-`execSync` behavior for
|
|
17
|
+
* every existing caller.
|
|
18
|
+
*/
|
|
19
|
+
export function deriveRepoFromGit(run) {
|
|
13
20
|
try {
|
|
14
|
-
const url =
|
|
21
|
+
const url = run
|
|
22
|
+
? run("git", ["config", "--get", "remote.origin.url"]).trim()
|
|
23
|
+
: execSync("git config --get remote.origin.url", { encoding: "utf8" }).trim();
|
|
15
24
|
const match = url.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
16
25
|
return match?.[1];
|
|
17
26
|
}
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createUploadsClient } from "../client.js";
|
|
2
|
-
import { buildDoctorReport, makeGhTarget, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
2
|
+
import { buildDoctorReport, makeGhTarget, 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 { ghKeyPrefix } from "../github.js";
|
|
6
|
+
import { ghKeyPrefix, ghMetadataForBranch } 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";
|
|
10
|
-
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
10
|
+
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentBranch, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
11
11
|
import { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCanonical, metadataProp, optBool, optPosInt, optString, optStringArray, optStringRecord, stateProp, usage, } from "./args.js";
|
|
12
12
|
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
13
13
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
@@ -415,9 +415,35 @@ export function createUploadsMcpTools(opts) {
|
|
|
415
415
|
const alt = optString(args, "alt");
|
|
416
416
|
const width = optPosInt(args, "width") ?? defaults.width;
|
|
417
417
|
const contentType = optString(args, "contentType");
|
|
418
|
+
// Bare-put branch staging (issue #403): local stdio MCP put mirrors
|
|
419
|
+
// the CLI default — no pr/issue/key/ref/prefix/destination, not
|
|
420
|
+
// noGit, on a non-default git branch stages to the branch prefix
|
|
421
|
+
// (identical key/metadata to `attach --branch`) instead of the
|
|
422
|
+
// dated layout. Never throws — see resolvePutStagingTarget.
|
|
423
|
+
const stagingTarget = resolvePutStagingTarget({
|
|
424
|
+
ghTarget: target,
|
|
425
|
+
keyHint: keyArg,
|
|
426
|
+
refArg,
|
|
427
|
+
prefixArg,
|
|
428
|
+
destinationArg: destArg,
|
|
429
|
+
noGit,
|
|
430
|
+
repoArg: optString(args, "repo") ?? defaults.repo,
|
|
431
|
+
run,
|
|
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;
|
|
418
443
|
const putShared = {
|
|
419
444
|
client,
|
|
420
445
|
ghTarget: target,
|
|
446
|
+
ghBranchTarget: stagingTarget,
|
|
421
447
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
422
448
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
423
449
|
ref: refArg ?? defaults.ref,
|
|
@@ -427,7 +453,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
427
453
|
replace: replaceArg,
|
|
428
454
|
optimize: optimizeOpts,
|
|
429
455
|
frame: frameOpts,
|
|
430
|
-
metadata,
|
|
456
|
+
metadata: putMetadata,
|
|
431
457
|
// The shared metadata description promises uploads.sh derives these
|
|
432
458
|
// "automatically where it can" — MCP has no --no-auto, so always on.
|
|
433
459
|
deriveImageFacts: true,
|
|
@@ -458,6 +484,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
458
484
|
frame: frameOpts,
|
|
459
485
|
optimize: optimizeOpts,
|
|
460
486
|
ghTarget: target,
|
|
487
|
+
ghBranchTarget: stagingTarget,
|
|
461
488
|
key: keyArg,
|
|
462
489
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
463
490
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
@@ -466,7 +493,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
466
493
|
deriveRepoFromGit: !noGit,
|
|
467
494
|
dryRun,
|
|
468
495
|
replace: replaceArg,
|
|
469
|
-
metadata,
|
|
496
|
+
metadata: putMetadata,
|
|
470
497
|
provenanceClient: "uploads-mcp",
|
|
471
498
|
alt: () => alt ?? sourceName,
|
|
472
499
|
width,
|
|
@@ -925,6 +952,31 @@ export function createUploadsMcpTools(opts) {
|
|
|
925
952
|
return client.list({ prefix, limit, cursor });
|
|
926
953
|
},
|
|
927
954
|
},
|
|
955
|
+
{
|
|
956
|
+
name: "staged",
|
|
957
|
+
description: "Read-only view of what's staged for a git branch (attach --branch / bare put on a non-default branch) and whether it will auto-attach once a PR opens. One list call against the branch staging prefix plus a repo-binding check (files:read only). Returns { repo, branch, files, binding }; binding.state is self/other/none/unknown and binding.autoAttach is true only for self.",
|
|
958
|
+
inputSchema: {
|
|
959
|
+
type: "object",
|
|
960
|
+
properties: {
|
|
961
|
+
branch: {
|
|
962
|
+
type: "string",
|
|
963
|
+
description: "Branch name (default: current git branch, worktree-safe).",
|
|
964
|
+
},
|
|
965
|
+
repo: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: "owner/name repo (default: gh/git remote inference).",
|
|
968
|
+
},
|
|
969
|
+
workspace: workspaceProp,
|
|
970
|
+
},
|
|
971
|
+
additionalProperties: false,
|
|
972
|
+
},
|
|
973
|
+
async handler(args) {
|
|
974
|
+
const { client } = clientFor(args);
|
|
975
|
+
const repo = resolveRepo(optString(args, "repo"), run);
|
|
976
|
+
const branch = optString(args, "branch") ?? resolveCurrentBranch(run);
|
|
977
|
+
return resolveStaged({ client, repo, branch });
|
|
978
|
+
},
|
|
979
|
+
},
|
|
928
980
|
{
|
|
929
981
|
name: "delete",
|
|
930
982
|
description: "Delete an uploaded object by key. Set dryRun to preview without deleting.",
|