@buildinternet/uploads 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +79 -64
- package/bin/uploads.js +0 -0
- package/dist/cli.js +81 -48
- package/dist/client.d.ts +68 -1
- package/dist/client.js +75 -12
- package/dist/commands/admin-enrollment.d.ts +7 -0
- package/dist/commands/admin-enrollment.js +59 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +133 -0
- package/dist/commands/login.d.ts +11 -0
- package/dist/commands/login.js +160 -0
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +39 -0
- package/dist/commands/setup.js +6 -16
- package/dist/commands.d.ts +47 -0
- package/dist/commands.js +226 -133
- package/dist/config-file.js +14 -2
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/io.d.ts +3 -0
- package/dist/io.js +9 -0
- package/dist/mcp/args.d.ts +4 -0
- package/dist/mcp/args.js +26 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +109 -0
- package/dist/mcp/stdio.d.ts +3 -0
- package/dist/mcp/stdio.js +14 -0
- package/dist/mcp/tools.d.ts +10 -0
- package/dist/mcp/tools.js +386 -0
- package/package.json +63 -60
package/dist/commands.js
CHANGED
|
@@ -5,66 +5,58 @@ import { parseCommandArgs, flagString, flagBool, flagInt, UsageError, } from "./
|
|
|
5
5
|
import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./config.js";
|
|
6
6
|
import { buildMarkdown } from "./embed.js";
|
|
7
7
|
import { UploadsError } from "./errors.js";
|
|
8
|
+
import { writeJson, writeStdout } from "./io.js";
|
|
8
9
|
import { ghAttachmentKey, ghKeyPrefix, attachmentsCommentBody, } from "./github.js";
|
|
9
10
|
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
10
|
-
async function writeStdout(text) {
|
|
11
|
-
if (!process.stdout.write(text)) {
|
|
12
|
-
await new Promise((resolve) => process.stdout.once("drain", resolve));
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
async function writeJson(value) {
|
|
16
|
-
await writeStdout(JSON.stringify(value, null, 2) + "\n");
|
|
17
|
-
}
|
|
18
11
|
// --- put ---
|
|
19
|
-
const PUT_HELP = `uploads put <file> [options]
|
|
20
|
-
|
|
21
|
-
Upload an image for GitHub embeds. Use "-" for stdin.
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
25
|
-
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
26
|
-
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
27
|
-
--ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
|
|
28
|
-
--alt <text> Alt text (default: filename)
|
|
29
|
-
--width <px> <img width=…> markdown (or UPLOADS_DEFAULT_WIDTH)
|
|
30
|
-
--content-type <mime> Override Content-Type
|
|
31
|
-
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
32
|
-
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
33
|
-
--format human|url|markdown|json
|
|
34
|
-
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
35
|
-
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
36
|
-
--comment With --pr/--issue: create/update the attachments comment via your local gh auth
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
40
|
-
uploads --env-file .env put ./shot.png
|
|
41
|
-
uploads --env-file .env put ./after.png --pr 123 --comment
|
|
12
|
+
const PUT_HELP = `uploads put <file> [options]
|
|
13
|
+
|
|
14
|
+
Upload an image for GitHub embeds. Use "-" for stdin.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
18
|
+
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
19
|
+
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
20
|
+
--ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
|
|
21
|
+
--alt <text> Alt text (default: filename)
|
|
22
|
+
--width <px> <img width=…> markdown (or UPLOADS_DEFAULT_WIDTH)
|
|
23
|
+
--content-type <mime> Override Content-Type
|
|
24
|
+
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
25
|
+
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
26
|
+
--format human|url|markdown|json
|
|
27
|
+
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
28
|
+
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
29
|
+
--comment With --pr/--issue: create/update the attachments comment via your local gh auth
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
33
|
+
uploads --env-file .env put ./shot.png
|
|
34
|
+
uploads --env-file .env put ./after.png --pr 123 --comment
|
|
42
35
|
`;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
38
|
+
* neither is present. Shared by the CLI flags and the MCP tool arguments.
|
|
39
|
+
*/
|
|
40
|
+
export function makeGhTarget(pr, issue, repoArg, run) {
|
|
47
41
|
if (pr === undefined && issue === undefined)
|
|
48
42
|
return undefined;
|
|
49
43
|
if (pr !== undefined && issue !== undefined) {
|
|
50
44
|
throw new UsageError("--pr and --issue are mutually exclusive");
|
|
51
45
|
}
|
|
52
|
-
const repo = resolveRepo(
|
|
46
|
+
const repo = resolveRepo(repoArg, run);
|
|
53
47
|
return { repo, kind: pr !== undefined ? "pull" : "issues", num: (pr ?? issue) };
|
|
54
48
|
}
|
|
49
|
+
/** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
|
|
50
|
+
function ghTargetFromFlags(flags, run) {
|
|
51
|
+
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
52
|
+
}
|
|
55
53
|
/**
|
|
56
54
|
* List every attachment under the target's prefix and create/update the
|
|
57
55
|
* managed comment. Throws on gh failure — callers decide whether that is
|
|
58
56
|
* fatal (`comment` command) or a warning (`put --comment`).
|
|
59
57
|
*/
|
|
60
|
-
async function syncAttachmentsComment(
|
|
61
|
-
const items =
|
|
62
|
-
let cursor;
|
|
63
|
-
do {
|
|
64
|
-
const page = await ctx.client.list({ prefix: ghKeyPrefix(target), cursor });
|
|
65
|
-
items.push(...page.items.map(({ key, url }) => ({ key, url })));
|
|
66
|
-
cursor = page.cursor ?? undefined;
|
|
67
|
-
} while (cursor);
|
|
58
|
+
export async function syncAttachmentsComment(client, target, run) {
|
|
59
|
+
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
|
|
68
60
|
if (items.length === 0)
|
|
69
61
|
return { action: "skipped", count: 0 };
|
|
70
62
|
const body = attachmentsCommentBody(items);
|
|
@@ -72,23 +64,23 @@ async function syncAttachmentsComment(ctx, target, run) {
|
|
|
72
64
|
return { action: created ? "created" : "updated", count: items.length };
|
|
73
65
|
}
|
|
74
66
|
// --- attach ---
|
|
75
|
-
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
76
|
-
|
|
77
|
-
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
78
|
-
comment. With no target, uses the pull request for the current branch.
|
|
79
|
-
|
|
80
|
-
Options:
|
|
81
|
-
--pr <num> Attach to this pull request
|
|
82
|
-
--issue <num> Attach to this issue
|
|
83
|
-
--repo <owner/repo> Repository (default: gh/git inference)
|
|
84
|
-
--no-comment Upload only; don't create/update the managed comment
|
|
85
|
-
--content-type <mime> Override Content-Type (applied to every file)
|
|
86
|
-
--workspace, -w <name> Override workspace
|
|
87
|
-
|
|
88
|
-
Examples:
|
|
89
|
-
uploads attach ./before.png ./after.png
|
|
90
|
-
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
91
|
-
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
67
|
+
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
68
|
+
|
|
69
|
+
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
70
|
+
comment. With no target, uses the pull request for the current branch.
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--pr <num> Attach to this pull request
|
|
74
|
+
--issue <num> Attach to this issue
|
|
75
|
+
--repo <owner/repo> Repository (default: gh/git inference)
|
|
76
|
+
--no-comment Upload only; don't create/update the managed comment
|
|
77
|
+
--content-type <mime> Override Content-Type (applied to every file)
|
|
78
|
+
--workspace, -w <name> Override workspace
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
uploads attach ./before.png ./after.png
|
|
82
|
+
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
83
|
+
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
92
84
|
`;
|
|
93
85
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
94
86
|
const parsed = parseCommandArgs(args);
|
|
@@ -124,7 +116,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
124
116
|
let commentError;
|
|
125
117
|
if (!parsed.flags.has("--no-comment")) {
|
|
126
118
|
try {
|
|
127
|
-
comment = await syncAttachmentsComment(ctx, target, run);
|
|
119
|
+
comment = await syncAttachmentsComment(ctx.client, target, run);
|
|
128
120
|
}
|
|
129
121
|
catch (err) {
|
|
130
122
|
commentError = err instanceof Error ? err.message : String(err);
|
|
@@ -230,7 +222,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
230
222
|
}
|
|
231
223
|
if (wantComment && ghTarget) {
|
|
232
224
|
try {
|
|
233
|
-
const sync = await syncAttachmentsComment(ctx, ghTarget, run);
|
|
225
|
+
const sync = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
234
226
|
if (!ctx.quiet && format === "human") {
|
|
235
227
|
process.stderr.write(`>> attachments comment ${sync.action}\n`);
|
|
236
228
|
}
|
|
@@ -243,14 +235,14 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
243
235
|
return 0;
|
|
244
236
|
}
|
|
245
237
|
// --- list ---
|
|
246
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
247
|
-
|
|
248
|
-
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
249
|
-
|
|
250
|
-
Examples:
|
|
251
|
-
uploads list --prefix screenshots/
|
|
252
|
-
uploads list --pr 123
|
|
253
|
-
uploads list --all --json
|
|
238
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
239
|
+
|
|
240
|
+
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
uploads list --prefix screenshots/
|
|
244
|
+
uploads list --pr 123
|
|
245
|
+
uploads list --all --json
|
|
254
246
|
`;
|
|
255
247
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
256
248
|
const parsed = parseCommandArgs(args);
|
|
@@ -270,13 +262,8 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
270
262
|
const limit = flagInt(parsed.flags, "--limit", "--limit");
|
|
271
263
|
const cursor = flagString(parsed.flags, "--cursor");
|
|
272
264
|
if (flagBool(parsed.flags, "--all")) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
do {
|
|
276
|
-
const page = await ctx.client.list({ prefix, limit, cursor: next ?? undefined });
|
|
277
|
-
items.push(...page.items);
|
|
278
|
-
next = page.cursor;
|
|
279
|
-
} while (next);
|
|
265
|
+
// --all may start from a caller-provided --cursor and drains from there.
|
|
266
|
+
const items = await ctx.client.listAll({ prefix, limit, cursor });
|
|
280
267
|
if (ctx.json)
|
|
281
268
|
await writeJson({ items, cursor: null });
|
|
282
269
|
else
|
|
@@ -296,10 +283,10 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
296
283
|
return 0;
|
|
297
284
|
}
|
|
298
285
|
// --- delete ---
|
|
299
|
-
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
300
|
-
|
|
301
|
-
Examples:
|
|
302
|
-
uploads delete screenshots/myapp/42/shot-a1b2c3.png
|
|
286
|
+
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
287
|
+
|
|
288
|
+
Examples:
|
|
289
|
+
uploads delete screenshots/myapp/42/shot-a1b2c3.png
|
|
303
290
|
`;
|
|
304
291
|
export async function runDelete(ctx, args, help = false) {
|
|
305
292
|
const parsed = parseCommandArgs(args);
|
|
@@ -327,16 +314,16 @@ export async function runDelete(ctx, args, help = false) {
|
|
|
327
314
|
return 0;
|
|
328
315
|
}
|
|
329
316
|
// --- comment ---
|
|
330
|
-
const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
|
|
331
|
-
|
|
332
|
-
Create or update the managed attachments comment on a GitHub PR or issue,
|
|
333
|
-
listing everything uploaded for it. Uses your local gh auth. Finds its own
|
|
334
|
-
prior comment via a hidden marker and edits it in place; never touches other
|
|
335
|
-
comments or the description.
|
|
336
|
-
|
|
337
|
-
Examples:
|
|
338
|
-
uploads --env-file .env comment --pr 123
|
|
339
|
-
uploads comment --issue 45 --repo buildinternet/uploads
|
|
317
|
+
const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
|
|
318
|
+
|
|
319
|
+
Create or update the managed attachments comment on a GitHub PR or issue,
|
|
320
|
+
listing everything uploaded for it. Uses your local gh auth. Finds its own
|
|
321
|
+
prior comment via a hidden marker and edits it in place; never touches other
|
|
322
|
+
comments or the description.
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
uploads --env-file .env comment --pr 123
|
|
326
|
+
uploads comment --issue 45 --repo buildinternet/uploads
|
|
340
327
|
`;
|
|
341
328
|
export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
342
329
|
const parsed = parseCommandArgs(args);
|
|
@@ -347,7 +334,7 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
347
334
|
const target = ghTargetFromFlags(parsed.flags, run);
|
|
348
335
|
if (!target)
|
|
349
336
|
throw new UsageError("comment requires --pr or --issue");
|
|
350
|
-
const result = await syncAttachmentsComment(ctx, target, run);
|
|
337
|
+
const result = await syncAttachmentsComment(ctx.client, target, run);
|
|
351
338
|
if (ctx.json) {
|
|
352
339
|
await writeJson({ ...target, ...result });
|
|
353
340
|
}
|
|
@@ -358,14 +345,91 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
358
345
|
}
|
|
359
346
|
return 0;
|
|
360
347
|
}
|
|
348
|
+
// --- usage / reconcile / purge ---
|
|
349
|
+
const USAGE_HELP = `uploads usage [--workspace <name>]
|
|
350
|
+
|
|
351
|
+
Show workspace storage and monthly upload counters (and limits when set).
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
uploads --env-file .env usage
|
|
355
|
+
uploads usage --json
|
|
356
|
+
`;
|
|
357
|
+
export async function runUsage(ctx, args, help = false) {
|
|
358
|
+
if (help || parseCommandArgs(args).help) {
|
|
359
|
+
process.stderr.write(USAGE_HELP);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
const result = await ctx.client.usage();
|
|
363
|
+
if (ctx.json) {
|
|
364
|
+
await writeJson(result);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
const lines = [
|
|
368
|
+
`workspace: ${result.workspace}`,
|
|
369
|
+
`bytes: ${result.bytes}${result.maxStorageBytes != null ? ` / ${result.maxStorageBytes} (${result.storageRemainingBytes} remaining)` : ""}`,
|
|
370
|
+
`objects: ${result.objects}`,
|
|
371
|
+
`uploads: ${result.uploadsInPeriod} this period (${result.periodStart})${result.maxUploadsPerPeriod != null ? ` / ${result.maxUploadsPerPeriod} (${result.uploadsRemaining} remaining)` : ""}`,
|
|
372
|
+
`updated: ${result.updatedAt}`,
|
|
373
|
+
];
|
|
374
|
+
await writeStdout(lines.join("\n") + "\n");
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
const RECONCILE_HELP = `uploads reconcile [--workspace <name>]
|
|
378
|
+
|
|
379
|
+
Rebuild ledger bytes/objects from storage (source of truth). Preserves the
|
|
380
|
+
monthly upload counter. Requires files:write.
|
|
381
|
+
|
|
382
|
+
Examples:
|
|
383
|
+
uploads --env-file .env reconcile
|
|
384
|
+
`;
|
|
385
|
+
export async function runReconcile(ctx, args, help = false) {
|
|
386
|
+
if (help || parseCommandArgs(args).help) {
|
|
387
|
+
process.stderr.write(RECONCILE_HELP);
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
const result = await ctx.client.reconcile();
|
|
391
|
+
if (ctx.json) {
|
|
392
|
+
await writeJson(result);
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
await writeStdout(result.changed
|
|
396
|
+
? `reconciled ${result.workspace}: ${result.previous.bytes}→${result.bytes} bytes, ${result.previous.objects}→${result.objects} objects\n`
|
|
397
|
+
: `reconciled ${result.workspace}: unchanged (${result.bytes} bytes, ${result.objects} objects)\n`);
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
const PURGE_HELP = `uploads purge-expired [--workspace <name>]
|
|
401
|
+
|
|
402
|
+
Delete objects older than the workspace retentionDays setting, then reconcile.
|
|
403
|
+
Skips if retention is unset. Requires files:delete.
|
|
404
|
+
|
|
405
|
+
Examples:
|
|
406
|
+
uploads --env-file .env purge-expired
|
|
407
|
+
`;
|
|
408
|
+
export async function runPurgeExpired(ctx, args, help = false) {
|
|
409
|
+
if (help || parseCommandArgs(args).help) {
|
|
410
|
+
process.stderr.write(PURGE_HELP);
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
const result = await ctx.client.purgeExpired();
|
|
414
|
+
if (ctx.json) {
|
|
415
|
+
await writeJson(result);
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
if ("skipped" in result) {
|
|
419
|
+
await writeStdout(`skipped: ${result.reason}\n`);
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
await writeStdout(`purged ${result.deleted} object(s), freed ${result.freedBytes} bytes (retention ${result.retentionDays}d)\n`);
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
361
425
|
// --- health & doctor ---
|
|
362
|
-
const HEALTH_HELP = `uploads health
|
|
363
|
-
|
|
364
|
-
API liveness (no auth).
|
|
365
|
-
|
|
366
|
-
Examples:
|
|
367
|
-
uploads health
|
|
368
|
-
uploads --api-url http://localhost:8787 health
|
|
426
|
+
const HEALTH_HELP = `uploads health
|
|
427
|
+
|
|
428
|
+
API liveness (no auth).
|
|
429
|
+
|
|
430
|
+
Examples:
|
|
431
|
+
uploads health
|
|
432
|
+
uploads --api-url http://localhost:8787 health
|
|
369
433
|
`;
|
|
370
434
|
export async function runHealth(ctx, args, help = false) {
|
|
371
435
|
if (help || parseCommandArgs(args).help) {
|
|
@@ -383,31 +447,28 @@ export async function runHealth(ctx, args, help = false) {
|
|
|
383
447
|
await writeStdout(result.ok ? `ok (${ctx.apiUrl})\n` : `unhealthy (${ctx.apiUrl})\n`);
|
|
384
448
|
return result.ok ? 0 : 1;
|
|
385
449
|
}
|
|
386
|
-
const DOCTOR_HELP = `uploads doctor [--workspace <name>]
|
|
387
|
-
|
|
388
|
-
Checks API health, token auth, and workspace/token alignment.
|
|
389
|
-
|
|
390
|
-
Examples:
|
|
391
|
-
uploads --env-file .env doctor
|
|
392
|
-
uploads --workspace acme --env-file .env doctor
|
|
450
|
+
const DOCTOR_HELP = `uploads doctor [--workspace <name>]
|
|
451
|
+
|
|
452
|
+
Checks API health, token auth, and workspace/token alignment.
|
|
453
|
+
|
|
454
|
+
Examples:
|
|
455
|
+
uploads --env-file .env doctor
|
|
456
|
+
uploads --workspace acme --env-file .env doctor
|
|
393
457
|
`;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return 0;
|
|
398
|
-
}
|
|
399
|
-
const mismatch = workspaceMismatch(ctx.config);
|
|
458
|
+
/** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
|
|
459
|
+
export async function buildDoctorReport(config, client) {
|
|
460
|
+
const mismatch = workspaceMismatch(config);
|
|
400
461
|
const hints = [];
|
|
401
462
|
if (mismatch)
|
|
402
463
|
hints.push(mismatch);
|
|
403
|
-
if (
|
|
464
|
+
if (config.apiUrl.includes("localhost") || config.apiUrl.includes("127.0.0.1")) {
|
|
404
465
|
hints.push("local API uses dev KV — prod tokens won't work unless minted with --local");
|
|
405
466
|
}
|
|
406
|
-
const health = await
|
|
467
|
+
const health = await client.health();
|
|
407
468
|
let authOk = false;
|
|
408
469
|
let authError;
|
|
409
470
|
try {
|
|
410
|
-
await
|
|
471
|
+
await client.list({ limit: 1 });
|
|
411
472
|
authOk = true;
|
|
412
473
|
}
|
|
413
474
|
catch (err) {
|
|
@@ -416,35 +477,67 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
416
477
|
hints.push("if this token works on api.uploads.sh, set UPLOADS_API_URL=https://api.uploads.sh");
|
|
417
478
|
}
|
|
418
479
|
}
|
|
419
|
-
|
|
420
|
-
|
|
480
|
+
let usage;
|
|
481
|
+
if (authOk) {
|
|
482
|
+
try {
|
|
483
|
+
const snap = await client.usage();
|
|
484
|
+
usage = {
|
|
485
|
+
ok: true,
|
|
486
|
+
bytes: snap.bytes,
|
|
487
|
+
objects: snap.objects,
|
|
488
|
+
uploadsInPeriod: snap.uploadsInPeriod,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
usage = {
|
|
493
|
+
ok: false,
|
|
494
|
+
error: err instanceof UploadsError ? err.message : String(err),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!config.configExists && !config.token) {
|
|
499
|
+
hints.push(`run uploads setup to configure ${config.configPath}`);
|
|
421
500
|
}
|
|
422
|
-
|
|
501
|
+
return {
|
|
423
502
|
ok: health.ok && authOk,
|
|
424
|
-
apiUrl:
|
|
425
|
-
workspace:
|
|
426
|
-
workspaceSource:
|
|
427
|
-
workspaceFromToken: workspaceFromToken(
|
|
428
|
-
configPath:
|
|
429
|
-
configExists:
|
|
503
|
+
apiUrl: config.apiUrl,
|
|
504
|
+
workspace: config.workspace,
|
|
505
|
+
workspaceSource: config.workspaceSource,
|
|
506
|
+
workspaceFromToken: workspaceFromToken(config.token),
|
|
507
|
+
configPath: config.configPath,
|
|
508
|
+
configExists: config.configExists,
|
|
430
509
|
health,
|
|
431
510
|
auth: { ok: authOk, error: authError },
|
|
511
|
+
usage,
|
|
512
|
+
warning: mismatch,
|
|
432
513
|
hints,
|
|
433
514
|
};
|
|
515
|
+
}
|
|
516
|
+
export async function runDoctor(ctx, args, help = false) {
|
|
517
|
+
if (help || parseCommandArgs(args).help) {
|
|
518
|
+
process.stderr.write(DOCTOR_HELP);
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
const report = await buildDoctorReport(ctx.config, ctx.client);
|
|
434
522
|
if (ctx.json) {
|
|
435
523
|
await writeJson(report);
|
|
436
524
|
return report.ok ? 0 : 1;
|
|
437
525
|
}
|
|
438
526
|
const lines = [
|
|
439
|
-
`config: ${
|
|
440
|
-
`api: ${
|
|
441
|
-
`workspace: ${
|
|
442
|
-
`auth: ${
|
|
527
|
+
`config: ${report.configPath}${report.configExists ? "" : " (missing)"}`,
|
|
528
|
+
`api: ${report.apiUrl} (${report.health.ok ? "ok" : "failed"})`,
|
|
529
|
+
`workspace: ${report.workspace}`,
|
|
530
|
+
`auth: ${report.auth.ok ? "ok" : `failed — ${report.auth.error ?? "no token"}`}`,
|
|
443
531
|
];
|
|
444
|
-
if (
|
|
445
|
-
lines.push(
|
|
446
|
-
|
|
447
|
-
|
|
532
|
+
if (report.usage) {
|
|
533
|
+
lines.push(report.usage.ok
|
|
534
|
+
? `usage: ${report.usage.bytes} bytes, ${report.usage.objects} objects, ${report.usage.uploadsInPeriod} uploads this period`
|
|
535
|
+
: `usage: failed — ${report.usage.error ?? "unknown"}`);
|
|
536
|
+
}
|
|
537
|
+
if (report.warning)
|
|
538
|
+
lines.push(`warning: ${report.warning}`);
|
|
539
|
+
for (const h of report.hints)
|
|
540
|
+
if (h !== report.warning)
|
|
448
541
|
lines.push(`hint: ${h}`);
|
|
449
542
|
await writeStdout(lines.join("\n") + "\n");
|
|
450
543
|
return report.ok ? 0 : 1;
|
package/dist/config-file.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
export const UPLOADS_CONFIG_KEYS = [
|
|
@@ -162,6 +162,10 @@ const INIT_HEADER = `# uploads.sh CLI — shared buildinternet config
|
|
|
162
162
|
/** Create or update UPLOADS_* keys in the shared config file. Preserves other keys. */
|
|
163
163
|
export function writeConfigKeys(path, keys, opts) {
|
|
164
164
|
const entries = Object.entries(keys).filter(([, v]) => v !== undefined && v !== "");
|
|
165
|
+
for (const [key, value] of entries) {
|
|
166
|
+
if (/[\r\n]/.test(value))
|
|
167
|
+
throw new Error(`invalid newline in ${key}`);
|
|
168
|
+
}
|
|
165
169
|
if (entries.length === 0) {
|
|
166
170
|
throw new Error("no config values to write");
|
|
167
171
|
}
|
|
@@ -187,7 +191,15 @@ export function writeConfigKeys(path, keys, opts) {
|
|
|
187
191
|
updated.push(key);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
|
-
|
|
194
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
195
|
+
writeFileSync(tmp, lines.join("\n").replace(/\n*$/, "\n"), { encoding: "utf8", mode: 0o600 });
|
|
196
|
+
renameSync(tmp, path);
|
|
197
|
+
try {
|
|
198
|
+
chmodSync(path, 0o600);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* Windows/filesystems may not support modes. */
|
|
202
|
+
}
|
|
191
203
|
return { path, created: !existed, updated };
|
|
192
204
|
}
|
|
193
205
|
export function configValuesFromClient(config, defaults) {
|
package/dist/config.js
CHANGED
|
@@ -141,6 +141,7 @@ export function resolveConfig(flags) {
|
|
|
141
141
|
function missingTokenMessage(configPath) {
|
|
142
142
|
return [
|
|
143
143
|
"UPLOADS_TOKEN is required.",
|
|
144
|
+
" uploads login # exchange an admin-provided enrollment code",
|
|
144
145
|
` uploads setup --token <token> # guided setup → ${configPath}`,
|
|
145
146
|
` uploads config init --token <token> # writes ${configPath}`,
|
|
146
147
|
" or set UPLOADS_TOKEN in env, pass --token, or use --env-file",
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
1
|
+
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
2
2
|
export declare class UploadsError extends Error {
|
|
3
3
|
readonly code: UploadsErrorCode;
|
|
4
4
|
readonly status?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { inferContentType, buildMarkdown } from "./embed.js";
|
|
|
2
2
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
3
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
4
4
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
5
|
-
export { createUploadsClient, type UploadsClient, type PutOptions, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, } from "./client.js";
|
|
5
|
+
export { createUploadsClient, type UploadsClient, type PutOptions, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
6
6
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
7
7
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/io.d.ts
ADDED
package/dist/io.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Backpressure-aware stdout helpers shared by the CLI commands and the stdio MCP transport. */
|
|
2
|
+
export async function writeStdout(text) {
|
|
3
|
+
if (!process.stdout.write(text)) {
|
|
4
|
+
await new Promise((resolve) => process.stdout.once("drain", resolve));
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export async function writeJson(value) {
|
|
8
|
+
await writeStdout(JSON.stringify(value, null, 2) + "\n");
|
|
9
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ToolArgs = Record<string, unknown>;
|
|
2
|
+
export declare function usage(msg: string): never;
|
|
3
|
+
export declare function optString(args: ToolArgs, name: string): string | undefined;
|
|
4
|
+
export declare function optPosInt(args: ToolArgs, name: string): number | undefined;
|
package/dist/mcp/args.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument helpers shared by the stdio MCP tool set (./tools.ts) and the
|
|
3
|
+
* remote worker's tool set (apps/mcp). Runtime-agnostic — usable from
|
|
4
|
+
* Workers as well as Node.
|
|
5
|
+
*/
|
|
6
|
+
import { UploadsError } from "../errors.js";
|
|
7
|
+
export function usage(msg) {
|
|
8
|
+
throw new UploadsError(msg, "USAGE");
|
|
9
|
+
}
|
|
10
|
+
export function optString(args, name) {
|
|
11
|
+
const v = args[name];
|
|
12
|
+
if (v === undefined || v === null)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof v !== "string")
|
|
15
|
+
usage(`${name} must be a string`);
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
export function optPosInt(args, name) {
|
|
19
|
+
const v = args[name];
|
|
20
|
+
if (v === undefined || v === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
|
|
23
|
+
usage(`${name} must be a positive integer`);
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { optPosInt, optString, usage, type ToolArgs } from "./args.js";
|
|
2
|
+
export interface McpTool {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
/** Hand-written JSON Schema for the tool's arguments. */
|
|
6
|
+
inputSchema: Record<string, unknown>;
|
|
7
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface McpServer {
|
|
10
|
+
/** Handle one JSON-RPC line. Undefined for notifications / client responses. */
|
|
11
|
+
handleLine(line: string): Promise<string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createMcpServer(opts: {
|
|
14
|
+
serverInfo: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
};
|
|
18
|
+
tools: McpTool[];
|
|
19
|
+
}): McpServer;
|