@buildinternet/uploads 0.8.0 → 0.10.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 +25 -9
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +5 -0
- package/dist/cli-brand.d.ts +89 -0
- package/dist/cli-brand.js +166 -0
- package/dist/cli-catalog.d.ts +32 -0
- package/dist/cli-catalog.js +187 -0
- package/dist/cli-help.d.ts +26 -0
- package/dist/cli-help.js +176 -0
- package/dist/cli-style.d.ts +42 -0
- package/dist/cli-style.js +108 -0
- package/dist/cli.js +67 -73
- package/dist/client.d.ts +19 -0
- package/dist/client.js +31 -0
- package/dist/commands/admin-enrollment.js +2 -1
- package/dist/commands/completion.d.ts +3 -0
- package/dist/commands/completion.js +284 -0
- package/dist/commands/config.js +9 -7
- package/dist/commands/install.js +2 -1
- package/dist/commands/invite.js +15 -2
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +55 -10
- package/dist/commands/mcp.js +2 -1
- package/dist/commands/session.d.ts +31 -0
- package/dist/commands/session.js +140 -0
- package/dist/commands/setup.js +2 -1
- package/dist/commands.js +125 -24
- package/dist/config-file.d.ts +12 -1
- package/dist/config-file.js +49 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/format-bytes.d.ts +5 -0
- package/dist/format-bytes.js +11 -0
- package/dist/github-gh.d.ts +7 -0
- package/dist/github-gh.js +24 -0
- package/dist/update-check.d.ts +10 -0
- package/dist/update-check.js +28 -12
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session commands: whoami / status (show identity) and logout (clear token).
|
|
3
|
+
*/
|
|
4
|
+
import { describeConfigSources, loadConfigFile, redactToken, removeConfigKeys, resolveConfig, resolveConfigPath, workspaceFromToken, } from "../config.js";
|
|
5
|
+
import { flagBool, flagString, parseCommandArgs } from "../cli-args.js";
|
|
6
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
7
|
+
import { writeJson } from "../io.js";
|
|
8
|
+
const WHOAMI_HELP = `uploads whoami
|
|
9
|
+
|
|
10
|
+
Show the active CLI identity: workspace, token (redacted), API URL, and where
|
|
11
|
+
each value came from. Alias: uploads status.
|
|
12
|
+
|
|
13
|
+
Does not call the API (offline). Use uploads doctor to verify the token works.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--path <file> Config file to inspect (default: shared buildinternet config)
|
|
17
|
+
--json JSON on stdout
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
uploads whoami
|
|
21
|
+
uploads status --json
|
|
22
|
+
uploads whoami --path ./my.env
|
|
23
|
+
`;
|
|
24
|
+
const LOGOUT_HELP = `uploads logout
|
|
25
|
+
|
|
26
|
+
Remove the saved UPLOADS_TOKEN from the shared config file so this machine is
|
|
27
|
+
no longer signed in for the CLI. Does not revoke the token on the server.
|
|
28
|
+
|
|
29
|
+
Environment variables (UPLOADS_TOKEN) are not unset — export them yourself if set.
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--path <file> Config file to edit (default: shared buildinternet config)
|
|
33
|
+
--json JSON on stdout
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
uploads logout
|
|
37
|
+
uploads logout --path ./my.env
|
|
38
|
+
`;
|
|
39
|
+
export function buildWhoamiReport(opts) {
|
|
40
|
+
const flags = {
|
|
41
|
+
envFile: opts.envFile,
|
|
42
|
+
token: opts.token,
|
|
43
|
+
workspace: opts.workspace,
|
|
44
|
+
apiUrl: opts.apiUrl,
|
|
45
|
+
};
|
|
46
|
+
const config = resolveConfig({ ...flags, requireToken: false });
|
|
47
|
+
const sources = describeConfigSources(flags);
|
|
48
|
+
const tokenInConfig = Boolean(loadConfigFile(config.configPath).UPLOADS_TOKEN);
|
|
49
|
+
return {
|
|
50
|
+
signedIn: Boolean(config.token),
|
|
51
|
+
workspace: config.workspace,
|
|
52
|
+
workspaceSource: sources.workspace,
|
|
53
|
+
workspaceFromToken: config.token ? workspaceFromToken(config.token) : undefined,
|
|
54
|
+
token: redactToken(config.token || undefined),
|
|
55
|
+
tokenSource: sources.token,
|
|
56
|
+
tokenInConfig,
|
|
57
|
+
apiUrl: config.apiUrl,
|
|
58
|
+
apiUrlSource: sources.apiUrl,
|
|
59
|
+
configPath: config.configPath,
|
|
60
|
+
configExists: config.configExists,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function formatWhoami(report) {
|
|
64
|
+
const cfg = `${report.configPath}${report.configExists ? "" : " (missing)"}`;
|
|
65
|
+
const lines = [
|
|
66
|
+
`signed in: ${report.signedIn ? "yes" : "no"}`,
|
|
67
|
+
`workspace: ${report.workspace} (${report.workspaceSource})`,
|
|
68
|
+
];
|
|
69
|
+
if (report.workspaceFromToken && report.workspaceFromToken !== report.workspace) {
|
|
70
|
+
lines.push(`token ws: ${report.workspaceFromToken} (encoded in token)`);
|
|
71
|
+
}
|
|
72
|
+
if (report.signedIn) {
|
|
73
|
+
lines.push(`token: ${report.token} (${report.tokenSource})`);
|
|
74
|
+
}
|
|
75
|
+
lines.push(`api: ${report.apiUrl} (${report.apiUrlSource})`);
|
|
76
|
+
lines.push(`config: ${cfg}`);
|
|
77
|
+
if (!report.signedIn) {
|
|
78
|
+
lines.push("", "hint: run uploads login to sign in");
|
|
79
|
+
}
|
|
80
|
+
else if (report.tokenSource === "env" && !report.tokenInConfig) {
|
|
81
|
+
lines.push("", "note: token is from the environment, not the config file", " uploads logout only clears the config file");
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n") + "\n";
|
|
84
|
+
}
|
|
85
|
+
function wantsJson(opts, parsed) {
|
|
86
|
+
return Boolean(opts.json || flagBool(parsed.flags, "--json"));
|
|
87
|
+
}
|
|
88
|
+
export async function runWhoami(args, opts, help = false) {
|
|
89
|
+
const parsed = parseCommandArgs(args);
|
|
90
|
+
if (help || parsed.help) {
|
|
91
|
+
writeCommandHelp(WHOAMI_HELP);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
const report = buildWhoamiReport({
|
|
95
|
+
envFile: flagString(parsed.flags, "--path") ?? opts.envFile,
|
|
96
|
+
token: opts.token,
|
|
97
|
+
workspace: opts.workspace,
|
|
98
|
+
apiUrl: opts.apiUrl,
|
|
99
|
+
});
|
|
100
|
+
if (wantsJson(opts, parsed))
|
|
101
|
+
await writeJson(report);
|
|
102
|
+
else
|
|
103
|
+
process.stdout.write(formatWhoami(report));
|
|
104
|
+
return report.signedIn ? 0 : 1;
|
|
105
|
+
}
|
|
106
|
+
export async function runLogout(args, opts, help = false) {
|
|
107
|
+
const parsed = parseCommandArgs(args);
|
|
108
|
+
if (help || parsed.help) {
|
|
109
|
+
writeCommandHelp(LOGOUT_HELP);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
const path = flagString(parsed.flags, "--path") ?? resolveConfigPath({ envFile: opts.envFile });
|
|
113
|
+
const hadFileToken = Boolean(loadConfigFile(path).UPLOADS_TOKEN);
|
|
114
|
+
const envTokenStillSet = Boolean(process.env.UPLOADS_TOKEN);
|
|
115
|
+
const result = removeConfigKeys(path, ["UPLOADS_TOKEN"]);
|
|
116
|
+
const payload = {
|
|
117
|
+
path: result.path,
|
|
118
|
+
removed: result.removed,
|
|
119
|
+
configExisted: result.existed,
|
|
120
|
+
hadTokenInConfig: hadFileToken,
|
|
121
|
+
envTokenStillSet,
|
|
122
|
+
};
|
|
123
|
+
if (wantsJson(opts, parsed)) {
|
|
124
|
+
await writeJson(payload);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (result.removed.includes("UPLOADS_TOKEN")) {
|
|
128
|
+
process.stdout.write(`signed out — removed UPLOADS_TOKEN from ${result.path}\n`);
|
|
129
|
+
}
|
|
130
|
+
else if (!result.existed) {
|
|
131
|
+
process.stdout.write(`no config file at ${result.path} — already signed out\n`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
process.stdout.write(`no UPLOADS_TOKEN in ${result.path} — already signed out\n`);
|
|
135
|
+
}
|
|
136
|
+
if (envTokenStillSet) {
|
|
137
|
+
process.stderr.write("note: UPLOADS_TOKEN is still set in the environment; unset it to fully sign out\n");
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createUploadsClient } from "../client.js";
|
|
|
2
2
|
import { DEFAULT_API_URL, DEFAULT_WORKSPACE, describeConfigSources, putDefaultsToConfigValues, redactToken, resolveApiUrl, resolveConfig, resolveConfigPath, resolvePutDefaults, writeConfigKeys, workspaceFromToken, } from "../config.js";
|
|
3
3
|
import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
4
4
|
import { UploadsError } from "../errors.js";
|
|
5
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
5
6
|
const SETUP_HELP = `uploads setup — guided CLI configuration
|
|
6
7
|
|
|
7
8
|
Writes UPLOADS_* keys to the shared buildinternet config file and prints
|
|
@@ -99,7 +100,7 @@ function formatWizard(status) {
|
|
|
99
100
|
export async function runSetup(args, opts, help = false) {
|
|
100
101
|
const parsed = parseCommandArgs(args);
|
|
101
102
|
if (help || parsed.help) {
|
|
102
|
-
|
|
103
|
+
writeCommandHelp(SETUP_HELP);
|
|
103
104
|
return 0;
|
|
104
105
|
}
|
|
105
106
|
const apiUrl = flagString(parsed.flags, "--api-url");
|
package/dist/commands.js
CHANGED
|
@@ -9,12 +9,14 @@ import { UploadsError } from "./errors.js";
|
|
|
9
9
|
import { writeJson, writeStdout } from "./io.js";
|
|
10
10
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
11
11
|
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
12
|
-
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
12
|
+
import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
13
13
|
import { resolvePutPrefix } from "./destinations.js";
|
|
14
14
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
15
15
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
16
16
|
import { buildCliProvenance } from "./provenance.js";
|
|
17
|
+
import { formatByteSize } from "./format-bytes.js";
|
|
17
18
|
import { packageVersion } from "./package-version.js";
|
|
19
|
+
import { writeCommandHelp } from "./cli-style.js";
|
|
18
20
|
/** Read a local file (or `-` for stdin). Missing path → FILE_NOT_FOUND (exit 2). */
|
|
19
21
|
export function readFileArg(fileArg) {
|
|
20
22
|
try {
|
|
@@ -44,6 +46,11 @@ Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
|
44
46
|
remain public even for private/internal GitHub repositories. Upload only media
|
|
45
47
|
that is safe at a predictable public URL.
|
|
46
48
|
|
|
49
|
+
Re-uploading the same key overwrites in place (no prompt) so embeds hot-swap;
|
|
50
|
+
human mode prints ">> replaced existing object (same URL)" after a real put,
|
|
51
|
+
or ">> would replace existing object (same URL)" on --dry-run when the key
|
|
52
|
+
already exists.
|
|
53
|
+
|
|
47
54
|
Human/json output includes durable url and (when dual-host applies) embedUrl.
|
|
48
55
|
MARKDOWN prefers embedUrl for GitHub. Override: UPLOADS_EMBED_PUBLIC_BASE_URL.
|
|
49
56
|
|
|
@@ -65,6 +72,8 @@ Options:
|
|
|
65
72
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
66
73
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
67
74
|
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
75
|
+
--auto Resolve current PR/issue and stamp gh.* metadata (default on)
|
|
76
|
+
--no-auto Skip gh.* auto-resolution (also skipped by --no-git or UPLOADS_NO_AUTO_META=1)
|
|
68
77
|
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
69
78
|
--format human|url|markdown|json
|
|
70
79
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
@@ -75,7 +84,7 @@ Options:
|
|
|
75
84
|
Re-uploading to an existing key WITH --meta replaces that file's
|
|
76
85
|
entire metadata set; without --meta the existing metadata is
|
|
77
86
|
preserved. Use "uploads meta set" to edit individual keys.
|
|
78
|
-
--dry-run Print key + public URL without uploading. Not with --comment/--gallery
|
|
87
|
+
--dry-run Print key + public URL without uploading; reports if the key would replace an existing object. Not with --comment/--gallery
|
|
79
88
|
|
|
80
89
|
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
|
|
81
90
|
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
@@ -107,6 +116,24 @@ export function makeGhTarget(pr, issue, repoArg, run) {
|
|
|
107
116
|
function ghTargetFromFlags(flags, run) {
|
|
108
117
|
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
109
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Best-effort GitHub target for the default put path (no --pr/--issue). A
|
|
121
|
+
* numeric --ref is classified as pull vs issue; otherwise the current branch's
|
|
122
|
+
* PR is resolved. Never throws — any failure yields undefined so the upload
|
|
123
|
+
* proceeds without gh metadata.
|
|
124
|
+
*/
|
|
125
|
+
function resolveAutoGhTarget(repoArg, ref, run) {
|
|
126
|
+
try {
|
|
127
|
+
const repo = resolveRepo(repoArg, run);
|
|
128
|
+
if (ref !== undefined && /^\d+$/.test(ref) && Number(ref) > 0) {
|
|
129
|
+
return classifyGhNumber(repo, Number.parseInt(ref, 10), run);
|
|
130
|
+
}
|
|
131
|
+
return resolveCurrentPullRequest(repo, run);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
110
137
|
/** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
|
|
111
138
|
export function optimizeOptionsFromFlags(flags, defaults) {
|
|
112
139
|
if (flags.has("--no-optimize") && typeof flags.get("--no-optimize") === "string") {
|
|
@@ -128,13 +155,20 @@ export function optimizeOptionsFromFlags(flags, defaults) {
|
|
|
128
155
|
}
|
|
129
156
|
function formatOptimizeNote(opt) {
|
|
130
157
|
if (opt.optimized) {
|
|
131
|
-
return `optimized ${opt.originalBytes} → ${opt.outputBytes}
|
|
158
|
+
return `optimized ${formatByteSize(opt.originalBytes)} → ${formatByteSize(opt.outputBytes)} (${opt.filename})`;
|
|
132
159
|
}
|
|
133
160
|
if (opt.skippedReason && opt.skippedReason !== "disabled") {
|
|
134
161
|
return `optimize skipped (${opt.skippedReason})`;
|
|
135
162
|
}
|
|
136
163
|
return undefined;
|
|
137
164
|
}
|
|
165
|
+
function writeReplacedNote(replaced, quiet, dryRun = false) {
|
|
166
|
+
if (!quiet && replaced) {
|
|
167
|
+
process.stderr.write(dryRun
|
|
168
|
+
? `>> would replace existing object (same URL)\n`
|
|
169
|
+
: `>> replaced existing object (same URL)\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
138
172
|
/** Frame (optional) then optimize — shared by put/attach/MCP. */
|
|
139
173
|
export async function prepareImageForUpload(bytes, filename, opts) {
|
|
140
174
|
let currentBytes = bytes;
|
|
@@ -239,6 +273,10 @@ Attachments are public and their repo/number/filename keys are predictable.
|
|
|
239
273
|
Private/internal GitHub repository visibility does not restrict access; upload
|
|
240
274
|
only media that is safe at a public URL.
|
|
241
275
|
|
|
276
|
+
Same filename under the same PR/issue overwrites in place (no prompt) so the
|
|
277
|
+
URL and every embed hot-swap. Human mode prints ">> replaced existing object
|
|
278
|
+
(same URL)" when that happens.
|
|
279
|
+
|
|
242
280
|
Still images are optimized to WebP by default (same as put). Use --no-optimize
|
|
243
281
|
to upload originals. Optional --frame wraps images in device/browser chrome.
|
|
244
282
|
|
|
@@ -273,11 +311,11 @@ Examples:
|
|
|
273
311
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
274
312
|
const parsed = parseCommandArgs(args);
|
|
275
313
|
if (help || parsed.help) {
|
|
276
|
-
|
|
314
|
+
writeCommandHelp(ATTACH_HELP);
|
|
277
315
|
return 0;
|
|
278
316
|
}
|
|
279
317
|
if (parsed.positionals.length === 0) {
|
|
280
|
-
|
|
318
|
+
writeCommandHelp(ATTACH_HELP);
|
|
281
319
|
return 2;
|
|
282
320
|
}
|
|
283
321
|
if (parsed.flags.has("--no-comment") && typeof parsed.flags.get("--no-comment") === "string") {
|
|
@@ -328,6 +366,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
328
366
|
}),
|
|
329
367
|
metadata,
|
|
330
368
|
});
|
|
369
|
+
writeReplacedNote(result.replaced, ctx.quiet || ctx.json);
|
|
331
370
|
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
332
371
|
results.push({
|
|
333
372
|
...result,
|
|
@@ -363,22 +402,27 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
363
402
|
}
|
|
364
403
|
if (!ctx.quiet && comment)
|
|
365
404
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
405
|
+
// attach auto-writes gh.* metadata; point the user at how to find it later.
|
|
406
|
+
if (!ctx.quiet) {
|
|
407
|
+
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
408
|
+
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
409
|
+
}
|
|
366
410
|
}
|
|
367
411
|
return 0;
|
|
368
412
|
}
|
|
369
413
|
export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
370
414
|
if (help) {
|
|
371
|
-
|
|
415
|
+
writeCommandHelp(PUT_HELP);
|
|
372
416
|
return 0;
|
|
373
417
|
}
|
|
374
418
|
const parsed = parseCommandArgs(args);
|
|
375
419
|
if (parsed.help) {
|
|
376
|
-
|
|
420
|
+
writeCommandHelp(PUT_HELP);
|
|
377
421
|
return 0;
|
|
378
422
|
}
|
|
379
423
|
const fileArg = parsed.positionals[0];
|
|
380
424
|
if (!fileArg) {
|
|
381
|
-
|
|
425
|
+
writeCommandHelp(PUT_HELP);
|
|
382
426
|
return 2;
|
|
383
427
|
}
|
|
384
428
|
const keyHint = flagString(parsed.flags, "--key");
|
|
@@ -390,13 +434,19 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
390
434
|
const nameFlag = flagString(parsed.flags, "--name");
|
|
391
435
|
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
392
436
|
// Validate --meta up front (fail fast, before reading/optimizing the file).
|
|
393
|
-
const
|
|
437
|
+
const userMeta = (() => {
|
|
394
438
|
const pairs = flagValues(parsed.flags, "--meta");
|
|
395
439
|
return pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
396
440
|
})();
|
|
397
441
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
398
442
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
399
443
|
}
|
|
444
|
+
if (parsed.flags.has("--auto") && typeof parsed.flags.get("--auto") === "string") {
|
|
445
|
+
throw new UsageError("--auto takes no value");
|
|
446
|
+
}
|
|
447
|
+
if (parsed.flags.has("--no-auto") && typeof parsed.flags.get("--no-auto") === "string") {
|
|
448
|
+
throw new UsageError("--no-auto takes no value");
|
|
449
|
+
}
|
|
400
450
|
if (wantComment && !ghTarget)
|
|
401
451
|
throw new UsageError("--comment requires --pr or --issue");
|
|
402
452
|
if (ghTarget) {
|
|
@@ -474,6 +524,43 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
474
524
|
process.stderr.write(`>> ${note}\n`);
|
|
475
525
|
}
|
|
476
526
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
527
|
+
// gh.* metadata: explicit --pr/--issue target wins over --meta; otherwise
|
|
528
|
+
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
529
|
+
// --no-auto, or UPLOADS_NO_AUTO_META disable auto; --auto forces past the
|
|
530
|
+
// config default but never past --no-git (no repo to resolve).
|
|
531
|
+
let metadata = userMeta;
|
|
532
|
+
let attachedRef;
|
|
533
|
+
if (ghTarget) {
|
|
534
|
+
const merged = { ...userMeta, ...ghMetadataFromTarget(ghTarget) };
|
|
535
|
+
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
536
|
+
metadata = merged;
|
|
537
|
+
attachedRef = merged["gh.ref"];
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
const autoEnabled = !noGit &&
|
|
541
|
+
!flagBool(parsed.flags, "--no-auto") &&
|
|
542
|
+
(flagBool(parsed.flags, "--auto") || defaults.noAutoMeta !== true);
|
|
543
|
+
if (autoEnabled) {
|
|
544
|
+
const autoTarget = resolveAutoGhTarget(flagString(parsed.flags, "--repo") ?? defaults.repo, flagString(parsed.flags, "--ref") ?? defaults.ref, run);
|
|
545
|
+
if (autoTarget) {
|
|
546
|
+
const autoMeta = ghMetadataFromTarget(autoTarget);
|
|
547
|
+
const merged = { ...autoMeta, ...userMeta };
|
|
548
|
+
// Auto resolution must never fail the upload: if merging the gh.* pairs
|
|
549
|
+
// would exceed the metadata caps, drop them and upload with --meta only.
|
|
550
|
+
try {
|
|
551
|
+
validateMetaMap(merged);
|
|
552
|
+
metadata = merged;
|
|
553
|
+
attachedRef = merged["gh.ref"];
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// keep metadata = userMeta (already validated); skip auto gh.*
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (attachedRef && !ctx.quiet && format === "human") {
|
|
562
|
+
process.stderr.write(`>> attached to ${attachedRef}\n`);
|
|
563
|
+
}
|
|
477
564
|
let key = ghTarget ? ghAttachmentKey(ghTarget, filename) : keyHint;
|
|
478
565
|
if (key && prepared.optimized)
|
|
479
566
|
key = rewriteKeyExtension(key, filename);
|
|
@@ -494,6 +581,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
494
581
|
}),
|
|
495
582
|
metadata,
|
|
496
583
|
});
|
|
584
|
+
if (format === "human")
|
|
585
|
+
writeReplacedNote(result.replaced, ctx.quiet, dryRun);
|
|
497
586
|
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
498
587
|
const markdown = buildMarkdown(embedSrc, { alt, width });
|
|
499
588
|
let gallery;
|
|
@@ -605,7 +694,7 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
605
694
|
const parsed = parseCommandArgs(args);
|
|
606
695
|
const action = parsed.positionals[0];
|
|
607
696
|
if (help || parsed.help || !action) {
|
|
608
|
-
|
|
697
|
+
writeCommandHelp(GALLERY_HELP);
|
|
609
698
|
return help || parsed.help ? 0 : 2;
|
|
610
699
|
}
|
|
611
700
|
switch (action) {
|
|
@@ -805,14 +894,21 @@ async function runFindFiles(ctx, filters, flags) {
|
|
|
805
894
|
if (ctx.json)
|
|
806
895
|
await writeJson(result);
|
|
807
896
|
else
|
|
808
|
-
for (const item of result.items)
|
|
809
|
-
|
|
897
|
+
for (const item of result.items) {
|
|
898
|
+
// LIST_HELP promises matched metadata in the output; render it inline
|
|
899
|
+
// (sorted for stable output) so human mode honors that, not just --json.
|
|
900
|
+
const meta = Object.entries(item.metadata)
|
|
901
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
902
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
903
|
+
.join(" ");
|
|
904
|
+
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
|
|
905
|
+
}
|
|
810
906
|
return 0;
|
|
811
907
|
}
|
|
812
908
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
813
909
|
const parsed = parseCommandArgs(args);
|
|
814
910
|
if (help || parsed.help) {
|
|
815
|
-
|
|
911
|
+
writeCommandHelp(LIST_HELP);
|
|
816
912
|
return 0;
|
|
817
913
|
}
|
|
818
914
|
const metaPairs = flagValues(parsed.flags, "--meta");
|
|
@@ -870,11 +966,11 @@ Examples:
|
|
|
870
966
|
export async function runFind(ctx, args, help = false) {
|
|
871
967
|
const parsed = parseCommandArgs(args);
|
|
872
968
|
if (help || parsed.help) {
|
|
873
|
-
|
|
969
|
+
writeCommandHelp(FIND_HELP);
|
|
874
970
|
return 0;
|
|
875
971
|
}
|
|
876
972
|
if (parsed.positionals.length === 0) {
|
|
877
|
-
|
|
973
|
+
writeCommandHelp(FIND_HELP);
|
|
878
974
|
return 2;
|
|
879
975
|
}
|
|
880
976
|
const filters = parseMetaFlags(parsed.positionals);
|
|
@@ -899,7 +995,7 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
899
995
|
const parsed = parseCommandArgs(args);
|
|
900
996
|
const action = parsed.positionals[0];
|
|
901
997
|
if (help || parsed.help || !action) {
|
|
902
|
-
|
|
998
|
+
writeCommandHelp(META_HELP);
|
|
903
999
|
return help || parsed.help ? 0 : 2;
|
|
904
1000
|
}
|
|
905
1001
|
switch (action) {
|
|
@@ -910,6 +1006,11 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
910
1006
|
const result = await ctx.client.getMetadata(key);
|
|
911
1007
|
if (ctx.json)
|
|
912
1008
|
await writeJson(result);
|
|
1009
|
+
else if (Object.keys(result.metadata).length === 0) {
|
|
1010
|
+
// Empty stdout reads as failure; a stderr note keeps stdout parseable.
|
|
1011
|
+
if (!ctx.quiet)
|
|
1012
|
+
process.stderr.write("(no metadata)\n");
|
|
1013
|
+
}
|
|
913
1014
|
else
|
|
914
1015
|
for (const [k, v] of Object.entries(result.metadata))
|
|
915
1016
|
await writeStdout(`${k}=${v}\n`);
|
|
@@ -954,12 +1055,12 @@ Examples:
|
|
|
954
1055
|
export async function runDelete(ctx, args, help = false) {
|
|
955
1056
|
const parsed = parseCommandArgs(args);
|
|
956
1057
|
if (help || parsed.help) {
|
|
957
|
-
|
|
1058
|
+
writeCommandHelp(DELETE_HELP);
|
|
958
1059
|
return 0;
|
|
959
1060
|
}
|
|
960
1061
|
const key = parsed.positionals[0];
|
|
961
1062
|
if (!key) {
|
|
962
|
-
|
|
1063
|
+
writeCommandHelp(DELETE_HELP);
|
|
963
1064
|
return 2;
|
|
964
1065
|
}
|
|
965
1066
|
if (flagBool(parsed.flags, "--dry-run")) {
|
|
@@ -991,7 +1092,7 @@ Examples:
|
|
|
991
1092
|
export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
992
1093
|
const parsed = parseCommandArgs(args);
|
|
993
1094
|
if (help || parsed.help) {
|
|
994
|
-
|
|
1095
|
+
writeCommandHelp(COMMENT_HELP);
|
|
995
1096
|
return 0;
|
|
996
1097
|
}
|
|
997
1098
|
const target = ghTargetFromFlags(parsed.flags, run);
|
|
@@ -1019,7 +1120,7 @@ Examples:
|
|
|
1019
1120
|
`;
|
|
1020
1121
|
export async function runUsage(ctx, args, help = false) {
|
|
1021
1122
|
if (help || parseCommandArgs(args).help) {
|
|
1022
|
-
|
|
1123
|
+
writeCommandHelp(USAGE_HELP);
|
|
1023
1124
|
return 0;
|
|
1024
1125
|
}
|
|
1025
1126
|
const result = await ctx.client.usage();
|
|
@@ -1047,7 +1148,7 @@ Examples:
|
|
|
1047
1148
|
`;
|
|
1048
1149
|
export async function runReconcile(ctx, args, help = false) {
|
|
1049
1150
|
if (help || parseCommandArgs(args).help) {
|
|
1050
|
-
|
|
1151
|
+
writeCommandHelp(RECONCILE_HELP);
|
|
1051
1152
|
return 0;
|
|
1052
1153
|
}
|
|
1053
1154
|
const result = await ctx.client.reconcile();
|
|
@@ -1070,7 +1171,7 @@ Examples:
|
|
|
1070
1171
|
`;
|
|
1071
1172
|
export async function runPurgeExpired(ctx, args, help = false) {
|
|
1072
1173
|
if (help || parseCommandArgs(args).help) {
|
|
1073
|
-
|
|
1174
|
+
writeCommandHelp(PURGE_HELP);
|
|
1074
1175
|
return 0;
|
|
1075
1176
|
}
|
|
1076
1177
|
const result = await ctx.client.purgeExpired();
|
|
@@ -1096,7 +1197,7 @@ Examples:
|
|
|
1096
1197
|
`;
|
|
1097
1198
|
export async function runHealth(ctx, args, help = false) {
|
|
1098
1199
|
if (help || parseCommandArgs(args).help) {
|
|
1099
|
-
|
|
1200
|
+
writeCommandHelp(HEALTH_HELP);
|
|
1100
1201
|
return 0;
|
|
1101
1202
|
}
|
|
1102
1203
|
const result = await createUploadsClient({
|
|
@@ -1180,7 +1281,7 @@ export async function buildDoctorReport(config, client) {
|
|
|
1180
1281
|
}
|
|
1181
1282
|
export async function runDoctor(ctx, args, help = false) {
|
|
1182
1283
|
if (help || parseCommandArgs(args).help) {
|
|
1183
|
-
|
|
1284
|
+
writeCommandHelp(DOCTOR_HELP);
|
|
1184
1285
|
return 0;
|
|
1185
1286
|
}
|
|
1186
1287
|
const report = await buildDoctorReport(ctx.config, ctx.client);
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
-
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF"];
|
|
2
|
+
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
|
@@ -12,6 +12,8 @@ export interface PutDefaults {
|
|
|
12
12
|
noOptimize?: boolean;
|
|
13
13
|
/** When true, optimize keeps EXIF/XMP/ICC (default strips). */
|
|
14
14
|
keepExif?: boolean;
|
|
15
|
+
/** When true, `put` does NOT auto-resolve/stamp gh.* on the default path. */
|
|
16
|
+
noAutoMeta?: boolean;
|
|
15
17
|
}
|
|
16
18
|
declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
|
|
17
19
|
export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
|
|
@@ -38,4 +40,13 @@ export declare function writeConfigKeys(path: string, keys: UploadsConfigValues,
|
|
|
38
40
|
updated: string[];
|
|
39
41
|
};
|
|
40
42
|
export declare function configValuesFromClient(config: Partial<UploadsClientConfig>, defaults?: PutDefaults): UploadsConfigValues;
|
|
43
|
+
/**
|
|
44
|
+
* Remove keys from the shared config file (e.g. logout clears UPLOADS_TOKEN).
|
|
45
|
+
* No-op for missing file/keys. Preserves other lines and comments.
|
|
46
|
+
*/
|
|
47
|
+
export declare function removeConfigKeys(path: string, keys: readonly string[]): {
|
|
48
|
+
path: string;
|
|
49
|
+
removed: string[];
|
|
50
|
+
existed: boolean;
|
|
51
|
+
};
|
|
41
52
|
export { PUT_DEFAULT_KEY_MAP };
|
package/dist/config-file.js
CHANGED
|
@@ -12,6 +12,7 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
12
12
|
"UPLOADS_NO_GIT",
|
|
13
13
|
"UPLOADS_NO_OPTIMIZE",
|
|
14
14
|
"UPLOADS_KEEP_EXIF",
|
|
15
|
+
"UPLOADS_NO_AUTO_META",
|
|
15
16
|
];
|
|
16
17
|
const PUT_DEFAULT_KEY_MAP = {
|
|
17
18
|
prefix: "UPLOADS_DEFAULT_PREFIX",
|
|
@@ -21,6 +22,7 @@ const PUT_DEFAULT_KEY_MAP = {
|
|
|
21
22
|
noGit: "UPLOADS_NO_GIT",
|
|
22
23
|
noOptimize: "UPLOADS_NO_OPTIMIZE",
|
|
23
24
|
keepExif: "UPLOADS_KEEP_EXIF",
|
|
25
|
+
noAutoMeta: "UPLOADS_NO_AUTO_META",
|
|
24
26
|
};
|
|
25
27
|
function isTruthyConfigFlag(value) {
|
|
26
28
|
if (!value)
|
|
@@ -44,6 +46,8 @@ export function putDefaultsToConfigValues(defaults) {
|
|
|
44
46
|
out.UPLOADS_NO_OPTIMIZE = "1";
|
|
45
47
|
if (defaults.keepExif)
|
|
46
48
|
out.UPLOADS_KEEP_EXIF = "1";
|
|
49
|
+
if (defaults.noAutoMeta)
|
|
50
|
+
out.UPLOADS_NO_AUTO_META = "1";
|
|
47
51
|
return out;
|
|
48
52
|
}
|
|
49
53
|
function parsePutDefaultsFromRaw(raw) {
|
|
@@ -65,6 +69,8 @@ function parsePutDefaultsFromRaw(raw) {
|
|
|
65
69
|
out.noOptimize = true;
|
|
66
70
|
if (isTruthyConfigFlag(raw.UPLOADS_KEEP_EXIF))
|
|
67
71
|
out.keepExif = true;
|
|
72
|
+
if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_META))
|
|
73
|
+
out.noAutoMeta = true;
|
|
68
74
|
return out;
|
|
69
75
|
}
|
|
70
76
|
function parsePutDefaultsFromEnv() {
|
|
@@ -83,6 +89,8 @@ function parsePutDefaultsFromEnv() {
|
|
|
83
89
|
raw.UPLOADS_NO_OPTIMIZE = process.env.UPLOADS_NO_OPTIMIZE;
|
|
84
90
|
if (process.env.UPLOADS_KEEP_EXIF)
|
|
85
91
|
raw.UPLOADS_KEEP_EXIF = process.env.UPLOADS_KEEP_EXIF;
|
|
92
|
+
if (process.env.UPLOADS_NO_AUTO_META)
|
|
93
|
+
raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
|
|
86
94
|
return parsePutDefaultsFromRaw(raw);
|
|
87
95
|
}
|
|
88
96
|
/** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
|
|
@@ -152,6 +160,8 @@ export function mergePutDefaults(...layers) {
|
|
|
152
160
|
out.noOptimize = layer.noOptimize;
|
|
153
161
|
if (layer.keepExif != null)
|
|
154
162
|
out.keepExif = layer.keepExif;
|
|
163
|
+
if (layer.noAutoMeta != null)
|
|
164
|
+
out.noAutoMeta = layer.noAutoMeta;
|
|
155
165
|
}
|
|
156
166
|
return out;
|
|
157
167
|
}
|
|
@@ -216,15 +226,7 @@ export function writeConfigKeys(path, keys, opts) {
|
|
|
216
226
|
updated.push(key);
|
|
217
227
|
}
|
|
218
228
|
}
|
|
219
|
-
|
|
220
|
-
writeFileSync(tmp, lines.join("\n").replace(/\n*$/, "\n"), { encoding: "utf8", mode: 0o600 });
|
|
221
|
-
renameSync(tmp, path);
|
|
222
|
-
try {
|
|
223
|
-
chmodSync(path, 0o600);
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
/* Windows/filesystems may not support modes. */
|
|
227
|
-
}
|
|
229
|
+
writeConfigFileAtomic(path, lines.join("\n"));
|
|
228
230
|
return { path, created: !existed, updated };
|
|
229
231
|
}
|
|
230
232
|
export function configValuesFromClient(config, defaults) {
|
|
@@ -238,4 +240,42 @@ export function configValuesFromClient(config, defaults) {
|
|
|
238
240
|
Object.assign(out, putDefaultsToConfigValues(defaults ?? {}));
|
|
239
241
|
return out;
|
|
240
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Remove keys from the shared config file (e.g. logout clears UPLOADS_TOKEN).
|
|
245
|
+
* No-op for missing file/keys. Preserves other lines and comments.
|
|
246
|
+
*/
|
|
247
|
+
export function removeConfigKeys(path, keys) {
|
|
248
|
+
if (!existsSync(path))
|
|
249
|
+
return { path, removed: [], existed: false };
|
|
250
|
+
const keySet = new Set(keys);
|
|
251
|
+
const removed = [];
|
|
252
|
+
const kept = [];
|
|
253
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
254
|
+
const parsed = parseEnvLine(line);
|
|
255
|
+
if (parsed && keySet.has(parsed.key)) {
|
|
256
|
+
if (!removed.includes(parsed.key))
|
|
257
|
+
removed.push(parsed.key);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
kept.push(line);
|
|
261
|
+
}
|
|
262
|
+
if (removed.length === 0)
|
|
263
|
+
return { path, removed: [], existed: true };
|
|
264
|
+
// Drop consecutive blank lines left by removals
|
|
265
|
+
const collapsed = kept.filter((line, i) => !(line === "" && i > 0 && kept[i - 1] === ""));
|
|
266
|
+
writeConfigFileAtomic(path, collapsed.join("\n"));
|
|
267
|
+
return { path, removed, existed: true };
|
|
268
|
+
}
|
|
269
|
+
/** Atomic write + mode 0o600 (best-effort on platforms without chmod). */
|
|
270
|
+
function writeConfigFileAtomic(path, body) {
|
|
271
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
272
|
+
writeFileSync(tmp, body.replace(/\n*$/, "\n"), { encoding: "utf8", mode: 0o600 });
|
|
273
|
+
renameSync(tmp, path);
|
|
274
|
+
try {
|
|
275
|
+
chmodSync(path, 0o600);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
/* Windows/filesystems may not support modes. */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
241
281
|
export { PUT_DEFAULT_KEY_MAP };
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, UPLOADS_CONFIG_KEYS, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config-file.js";
|
|
1
|
+
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, removeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, UPLOADS_CONFIG_KEYS, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config-file.js";
|
|
2
2
|
export interface UploadsClientConfig {
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
workspace: string;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { loadConfigFile, resolveConfigPath } from "./config-file.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
|
-
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, UPLOADS_CONFIG_KEYS, } from "./config-file.js";
|
|
4
|
+
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, removeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, UPLOADS_CONFIG_KEYS, } from "./config-file.js";
|
|
5
5
|
export const DEFAULT_API_URL = "https://api.uploads.sh";
|
|
6
6
|
export const DEFAULT_WORKSPACE = "default";
|
|
7
7
|
const TOKEN_WORKSPACE_RE = /^up_([a-z0-9][a-z0-9-]{1,62})_/;
|