@buildinternet/uploads 0.53.0 → 0.55.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 +26 -4
- package/dist/cli-catalog.js +15 -0
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +10 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +24 -0
- package/dist/commands/docs.d.ts +8 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/feed.d.ts +2 -0
- package/dist/commands/feed.js +133 -0
- package/dist/comment-render.generated.d.ts +1 -1
- package/dist/docs.d.ts +52 -0
- package/dist/docs.js +439 -0
- package/dist/github.d.ts +12 -0
- package/dist/github.js +40 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp/docs-tool.d.ts +4 -0
- package/dist/mcp/docs-tool.js +35 -0
- package/dist/mcp/output-schemas.d.ts +2 -0
- package/dist/mcp/output-schemas.js +53 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.d.ts +2 -1
- package/dist/mcp/tools.js +108 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ uploads put --url https://cdn.example/shot.png --pr 123
|
|
|
31
31
|
uploads put --url http://localhost:4321/shot.png
|
|
32
32
|
uploads gallery create --title "Release screenshots"
|
|
33
33
|
uploads put ./after.png --gallery gal_example
|
|
34
|
+
uploads feed create --repo owner/repo
|
|
35
|
+
uploads feed create --repo owner/repo --pr 123
|
|
34
36
|
# custom metadata (queryable): page URL, in-app path, which surface
|
|
35
37
|
uploads put ./shot.png --meta url=https://app.example/settings --meta path=/settings --meta app=web
|
|
36
38
|
uploads meta get screenshots/myapp/42/shot.webp
|
|
@@ -43,9 +45,9 @@ Inside this monorepo only, `pnpm uploads …` builds the package first so you pi
|
|
|
43
45
|
up local source; product docs and PR “how to try it” examples should use the
|
|
44
46
|
global `uploads` form above.
|
|
45
47
|
|
|
46
|
-
Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
48
|
+
Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `feed`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
47
49
|
`reconcile`, `purge-expired`, `setup`, `install`, `login`, `whoami` (alias `status`),
|
|
48
|
-
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `mcp`,
|
|
50
|
+
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `docs`, `mcp`,
|
|
49
51
|
`completion`.
|
|
50
52
|
|
|
51
53
|
**Help:** bare `uploads` / `uploads help` / `--help` shows essentials; use
|
|
@@ -192,13 +194,33 @@ When adding several keys, `uploads gallery add` processes them sequentially and
|
|
|
192
194
|
individual failures in `--json` output. Gallery item updates use the API's current version to
|
|
193
195
|
avoid overwriting concurrent changes.
|
|
194
196
|
|
|
197
|
+
## Change feeds
|
|
198
|
+
|
|
199
|
+
A feed is a public newest-first page of screenshots already tagged with a GitHub `owner/repo`,
|
|
200
|
+
or with one pull request or issue. Same product — `--pr` / `--issue` / `--github` is one extra
|
|
201
|
+
filter. It is a live query, not a curated gallery. Use a gallery when you pick the files; use
|
|
202
|
+
a repo feed for the latest shots across a repo; use a PR feed when reviewers should see only
|
|
203
|
+
that pull request. The API returns the canonical public URL.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uploads feed create --repo owner/repo
|
|
207
|
+
uploads feed create --repo owner/repo --pr 123
|
|
208
|
+
uploads feed create --github owner/repo#123
|
|
209
|
+
uploads feed create --repo owner/repo --path /settings
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Creating the same scope again returns the existing URL. Anyone who knows that URL can
|
|
213
|
+
view the feed. Each shot also has `/c/<id>/<item>` with previous / next. Syncing a
|
|
214
|
+
managed PR comment creates the PR feed if needed and points image clicks at that pager.
|
|
215
|
+
MCP: `feed_create` (`repo`, plus `pr` / `issue` / `github` / `path`) and `feed_get`.
|
|
216
|
+
|
|
195
217
|
Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
|
|
196
218
|
|
|
197
219
|
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
198
220
|
|
|
199
221
|
## MCP server
|
|
200
222
|
|
|
201
|
-
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows
|
|
223
|
+
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows (`gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, `gallery_find_by_reference`) and change feeds (`feed_create`, `feed_get` — pass `pr` or `github` to scope a feed to one pull request). Gallery and feed tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `get_metadata`, `set_metadata`, `find_files`, `usage`, `reconcile`, `purge_expired`, `comment`, `whoami`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. `put` and `attach` accept a `metadata` param (same `gh.*` auto-injection as the CLI's `attach`); `get_metadata`, `set_metadata`, and `find_files` mirror `uploads meta get` / `meta set` / `find`. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`whoami` needs no auth).
|
|
202
224
|
|
|
203
225
|
```json
|
|
204
226
|
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
@@ -208,7 +230,7 @@ Or with `UPLOADS_TOKEN`/`UPLOADS_WORKSPACE` in the environment or user config. C
|
|
|
208
230
|
|
|
209
231
|
The MCP Registry lists this server as `sh.uploads/mcp`.
|
|
210
232
|
|
|
211
|
-
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations (including `get_metadata` / `set_metadata` / `find_files`) plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `
|
|
233
|
+
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations (including `get_metadata` / `set_metadata` / `find_files`) plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, `gallery_find_by_reference`, `feed_create`, and `feed_get`; all use the same bearer-token workspace scopes and gallery/feed URLs come from the API — see `apps/mcp` in the repo. The hosted `put` also accepts a `metadata` param. `uploads install` registers the skills + hosted MCP with whichever of Claude Code, Codex, and Grok are on PATH (a missing CLI is skipped) + Grok/Cursor hooks (short progress; `--verbose` for underlying output). Claude and Codex use their plugins for the same pre-PR screenshot reminder (`uploads hook pre-pr-screenshot`). Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
|
|
212
234
|
|
|
213
235
|
## Programmatic use
|
|
214
236
|
|
package/dist/cli-catalog.js
CHANGED
|
@@ -156,6 +156,16 @@ export const ROOT_COMMANDS = [
|
|
|
156
156
|
{ name: "unlink", summary: "Unlink a gallery from GitHub" },
|
|
157
157
|
],
|
|
158
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: "feed",
|
|
161
|
+
summary: "Create a public newest-first screenshot feed for a GitHub repo or pull request",
|
|
162
|
+
subcommands: [
|
|
163
|
+
{ name: "create", summary: "Create or reuse a repo or PR change feed" },
|
|
164
|
+
{ name: "show", summary: "Show a feed" },
|
|
165
|
+
{ name: "list", summary: "List feeds" },
|
|
166
|
+
{ name: "delete", summary: "Delete a feed record" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
159
169
|
{
|
|
160
170
|
name: "comment",
|
|
161
171
|
summary: "Create/update a PR/issue attachments comment (via gh)",
|
|
@@ -237,6 +247,11 @@ export const ROOT_COMMANDS = [
|
|
|
237
247
|
name: "changelog",
|
|
238
248
|
summary: "Show recent product updates (and a link to the full changelog)",
|
|
239
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: "docs",
|
|
252
|
+
summary: "Search public uploads.sh docs (or fetch one page as markdown)",
|
|
253
|
+
usage: "docs [query]",
|
|
254
|
+
},
|
|
240
255
|
{
|
|
241
256
|
name: "login",
|
|
242
257
|
summary: "Sign in via browser (or an enrollment code) and save credentials",
|
package/dist/cli-help.js
CHANGED
|
@@ -165,6 +165,7 @@ ${section(style, "Examples:")}
|
|
|
165
165
|
${style.command("uploads attach")} ./artifact.zip --issue 45 --no-comment
|
|
166
166
|
${style.command("uploads attach")} ./shot.png --meta path=/settings --state after
|
|
167
167
|
${style.command("uploads gallery")} create --title "Release screenshots"
|
|
168
|
+
${style.command("uploads feed")} create --repo owner/repo
|
|
168
169
|
${style.command("uploads doctor")}
|
|
169
170
|
${style.command("uploads logout")}
|
|
170
171
|
${style.command("uploads --version")}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { commandSummary, suggestCommand } from "./cli-suggest.js";
|
|
|
7
7
|
import { writeJson } from "./io.js";
|
|
8
8
|
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
9
9
|
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runIngest, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
10
|
+
import { runFeed } from "./commands/feed.js";
|
|
10
11
|
import { runConfig } from "./commands/config.js";
|
|
11
12
|
import { runSetup } from "./commands/setup.js";
|
|
12
13
|
import { runLogin } from "./commands/login.js";
|
|
@@ -17,6 +18,7 @@ import { runInstall } from "./commands/install.js";
|
|
|
17
18
|
import { runHook } from "./commands/hook.js";
|
|
18
19
|
import { runUpdate } from "./commands/update.js";
|
|
19
20
|
import { runChangelog } from "./commands/changelog.js";
|
|
21
|
+
import { runDocs } from "./commands/docs.js";
|
|
20
22
|
import { runCompletion } from "./commands/completion.js";
|
|
21
23
|
import { runLogout, runWhoami } from "./commands/session.js";
|
|
22
24
|
import { runTelemetry } from "./commands/telemetry.js";
|
|
@@ -243,6 +245,10 @@ export async function runCli(argv) {
|
|
|
243
245
|
// Public feed — no token, independent of the API origin.
|
|
244
246
|
code = await runChangelog(cmdArgs, { json }, showHelp);
|
|
245
247
|
break;
|
|
248
|
+
case "docs":
|
|
249
|
+
// Public catalog — no token, independent of the API origin.
|
|
250
|
+
code = await runDocs(cmdArgs, { json }, showHelp);
|
|
251
|
+
break;
|
|
246
252
|
case "config":
|
|
247
253
|
code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
248
254
|
break;
|
|
@@ -304,6 +310,7 @@ export async function runCli(argv) {
|
|
|
304
310
|
case "staged":
|
|
305
311
|
case "screenshot":
|
|
306
312
|
case "gallery":
|
|
313
|
+
case "feed":
|
|
307
314
|
case "list":
|
|
308
315
|
case "find":
|
|
309
316
|
case "meta":
|
|
@@ -332,6 +339,9 @@ export async function runCli(argv) {
|
|
|
332
339
|
case "gallery":
|
|
333
340
|
code = await runGallery(ctx, cmdArgs, showHelp);
|
|
334
341
|
break;
|
|
342
|
+
case "feed":
|
|
343
|
+
code = await runFeed(ctx, cmdArgs, showHelp);
|
|
344
|
+
break;
|
|
335
345
|
case "comment":
|
|
336
346
|
code = await runComment(ctx, cmdArgs, showHelp);
|
|
337
347
|
break;
|
package/dist/client.d.ts
CHANGED
|
@@ -257,6 +257,59 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
257
257
|
limit?: number;
|
|
258
258
|
cursor?: string;
|
|
259
259
|
}
|
|
260
|
+
/** A workspace-owned, query-backed public repo change feed. */
|
|
261
|
+
export interface FeedItem {
|
|
262
|
+
id: string;
|
|
263
|
+
objectKey: string;
|
|
264
|
+
filename: string;
|
|
265
|
+
status: "available" | "missing" | "withheld";
|
|
266
|
+
url: string | null;
|
|
267
|
+
embedUrl: string | null;
|
|
268
|
+
/** Canonical `/c/<id>/<item>` page when the API computed one. */
|
|
269
|
+
pageUrl?: string;
|
|
270
|
+
contentType: string | null;
|
|
271
|
+
size: number | null;
|
|
272
|
+
uploaded: string | null;
|
|
273
|
+
modified: string | null;
|
|
274
|
+
path: string | null;
|
|
275
|
+
state: string | null;
|
|
276
|
+
posterUrl?: string;
|
|
277
|
+
videoDimensions?: {
|
|
278
|
+
width: number;
|
|
279
|
+
height: number;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export interface Feed {
|
|
283
|
+
id: string;
|
|
284
|
+
/** Canonical public URL returned by the API; clients must not construct it. */
|
|
285
|
+
url: string;
|
|
286
|
+
workspace: string;
|
|
287
|
+
repo: string;
|
|
288
|
+
path: string | null;
|
|
289
|
+
number: number | null;
|
|
290
|
+
kind: "pull" | "issue" | null;
|
|
291
|
+
title: string;
|
|
292
|
+
createdAt: string;
|
|
293
|
+
updatedAt: string;
|
|
294
|
+
items: FeedItem[];
|
|
295
|
+
}
|
|
296
|
+
export type FeedSummary = Omit<Feed, "items">;
|
|
297
|
+
export interface FeedListOptions {
|
|
298
|
+
limit?: number;
|
|
299
|
+
cursor?: string;
|
|
300
|
+
}
|
|
301
|
+
export interface FeedListResult {
|
|
302
|
+
feeds: FeedSummary[];
|
|
303
|
+
nextCursor: string | null;
|
|
304
|
+
}
|
|
305
|
+
export interface CreateFeedOptions {
|
|
306
|
+
repo: string;
|
|
307
|
+
path?: string | null;
|
|
308
|
+
number?: number | null;
|
|
309
|
+
kind?: "pull" | "issue" | null;
|
|
310
|
+
pr?: number | null;
|
|
311
|
+
issue?: number | null;
|
|
312
|
+
}
|
|
260
313
|
/**
|
|
261
314
|
* Reasons the bot did not post. The CLI falls back to the local `gh` path
|
|
262
315
|
* for all of these except `not_authorized` (issue #297 baseline control):
|
|
@@ -734,6 +787,13 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
734
787
|
id: string;
|
|
735
788
|
}>;
|
|
736
789
|
findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
|
|
790
|
+
createFeed(opts: CreateFeedOptions): Promise<Feed>;
|
|
791
|
+
getFeed(id: string): Promise<Feed>;
|
|
792
|
+
listFeeds(opts?: FeedListOptions): Promise<FeedListResult>;
|
|
793
|
+
deleteFeed(id: string): Promise<{
|
|
794
|
+
deleted: boolean;
|
|
795
|
+
id: string;
|
|
796
|
+
}>;
|
|
737
797
|
/**
|
|
738
798
|
* Upsert the managed attachments comment. `resync: true` marks an
|
|
739
799
|
* explicit "make the comment state correct" call (`uploads comment`), so
|
package/dist/client.js
CHANGED
|
@@ -307,6 +307,9 @@ function usageBase(config) {
|
|
|
307
307
|
function galleriesBase(config) {
|
|
308
308
|
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/galleries`;
|
|
309
309
|
}
|
|
310
|
+
function feedsBase(config) {
|
|
311
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/feeds`;
|
|
312
|
+
}
|
|
310
313
|
function mapApiError(status, error, code, requiredScope, existingUrl) {
|
|
311
314
|
const normalized = error.toLowerCase();
|
|
312
315
|
if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
|
|
@@ -652,6 +655,27 @@ export function createUploadsClient(config) {
|
|
|
652
655
|
params.set("cursor", opts.cursor);
|
|
653
656
|
return request("GET", galleriesBase(config) + "/by-reference?" + params);
|
|
654
657
|
},
|
|
658
|
+
async createFeed(opts) {
|
|
659
|
+
return request("POST", feedsBase(config), {
|
|
660
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
async getFeed(id) {
|
|
665
|
+
return request("GET", `${feedsBase(config)}/${encodeURIComponent(id)}`);
|
|
666
|
+
},
|
|
667
|
+
async listFeeds(opts = {}) {
|
|
668
|
+
const params = new URLSearchParams();
|
|
669
|
+
if (opts.limit != null)
|
|
670
|
+
params.set("limit", String(opts.limit));
|
|
671
|
+
if (opts.cursor)
|
|
672
|
+
params.set("cursor", opts.cursor);
|
|
673
|
+
const qs = params.toString();
|
|
674
|
+
return request("GET", `${feedsBase(config)}${qs ? `?${qs}` : ""}`);
|
|
675
|
+
},
|
|
676
|
+
async deleteFeed(id) {
|
|
677
|
+
return request("DELETE", `${feedsBase(config)}/${encodeURIComponent(id)}`);
|
|
678
|
+
},
|
|
655
679
|
/**
|
|
656
680
|
* Upsert the managed attachments comment. `resync: true` marks an
|
|
657
681
|
* explicit "make the comment state correct" call (`uploads comment`), so
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SearchDocsOptions } from "../docs.js";
|
|
2
|
+
export interface RunDocsOptions {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
fetch?: SearchDocsOptions["fetchImpl"];
|
|
5
|
+
url?: string;
|
|
6
|
+
catalog?: SearchDocsOptions["catalog"];
|
|
7
|
+
}
|
|
8
|
+
export declare function runDocs(args: string[], opts?: RunDocsOptions, help?: boolean): Promise<number>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
2
|
+
import { UploadsError } from "../errors.js";
|
|
3
|
+
import { DEFAULT_DOCS_LIMIT, MAX_DOCS_LIMIT, formatDocsHuman, searchDocs, } from "../docs.js";
|
|
4
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
+
import { writeJson, writeStdout } from "../io.js";
|
|
6
|
+
const DOCS_HELP = `uploads docs — search public uploads.sh documentation
|
|
7
|
+
|
|
8
|
+
Lists the docs catalog, searches by topic, or fetches one page as markdown.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
uploads docs [query…] [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--page <slug> Fetch the full markdown of one page (slug, path, or URL)
|
|
15
|
+
--limit <n> Number of search hits to show (default ${DEFAULT_DOCS_LIMIT}, max ${MAX_DOCS_LIMIT})
|
|
16
|
+
--json JSON on stdout (also accepts global --json)
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
uploads docs
|
|
20
|
+
uploads docs stage before a PR
|
|
21
|
+
uploads docs attach
|
|
22
|
+
uploads docs --page agents
|
|
23
|
+
uploads docs --json "github app"
|
|
24
|
+
`;
|
|
25
|
+
export async function runDocs(args, opts = {}, help = false) {
|
|
26
|
+
const parsed = parseCommandArgs(args);
|
|
27
|
+
if (help || parsed.help) {
|
|
28
|
+
writeCommandHelp(DOCS_HELP);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const json = Boolean(opts.json) || flagBool(parsed.flags, "--json");
|
|
32
|
+
const page = flagString(parsed.flags, "--page");
|
|
33
|
+
const limit = flagInt(parsed.flags, "--limit", "--limit");
|
|
34
|
+
if (limit !== undefined && limit > MAX_DOCS_LIMIT) {
|
|
35
|
+
throw new UsageError(`--limit must be ${MAX_DOCS_LIMIT} or less (got ${limit})`, {
|
|
36
|
+
example: `uploads docs --limit ${MAX_DOCS_LIMIT}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const query = parsed.positionals.join(" ").trim();
|
|
40
|
+
if (page && query) {
|
|
41
|
+
throw new UsageError("pass a search query or --page, not both", {
|
|
42
|
+
example: "uploads docs --page attach",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
let doc;
|
|
46
|
+
try {
|
|
47
|
+
doc = await searchDocs({
|
|
48
|
+
query: query || undefined,
|
|
49
|
+
page: page || undefined,
|
|
50
|
+
limit,
|
|
51
|
+
fetchImpl: opts.fetch,
|
|
52
|
+
url: opts.url,
|
|
53
|
+
catalog: opts.catalog,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof UploadsError && err.code === "USAGE") {
|
|
58
|
+
throw new UsageError(err.message, { example: "uploads docs --page attach" });
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
if (json)
|
|
63
|
+
await writeJson(doc);
|
|
64
|
+
else
|
|
65
|
+
await writeStdout(formatDocsHuman(doc));
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
2
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
3
|
+
import { parseGithubIssueRef } from "../github.js";
|
|
4
|
+
import { resolveRepo } from "../github-gh.js";
|
|
5
|
+
import { writeJson, writeStdout } from "../io.js";
|
|
6
|
+
const FEED_HELP = `uploads feed <command> [args]
|
|
7
|
+
|
|
8
|
+
A public newest-first feed of screenshots tagged with a GitHub owner/repo,
|
|
9
|
+
or one pull request / issue. Same product — pass --pr, --issue, or --github
|
|
10
|
+
to scope it. A gallery is a hand-picked list; a feed is live. Anyone who
|
|
11
|
+
knows the URL can view it. Creating the same scope again returns the
|
|
12
|
+
existing feed.
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
create [--repo <owner/repo>] [--pr <n> | --issue <n> | --github <ref>] [--path <page-path>]
|
|
16
|
+
show <feed-id>
|
|
17
|
+
list [--limit <n>] [--cursor <c>]
|
|
18
|
+
delete <feed-id>
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
uploads feed create
|
|
22
|
+
uploads feed create --repo acme/app
|
|
23
|
+
uploads feed create --repo acme/app --pr 123
|
|
24
|
+
uploads feed create --github acme/app#123
|
|
25
|
+
uploads feed create --repo acme/app --path /settings
|
|
26
|
+
uploads feed show feed_example
|
|
27
|
+
`;
|
|
28
|
+
function resolveCreateOptions(parsed) {
|
|
29
|
+
const github = flagString(parsed.flags, "--github");
|
|
30
|
+
const pr = flagInt(parsed.flags, "--pr", "--pr");
|
|
31
|
+
const issue = flagInt(parsed.flags, "--issue", "--issue");
|
|
32
|
+
const path = flagString(parsed.flags, "--path");
|
|
33
|
+
const repoFlag = flagString(parsed.flags, "--repo");
|
|
34
|
+
if (pr != null && issue != null) {
|
|
35
|
+
throw new UsageError("--pr and --issue are mutually exclusive", {
|
|
36
|
+
example: "uploads feed create --repo owner/repo --pr 123",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (github && (pr != null || issue != null)) {
|
|
40
|
+
throw new UsageError("--github cannot be combined with --pr or --issue", {
|
|
41
|
+
example: "uploads feed create --github owner/repo#123",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (github) {
|
|
45
|
+
const ref = parseGithubIssueRef(github);
|
|
46
|
+
if (!ref) {
|
|
47
|
+
throw new UsageError("--github must be owner/repo#number or a GitHub issue/PR URL", {
|
|
48
|
+
example: "uploads feed create --github owner/repo#123",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (repoFlag && repoFlag.trim().toLowerCase() !== ref.repo) {
|
|
52
|
+
throw new UsageError("--repo does not match --github", {
|
|
53
|
+
example: "uploads feed create --github owner/repo#123",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
repo: ref.repo,
|
|
58
|
+
path,
|
|
59
|
+
number: ref.number,
|
|
60
|
+
kind: ref.kind,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
repo: resolveRepo(repoFlag).toLowerCase(),
|
|
65
|
+
path,
|
|
66
|
+
...(pr != null ? { number: pr, kind: "pull" } : {}),
|
|
67
|
+
...(issue != null ? { number: issue, kind: "issue" } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function runFeed(ctx, args, help = false) {
|
|
71
|
+
const parsed = parseCommandArgs(args);
|
|
72
|
+
const action = parsed.positionals[0];
|
|
73
|
+
if (help || parsed.help) {
|
|
74
|
+
writeCommandHelp(FEED_HELP);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (!action) {
|
|
78
|
+
throw new UsageError("feed requires a subcommand: create, show, list, or delete", {
|
|
79
|
+
example: "uploads feed create --repo owner/repo --pr 123",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
switch (action) {
|
|
83
|
+
case "create": {
|
|
84
|
+
const feed = await ctx.client.createFeed(resolveCreateOptions(parsed));
|
|
85
|
+
if (ctx.json)
|
|
86
|
+
await writeJson(feed);
|
|
87
|
+
else
|
|
88
|
+
await writeStdout(`${feed.url}\n`);
|
|
89
|
+
if (!ctx.quiet && !ctx.json)
|
|
90
|
+
process.stderr.write("warning: feeds are public to anyone with the URL\n");
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case "show": {
|
|
94
|
+
const id = parsed.positionals[1];
|
|
95
|
+
if (!id)
|
|
96
|
+
throw new UsageError("feed show requires a feed ID");
|
|
97
|
+
const feed = await ctx.client.getFeed(id);
|
|
98
|
+
if (ctx.json)
|
|
99
|
+
await writeJson(feed);
|
|
100
|
+
else
|
|
101
|
+
await writeStdout(`${feed.url}\n`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
case "list": {
|
|
105
|
+
const page = await ctx.client.listFeeds({
|
|
106
|
+
limit: flagInt(parsed.flags, "--limit", "--limit"),
|
|
107
|
+
cursor: flagString(parsed.flags, "--cursor"),
|
|
108
|
+
});
|
|
109
|
+
if (ctx.json)
|
|
110
|
+
await writeJson(page);
|
|
111
|
+
else {
|
|
112
|
+
for (const feed of page.feeds)
|
|
113
|
+
await writeStdout(`${feed.id} ${feed.url} ${feed.title}\n`);
|
|
114
|
+
if (page.nextCursor)
|
|
115
|
+
process.stderr.write(`cursor: ${page.nextCursor}\n`);
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
case "delete": {
|
|
120
|
+
const id = parsed.positionals[1];
|
|
121
|
+
if (!id)
|
|
122
|
+
throw new UsageError("feed delete requires a feed ID");
|
|
123
|
+
const result = await ctx.client.deleteFeed(id);
|
|
124
|
+
if (ctx.json)
|
|
125
|
+
await writeJson(result);
|
|
126
|
+
else if (!ctx.quiet)
|
|
127
|
+
process.stderr.write(`deleted feed ${result.id}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
throw new UsageError(`unknown feed command: ${action} (expected create, show, list, or delete)`, { example: "uploads feed create --repo owner/repo --pr 123" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -90,7 +90,7 @@ export interface AttachmentItem {
|
|
|
90
90
|
url: string | null;
|
|
91
91
|
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
92
92
|
embedUrl?: string | null;
|
|
93
|
-
/**
|
|
93
|
+
/** Preferred click-through (PR feed item, `/f/` file page, or equivalent). Falls back to `url`. */
|
|
94
94
|
pageUrl?: string | null;
|
|
95
95
|
/**
|
|
96
96
|
* The only canonical metadata the managed comment renders (issue #365).
|
package/dist/docs.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const DOCS_HUB_URL = "https://uploads.sh/docs";
|
|
2
|
+
export declare const DOCS_JSON_URL = "https://uploads.sh/docs.json";
|
|
3
|
+
export declare const DEFAULT_DOCS_LIMIT = 5;
|
|
4
|
+
export declare const MAX_DOCS_LIMIT = 50;
|
|
5
|
+
export declare const DOCS_SNIPPET_MAX_CHARS = 1500;
|
|
6
|
+
export declare const DOCS_PAGE_MAX_CHARS = 50000;
|
|
7
|
+
export type DocsCatalogPage = {
|
|
8
|
+
page: string;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
aliases: string[];
|
|
13
|
+
};
|
|
14
|
+
export type DocsCatalog = {
|
|
15
|
+
url: string;
|
|
16
|
+
pages: DocsCatalogPage[];
|
|
17
|
+
};
|
|
18
|
+
export type DocsSearchHit = {
|
|
19
|
+
title: string;
|
|
20
|
+
url: string;
|
|
21
|
+
page: string;
|
|
22
|
+
snippet: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
truncated?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type DocsSearchDocument = {
|
|
27
|
+
url: string;
|
|
28
|
+
query?: string;
|
|
29
|
+
results: DocsSearchHit[];
|
|
30
|
+
total: number;
|
|
31
|
+
};
|
|
32
|
+
export type SearchDocsOptions = {
|
|
33
|
+
query?: string;
|
|
34
|
+
page?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
url?: string;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
userAgent?: string;
|
|
40
|
+
/** Skip the network catalog fetch (tests). */
|
|
41
|
+
catalog?: DocsCatalog;
|
|
42
|
+
};
|
|
43
|
+
/** Built-in catalog used when /docs.json is unreachable. */
|
|
44
|
+
export declare const FALLBACK_DOCS_CATALOG: DocsCatalog;
|
|
45
|
+
export declare function parseDocsCatalog(raw: unknown): DocsCatalog;
|
|
46
|
+
export declare function resolveDocsPage(catalog: DocsCatalog, raw: string): DocsCatalogPage | undefined;
|
|
47
|
+
export declare function rankDocsPages(catalog: DocsCatalog, query: string): DocsCatalogPage[];
|
|
48
|
+
export declare function truncateDocsSnippet(text: string, maxChars?: number): string;
|
|
49
|
+
/** Human terminal output for catalog/search hits. A fetched page prints its body. */
|
|
50
|
+
export declare function formatDocsHuman(doc: DocsSearchDocument): string;
|
|
51
|
+
export declare function loadDocsCatalog(opts?: SearchDocsOptions): Promise<DocsCatalog>;
|
|
52
|
+
export declare function searchDocs(opts?: SearchDocsOptions): Promise<DocsSearchDocument>;
|