@buildinternet/uploads 0.26.1 → 0.28.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 +5 -0
- package/dist/cli-brand.js +4 -1
- package/dist/cli-catalog.js +6 -0
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +4 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +7 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +1 -1
- package/dist/commands/screenshot.js +85 -12
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +125 -0
- package/dist/commands.d.ts +33 -1
- package/dist/commands.js +121 -15
- package/dist/github-gh.d.ts +23 -1
- package/dist/github-gh.js +59 -8
- package/dist/github.d.ts +6 -0
- package/dist/github.js +12 -0
- package/dist/install-source.d.ts +14 -0
- package/dist/install-source.js +42 -0
- package/dist/mcp/tools.js +42 -23
- package/dist/sidecar.d.ts +31 -0
- package/dist/sidecar.js +111 -0
- package/dist/update-check.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,11 @@ explicit deletes. Promotion (auto or `--promote`) does skip files staged
|
|
|
98
98
|
more than 30 days before the PR opens, though; they're still there, just no
|
|
99
99
|
longer auto-promoted.
|
|
100
100
|
|
|
101
|
+
**Metadata edits re-sync the comment too:** `uploads meta set` on a `gh/…`-keyed
|
|
102
|
+
object refreshes the managed comment automatically when it touches `path` or
|
|
103
|
+
`state` — best-effort, so backfilled metadata shows up without waiting on the
|
|
104
|
+
next `attach`.
|
|
105
|
+
|
|
101
106
|
**Bare `put` stages too, by default (issue #403):** on a non-default git
|
|
102
107
|
branch, a `put` with none of
|
|
103
108
|
`--pr`/`--issue`/`--key`/`--ref`/`--prefix`/`--destination` set
|
package/dist/cli-brand.js
CHANGED
|
@@ -156,7 +156,10 @@ function boxLines(lines, options = {}) {
|
|
|
156
156
|
].join("\n");
|
|
157
157
|
}
|
|
158
158
|
export function formatUpdateBanner(options) {
|
|
159
|
-
return boxLines([`Update available ${options.current} → ${options.latest}`, `
|
|
159
|
+
return boxLines([`Update available ${options.current} → ${options.latest}`, `uploads update`], {
|
|
160
|
+
color: options.color,
|
|
161
|
+
tone: BRAND.accent,
|
|
162
|
+
});
|
|
160
163
|
}
|
|
161
164
|
export function formatAuthBanner(options = {}) {
|
|
162
165
|
return boxLines(["Sign in via browser", "uploads login"], {
|
package/dist/cli-catalog.js
CHANGED
|
@@ -60,6 +60,7 @@ export const SCREENSHOT_FLAGS = [
|
|
|
60
60
|
"--light",
|
|
61
61
|
"--wait",
|
|
62
62
|
"--out",
|
|
63
|
+
"--no-sidecar",
|
|
63
64
|
"--no-upload",
|
|
64
65
|
"--destination",
|
|
65
66
|
"--prefix",
|
|
@@ -187,6 +188,11 @@ export const ROOT_COMMANDS = [
|
|
|
187
188
|
{ name: "all", summary: "Install skills and MCP (default)" },
|
|
188
189
|
],
|
|
189
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: "update",
|
|
193
|
+
summary: "Update the CLI, then refresh the agent skills + MCP registration",
|
|
194
|
+
essential: true,
|
|
195
|
+
},
|
|
190
196
|
{
|
|
191
197
|
name: "login",
|
|
192
198
|
summary: "Sign in via browser (or an enrollment code) and save credentials",
|
package/dist/cli-help.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runInvite } from "./commands/invite.js";
|
|
|
12
12
|
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
13
13
|
import { runMcp } from "./commands/mcp.js";
|
|
14
14
|
import { runInstall } from "./commands/install.js";
|
|
15
|
+
import { runUpdate } from "./commands/update.js";
|
|
15
16
|
import { runCompletion } from "./commands/completion.js";
|
|
16
17
|
import { runLogout, runWhoami } from "./commands/session.js";
|
|
17
18
|
import { runTelemetry } from "./commands/telemetry.js";
|
|
@@ -268,6 +269,9 @@ export async function runCli(argv) {
|
|
|
268
269
|
case "install":
|
|
269
270
|
code = await runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
|
|
270
271
|
break;
|
|
272
|
+
case "update":
|
|
273
|
+
code = await runUpdate(cmdArgs, { globals: parsed.globals }, showHelp);
|
|
274
|
+
break;
|
|
271
275
|
case "completion":
|
|
272
276
|
case "completions":
|
|
273
277
|
code = await runCompletion(cmdArgs, showHelp);
|
package/dist/client.d.ts
CHANGED
|
@@ -519,10 +519,18 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
519
519
|
id: string;
|
|
520
520
|
}>;
|
|
521
521
|
findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
|
|
522
|
+
/**
|
|
523
|
+
* Upsert the managed attachments comment. `resync: true` marks an
|
|
524
|
+
* explicit "make the comment state correct" call (`uploads comment`), so
|
|
525
|
+
* the server hunts for the marker — and collapses any duplicate — instead
|
|
526
|
+
* of patching its cached comment id (issue #480). Older servers ignore
|
|
527
|
+
* the field.
|
|
528
|
+
*/
|
|
522
529
|
upsertGithubComment(opts: {
|
|
523
530
|
repo: string;
|
|
524
531
|
num: number;
|
|
525
532
|
kind: "pull" | "issues";
|
|
533
|
+
resync?: boolean;
|
|
526
534
|
}): Promise<GithubCommentResult>;
|
|
527
535
|
/**
|
|
528
536
|
* Promote a workspace's branch-staged attachments into a PR's stable
|
package/dist/client.js
CHANGED
|
@@ -479,6 +479,13 @@ export function createUploadsClient(config) {
|
|
|
479
479
|
params.set("cursor", opts.cursor);
|
|
480
480
|
return request("GET", galleriesBase(config) + "/by-reference?" + params);
|
|
481
481
|
},
|
|
482
|
+
/**
|
|
483
|
+
* Upsert the managed attachments comment. `resync: true` marks an
|
|
484
|
+
* explicit "make the comment state correct" call (`uploads comment`), so
|
|
485
|
+
* the server hunts for the marker — and collapses any duplicate — instead
|
|
486
|
+
* of patching its cached comment id (issue #480). Older servers ignore
|
|
487
|
+
* the field.
|
|
488
|
+
*/
|
|
482
489
|
async upsertGithubComment(opts) {
|
|
483
490
|
return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/comment`, {
|
|
484
491
|
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type GlobalFlags } from "../cli-args.js";
|
|
2
2
|
import { type CommandRunner } from "../github-gh.js";
|
|
3
3
|
export declare const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
4
|
+
export interface StepResult {
|
|
5
|
+
command: string[];
|
|
6
|
+
ok: boolean;
|
|
7
|
+
skipped?: "dry-run" | "sign-in";
|
|
8
|
+
error?: string;
|
|
9
|
+
output?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runStep(run: CommandRunner, command: string[]): StepResult;
|
|
4
12
|
export declare function runInstall(args: string[], opts: {
|
|
5
13
|
globals: GlobalFlags;
|
|
6
14
|
json?: boolean;
|
package/dist/commands/install.js
CHANGED
|
@@ -46,7 +46,7 @@ function redactor(token) {
|
|
|
46
46
|
return out;
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
function runStep(run, command) {
|
|
49
|
+
export function runStep(run, command) {
|
|
50
50
|
try {
|
|
51
51
|
const output = run(command[0], command.slice(1)).trim();
|
|
52
52
|
return { command, ok: true, output: output || undefined };
|
|
@@ -2,15 +2,16 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
-
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, } from "../commands.js";
|
|
5
|
+
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, } from "../commands.js";
|
|
6
6
|
import { resolvePutDefaults } from "../config.js";
|
|
7
7
|
import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
|
|
8
8
|
import { resolvePutPrefix } from "../destinations.js";
|
|
9
9
|
import { execRunner, ghMetadataFromTargetWithTitle, resolveRepo, } from "../github-gh.js";
|
|
10
|
-
import { ghBranchAttachmentKey
|
|
10
|
+
import { ghBranchAttachmentKey } from "../github.js";
|
|
11
11
|
import { safeCaptureFacts } from "../capture-facts.js";
|
|
12
12
|
import { parseMetaFlags, validateMetaMap } from "../metadata.js";
|
|
13
13
|
import { mergeDerivedMeta } from "../metadata-vocab.js";
|
|
14
|
+
import { writeSidecarMeta } from "../sidecar.js";
|
|
14
15
|
import { writeJson, writeStdout } from "../io.js";
|
|
15
16
|
import { assertHideSelector, captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
16
17
|
const SCREENSHOT_HELP = `uploads screenshot <target> [options]
|
|
@@ -33,6 +34,15 @@ fail fast with a clear error instead of sending a doomed request.
|
|
|
33
34
|
After capture, screenshots share the put upload pipeline: optional --frame,
|
|
34
35
|
optimize-by-default, --pr/--issue attachment + --comment, --gallery, --meta.
|
|
35
36
|
|
|
37
|
+
Branch staging by default (pre-PR): with no --pr/--issue/--branch/--key/--ref/
|
|
38
|
+
--prefix/--destination, a screenshot taken on a non-default git branch stages
|
|
39
|
+
under gh/<owner>/<repo>/branch/<branch>/<name> instead of the dated
|
|
40
|
+
screenshots/<repo>/<date>/... layout — same key/metadata as an explicit
|
|
41
|
+
--branch, carrying every derived fact (path/url/env/viewport, --state) along.
|
|
42
|
+
Staged files auto-attach with full metadata the first time you attach to that
|
|
43
|
+
branch's PR once it opens (or run "uploads attach --promote"). Use --no-git,
|
|
44
|
+
or an explicit --ref/--prefix, to opt back into the dated layout.
|
|
45
|
+
|
|
36
46
|
Options:
|
|
37
47
|
--via auto|local|remote Capture backend (default: auto, or UPLOADS_SCREENSHOT_VIA)
|
|
38
48
|
--browser <path> Explicit local browser executable (or UPLOADS_CHROME_PATH / CHROME_PATH)
|
|
@@ -51,7 +61,12 @@ Options:
|
|
|
51
61
|
on --via remote — neutralizes animations via injected CSS)
|
|
52
62
|
--eval <js> Run JS in the page after settle, before capture (--via local only)
|
|
53
63
|
--init-script <file> Inject a JS file before navigation (--via local only)
|
|
54
|
-
--out <file> Also write the PNG to a local file
|
|
64
|
+
--out <file> Also write the PNG to a local file. Also writes a sidecar manifest,
|
|
65
|
+
<file>.uploads.json, recording this capture's derived metadata
|
|
66
|
+
(path/url/env/viewport, plus --state if given) with a content hash; a
|
|
67
|
+
later \`put\`/\`attach\` of this exact file picks the metadata back up
|
|
68
|
+
automatically (explicit --meta/--state still win). See --no-sidecar.
|
|
69
|
+
--no-sidecar Don't write the <file>.uploads.json sidecar alongside --out
|
|
55
70
|
--no-upload Skip hosting; requires --out (local file only)
|
|
56
71
|
--destination <id> Typed root: screenshots | gh | f
|
|
57
72
|
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
@@ -171,6 +186,9 @@ captureImpl = captureScreenshot) {
|
|
|
171
186
|
const noUpload = flagBool(parsed.flags, "--no-upload");
|
|
172
187
|
if (noUpload && !outFile)
|
|
173
188
|
throw new UsageError("--no-upload requires --out");
|
|
189
|
+
const noSidecar = flagBool(parsed.flags, "--no-sidecar");
|
|
190
|
+
if (noSidecar && !outFile)
|
|
191
|
+
throw new UsageError("--no-sidecar requires --out");
|
|
174
192
|
const keyHint = flagString(parsed.flags, "--key");
|
|
175
193
|
const destFlag = flagString(parsed.flags, "--destination");
|
|
176
194
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
@@ -210,14 +228,38 @@ captureImpl = captureScreenshot) {
|
|
|
210
228
|
if (noUpload)
|
|
211
229
|
throw new UsageError("--dry-run cannot be combined with --no-upload");
|
|
212
230
|
}
|
|
231
|
+
const putDefaults = resolvePutDefaults({ envFile: ctx.envFile }, rawDefaults);
|
|
232
|
+
const noGit = flagBool(parsed.flags, "--no-git") || putDefaults.noGit === true;
|
|
213
233
|
const branchRepo = branchArg !== undefined ? resolveRepo(flagString(parsed.flags, "--repo"), run) : undefined;
|
|
234
|
+
// Auto branch staging (issue #469 lever 1): mirrors bare `put`'s auto-staging
|
|
235
|
+
// (issue #403). When no --branch/--pr/--issue/--key/--ref/--prefix/--destination
|
|
236
|
+
// is given and git use isn't disabled, a screenshot taken on a non-default
|
|
237
|
+
// git branch stages the same way explicit `--branch`/bare `put` do — same
|
|
238
|
+
// key shape, same gh.* metadata — instead of landing on the dated
|
|
239
|
+
// `screenshots/<repo>/<date>/...` layout. This is what lets derived
|
|
240
|
+
// metadata (path/url/env/viewport, --state) ride through to PR-open
|
|
241
|
+
// promotion when the capture happens before the PR exists. Skipped
|
|
242
|
+
// entirely when --branch was given explicitly (already handled above).
|
|
243
|
+
const autoStagingTarget = branchArg === undefined
|
|
244
|
+
? resolvePutStagingTarget({
|
|
245
|
+
ghTarget,
|
|
246
|
+
keyHint,
|
|
247
|
+
refArg: flagString(parsed.flags, "--ref"),
|
|
248
|
+
prefixArg: prefixFlag,
|
|
249
|
+
destinationArg: destFlag,
|
|
250
|
+
noGit,
|
|
251
|
+
repoArg: flagString(parsed.flags, "--repo") ?? putDefaults.repo,
|
|
252
|
+
run,
|
|
253
|
+
})
|
|
254
|
+
: undefined;
|
|
255
|
+
const stagingTarget = branchArg !== undefined ? { repo: branchRepo, branch: branchArg } : autoStagingTarget;
|
|
214
256
|
let resolvedPrefix;
|
|
215
257
|
try {
|
|
216
258
|
resolvedPrefix = resolvePutPrefix({
|
|
217
259
|
destination: destFlag,
|
|
218
260
|
prefix: prefixFlag,
|
|
219
261
|
key: keyHint,
|
|
220
|
-
ghAttachment: Boolean(ghTarget) ||
|
|
262
|
+
ghAttachment: Boolean(ghTarget) || stagingTarget !== undefined,
|
|
221
263
|
});
|
|
222
264
|
}
|
|
223
265
|
catch (err) {
|
|
@@ -233,7 +275,6 @@ captureImpl = captureScreenshot) {
|
|
|
233
275
|
return raw;
|
|
234
276
|
throw new UsageError(`invalid --format: ${raw}`);
|
|
235
277
|
})();
|
|
236
|
-
const putDefaults = resolvePutDefaults({ envFile: ctx.envFile }, rawDefaults);
|
|
237
278
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, putDefaults);
|
|
238
279
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
239
280
|
const altFlag = flagString(parsed.flags, "--alt");
|
|
@@ -248,9 +289,8 @@ captureImpl = captureScreenshot) {
|
|
|
248
289
|
metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
|
|
249
290
|
validateMetaMap(metadata);
|
|
250
291
|
}
|
|
251
|
-
else if (
|
|
252
|
-
metadata =
|
|
253
|
-
validateMetaMap(metadata);
|
|
292
|
+
else if (stagingTarget !== undefined) {
|
|
293
|
+
metadata = mergeStagingMeta(withFacts, stagingTarget);
|
|
254
294
|
}
|
|
255
295
|
else if (Object.keys(withFacts).length > 0) {
|
|
256
296
|
validateMetaMap(withFacts);
|
|
@@ -282,6 +322,8 @@ captureImpl = captureScreenshot) {
|
|
|
282
322
|
writeFileSync(outFile, captured.png);
|
|
283
323
|
if (logHuman)
|
|
284
324
|
process.stderr.write(`>> wrote ${outFile}\n`);
|
|
325
|
+
if (!noSidecar)
|
|
326
|
+
writeSidecarMeta(outFile, captured.png, withFacts);
|
|
285
327
|
}
|
|
286
328
|
if (noUpload) {
|
|
287
329
|
if (ctx.json) {
|
|
@@ -294,8 +336,8 @@ captureImpl = captureScreenshot) {
|
|
|
294
336
|
}
|
|
295
337
|
const repo = flagString(parsed.flags, "--repo") ?? putDefaults.repo;
|
|
296
338
|
const ref = flagString(parsed.flags, "--ref") ?? putDefaults.ref;
|
|
297
|
-
const branchKey =
|
|
298
|
-
? ghBranchAttachmentKey(
|
|
339
|
+
const branchKey = stagingTarget !== undefined
|
|
340
|
+
? ghBranchAttachmentKey(stagingTarget.repo, stagingTarget.branch, captured.filename)
|
|
299
341
|
: undefined;
|
|
300
342
|
const alt = altFlag ?? basename(captured.filename);
|
|
301
343
|
const { result, prepared, markdown } = await uploadPreparedImage(ctx.client, captured.png, captured.filename, {
|
|
@@ -306,13 +348,26 @@ captureImpl = captureScreenshot) {
|
|
|
306
348
|
prefix: resolvedPrefix ?? putDefaults.prefix,
|
|
307
349
|
repo,
|
|
308
350
|
ref,
|
|
309
|
-
deriveRepoFromGit: !
|
|
351
|
+
deriveRepoFromGit: !noGit,
|
|
310
352
|
dryRun,
|
|
311
353
|
metadata,
|
|
312
354
|
provenanceClient: "uploads-cli-screenshot",
|
|
313
355
|
alt: () => alt,
|
|
314
356
|
width,
|
|
315
357
|
});
|
|
358
|
+
// Staging note (issue #469 lever 1, mirrors #403's bare-put note): only for
|
|
359
|
+
// the auto-staged case — explicit `--branch` keeps its own "staged: these
|
|
360
|
+
// auto-attach..." wording below. Same suppression as put's note (--quiet,
|
|
361
|
+
// UPLOADS_NO_NUDGE=1).
|
|
362
|
+
const stagingNote = autoStagingTarget && !ctx.quiet && !putDefaults.noNudge
|
|
363
|
+
? putStagingNoteText(autoStagingTarget.branch)
|
|
364
|
+
: undefined;
|
|
365
|
+
// Stage-time binding warning (issue #398), same check bare put/attach
|
|
366
|
+
// --branch run, now also reachable from screenshot's staging paths
|
|
367
|
+
// (explicit --branch and auto-staging alike).
|
|
368
|
+
const bindingWarning = stagingTarget !== undefined
|
|
369
|
+
? await resolveStageBindingWarning({ ctx, defaults: putDefaults, repo: stagingTarget.repo })
|
|
370
|
+
: undefined;
|
|
316
371
|
let gallery;
|
|
317
372
|
if (galleryId) {
|
|
318
373
|
try {
|
|
@@ -347,8 +402,25 @@ captureImpl = captureScreenshot) {
|
|
|
347
402
|
if (prepared.optimized) {
|
|
348
403
|
process.stderr.write(`>> optimized ${prepared.originalBytes} → ${prepared.outputBytes} bytes\n`);
|
|
349
404
|
}
|
|
350
|
-
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n
|
|
405
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n`);
|
|
406
|
+
if (stagingTarget !== undefined) {
|
|
407
|
+
process.stderr.write(`>> find these later: uploads find gh.branch=${stagingTarget.branch.toLowerCase()}\n`);
|
|
408
|
+
if (autoStagingTarget) {
|
|
409
|
+
if (stagingNote)
|
|
410
|
+
process.stderr.write(`${stagingNote}\n`);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
process.stderr.write(`>> staged: these auto-attach to this branch's PR when it opens ` +
|
|
414
|
+
`(or run \`uploads attach --promote\` after opening)\n`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (bindingWarning)
|
|
418
|
+
process.stderr.write(`${bindingWarning}\n`);
|
|
419
|
+
process.stderr.write("\n");
|
|
351
420
|
}
|
|
421
|
+
// One JSON `hint` slot (mirrors bare put): the binding warning is more
|
|
422
|
+
// actionable than the generic staging note, so it wins when both fire.
|
|
423
|
+
const jsonHint = bindingWarning ?? stagingNote;
|
|
352
424
|
switch (format) {
|
|
353
425
|
case "json":
|
|
354
426
|
await writeJson({
|
|
@@ -363,6 +435,7 @@ captureImpl = captureScreenshot) {
|
|
|
363
435
|
backend: captured.backend,
|
|
364
436
|
gallery,
|
|
365
437
|
...(dryRun ? { dryRun: true } : {}),
|
|
438
|
+
...(jsonHint ? { hint: jsonHint } : {}),
|
|
366
439
|
});
|
|
367
440
|
break;
|
|
368
441
|
case "url":
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GlobalFlags } from "../cli-args.js";
|
|
2
|
+
import { type CommandRunner } from "../github-gh.js";
|
|
3
|
+
import { type InstallSource } from "../install-source.js";
|
|
4
|
+
import { type UpdateStatus } from "../update-check.js";
|
|
5
|
+
export interface RunUpdateOptions {
|
|
6
|
+
globals: GlobalFlags;
|
|
7
|
+
/** Injected in tests. */
|
|
8
|
+
runner?: CommandRunner;
|
|
9
|
+
/** Injected in tests; defaults to detection from this module's path. */
|
|
10
|
+
source?: InstallSource;
|
|
11
|
+
/** Injected in tests; defaults to a cache-bypassing registry check. */
|
|
12
|
+
check?: () => Promise<UpdateStatus>;
|
|
13
|
+
}
|
|
14
|
+
export declare function runUpdate(args: string[], opts: RunUpdateOptions, help?: boolean): Promise<number>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { flagBool, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
4
|
+
import { execRunner } from "../github-gh.js";
|
|
5
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
6
|
+
import { detectInstallSource } from "../install-source.js";
|
|
7
|
+
import { checkForUpdate } from "../update-check.js";
|
|
8
|
+
import { runInstall, runStep } from "./install.js";
|
|
9
|
+
const UPDATE_HELP = `uploads update — update the CLI and refresh agent integrations
|
|
10
|
+
|
|
11
|
+
Upgrades the globally installed npm package, then re-runs \`uploads install\` so
|
|
12
|
+
the agent skills and the MCP registration match the new version. Skills and the
|
|
13
|
+
MCP registration drift on their own, so this refreshes them even when the CLI is
|
|
14
|
+
already current.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
uploads update [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--dry-run Print the plan without running anything
|
|
21
|
+
--skip-install Upgrade the npm package only; leave skills and MCP alone
|
|
22
|
+
--verbose Show the output of the underlying commands
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
uploads update
|
|
26
|
+
uploads update --dry-run
|
|
27
|
+
uploads update --skip-install
|
|
28
|
+
`;
|
|
29
|
+
/** Why an upgrade was skipped, phrased for the user. */
|
|
30
|
+
const SKIP_REASON = {
|
|
31
|
+
workspace: "this is a workspace checkout, not a global install",
|
|
32
|
+
npx: "this ran from an npx cache, which is discarded after the run",
|
|
33
|
+
unknown: "this is a local project dependency, not a global install",
|
|
34
|
+
};
|
|
35
|
+
function thisModulePath() {
|
|
36
|
+
const path = fileURLToPath(import.meta.url);
|
|
37
|
+
try {
|
|
38
|
+
return realpathSync(path);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isPermissionError(message) {
|
|
45
|
+
return /EACCES|EPERM|permission denied/i.test(message);
|
|
46
|
+
}
|
|
47
|
+
export async function runUpdate(args, opts, help = false) {
|
|
48
|
+
const parsed = parseCommandArgs(args);
|
|
49
|
+
if (help || parsed.help) {
|
|
50
|
+
writeCommandHelp(UPDATE_HELP);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.positionals.length > 0) {
|
|
54
|
+
throw new UsageError(`update takes no arguments (got ${parsed.positionals[0]})`);
|
|
55
|
+
}
|
|
56
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
57
|
+
const verbose = flagBool(parsed.flags, "--verbose");
|
|
58
|
+
const skipInstall = flagBool(parsed.flags, "--skip-install");
|
|
59
|
+
const run = opts.runner ?? execRunner;
|
|
60
|
+
const source = opts.source ?? detectInstallSource(thisModulePath());
|
|
61
|
+
// ttlMs 0 bypasses the once-a-day cache: `update` must not trust yesterday's read.
|
|
62
|
+
const status = await (opts.check ?? (() => checkForUpdate({ ttlMs: 0 })))();
|
|
63
|
+
const willUpgrade = status.updateAvailable && source.kind === "global";
|
|
64
|
+
// Resolved through PATH: in the normal single-install case this is the
|
|
65
|
+
// just-upgraded binary, but with multiple `uploads` binaries on PATH it
|
|
66
|
+
// could resolve to a different, stale one. Accepted risk, not a guarantee.
|
|
67
|
+
const refreshCommand = ["uploads", "install"];
|
|
68
|
+
// --- plan ---
|
|
69
|
+
if (willUpgrade) {
|
|
70
|
+
process.stdout.write(`CLI ${status.current} → ${status.latest}\n`);
|
|
71
|
+
}
|
|
72
|
+
else if (status.updateAvailable) {
|
|
73
|
+
process.stdout.write(`CLI ${status.current} is behind ${status.latest}, but the upgrade is skipped — ` +
|
|
74
|
+
`${SKIP_REASON[source.kind] ?? "the install source is not a global install"}.\n` +
|
|
75
|
+
`Upgrade by hand with: ${source.upgradeCommand.join(" ")}\n`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.stdout.write(`CLI already at ${status.current}\n`);
|
|
79
|
+
}
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
if (willUpgrade) {
|
|
82
|
+
process.stdout.write(`upgrade: would run — ${source.upgradeCommand.join(" ")}\n`);
|
|
83
|
+
}
|
|
84
|
+
if (!skipInstall) {
|
|
85
|
+
process.stdout.write(`refresh: would run — ${refreshCommand.join(" ")}\n`);
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
// --- upgrade ---
|
|
90
|
+
if (willUpgrade) {
|
|
91
|
+
process.stdout.write("Upgrading the CLI…\n");
|
|
92
|
+
const result = runStep(run, source.upgradeCommand);
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
const message = result.error ?? "";
|
|
95
|
+
process.stderr.write(`upgrade: failed — ${message}\n`);
|
|
96
|
+
if (isPermissionError(message)) {
|
|
97
|
+
process.stderr.write("The global install directory is not writable by your user. Fix the ownership " +
|
|
98
|
+
`of your npm prefix so global installs work without sudo, then re-run \`uploads update\`.\n`);
|
|
99
|
+
}
|
|
100
|
+
process.stderr.write(`Run it by hand: ${source.upgradeCommand.join(" ")}\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write("upgrade: ok\n");
|
|
104
|
+
if (verbose && result.output)
|
|
105
|
+
process.stdout.write(` ${result.output}\n`);
|
|
106
|
+
}
|
|
107
|
+
if (skipInstall)
|
|
108
|
+
return 0;
|
|
109
|
+
// --- refresh ---
|
|
110
|
+
// After an upgrade the in-process code is the OLD version, so spawn the newly
|
|
111
|
+
// installed binary. Its skill list is the one that should be installed.
|
|
112
|
+
if (willUpgrade) {
|
|
113
|
+
process.stdout.write("Refreshing skills and MCP…\n");
|
|
114
|
+
const result = runStep(run, refreshCommand);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
process.stderr.write(`refresh: failed — ${result.error ?? ""}\n`);
|
|
117
|
+
process.stderr.write("Run it by hand: uploads install\n");
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(result.output ? `${result.output}\n` : "refresh: ok\n");
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
// Nothing changed, so the in-process install code is already current.
|
|
124
|
+
return runInstall(verbose ? ["--verbose"] : [], { globals: opts.globals, runner: run });
|
|
125
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -166,7 +166,31 @@ export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]):
|
|
|
166
166
|
*/
|
|
167
167
|
export declare class GithubCommentAuthorizationError extends Error {
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
/**
|
|
170
|
+
* `opts.resync` marks an explicit `uploads comment` invocation rather than a
|
|
171
|
+
* background sync (attach, screenshot, put --comment). It costs the server one
|
|
172
|
+
* extra comment listing and in exchange collapses any duplicate managed
|
|
173
|
+
* comment (issue #480) — worth it on the rare, explicitly-asked-for resync,
|
|
174
|
+
* not on every attach.
|
|
175
|
+
*/
|
|
176
|
+
export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner, workspace?: string, opts?: {
|
|
177
|
+
resync?: boolean;
|
|
178
|
+
}): Promise<AttachmentsCommentResult>;
|
|
179
|
+
/**
|
|
180
|
+
* Lever 3 (issue #469): a nudge for when an image lands on a PR/issue with
|
|
181
|
+
* no `path` metadata — `path` is one of the highest-value queryable tags
|
|
182
|
+
* (same tier as `state=`), and unlike `uploads screenshot` (which derives it
|
|
183
|
+
* from the captured URL), a plain `attach`/`put --pr`/`put --issue` of an
|
|
184
|
+
* already-existing image has nothing to derive it from, so it's easy to
|
|
185
|
+
* forget. Fires once per batch (not per file) — checks the metadata the
|
|
186
|
+
* server actually stored (`PutResult.metadata`), not what was requested, so
|
|
187
|
+
* a merge/validation drop still surfaces the gap. Non-image uploads (zips,
|
|
188
|
+
* PDFs, etc.) are exempt — "findable by page" doesn't apply to them.
|
|
189
|
+
*/
|
|
190
|
+
export declare function pathMetaHintFor(uploads: {
|
|
191
|
+
contentType: string;
|
|
192
|
+
metadata?: Record<string, string>;
|
|
193
|
+
}[]): string | undefined;
|
|
170
194
|
export type AttachUploadItem = PutResult & {
|
|
171
195
|
file: string;
|
|
172
196
|
markdown: string;
|
|
@@ -355,6 +379,14 @@ export declare function resolvePutStagingTarget(opts: {
|
|
|
355
379
|
repoArg: string | undefined;
|
|
356
380
|
run: CommandRunner;
|
|
357
381
|
}): BranchTarget | undefined;
|
|
382
|
+
/**
|
|
383
|
+
* Merges a staging target's `gh.*` branch metadata over `base` and validates
|
|
384
|
+
* the result (same builder, same contract as `attach --branch`) — the one
|
|
385
|
+
* merge+validate step shared by every staging call site: `runPut`,
|
|
386
|
+
* `runScreenshot`, and both the local stdio MCP `put` and `screenshot`
|
|
387
|
+
* tools.
|
|
388
|
+
*/
|
|
389
|
+
export declare function mergeStagingMeta(base: Record<string, string> | undefined, target: BranchTarget): Record<string, string>;
|
|
358
390
|
/**
|
|
359
391
|
* The bare-put staging note's wording (issue #403): replaces the #393 nudge
|
|
360
392
|
* for the (now default) case where a bare put on a non-default branch stages
|