@buildinternet/uploads 0.38.0 → 0.41.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/dist/cli-catalog.js +9 -0
- package/dist/cli.js +6 -2
- package/dist/client.d.ts +99 -9
- package/dist/client.js +112 -30
- package/dist/commands/screenshot.js +24 -7
- package/dist/commands.d.ts +46 -1
- package/dist/commands.js +298 -31
- package/dist/comment-config.d.ts +3 -0
- package/dist/comment-config.js +13 -0
- package/dist/github.d.ts +62 -0
- package/dist/github.js +84 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +17 -0
- package/dist/mcp/tools.js +65 -11
- package/dist/metadata-vocab.d.ts +1 -1
- package/dist/metadata-vocab.js +1 -0
- package/package.json +3 -3
package/dist/cli-catalog.js
CHANGED
|
@@ -164,8 +164,17 @@ export const ROOT_COMMANDS = [
|
|
|
164
164
|
subcommands: [
|
|
165
165
|
{ name: "link", summary: "Claim or inspect the repo binding" },
|
|
166
166
|
{ name: "doctor", summary: "Check the GitHub App's webhook event subscriptions" },
|
|
167
|
+
{
|
|
168
|
+
name: "rotate-prefix",
|
|
169
|
+
summary: "Rotate a private repo's randomized attachment URL prefix",
|
|
170
|
+
},
|
|
167
171
|
],
|
|
168
172
|
},
|
|
173
|
+
{
|
|
174
|
+
name: "ingest",
|
|
175
|
+
usage: "ingest (--pr <n> | --issue <n>)",
|
|
176
|
+
summary: "Mirror GitHub-native user-attachments media from a PR/issue into the workspace",
|
|
177
|
+
},
|
|
169
178
|
{
|
|
170
179
|
name: "list",
|
|
171
180
|
summary: "List objects (--meta k=v filters by queryable metadata)",
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { formatRootHelp, formatUnknownCommand, wantsFullHelp } from "./cli-help.
|
|
|
6
6
|
import { commandSummary, suggestCommand } from "./cli-suggest.js";
|
|
7
7
|
import { writeJson } from "./io.js";
|
|
8
8
|
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
9
|
-
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
9
|
+
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runIngest, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
10
10
|
import { runConfig } from "./commands/config.js";
|
|
11
11
|
import { runSetup } from "./commands/setup.js";
|
|
12
12
|
import { runLogin } from "./commands/login.js";
|
|
@@ -308,7 +308,8 @@ export async function runCli(argv) {
|
|
|
308
308
|
case "purge-expired":
|
|
309
309
|
case "doctor":
|
|
310
310
|
case "comment":
|
|
311
|
-
case "github":
|
|
311
|
+
case "github":
|
|
312
|
+
case "ingest": {
|
|
312
313
|
const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
|
|
313
314
|
switch (parsed.command) {
|
|
314
315
|
case "attach":
|
|
@@ -332,6 +333,9 @@ export async function runCli(argv) {
|
|
|
332
333
|
case "github":
|
|
333
334
|
code = await runGithub(ctx, cmdArgs, showHelp);
|
|
334
335
|
break;
|
|
336
|
+
case "ingest":
|
|
337
|
+
code = await runIngest(ctx, cmdArgs, showHelp);
|
|
338
|
+
break;
|
|
335
339
|
case "list":
|
|
336
340
|
code = await runList(ctx, cmdArgs, showHelp);
|
|
337
341
|
break;
|
package/dist/client.d.ts
CHANGED
|
@@ -256,7 +256,42 @@ export type GithubCommentResult = {
|
|
|
256
256
|
fixUrl?: string;
|
|
257
257
|
required?: string[];
|
|
258
258
|
};
|
|
259
|
-
/** `POST /v1/:workspace/github/
|
|
259
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix` request (server contract, issue #631/#613). */
|
|
260
|
+
export interface ResolveGhPrefixOptions {
|
|
261
|
+
repo: string;
|
|
262
|
+
branch?: string;
|
|
263
|
+
target?: {
|
|
264
|
+
kind: "pull" | "issues";
|
|
265
|
+
num: number;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix` response (server contract, issue #631/#613). */
|
|
269
|
+
export type ResolveGhPrefixResult = {
|
|
270
|
+
mode: "plain";
|
|
271
|
+
} | {
|
|
272
|
+
mode: "private";
|
|
273
|
+
prefixId: string;
|
|
274
|
+
activePrefixIds?: string[];
|
|
275
|
+
};
|
|
276
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix/rotate` request (server contract, issue #631/#613). */
|
|
277
|
+
export interface RotateGhPrefixOptions {
|
|
278
|
+
repo: string;
|
|
279
|
+
/** Mutually exclusive with `repoLevel`: rotate one branch's id. */
|
|
280
|
+
branch?: string;
|
|
281
|
+
/** Mutually exclusive with `branch`: rotate the repo-level id shared by
|
|
282
|
+
* issue attachments and ingested assets. */
|
|
283
|
+
repoLevel?: boolean;
|
|
284
|
+
}
|
|
285
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix/rotate` response (server contract, issue #631/#613). */
|
|
286
|
+
export type RotateGhPrefixResult = {
|
|
287
|
+
rotated: false;
|
|
288
|
+
reason: string;
|
|
289
|
+
} | {
|
|
290
|
+
rotated: true;
|
|
291
|
+
prefixId: string;
|
|
292
|
+
moved: number;
|
|
293
|
+
};
|
|
294
|
+
/** `POST /v1/workspaces/:workspace/github/promote` request/response (server contract, PR #310). */
|
|
260
295
|
export interface PromoteBranchAttachmentsOptions {
|
|
261
296
|
repo: string;
|
|
262
297
|
num: number;
|
|
@@ -270,7 +305,7 @@ export interface PromoteBranchAttachmentsResult {
|
|
|
270
305
|
promoted: string[];
|
|
271
306
|
skipped: PromoteSkip[];
|
|
272
307
|
}
|
|
273
|
-
/** `GET`/`POST /v1/:workspace/github/link` result (server contract, phase 4b). */
|
|
308
|
+
/** `GET`/`POST /v1/workspaces/:workspace/github/link` result (server contract, phase 4b). */
|
|
274
309
|
export interface GithubLinkResult {
|
|
275
310
|
repo: string;
|
|
276
311
|
linked: boolean;
|
|
@@ -289,14 +324,14 @@ export interface GithubLinkClaimResult extends GithubLinkResult {
|
|
|
289
324
|
*/
|
|
290
325
|
reason?: "not_authorized";
|
|
291
326
|
}
|
|
292
|
-
/** `DELETE /v1/:workspace/github/link` result (issue #318, self-serve unlink). */
|
|
327
|
+
/** `DELETE /v1/workspaces/:workspace/github/link` result (issue #318, self-serve unlink). */
|
|
293
328
|
export interface GithubLinkUnlinkResult {
|
|
294
329
|
repo: string;
|
|
295
330
|
unlinked: boolean;
|
|
296
331
|
reason?: "not_linked";
|
|
297
332
|
}
|
|
298
333
|
/**
|
|
299
|
-
* `GET /v1/:workspace/github/repo-link` result (issue #398). Deliberately
|
|
334
|
+
* `GET /v1/workspaces/:workspace/github/repo-link` result (issue #398). Deliberately
|
|
300
335
|
* minimal relative to `GithubLinkResult`: never names the owning workspace
|
|
301
336
|
* when it isn't this one — "self"/"other"/"none" is all the stage-time
|
|
302
337
|
* warning needs, and anything richer would leak cross-tenant info to a
|
|
@@ -305,10 +340,31 @@ export interface GithubLinkUnlinkResult {
|
|
|
305
340
|
export interface GithubRepoLinkResult {
|
|
306
341
|
binding: "self" | "other" | "none";
|
|
307
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* `POST /v1/workspaces/:workspace/github/ingest` result (Task 6, manual/
|
|
345
|
+
* backfill entry point for GitHub-native `user-attachments` media). `repo`
|
|
346
|
+
* comes back lowercased by the server.
|
|
347
|
+
*/
|
|
348
|
+
export interface IngestGithubResult {
|
|
349
|
+
repo: string;
|
|
350
|
+
kind: "pull" | "issues";
|
|
351
|
+
num: number;
|
|
352
|
+
/** Object keys newly mirrored into the workspace this call. */
|
|
353
|
+
ingested: string[];
|
|
354
|
+
/** Object keys whose previously-detached ledger row was un-detached. */
|
|
355
|
+
reattached: string[];
|
|
356
|
+
/** Object keys detached because they're no longer referenced. */
|
|
357
|
+
detached: string[];
|
|
358
|
+
/** Attachment URLs skipped, with the reason. */
|
|
359
|
+
skipped: {
|
|
360
|
+
url: string;
|
|
361
|
+
reason: string;
|
|
362
|
+
}[];
|
|
363
|
+
}
|
|
308
364
|
export interface HealthResult {
|
|
309
365
|
ok: boolean;
|
|
310
366
|
}
|
|
311
|
-
/** `GET /v1/:workspace/github/health` result (issue #293 follow-up). */
|
|
367
|
+
/** `GET /v1/workspaces/:workspace/github/health` result (issue #293 follow-up). */
|
|
312
368
|
export interface GithubHealthResult {
|
|
313
369
|
configured: boolean;
|
|
314
370
|
ok: boolean;
|
|
@@ -546,10 +602,12 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
546
602
|
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
547
603
|
patchMetadata(key: string, opts: PatchMetadataOptions): Promise<GetMetadataResult>;
|
|
548
604
|
/**
|
|
549
|
-
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` —
|
|
550
|
-
* over queryable metadata and/or a case-insensitive
|
|
551
|
-
* At least one of non-empty `filters` or `opts.name`
|
|
552
|
-
* `filters` must be pre-validated when present (see
|
|
605
|
+
* `GET /v1/workspaces/:workspace/files/search?meta.<k>=<v>&…&name=…` —
|
|
606
|
+
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
607
|
+
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
608
|
+
* is required. `filters` must be pre-validated when present (see
|
|
609
|
+
* `metadata.ts`). The canonical search route is non-paginated (server cap
|
|
610
|
+
* 100, narrowable via `limit`), so `cursor` is always null.
|
|
553
611
|
*/
|
|
554
612
|
findFiles(filters?: Record<string, string>, opts?: FindFilesOptions): Promise<FindFilesResult>;
|
|
555
613
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
@@ -585,6 +643,26 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
585
643
|
kind: "pull" | "issues";
|
|
586
644
|
resync?: boolean;
|
|
587
645
|
}): Promise<GithubCommentResult>;
|
|
646
|
+
/**
|
|
647
|
+
* Resolve the GitHub-key mode (plain vs. randomized private prefix, issue
|
|
648
|
+
* #631) a caller should stage/list attachments under for `repo`. Fail-open:
|
|
649
|
+
* ANY failure — a 404 from an older/self-hosted server, a network error, a
|
|
650
|
+
* non-2xx response, or a malformed body — resolves to `{ mode: "plain" }`
|
|
651
|
+
* silently (no stderr noise), never throws. Cached per-process, keyed by
|
|
652
|
+
* repo+branch+target, so repeated calls for the same coordinate (e.g. the
|
|
653
|
+
* gh-fallback comment gather re-checking on every sync) cost one request.
|
|
654
|
+
*/
|
|
655
|
+
resolveGhPrefix(opts: ResolveGhPrefixOptions): Promise<ResolveGhPrefixResult>;
|
|
656
|
+
/**
|
|
657
|
+
* Rotate the active private-repo attachment prefix for `opts.repo` +
|
|
658
|
+
* (`opts.branch` or `opts.repoLevel`) (issue #631). Unlike
|
|
659
|
+
* `resolveGhPrefix`, this is NOT fail-open: it's an explicit, caller-
|
|
660
|
+
* initiated action, so a failure (including a 404 from an older/self-
|
|
661
|
+
* hosted server without this route) throws `UploadsError` — the CLI
|
|
662
|
+
* command decides how to present that, rather than this method silently
|
|
663
|
+
* degrading to a shape that would look like success.
|
|
664
|
+
*/
|
|
665
|
+
rotateGhPrefix(opts: RotateGhPrefixOptions): Promise<RotateGhPrefixResult>;
|
|
588
666
|
/**
|
|
589
667
|
* Promote a workspace's branch-staged attachments into a PR's stable
|
|
590
668
|
* attachment prefix (server contract, PR #310 — degrade-safe callers
|
|
@@ -617,6 +695,18 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
617
695
|
* `UploadsError` (status 404) on an older/self-hosted server without
|
|
618
696
|
* this route — callers treat that as "unknown", not "broken".
|
|
619
697
|
*/
|
|
698
|
+
/**
|
|
699
|
+
* `POST /v1/workspaces/:workspace/github/ingest` (Task 6) — manual/
|
|
700
|
+
* backfill mirror of a PR/issue's `github.com/user-attachments` media
|
|
701
|
+
* into the workspace. Only ever had the canonical `/v1/workspaces`
|
|
702
|
+
* route (no old bearer-only alias) — unlike the other `github/*` client
|
|
703
|
+
* methods above, which moved onto it in issue #613.
|
|
704
|
+
*/
|
|
705
|
+
ingestGithub(input: {
|
|
706
|
+
repo: string;
|
|
707
|
+
kind: "pull" | "issues";
|
|
708
|
+
num: number;
|
|
709
|
+
}): Promise<IngestGithubResult>;
|
|
620
710
|
githubHealth(): Promise<GithubHealthResult>;
|
|
621
711
|
/**
|
|
622
712
|
* Self-serve unlink (issue #318): removes `repo`'s binding, but only if
|
package/dist/client.js
CHANGED
|
@@ -186,14 +186,18 @@ export function mintWorkspaceToken(apiUrl, accessToken, input) {
|
|
|
186
186
|
function encodeKeyPath(key) {
|
|
187
187
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
// Canonical files surface (issue #613): every file operation now goes to
|
|
190
|
+
// `/v1/workspaces/:workspace/files`. Per-key ops share handlers with the
|
|
191
|
+
// legacy wildcard (#636); list/find/facets adapt the canonical envelopes in
|
|
192
|
+
// their wrappers below (#613 shape reconciliation).
|
|
193
|
+
function canonicalFilesBase(config) {
|
|
194
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/files`;
|
|
191
195
|
}
|
|
192
196
|
function usageBase(config) {
|
|
193
|
-
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
|
|
197
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/usage`;
|
|
194
198
|
}
|
|
195
199
|
function galleriesBase(config) {
|
|
196
|
-
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/galleries`;
|
|
200
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/galleries`;
|
|
197
201
|
}
|
|
198
202
|
function mapApiError(status, error, code, requiredScope, existingUrl) {
|
|
199
203
|
const normalized = error.toLowerCase();
|
|
@@ -306,18 +310,30 @@ export function createUploadsClient(config) {
|
|
|
306
310
|
if (opts.metadata)
|
|
307
311
|
params.set("metadata", "1");
|
|
308
312
|
const qs = params.toString();
|
|
309
|
-
|
|
313
|
+
// Canonical list envelope is `{files, prefixes, cursor}` with queryable
|
|
314
|
+
// metadata always hydrated (issue #613 — the session shape won the
|
|
315
|
+
// reconciliation). This client keeps its historical `{items, cursor}`
|
|
316
|
+
// contract: rename the array and honor `opts.metadata` by stripping the
|
|
317
|
+
// hydrated maps when the caller didn't ask for them.
|
|
318
|
+
const page = await request("GET", `${canonicalFilesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
310
319
|
return {
|
|
311
|
-
|
|
312
|
-
items: page.
|
|
313
|
-
...item
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
cursor: page.cursor,
|
|
321
|
+
items: page.files.map((item) => {
|
|
322
|
+
const { metadata, ...rest } = item;
|
|
323
|
+
return {
|
|
324
|
+
...rest,
|
|
325
|
+
...(opts.metadata && metadata !== undefined ? { metadata } : {}),
|
|
326
|
+
embedUrl: resolveEmbedUrl(item.url, item.embedUrl),
|
|
327
|
+
};
|
|
328
|
+
}),
|
|
316
329
|
};
|
|
317
330
|
}
|
|
318
331
|
async function getGallery(id) {
|
|
319
332
|
return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
|
|
320
333
|
}
|
|
334
|
+
// Per-process cache for resolveGhPrefix, keyed by repo+branch+target — see
|
|
335
|
+
// that method's doc.
|
|
336
|
+
const resolveGhPrefixCache = new Map();
|
|
321
337
|
return {
|
|
322
338
|
async put(body, opts) {
|
|
323
339
|
const key = opts.key ??
|
|
@@ -332,7 +348,7 @@ export function createUploadsClient(config) {
|
|
|
332
348
|
const contentType = opts.contentType ?? inferContentType(opts.filename);
|
|
333
349
|
if (opts.dryRun) {
|
|
334
350
|
const qs = opts.replace ? "dryRun=1&replace=1" : "dryRun=1";
|
|
335
|
-
const preview = await request("PUT", `${
|
|
351
|
+
const preview = await request("PUT", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}?${qs}`);
|
|
336
352
|
if (preview.url == null) {
|
|
337
353
|
throw new UploadsError("workspace has no publicBaseUrl (cannot resolve a public URL)", "NO_PUBLIC_URL");
|
|
338
354
|
}
|
|
@@ -364,7 +380,7 @@ export function createUploadsClient(config) {
|
|
|
364
380
|
headers[`X-Uploads-Meta-${k}`] = v;
|
|
365
381
|
}
|
|
366
382
|
}
|
|
367
|
-
const result = await request("PUT", `${
|
|
383
|
+
const result = await request("PUT", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`, {
|
|
368
384
|
body,
|
|
369
385
|
headers,
|
|
370
386
|
});
|
|
@@ -391,24 +407,26 @@ export function createUploadsClient(config) {
|
|
|
391
407
|
return items;
|
|
392
408
|
},
|
|
393
409
|
async delete(key) {
|
|
394
|
-
return request("DELETE", `${
|
|
410
|
+
return request("DELETE", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`);
|
|
395
411
|
},
|
|
396
412
|
/** `GET /v1/:workspace/files/:key?metadata=1` — the object's queryable metadata. */
|
|
397
413
|
async getMetadata(key) {
|
|
398
|
-
return request("GET", `${
|
|
414
|
+
return request("GET", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}?metadata=1`);
|
|
399
415
|
},
|
|
400
416
|
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
401
417
|
async patchMetadata(key, opts) {
|
|
402
|
-
return request("PATCH", `${
|
|
418
|
+
return request("PATCH", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`, {
|
|
403
419
|
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
404
420
|
headers: { "Content-Type": "application/json" },
|
|
405
421
|
});
|
|
406
422
|
},
|
|
407
423
|
/**
|
|
408
|
-
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` —
|
|
409
|
-
* over queryable metadata and/or a case-insensitive
|
|
410
|
-
* At least one of non-empty `filters` or `opts.name`
|
|
411
|
-
* `filters` must be pre-validated when present (see
|
|
424
|
+
* `GET /v1/workspaces/:workspace/files/search?meta.<k>=<v>&…&name=…` —
|
|
425
|
+
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
426
|
+
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
427
|
+
* is required. `filters` must be pre-validated when present (see
|
|
428
|
+
* `metadata.ts`). The canonical search route is non-paginated (server cap
|
|
429
|
+
* 100, narrowable via `limit`), so `cursor` is always null.
|
|
412
430
|
*/
|
|
413
431
|
async findFiles(filters = {}, opts = {}) {
|
|
414
432
|
const params = new URLSearchParams();
|
|
@@ -420,18 +438,19 @@ export function createUploadsClient(config) {
|
|
|
420
438
|
params.set("prefix", opts.prefix);
|
|
421
439
|
if (opts.limit != null)
|
|
422
440
|
params.set("limit", String(opts.limit));
|
|
423
|
-
|
|
441
|
+
const result = await request("GET", `${canonicalFilesBase(config)}/search?${params.toString()}`);
|
|
442
|
+
return { items: result.items, cursor: null, truncated: result.truncated };
|
|
424
443
|
},
|
|
425
444
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
426
445
|
async listMetadataKeys() {
|
|
427
|
-
return request("GET", `${
|
|
446
|
+
return request("GET", `${canonicalFilesBase(config)}/facets`);
|
|
428
447
|
},
|
|
429
448
|
/** `GET /v1/:workspace/files/facets?key=` — distinct values for one key. */
|
|
430
449
|
async listMetadataValues(key) {
|
|
431
|
-
return request("GET", `${
|
|
450
|
+
return request("GET", `${canonicalFilesBase(config)}/facets?${new URLSearchParams({ key })}`);
|
|
432
451
|
},
|
|
433
452
|
async head(key) {
|
|
434
|
-
const result = await request("GET", `${
|
|
453
|
+
const result = await request("GET", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`);
|
|
435
454
|
return { ...result, embedUrl: resolveEmbedUrl(result.url, result.embedUrl) };
|
|
436
455
|
},
|
|
437
456
|
async createGallery(opts) {
|
|
@@ -499,7 +518,54 @@ export function createUploadsClient(config) {
|
|
|
499
518
|
* the field.
|
|
500
519
|
*/
|
|
501
520
|
async upsertGithubComment(opts) {
|
|
502
|
-
return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/comment`, {
|
|
521
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/comment`, {
|
|
522
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
523
|
+
headers: { "Content-Type": "application/json" },
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
/**
|
|
527
|
+
* Resolve the GitHub-key mode (plain vs. randomized private prefix, issue
|
|
528
|
+
* #631) a caller should stage/list attachments under for `repo`. Fail-open:
|
|
529
|
+
* ANY failure — a 404 from an older/self-hosted server, a network error, a
|
|
530
|
+
* non-2xx response, or a malformed body — resolves to `{ mode: "plain" }`
|
|
531
|
+
* silently (no stderr noise), never throws. Cached per-process, keyed by
|
|
532
|
+
* repo+branch+target, so repeated calls for the same coordinate (e.g. the
|
|
533
|
+
* gh-fallback comment gather re-checking on every sync) cost one request.
|
|
534
|
+
*/
|
|
535
|
+
async resolveGhPrefix(opts) {
|
|
536
|
+
const cacheKey = JSON.stringify([
|
|
537
|
+
opts.repo.toLowerCase(),
|
|
538
|
+
opts.branch ?? "",
|
|
539
|
+
opts.target ?? null,
|
|
540
|
+
]);
|
|
541
|
+
const cached = resolveGhPrefixCache.get(cacheKey);
|
|
542
|
+
if (cached)
|
|
543
|
+
return cached;
|
|
544
|
+
const resolved = await (async () => {
|
|
545
|
+
try {
|
|
546
|
+
return await request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/private-prefix`, {
|
|
547
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
548
|
+
headers: { "Content-Type": "application/json" },
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return { mode: "plain" };
|
|
553
|
+
}
|
|
554
|
+
})();
|
|
555
|
+
resolveGhPrefixCache.set(cacheKey, resolved);
|
|
556
|
+
return resolved;
|
|
557
|
+
},
|
|
558
|
+
/**
|
|
559
|
+
* Rotate the active private-repo attachment prefix for `opts.repo` +
|
|
560
|
+
* (`opts.branch` or `opts.repoLevel`) (issue #631). Unlike
|
|
561
|
+
* `resolveGhPrefix`, this is NOT fail-open: it's an explicit, caller-
|
|
562
|
+
* initiated action, so a failure (including a 404 from an older/self-
|
|
563
|
+
* hosted server without this route) throws `UploadsError` — the CLI
|
|
564
|
+
* command decides how to present that, rather than this method silently
|
|
565
|
+
* degrading to a shape that would look like success.
|
|
566
|
+
*/
|
|
567
|
+
async rotateGhPrefix(opts) {
|
|
568
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/private-prefix/rotate`, {
|
|
503
569
|
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
504
570
|
headers: { "Content-Type": "application/json" },
|
|
505
571
|
});
|
|
@@ -511,7 +577,7 @@ export function createUploadsClient(config) {
|
|
|
511
577
|
* that doesn't have this route yet, as "nothing promoted").
|
|
512
578
|
*/
|
|
513
579
|
async promoteBranchAttachments(opts) {
|
|
514
|
-
return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/promote`, {
|
|
580
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/promote`, {
|
|
515
581
|
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
516
582
|
headers: { "Content-Type": "application/json" },
|
|
517
583
|
});
|
|
@@ -520,7 +586,7 @@ export function createUploadsClient(config) {
|
|
|
520
586
|
* `UploadsError` (status 404) on an older/self-hosted server without this
|
|
521
587
|
* route — callers treat that as "bindings unsupported". */
|
|
522
588
|
async githubLinkStatus(repo) {
|
|
523
|
-
return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
|
|
589
|
+
return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
|
|
524
590
|
},
|
|
525
591
|
/**
|
|
526
592
|
* Explicitly claim `repo` for this workspace (first-claim-wins — see
|
|
@@ -530,7 +596,7 @@ export function createUploadsClient(config) {
|
|
|
530
596
|
* server without this route.
|
|
531
597
|
*/
|
|
532
598
|
async githubLinkClaim(repo) {
|
|
533
|
-
return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link`, {
|
|
599
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link`, {
|
|
534
600
|
body: new TextEncoder().encode(JSON.stringify({ repo })),
|
|
535
601
|
headers: { "Content-Type": "application/json" },
|
|
536
602
|
});
|
|
@@ -543,15 +609,31 @@ export function createUploadsClient(config) {
|
|
|
543
609
|
* caller (the stage warning) treats ANY failure here as "stay silent".
|
|
544
610
|
*/
|
|
545
611
|
async githubRepoLinkStatus(repo) {
|
|
546
|
-
return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/repo-link?repo=${encodeURIComponent(repo)}`);
|
|
612
|
+
return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/repo-link?repo=${encodeURIComponent(repo)}`);
|
|
547
613
|
},
|
|
548
614
|
/**
|
|
549
615
|
* GitHub App configuration + webhook event subscription check. Throws
|
|
550
616
|
* `UploadsError` (status 404) on an older/self-hosted server without
|
|
551
617
|
* this route — callers treat that as "unknown", not "broken".
|
|
552
618
|
*/
|
|
619
|
+
/**
|
|
620
|
+
* `POST /v1/workspaces/:workspace/github/ingest` (Task 6) — manual/
|
|
621
|
+
* backfill mirror of a PR/issue's `github.com/user-attachments` media
|
|
622
|
+
* into the workspace. Only ever had the canonical `/v1/workspaces`
|
|
623
|
+
* route (no old bearer-only alias) — unlike the other `github/*` client
|
|
624
|
+
* methods above, which moved onto it in issue #613.
|
|
625
|
+
*/
|
|
626
|
+
async ingestGithub(input) {
|
|
627
|
+
const body = input.kind === "pull"
|
|
628
|
+
? { repo: input.repo, pr: input.num }
|
|
629
|
+
: { repo: input.repo, issue: input.num };
|
|
630
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/ingest`, {
|
|
631
|
+
body: new TextEncoder().encode(JSON.stringify(body)),
|
|
632
|
+
headers: { "Content-Type": "application/json" },
|
|
633
|
+
});
|
|
634
|
+
},
|
|
553
635
|
async githubHealth() {
|
|
554
|
-
return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/health`);
|
|
636
|
+
return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/health`);
|
|
555
637
|
},
|
|
556
638
|
/**
|
|
557
639
|
* Self-serve unlink (issue #318): removes `repo`'s binding, but only if
|
|
@@ -561,7 +643,7 @@ export function createUploadsClient(config) {
|
|
|
561
643
|
* older/self-hosted server without this route.
|
|
562
644
|
*/
|
|
563
645
|
async githubLinkUnlink(repo) {
|
|
564
|
-
return request("DELETE", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
|
|
646
|
+
return request("DELETE", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
|
|
565
647
|
},
|
|
566
648
|
async health() {
|
|
567
649
|
return request("GET", `${config.apiUrl}/health`, { auth: false });
|
|
@@ -2,12 +2,12 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { extractDashValue, 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, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, } from "../commands.js";
|
|
5
|
+
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, resolveGhPrefixSafe, } 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 {
|
|
10
|
+
import { deriveRepoSlugFromGit } from "../keys.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";
|
|
@@ -360,7 +360,11 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
360
360
|
// Explicit input (--meta plus the dedicated flags) wins over capture facts.
|
|
361
361
|
const explicitMeta = { ...metaExtras, ...stateAppMetaFromFlags(parsed.flags) };
|
|
362
362
|
const deriveMeta = derivedMetaEnabled(parsed.flags, putDefaults);
|
|
363
|
-
const
|
|
363
|
+
const repoSlug = deriveMeta && !noGit ? deriveRepoSlugFromGit(run) : undefined;
|
|
364
|
+
const withFacts = mergeDerivedMeta(explicitMeta, {
|
|
365
|
+
...(deriveMeta ? safeCaptureFacts(target, viewport, colorScheme) : {}),
|
|
366
|
+
...(repoSlug ? { repo: repoSlug } : {}),
|
|
367
|
+
});
|
|
364
368
|
let metadata = withFacts;
|
|
365
369
|
if (ghTarget) {
|
|
366
370
|
metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
|
|
@@ -443,15 +447,28 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
443
447
|
}
|
|
444
448
|
const repo = flagString(parsed.flags, "--repo") ?? putDefaults.repo;
|
|
445
449
|
const ref = flagString(parsed.flags, "--ref") ?? putDefaults.ref;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
// Resolved once (issue #631), only when it's actually needed for the
|
|
451
|
+
// upload about to happen (never for the noUpload/no-target bailouts
|
|
452
|
+
// above) — never per file (screenshot only ever uploads one).
|
|
453
|
+
const ghPrefix = ghTarget
|
|
454
|
+
? await resolveGhPrefixSafe(ctx.client, {
|
|
455
|
+
repo: ghTarget.repo,
|
|
456
|
+
target: { kind: ghTarget.kind, num: ghTarget.num },
|
|
457
|
+
})
|
|
458
|
+
: stagingTarget !== undefined
|
|
459
|
+
? await resolveGhPrefixSafe(ctx.client, {
|
|
460
|
+
repo: stagingTarget.repo,
|
|
461
|
+
branch: stagingTarget.branch,
|
|
462
|
+
})
|
|
463
|
+
: undefined;
|
|
449
464
|
const alt = altFlag ?? basename(captured.filename);
|
|
450
465
|
const { result, prepared, markdown } = await uploadPreparedImage(ctx.client, finalPng, captured.filename, {
|
|
451
466
|
frame: frameOpts,
|
|
452
467
|
optimize: optimizeOpts,
|
|
453
468
|
ghTarget,
|
|
454
|
-
|
|
469
|
+
ghBranchTarget: stagingTarget,
|
|
470
|
+
ghPrefix,
|
|
471
|
+
key: keyHint,
|
|
455
472
|
prefix: resolvedPrefix ?? putDefaults.prefix,
|
|
456
473
|
repo,
|
|
457
474
|
ref,
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PutResult, type UploadsClient } from "./client.js";
|
|
1
|
+
import { type PutResult, type ResolveGhPrefixOptions, type ResolveGhPrefixResult, type UploadsClient } from "./client.js";
|
|
2
2
|
import { type CommandFlags } from "./cli-args.js";
|
|
3
3
|
import { type ResolvedConfig } from "./config.js";
|
|
4
4
|
import { type GhTarget } from "./github.js";
|
|
@@ -11,6 +11,42 @@ import type { DetectRoots } from "./screenshot-local.js";
|
|
|
11
11
|
export declare const UPLOAD_BATCH_CONCURRENCY = 8;
|
|
12
12
|
/** @deprecated Use UPLOAD_BATCH_CONCURRENCY. */
|
|
13
13
|
export declare const ATTACH_CONCURRENCY = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Fail-open wrapper around `client.resolveGhPrefix` (issue #631): resolves to
|
|
16
|
+
* `{ mode: "plain" }` on ANY failure — an HTTP/network error (already handled
|
|
17
|
+
* inside `resolveGhPrefix` itself), or a self-hosted/older server or test
|
|
18
|
+
* double that lacks the method entirely (the outer try/catch here). Never
|
|
19
|
+
* blocks an upload or a read-back, and never logs — this is not an error.
|
|
20
|
+
* Call once per command invocation and thread the resolved mode through
|
|
21
|
+
* (uploadPuts/uploadAttachments/uploadBranchAttachments each do this once
|
|
22
|
+
* internally, ahead of their per-file loop); `resolveGhPrefix` itself also
|
|
23
|
+
* caches per-process by repo+branch+target, so repeat callers in the same
|
|
24
|
+
* process (e.g. attach's promote + comment-sync + upload, all for the same
|
|
25
|
+
* target) cost one request total.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveGhPrefixSafe(client: UploadsClient, opts: ResolveGhPrefixOptions): Promise<ResolveGhPrefixResult>;
|
|
28
|
+
/**
|
|
29
|
+
* The list of prefixes to fan a multi-prefix list/gather across (issue
|
|
30
|
+
* #631): the plain prefix plus every active private prefix, if any — a
|
|
31
|
+
* repo's history can be split across the plain shape and MULTIPLE private
|
|
32
|
+
* prefixes (e.g. a prefix rotation, or the repo went private after some
|
|
33
|
+
* files were uploaded), not just the currently-resolved one. Falls back to
|
|
34
|
+
* `[prefixId]` when the server omits `activePrefixIds` (optional field — an
|
|
35
|
+
* older/self-hosted worker), so a private repo is never listed as zero
|
|
36
|
+
* private prefixes. Collapses to `[plainPrefix]` in plain mode, so callers
|
|
37
|
+
* that special-case a single-prefix array stay byte-identical to pre-#631.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ghListPrefixes(plainPrefix: string, ghPrefix: ResolveGhPrefixResult, privatePrefixFor: (prefixId: string) => string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Merge-list helper for a multi-prefix fan-out: runs `fetchItems` per prefix
|
|
42
|
+
* concurrently and concatenates in prefix order. Encodes the first-prefix-
|
|
43
|
+
* only cursor rule once — a cursor is opaque and scoped to the prefix it was
|
|
44
|
+
* minted against, so a multi-prefix merge only ever hands it to the FIRST
|
|
45
|
+
* prefix; every other prefix always starts from its own beginning (undefined
|
|
46
|
+
* cursor), or a cursor minted for one prefix's keyspace would get replayed
|
|
47
|
+
* against a different one.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ghMergedList<T>(prefixes: readonly string[], cursor: string | undefined, fetchItems: (prefix: string, cursor: string | undefined) => Promise<T[]>): Promise<T[]>;
|
|
14
50
|
export { formatUsageHuman } from "./format-usage.js";
|
|
15
51
|
export interface CliContext {
|
|
16
52
|
config: ResolvedConfig;
|
|
@@ -90,6 +126,14 @@ export interface UploadPreparedImageOptions {
|
|
|
90
126
|
* --branch` for the same filename via `ghBranchAttachmentKey`.
|
|
91
127
|
*/
|
|
92
128
|
ghBranchTarget?: BranchTarget;
|
|
129
|
+
/**
|
|
130
|
+
* Resolved GitHub-key mode (issue #631), from a single upstream
|
|
131
|
+
* `resolveGhPrefixSafe` call — never resolved here. Passed straight to
|
|
132
|
+
* `ghAttachmentKeyForMode`/`ghBranchAttachmentKeyForMode`, which own the
|
|
133
|
+
* plain-vs-private branch. Ignored when neither `ghTarget` nor
|
|
134
|
+
* `ghBranchTarget` is set.
|
|
135
|
+
*/
|
|
136
|
+
ghPrefix?: ResolveGhPrefixResult;
|
|
93
137
|
key?: string;
|
|
94
138
|
prefix?: string;
|
|
95
139
|
repo?: string;
|
|
@@ -457,6 +501,7 @@ export declare function runFind(ctx: CliContext, args: string[], help?: boolean)
|
|
|
457
501
|
export declare function runMeta(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
458
502
|
export declare function runDelete(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
459
503
|
export declare function runComment(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
504
|
+
export declare function runIngest(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
460
505
|
export declare function runGithub(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
461
506
|
export declare function runUsage(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
462
507
|
export declare function runReconcile(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|