@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.
@@ -0,0 +1,176 @@
1
+ import { DEFAULT_TAGLINE, formatAuthBanner, formatBrandHeader, formatUpdateBanner, } from "./cli-brand.js";
2
+ import { ROOT_COMMANDS } from "./cli-catalog.js";
3
+ import { colorEnabled, createStyle, padCmd } from "./cli-style.js";
4
+ import { packageVersion } from "./package-version.js";
5
+ const CMD_WIDTH = 22;
6
+ function toRow(c) {
7
+ return [c.usage ?? c.name, c.summary];
8
+ }
9
+ /** Preferred order for the short essentials help (subset of ROOT_COMMANDS). */
10
+ const ESSENTIAL_ORDER = [
11
+ "put",
12
+ "attach",
13
+ "login",
14
+ "whoami",
15
+ "list",
16
+ "delete",
17
+ "doctor",
18
+ "install",
19
+ ];
20
+ /** Day-to-day commands shown on bare `uploads` / `uploads help`. */
21
+ const ESSENTIALS = ESSENTIAL_ORDER.map((name) => {
22
+ const cmd = ROOT_COMMANDS.find((c) => c.name === name);
23
+ if (!cmd)
24
+ throw new Error(`cli-catalog missing essential command: ${name}`);
25
+ return toRow(cmd);
26
+ });
27
+ /** Full catalog (same surface as before, still discoverable via help --all). */
28
+ const ALL_COMMANDS = ROOT_COMMANDS.map(toRow);
29
+ function rows(style, items) {
30
+ return items
31
+ .map(([name, desc]) => ` ${padCmd(name, CMD_WIDTH, style)}${style.body(desc)}`)
32
+ .join("\n");
33
+ }
34
+ function section(style, title) {
35
+ return style.heading(title);
36
+ }
37
+ function header(style, opts) {
38
+ const version = opts.version ?? packageVersion();
39
+ const brandMark = opts.brandMark !== false;
40
+ const parts = [];
41
+ // Auth first — loud and above everything when there's no token yet.
42
+ if (opts.needsAuth) {
43
+ parts.push(formatAuthBanner({ color: style.enabled }));
44
+ }
45
+ // Half-block mark when color is on. Plain three-line title otherwise so
46
+ // piped/agent output stays compact and greppable.
47
+ if (!brandMark || !style.enabled) {
48
+ parts.push(`${style.title("uploads.sh")}\n` +
49
+ `${style.muted(DEFAULT_TAGLINE)}\n` +
50
+ `${style.muted(`v${version}`)}\n`);
51
+ }
52
+ else {
53
+ parts.push(formatBrandHeader({
54
+ color: true,
55
+ label: "uploads.sh",
56
+ tagline: DEFAULT_TAGLINE,
57
+ version,
58
+ }));
59
+ }
60
+ if (opts.latestVersion && opts.latestVersion !== version) {
61
+ parts.push(formatUpdateBanner({
62
+ current: version,
63
+ latest: opts.latestVersion,
64
+ color: style.enabled,
65
+ }));
66
+ }
67
+ return parts.join("");
68
+ }
69
+ function essentialsBody(style, opts) {
70
+ return `${header(style, opts)}
71
+ ${section(style, "Usage:")}
72
+ uploads [globals] <command> [args]
73
+
74
+ ${section(style, "Essentials:")}
75
+ ${rows(style, ESSENTIALS)}
76
+
77
+ ${section(style, "More help:")}
78
+ ${padCmd("uploads help --all", CMD_WIDTH, style)}${style.body("Full command list, globals, and config")}
79
+ ${padCmd("uploads <cmd> --help", CMD_WIDTH, style)}${style.body("Per-command options and examples")}
80
+
81
+ ${section(style, "Globals (before command):")}
82
+ ${style.muted("--api-url, --token, --workspace/-w, --env-file, --json, --quiet, --version/-V")}
83
+
84
+ ${section(style, "Examples:")}
85
+ ${style.command("uploads login")}
86
+ ${style.command("uploads whoami")}
87
+ ${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
88
+ ${style.command("uploads put")} ./after.png --pr 123 --comment
89
+ ${style.command("uploads put")} ./bug.png --issue 45
90
+ ${style.command("uploads put")} ./shot.png --meta app=myapp --meta page=settings
91
+ ${style.command("uploads attach")} ./before.png ./after.png
92
+ ${style.command("uploads attach")} ./shot.png --pr 123 --repo myorg/myapp
93
+ ${style.command("uploads attach")} ./shot.png --meta app=myapp --meta page=settings
94
+ ${style.command("uploads doctor")}
95
+ ${style.command("uploads install")}
96
+ ${style.command("uploads logout")}
97
+ `;
98
+ }
99
+ function fullBody(style, opts) {
100
+ return `${header(style, opts)}
101
+ ${section(style, "Usage:")}
102
+ uploads [globals] <command> [args]
103
+
104
+ ${section(style, "Config")} ${style.muted("(first match wins, per key):")}
105
+ CLI flags --api-url, --token, --workspace
106
+ environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
107
+ --env-file <path>
108
+ $BUILDINTERNET_CONFIG
109
+ ~/.config/buildinternet/config
110
+
111
+ ${section(style, "Workspace")} ${style.muted("(within config layers):")}
112
+ --workspace, -w override — global (before command) or per-command (after)
113
+ UPLOADS_WORKSPACE env / config file
114
+ (else inferred from token up_<name>_…, else "default")
115
+
116
+ ${section(style, "Other globals")} ${style.muted("(before command):")}
117
+ --api-url <url> default: https://api.uploads.sh
118
+ --token <token> or UPLOADS_TOKEN
119
+ --env-file <path>
120
+ --json JSON on stdout
121
+ --quiet Suppress stderr progress and update hints
122
+ --version, -V Print package version and exit
123
+
124
+ ${section(style, "Commands:")}
125
+ ${rows(style, ALL_COMMANDS)}
126
+
127
+ ${section(style, "Put/list defaults")} ${style.muted("(config file or env):")}
128
+ UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
129
+ UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
130
+
131
+ ${section(style, "Update hints")} ${style.muted("(stderr, once/day):")} silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
132
+
133
+ ${section(style, "Examples:")}
134
+ ${style.command("uploads login")}
135
+ ${style.command("uploads whoami")}
136
+ ${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
137
+ ${style.command("uploads put")} ./after.png --pr 123 --comment
138
+ ${style.command("uploads put")} ./bug.png --issue 45 --repo myorg/myapp
139
+ ${style.command("uploads put")} ./shot.png --dry-run --format url
140
+ ${style.command("uploads put")} ./shot.png --meta app=myapp --meta page=settings
141
+ ${style.command("uploads attach")} ./before.png ./after.png
142
+ ${style.command("uploads attach")} ./shot.png --pr 123 --repo myorg/myapp
143
+ ${style.command("uploads attach")} ./artifact.zip --issue 45 --no-comment
144
+ ${style.command("uploads attach")} ./shot.png --meta app=myapp --meta page=settings
145
+ ${style.command("uploads gallery")} create --title "Release screenshots"
146
+ ${style.command("uploads doctor")}
147
+ ${style.command("uploads logout")}
148
+ ${style.command("uploads --version")}
149
+
150
+ ${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up the agent skill and the hosted MCP server")}
151
+ ${style.body("(https://agents.uploads.sh/mcp, workspace inferred from the token). Run")}
152
+ ${style.body("`uploads mcp` for local stdio, or use createUploadsWorkerFileTools()")}
153
+ ${style.body("from @buildinternet/uploads/agent on the Worker.")}
154
+
155
+ ${style.muted("Tip: uploads help essentials only")}
156
+ ${style.muted(" uploads help --all this full listing")}
157
+ `;
158
+ }
159
+ /**
160
+ * Root help text. Default is a short essentials view; pass `full: true` for
161
+ * the complete command + config dump (`uploads help --all`).
162
+ */
163
+ export function formatRootHelp(options = {}) {
164
+ const style = options.style ??
165
+ createStyle(options.color !== undefined ? options.color : colorEnabled(process.stderr));
166
+ const body = options.full ? fullBody(style, options) : essentialsBody(style, options);
167
+ return body.endsWith("\n") ? body : `${body}\n`;
168
+ }
169
+ /** True when argv for the `help` command requests the full listing. */
170
+ export function wantsFullHelp(args) {
171
+ for (const a of args) {
172
+ if (a === "--all" || a === "-a" || a === "all")
173
+ return true;
174
+ }
175
+ return false;
176
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Minimal ANSI styling for CLI help hierarchy.
3
+ * Accent palette matches packages/ui tokens (truecolor when enabled).
4
+ * Honors NO_COLOR / FORCE_COLOR and the target stream's TTY status.
5
+ * @see https://no-color.org/
6
+ * @see packages/ui/src/tokens.css
7
+ */
8
+ export type StyleFn = (text: string) => string;
9
+ export interface CliStyle {
10
+ bold: StyleFn;
11
+ dim: StyleFn;
12
+ /** Section headings — brand accent violet */
13
+ heading: StyleFn;
14
+ /** Command / flag names — brand green */
15
+ command: StyleFn;
16
+ /** Muted secondary text — token muted gray */
17
+ muted: StyleFn;
18
+ /** Body / description text */
19
+ body: StyleFn;
20
+ /** High-emphasis title / wordmark */
21
+ title: StyleFn;
22
+ /** Errors / unknown-command banner — brand red */
23
+ error: StyleFn;
24
+ /** Brand accent (links, tips) */
25
+ accent: StyleFn;
26
+ enabled: boolean;
27
+ }
28
+ /** Whether color should be enabled for a given stream. */
29
+ export declare function colorEnabled(stream?: {
30
+ isTTY?: boolean;
31
+ }, env?: NodeJS.ProcessEnv): boolean;
32
+ export declare function createStyle(enabled: boolean): CliStyle;
33
+ /** Pad a left column so descriptions line up (ANSI-aware length). */
34
+ export declare function padCmd(name: string, width: number, style: CliStyle): string;
35
+ /**
36
+ * Apply root-help visual hierarchy to a plain multi-line command help string:
37
+ * accent section headers, green flags/commands, muted body.
38
+ * No-op when color is disabled (returns text unchanged aside from trailing newline).
39
+ */
40
+ export declare function formatCommandHelp(text: string, style?: CliStyle): string;
41
+ /** Write styled command help to stderr (or a custom writer). */
42
+ export declare function writeCommandHelp(text: string, write?: (chunk: string) => void): void;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Minimal ANSI styling for CLI help hierarchy.
3
+ * Accent palette matches packages/ui tokens (truecolor when enabled).
4
+ * Honors NO_COLOR / FORCE_COLOR and the target stream's TTY status.
5
+ * @see https://no-color.org/
6
+ * @see packages/ui/src/tokens.css
7
+ */
8
+ import { BRAND } from "./cli-brand.js";
9
+ const identity = (t) => t;
10
+ const RESET = "\u001b[0m";
11
+ const BOLD = "\u001b[1m";
12
+ function fgTrue(c) {
13
+ return `\u001b[38;2;${c.r};${c.g};${c.b}m`;
14
+ }
15
+ function wrapRgb(c, bold = false) {
16
+ const open = (bold ? BOLD : "") + fgTrue(c);
17
+ return (text) => `${open}${text}${RESET}`;
18
+ }
19
+ /** Whether color should be enabled for a given stream. */
20
+ export function colorEnabled(stream = process.stderr, env = process.env) {
21
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
22
+ return false;
23
+ if (env.FORCE_COLOR === "0")
24
+ return false;
25
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "")
26
+ return true;
27
+ return stream.isTTY === true;
28
+ }
29
+ export function createStyle(enabled) {
30
+ if (!enabled) {
31
+ return {
32
+ bold: identity,
33
+ dim: identity,
34
+ heading: identity,
35
+ command: identity,
36
+ muted: identity,
37
+ body: identity,
38
+ title: identity,
39
+ error: identity,
40
+ accent: identity,
41
+ enabled: false,
42
+ };
43
+ }
44
+ return {
45
+ bold: (text) => `${BOLD}${fgTrue(BRAND.fg)}${text}${RESET}`,
46
+ dim: (text) => `\u001b[2m${text}${RESET}`,
47
+ heading: wrapRgb(BRAND.accent, true),
48
+ command: wrapRgb(BRAND.green, true),
49
+ muted: wrapRgb(BRAND.muted),
50
+ body: wrapRgb(BRAND.body),
51
+ title: wrapRgb(BRAND.fg, true),
52
+ error: wrapRgb(BRAND.red, true),
53
+ accent: wrapRgb(BRAND.accent),
54
+ enabled: true,
55
+ };
56
+ }
57
+ /** Pad a left column so descriptions line up (ANSI-aware length). */
58
+ export function padCmd(name, width, style) {
59
+ const pad = Math.max(0, width - name.length);
60
+ return style.command(name) + " ".repeat(pad);
61
+ }
62
+ /** Section labels used across command --help blocks. */
63
+ const SECTION_RE = /^(Options|Examples|Commands|Subcommands|Keys|Shells|Usage|What it does|What runs under the hood|Exit codes|Config|Workspace):\s*$/;
64
+ /** Flag column: ` --pr <num> …` or ` --workspace, -w <name> …`. */
65
+ const FLAG_RE = /^(\s+)(-[\w-]+(?:,\s*-[\w-]+)?(?:\s+<[^>]+>)?)(\s{2,})(.*)$/;
66
+ /** Example / invocation line starting with the CLI name. */
67
+ const EXAMPLE_RE = /^(\s*)(uploads(?:\s+\S+)*)(.*)$/;
68
+ /**
69
+ * Apply root-help visual hierarchy to a plain multi-line command help string:
70
+ * accent section headers, green flags/commands, muted body.
71
+ * No-op when color is disabled (returns text unchanged aside from trailing newline).
72
+ */
73
+ export function formatCommandHelp(text, style = createStyle(colorEnabled(process.stderr))) {
74
+ const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
75
+ if (!style.enabled)
76
+ return `${normalized}\n`;
77
+ const lines = normalized.split("\n");
78
+ let firstContent = true;
79
+ const out = lines.map((line) => {
80
+ if (line.trim() === "")
81
+ return line;
82
+ // Synopsis line (first non-empty): high emphasis
83
+ if (firstContent) {
84
+ firstContent = false;
85
+ return style.title(line);
86
+ }
87
+ if (SECTION_RE.test(line))
88
+ return style.heading(line);
89
+ const flag = FLAG_RE.exec(line);
90
+ if (flag) {
91
+ const [, indent, name, gap, desc] = flag;
92
+ return `${indent}${style.command(name)}${gap}${style.body(desc)}`;
93
+ }
94
+ const example = EXAMPLE_RE.exec(line);
95
+ if (example && line.trimStart().startsWith("uploads")) {
96
+ const [, indent, cmd, rest] = example;
97
+ return `${indent}${style.command(cmd)}${style.body(rest)}`;
98
+ }
99
+ return style.body(line);
100
+ });
101
+ return `${out.join("\n")}\n`;
102
+ }
103
+ /** Write styled command help to stderr (or a custom writer). */
104
+ export function writeCommandHelp(text, write = (c) => {
105
+ process.stderr.write(c);
106
+ }) {
107
+ write(formatCommandHelp(text));
108
+ }
package/dist/cli.js CHANGED
@@ -2,6 +2,8 @@ import { createUploadsClient } from "./client.js";
2
2
  import { resolveApiUrl, resolveConfig } from "./config.js";
3
3
  import { UploadsError } from "./errors.js";
4
4
  import { commandWorkspace, flagString, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
5
+ import { formatRootHelp, wantsFullHelp } from "./cli-help.js";
6
+ import { colorEnabled, createStyle } from "./cli-style.js";
5
7
  import { runPut, runAttach, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
6
8
  import { runConfig } from "./commands/config.js";
7
9
  import { runSetup } from "./commands/setup.js";
@@ -10,75 +12,34 @@ import { runInvite } from "./commands/invite.js";
10
12
  import { runAdmin } from "./commands/admin-enrollment.js";
11
13
  import { runMcp } from "./commands/mcp.js";
12
14
  import { runInstall } from "./commands/install.js";
15
+ import { runCompletion } from "./commands/completion.js";
16
+ import { runLogout, runWhoami } from "./commands/session.js";
13
17
  import { packageVersion } from "./package-version.js";
14
- import { maybeHintUpdate } from "./update-check.js";
15
- const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
16
-
17
- Usage:
18
- uploads [globals] <command> [args]
19
-
20
- Config (first match wins, per key):
21
- CLI flags --api-url, --token, --workspace
22
- environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
23
- --env-file <path>
24
- $BUILDINTERNET_CONFIG
25
- ~/.config/buildinternet/config
26
-
27
- Workspace (within config layers):
28
- --workspace, -w override — global (before command) or per-command (after)
29
- UPLOADS_WORKSPACE env / config file
30
- (else inferred from token up_<name>_…, else "default")
31
-
32
- Other globals (before command):
33
- --api-url <url> default: https://api.uploads.sh
34
- --token <token> or UPLOADS_TOKEN
35
- --env-file <path>
36
- --json JSON on stdout
37
- --quiet Suppress stderr progress and update hints
38
- --version, -V Print package version and exit
39
-
40
- Commands:
41
- attach <file...> Attach media to the current PR (stable URLs + managed comment)
42
- put <file> Upload (+ URL + markdown for GitHub)
43
- gallery Create and organize public media galleries
44
- comment Create/update a PR/issue attachments comment (via gh)
45
- list List objects (--meta k=v filters by queryable metadata)
46
- find k=v... List objects matching metadata (alias for list --meta)
47
- meta Get/set an object's queryable metadata
48
- delete <key> Delete object
49
- usage Workspace storage / upload counters
50
- reconcile Rebuild usage ledger from storage
51
- purge-expired Delete objects past retentionDays
52
- setup Inspect/configure advanced CLI settings
53
- install Install the agent skill + register the remote MCP server
54
- login Sign in via browser (or an enrollment code) and save credentials
55
- invite Invite a teammate to a workspace (workspace admin; device login)
56
- admin Site-operator invitation management (ADMIN_TOKEN)
57
- config Show path, init, or set shared config
58
- doctor Health + auth + workspace checks
59
- health API liveness (no auth)
60
- mcp Serve MCP over stdio (tools mirror the CLI)
61
-
62
- Put/list defaults (config file or env):
63
- UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
64
- UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
65
-
66
- Update hints (stderr, once/day): silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
67
-
68
- Examples:
69
- uploads setup
70
- uploads setup --token up_default_… --repo myorg/myapp
71
- uploads attach ./before.png ./after.png
72
- uploads put ./shot.png --ref 42
73
- uploads gallery create --title "Release screenshots"
74
- uploads doctor
75
- uploads --version
76
-
77
- Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
78
- (https://agents.uploads.sh/mcp, workspace inferred from the token). Run
79
- \`uploads mcp\` for local stdio, or use createUploadsWorkerFileTools()
80
- from @buildinternet/uploads/agent on the Worker.
81
- `;
18
+ import { checkForUpdate, maybeHintUpdate } from "./update-check.js";
19
+ async function writeRootHelp(options = {}) {
20
+ // Best-effort version check so the help header can show a banner when outdated.
21
+ // Short timeout; cache still applies (once/day). Never blocks help on network.
22
+ const update = await checkForUpdate({ timeoutMs: 800 });
23
+ // Empty token (and no env/config) → first-run auth banner.
24
+ let needsAuth = true;
25
+ try {
26
+ const cfg = resolveConfig({
27
+ requireToken: false,
28
+ token: options.token,
29
+ envFile: options.envFile,
30
+ });
31
+ needsAuth = !cfg.token;
32
+ }
33
+ catch {
34
+ needsAuth = true;
35
+ }
36
+ process.stderr.write(formatRootHelp({
37
+ full: options.full,
38
+ version: update.current,
39
+ latestVersion: update.updateAvailable ? update.latest : undefined,
40
+ needsAuth,
41
+ }));
42
+ }
82
43
  function createContext(globals, requireToken, commandArgs) {
83
44
  const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
84
45
  const config = resolveConfig({
@@ -191,9 +152,18 @@ export async function runCli(argv) {
191
152
  process.stdout.write(`${packageVersion()}\n`);
192
153
  return 0;
193
154
  }
194
- if (!parsed.command) {
195
- process.stderr.write(ROOT_HELP);
196
- return parsed.help ? 0 : 2;
155
+ // Root help: bare `uploads`, `--help`/`-h`, or `help` / `help --all`.
156
+ const isHelpCommand = parsed.command === "help";
157
+ if (!parsed.command || isHelpCommand) {
158
+ const helpArgs = isHelpCommand ? parsed.rest.slice(1) : parsed.rest;
159
+ const full = Boolean(parsed.globals.all) || wantsFullHelp(helpArgs);
160
+ await writeRootHelp({
161
+ full,
162
+ token: parsed.globals.token,
163
+ envFile: parsed.globals.envFile,
164
+ });
165
+ // Explicit help exits 0; bare `uploads` is usage → 2.
166
+ return parsed.help || isHelpCommand ? 0 : 2;
197
167
  }
198
168
  const cmdArgs = parsed.rest.slice(1);
199
169
  const showHelp = parsed.help || cmdArgs.some(isHelpFlag);
@@ -211,6 +181,19 @@ export async function runCli(argv) {
211
181
  case "login":
212
182
  code = await runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
213
183
  break;
184
+ case "whoami":
185
+ case "status":
186
+ code = await runWhoami(cmdArgs, {
187
+ json,
188
+ envFile: parsed.globals.envFile,
189
+ token: parsed.globals.token,
190
+ workspace: parsed.globals.workspace,
191
+ apiUrl: parsed.globals.apiUrl,
192
+ }, showHelp);
193
+ break;
194
+ case "logout":
195
+ code = await runLogout(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
196
+ break;
214
197
  case "invite":
215
198
  code = await runInvite(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
216
199
  break;
@@ -223,6 +206,10 @@ export async function runCli(argv) {
223
206
  case "install":
224
207
  code = await runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
225
208
  break;
209
+ case "completion":
210
+ case "completions":
211
+ code = await runCompletion(cmdArgs, showHelp);
212
+ break;
226
213
  case "attach":
227
214
  case "put":
228
215
  case "gallery":
@@ -276,9 +263,16 @@ export async function runCli(argv) {
276
263
  }
277
264
  break;
278
265
  }
279
- default:
280
- process.stderr.write(`unknown command: ${parsed.command}\n\n${ROOT_HELP}`);
266
+ default: {
267
+ const style = createStyle(colorEnabled(process.stderr));
268
+ process.stderr.write(`${style.error(`unknown command: ${parsed.command}`)}\n\n`);
269
+ await writeRootHelp({
270
+ full: false,
271
+ token: parsed.globals.token,
272
+ envFile: parsed.globals.envFile,
273
+ });
281
274
  return 2;
275
+ }
282
276
  }
283
277
  // Best-effort; skipped for mcp, --quiet/--json, and opt-out env vars.
284
278
  if (code === 0 && !showHelp) {
package/dist/client.d.ts CHANGED
@@ -60,6 +60,11 @@ export interface PutResult {
60
60
  embedUrl: string | null;
61
61
  size: number;
62
62
  contentType: string;
63
+ /**
64
+ * True when the put overwrote an existing key, or (with dryRun) when a put
65
+ * at this key would overwrite. Always set by the API for put/dry-run.
66
+ */
67
+ replaced?: boolean;
63
68
  metadata?: Record<string, string>;
64
69
  }
65
70
  export interface ListItem {
@@ -282,6 +287,18 @@ export interface MintWorkspaceSummary {
282
287
  export declare function listMintWorkspaces(apiUrl: string, accessToken: string): Promise<{
283
288
  workspaces: MintWorkspaceSummary[];
284
289
  }>;
290
+ export interface CreateWorkspaceResult {
291
+ name: string;
292
+ publicBaseUrl: string;
293
+ selfServe: boolean;
294
+ }
295
+ /**
296
+ * POST /v1/workspaces — self-serve workspace creation from a device-flow
297
+ * session (presented as a bearer). Throws `UsageError` with a message tuned
298
+ * for CLI display: a linked-GitHub requirement gets an actionable pointer,
299
+ * everything else surfaces the server's message.
300
+ */
301
+ export declare function createWorkspaceRequest(apiUrl: string, accessToken: string, name: string): Promise<CreateWorkspaceResult>;
285
302
  export interface MintTokenResult {
286
303
  token: string;
287
304
  workspace: string;
@@ -306,6 +323,8 @@ export declare function createWorkspaceInvite(apiUrl: string, accessToken: strin
306
323
  status: string;
307
324
  };
308
325
  acceptUrl?: string;
326
+ /** Whether the install can send invite emails; absent on older auth workers. */
327
+ emailConfigured?: boolean;
309
328
  }>;
310
329
  /**
311
330
  * POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
package/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { inferContentType } from "./embed.js";
2
+ import { UsageError } from "./cli-args.js";
2
3
  import { UploadsError } from "./errors.js";
3
4
  import { buildScreenshotKey } from "./keys.js";
4
5
  import { packageVersion } from "./package-version.js";
@@ -112,6 +113,31 @@ export function listMintWorkspaces(apiUrl, accessToken) {
112
113
  headers: { Authorization: `Bearer ${accessToken}` },
113
114
  });
114
115
  }
116
+ /**
117
+ * POST /v1/workspaces — self-serve workspace creation from a device-flow
118
+ * session (presented as a bearer). Throws `UsageError` with a message tuned
119
+ * for CLI display: a linked-GitHub requirement gets an actionable pointer,
120
+ * everything else surfaces the server's message.
121
+ */
122
+ export async function createWorkspaceRequest(apiUrl, accessToken, name) {
123
+ try {
124
+ const { workspace } = await jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/workspaces`, {
125
+ method: "POST",
126
+ headers: {
127
+ Authorization: `Bearer ${accessToken}`,
128
+ "Content-Type": "application/json",
129
+ },
130
+ body: JSON.stringify({ name }),
131
+ });
132
+ return workspace;
133
+ }
134
+ catch (err) {
135
+ if (err instanceof UploadsError && err.code === "GITHUB_REQUIRED") {
136
+ throw new UsageError("creating a workspace requires a linked GitHub account — connect one at https://uploads.sh/account/profile and re-run `uploads login`");
137
+ }
138
+ throw new UsageError(err instanceof Error ? err.message : "workspace creation failed");
139
+ }
140
+ }
115
141
  /**
116
142
  * POST /me/workspaces/:name/invites — org invitation for a workspace.
117
143
  * Requires a Better Auth session bearer (device flow), not a workspace token.
@@ -176,6 +202,9 @@ function mapApiError(status, error, code) {
176
202
  if (code === "upload_budget_exceeded") {
177
203
  return new UploadsError(error, "UPLOAD_BUDGET", status);
178
204
  }
205
+ if (code === "github_required") {
206
+ return new UploadsError(error, "GITHUB_REQUIRED", status);
207
+ }
179
208
  return new UploadsError(error, "API_ERROR", status);
180
209
  }
181
210
  /**
@@ -277,6 +306,7 @@ export function createUploadsClient(config) {
277
306
  embedUrl: resolveEmbedUrl(preview.url, preview.embedUrl),
278
307
  size: body.byteLength,
279
308
  contentType,
309
+ replaced: preview.replaced === true,
280
310
  };
281
311
  }
282
312
  const headers = { "Content-Type": contentType };
@@ -305,6 +335,7 @@ export function createUploadsClient(config) {
305
335
  ...result,
306
336
  url: result.url,
307
337
  embedUrl: resolveEmbedUrl(result.url, result.embedUrl),
338
+ replaced: result.replaced === true,
308
339
  };
309
340
  },
310
341
  list,
@@ -1,5 +1,6 @@
1
1
  import { createEnrollment } from "../client.js";
2
2
  import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
3
+ import { writeCommandHelp } from "../cli-style.js";
3
4
  const HELP = `uploads admin invite create [options]
4
5
 
5
6
  Admin-only: create a short-lived invitation for an existing workspace.
@@ -65,7 +66,7 @@ export function parseScopes(raw) {
65
66
  export async function runAdmin(args, opts, help = false) {
66
67
  const parsed = parseCommandArgs(args);
67
68
  if (help || parsed.help) {
68
- process.stderr.write(HELP);
69
+ writeCommandHelp(HELP);
69
70
  return 0;
70
71
  }
71
72
  if (!["invite", "enrollment"].includes(parsed.positionals[0] ?? "") ||
@@ -0,0 +1,3 @@
1
+ import { type CompletionShell } from "../cli-catalog.js";
2
+ export declare function generateCompletionScript(shell: CompletionShell): string;
3
+ export declare function runCompletion(args: string[], help?: boolean): Promise<number>;