@buildinternet/uploads 0.9.0 → 0.10.1

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.
@@ -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
+ }
@@ -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
- process.stderr.write(SETUP_HELP);
103
+ writeCommandHelp(SETUP_HELP);
103
104
  return 0;
104
105
  }
105
106
  const apiUrl = flagString(parsed.flags, "--api-url");
package/dist/commands.js CHANGED
@@ -14,7 +14,9 @@ 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
 
@@ -77,7 +84,7 @@ Options:
77
84
  Re-uploading to an existing key WITH --meta replaces that file's
78
85
  entire metadata set; without --meta the existing metadata is
79
86
  preserved. Use "uploads meta set" to edit individual keys.
80
- --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
81
88
 
82
89
  Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
83
90
  Scripted formats (json|url|markdown) also print failures on stdout.
@@ -148,13 +155,20 @@ export function optimizeOptionsFromFlags(flags, defaults) {
148
155
  }
149
156
  function formatOptimizeNote(opt) {
150
157
  if (opt.optimized) {
151
- return `optimized ${opt.originalBytes} → ${opt.outputBytes} bytes (${opt.filename})`;
158
+ return `optimized ${formatByteSize(opt.originalBytes)} → ${formatByteSize(opt.outputBytes)} (${opt.filename})`;
152
159
  }
153
160
  if (opt.skippedReason && opt.skippedReason !== "disabled") {
154
161
  return `optimize skipped (${opt.skippedReason})`;
155
162
  }
156
163
  return undefined;
157
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
+ }
158
172
  /** Frame (optional) then optimize — shared by put/attach/MCP. */
159
173
  export async function prepareImageForUpload(bytes, filename, opts) {
160
174
  let currentBytes = bytes;
@@ -259,6 +273,10 @@ Attachments are public and their repo/number/filename keys are predictable.
259
273
  Private/internal GitHub repository visibility does not restrict access; upload
260
274
  only media that is safe at a public URL.
261
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
+
262
280
  Still images are optimized to WebP by default (same as put). Use --no-optimize
263
281
  to upload originals. Optional --frame wraps images in device/browser chrome.
264
282
 
@@ -293,11 +311,11 @@ Examples:
293
311
  export async function runAttach(ctx, args, help = false, run = execRunner) {
294
312
  const parsed = parseCommandArgs(args);
295
313
  if (help || parsed.help) {
296
- process.stderr.write(ATTACH_HELP);
314
+ writeCommandHelp(ATTACH_HELP);
297
315
  return 0;
298
316
  }
299
317
  if (parsed.positionals.length === 0) {
300
- process.stderr.write(ATTACH_HELP);
318
+ writeCommandHelp(ATTACH_HELP);
301
319
  return 2;
302
320
  }
303
321
  if (parsed.flags.has("--no-comment") && typeof parsed.flags.get("--no-comment") === "string") {
@@ -348,6 +366,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
348
366
  }),
349
367
  metadata,
350
368
  });
369
+ writeReplacedNote(result.replaced, ctx.quiet || ctx.json);
351
370
  const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
352
371
  results.push({
353
372
  ...result,
@@ -393,17 +412,17 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
393
412
  }
394
413
  export async function runPut(ctx, args, help = false, run = execRunner) {
395
414
  if (help) {
396
- process.stderr.write(PUT_HELP);
415
+ writeCommandHelp(PUT_HELP);
397
416
  return 0;
398
417
  }
399
418
  const parsed = parseCommandArgs(args);
400
419
  if (parsed.help) {
401
- process.stderr.write(PUT_HELP);
420
+ writeCommandHelp(PUT_HELP);
402
421
  return 0;
403
422
  }
404
423
  const fileArg = parsed.positionals[0];
405
424
  if (!fileArg) {
406
- process.stderr.write(PUT_HELP);
425
+ writeCommandHelp(PUT_HELP);
407
426
  return 2;
408
427
  }
409
428
  const keyHint = flagString(parsed.flags, "--key");
@@ -562,6 +581,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
562
581
  }),
563
582
  metadata,
564
583
  });
584
+ if (format === "human")
585
+ writeReplacedNote(result.replaced, ctx.quiet, dryRun);
565
586
  const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
566
587
  const markdown = buildMarkdown(embedSrc, { alt, width });
567
588
  let gallery;
@@ -673,7 +694,7 @@ export async function runGallery(ctx, args, help = false) {
673
694
  const parsed = parseCommandArgs(args);
674
695
  const action = parsed.positionals[0];
675
696
  if (help || parsed.help || !action) {
676
- process.stderr.write(GALLERY_HELP);
697
+ writeCommandHelp(GALLERY_HELP);
677
698
  return help || parsed.help ? 0 : 2;
678
699
  }
679
700
  switch (action) {
@@ -887,7 +908,7 @@ async function runFindFiles(ctx, filters, flags) {
887
908
  export async function runList(ctx, args, help = false, run = execRunner) {
888
909
  const parsed = parseCommandArgs(args);
889
910
  if (help || parsed.help) {
890
- process.stderr.write(LIST_HELP);
911
+ writeCommandHelp(LIST_HELP);
891
912
  return 0;
892
913
  }
893
914
  const metaPairs = flagValues(parsed.flags, "--meta");
@@ -945,11 +966,11 @@ Examples:
945
966
  export async function runFind(ctx, args, help = false) {
946
967
  const parsed = parseCommandArgs(args);
947
968
  if (help || parsed.help) {
948
- process.stderr.write(FIND_HELP);
969
+ writeCommandHelp(FIND_HELP);
949
970
  return 0;
950
971
  }
951
972
  if (parsed.positionals.length === 0) {
952
- process.stderr.write(FIND_HELP);
973
+ writeCommandHelp(FIND_HELP);
953
974
  return 2;
954
975
  }
955
976
  const filters = parseMetaFlags(parsed.positionals);
@@ -974,7 +995,7 @@ export async function runMeta(ctx, args, help = false) {
974
995
  const parsed = parseCommandArgs(args);
975
996
  const action = parsed.positionals[0];
976
997
  if (help || parsed.help || !action) {
977
- process.stderr.write(META_HELP);
998
+ writeCommandHelp(META_HELP);
978
999
  return help || parsed.help ? 0 : 2;
979
1000
  }
980
1001
  switch (action) {
@@ -1034,12 +1055,12 @@ Examples:
1034
1055
  export async function runDelete(ctx, args, help = false) {
1035
1056
  const parsed = parseCommandArgs(args);
1036
1057
  if (help || parsed.help) {
1037
- process.stderr.write(DELETE_HELP);
1058
+ writeCommandHelp(DELETE_HELP);
1038
1059
  return 0;
1039
1060
  }
1040
1061
  const key = parsed.positionals[0];
1041
1062
  if (!key) {
1042
- process.stderr.write(DELETE_HELP);
1063
+ writeCommandHelp(DELETE_HELP);
1043
1064
  return 2;
1044
1065
  }
1045
1066
  if (flagBool(parsed.flags, "--dry-run")) {
@@ -1071,7 +1092,7 @@ Examples:
1071
1092
  export async function runComment(ctx, args, help = false, run = execRunner) {
1072
1093
  const parsed = parseCommandArgs(args);
1073
1094
  if (help || parsed.help) {
1074
- process.stderr.write(COMMENT_HELP);
1095
+ writeCommandHelp(COMMENT_HELP);
1075
1096
  return 0;
1076
1097
  }
1077
1098
  const target = ghTargetFromFlags(parsed.flags, run);
@@ -1099,7 +1120,7 @@ Examples:
1099
1120
  `;
1100
1121
  export async function runUsage(ctx, args, help = false) {
1101
1122
  if (help || parseCommandArgs(args).help) {
1102
- process.stderr.write(USAGE_HELP);
1123
+ writeCommandHelp(USAGE_HELP);
1103
1124
  return 0;
1104
1125
  }
1105
1126
  const result = await ctx.client.usage();
@@ -1127,7 +1148,7 @@ Examples:
1127
1148
  `;
1128
1149
  export async function runReconcile(ctx, args, help = false) {
1129
1150
  if (help || parseCommandArgs(args).help) {
1130
- process.stderr.write(RECONCILE_HELP);
1151
+ writeCommandHelp(RECONCILE_HELP);
1131
1152
  return 0;
1132
1153
  }
1133
1154
  const result = await ctx.client.reconcile();
@@ -1150,7 +1171,7 @@ Examples:
1150
1171
  `;
1151
1172
  export async function runPurgeExpired(ctx, args, help = false) {
1152
1173
  if (help || parseCommandArgs(args).help) {
1153
- process.stderr.write(PURGE_HELP);
1174
+ writeCommandHelp(PURGE_HELP);
1154
1175
  return 0;
1155
1176
  }
1156
1177
  const result = await ctx.client.purgeExpired();
@@ -1176,7 +1197,7 @@ Examples:
1176
1197
  `;
1177
1198
  export async function runHealth(ctx, args, help = false) {
1178
1199
  if (help || parseCommandArgs(args).help) {
1179
- process.stderr.write(HEALTH_HELP);
1200
+ writeCommandHelp(HEALTH_HELP);
1180
1201
  return 0;
1181
1202
  }
1182
1203
  const result = await createUploadsClient({
@@ -1260,7 +1281,7 @@ export async function buildDoctorReport(config, client) {
1260
1281
  }
1261
1282
  export async function runDoctor(ctx, args, help = false) {
1262
1283
  if (help || parseCommandArgs(args).help) {
1263
- process.stderr.write(DOCTOR_HELP);
1284
+ writeCommandHelp(DOCTOR_HELP);
1264
1285
  return 0;
1265
1286
  }
1266
1287
  const report = await buildDoctorReport(ctx.config, ctx.client);
@@ -40,4 +40,13 @@ export declare function writeConfigKeys(path: string, keys: UploadsConfigValues,
40
40
  updated: string[];
41
41
  };
42
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
+ };
43
52
  export { PUT_DEFAULT_KEY_MAP };
@@ -226,15 +226,7 @@ export function writeConfigKeys(path, keys, opts) {
226
226
  updated.push(key);
227
227
  }
228
228
  }
229
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
230
- writeFileSync(tmp, lines.join("\n").replace(/\n*$/, "\n"), { encoding: "utf8", mode: 0o600 });
231
- renameSync(tmp, path);
232
- try {
233
- chmodSync(path, 0o600);
234
- }
235
- catch {
236
- /* Windows/filesystems may not support modes. */
237
- }
229
+ writeConfigFileAtomic(path, lines.join("\n"));
238
230
  return { path, created: !existed, updated };
239
231
  }
240
232
  export function configValuesFromClient(config, defaults) {
@@ -248,4 +240,42 @@ export function configValuesFromClient(config, defaults) {
248
240
  Object.assign(out, putDefaultsToConfigValues(defaults ?? {}));
249
241
  return out;
250
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
+ }
251
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})_/;
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
1
+ export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "GITHUB_REQUIRED" | "API_ERROR" | "NETWORK" | "USAGE";
2
2
  export declare class UploadsError extends Error {
3
3
  readonly code: UploadsErrorCode;
4
4
  readonly status?: number;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Human-readable size for CLI notes (1024-based).
3
+ * files-sdk has no size formatter — only raw `size` on head/upload results.
4
+ */
5
+ export declare function formatByteSize(bytes: number): string;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Human-readable size for CLI notes (1024-based).
3
+ * files-sdk has no size formatter — only raw `size` on head/upload results.
4
+ */
5
+ export function formatByteSize(bytes) {
6
+ if (!Number.isFinite(bytes) || bytes <= 0)
7
+ return "0 B";
8
+ const units = ["B", "KB", "MB", "GB", "TB"];
9
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
10
+ return `${(bytes / 1024 ** exponent).toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;
11
+ }
@@ -23,6 +23,16 @@ export declare function parseSemver(version: string): [number, number, number] |
23
23
  export declare function isNewerVersion(latest: string, current: string): boolean;
24
24
  export declare function readUpdateCache(path: string): UpdateCache | undefined;
25
25
  export declare function writeUpdateCache(path: string, cache: UpdateCache): void;
26
+ export interface UpdateStatus {
27
+ current: string;
28
+ latest?: string;
29
+ updateAvailable: boolean;
30
+ }
31
+ /**
32
+ * Resolve current vs latest published version (cache + optional network).
33
+ * Always resolves; never throws. Does not print.
34
+ */
35
+ export declare function checkForUpdate(opts?: UpdateCheckOptions): Promise<UpdateStatus>;
26
36
  /**
27
37
  * If a newer published version is known (or can be fetched within the timeout),
28
38
  * write a one-line stderr hint. Always resolves; never throws.
@@ -69,20 +69,21 @@ export function writeUpdateCache(path, cache) {
69
69
  }
70
70
  }
71
71
  /**
72
- * If a newer published version is known (or can be fetched within the timeout),
73
- * write a one-line stderr hint. Always resolves; never throws.
72
+ * Resolve current vs latest published version (cache + optional network).
73
+ * Always resolves; never throws. Does not print.
74
74
  */
75
- export async function maybeHintUpdate(opts = {}) {
75
+ export async function checkForUpdate(opts = {}) {
76
+ const current = opts.currentVersion ?? packageVersion();
76
77
  try {
77
- if (opts.quiet || opts.command === "mcp")
78
- return;
79
- if (truthyEnv("UPLOADS_NO_UPDATE") || truthyEnv("NO_UPDATE_NOTIFIER"))
80
- return;
81
- const current = opts.currentVersion ?? packageVersion();
78
+ if (opts.quiet || opts.command === "mcp") {
79
+ return { current, updateAvailable: false };
80
+ }
81
+ if (truthyEnv("UPLOADS_NO_UPDATE") || truthyEnv("NO_UPDATE_NOTIFIER")) {
82
+ return { current, updateAvailable: false };
83
+ }
82
84
  const cachePath = opts.cachePath ?? defaultCachePath();
83
85
  const now = opts.now ?? Date.now();
84
86
  const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
85
- const write = opts.write ?? ((text) => process.stderr.write(text));
86
87
  const cached = readUpdateCache(cachePath);
87
88
  let latest;
88
89
  if (cached && now - cached.checkedAt < ttlMs && cached.current === current) {
@@ -98,9 +99,24 @@ export async function maybeHintUpdate(opts = {}) {
98
99
  latest = cached.latest; // stale cache if network failed
99
100
  }
100
101
  }
101
- if (latest && isNewerVersion(latest, current)) {
102
- write(`hint: ${PACKAGE_NAME}@${latest} is available (you have ${current}). Update: npm i -g ${PACKAGE_NAME}\n`);
103
- }
102
+ const updateAvailable = Boolean(latest && isNewerVersion(latest, current));
103
+ return { current, latest, updateAvailable };
104
+ }
105
+ catch {
106
+ return { current, updateAvailable: false };
107
+ }
108
+ }
109
+ /**
110
+ * If a newer published version is known (or can be fetched within the timeout),
111
+ * write a one-line stderr hint. Always resolves; never throws.
112
+ */
113
+ export async function maybeHintUpdate(opts = {}) {
114
+ try {
115
+ const status = await checkForUpdate(opts);
116
+ if (!status.updateAvailable || !status.latest)
117
+ return;
118
+ const write = opts.write ?? ((text) => process.stderr.write(text));
119
+ write(`hint: ${PACKAGE_NAME}@${status.latest} is available (you have ${status.current}). Update: npm i -g ${PACKAGE_NAME}\n`);
104
120
  }
105
121
  catch {
106
122
  // Never surface update-check failures.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,