@buildinternet/uploads 0.31.0 → 0.33.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/dist/client.d.ts CHANGED
@@ -437,6 +437,7 @@ export interface MintWorkspaceSummary {
437
437
  /** GET /v1/tokens — workspaces the signed-in user can mint tokens for. */
438
438
  export declare function listMintWorkspaces(apiUrl: string, accessToken: string): Promise<{
439
439
  workspaces: MintWorkspaceSummary[];
440
+ suggestedWorkspace?: string;
440
441
  }>;
441
442
  export interface CreateWorkspaceResult {
442
443
  name: string;
@@ -11,7 +11,7 @@ non-secret page URL plus a code you share separately). The legacy
11
11
 
12
12
  Options:
13
13
  --admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
14
- --workspace <name> Default: default
14
+ --workspace <name> Required — the workspace the invite grants access to
15
15
  --label <label>
16
16
  --expires-in <seconds> Default: server policy
17
17
  --token-expires-in <seconds> Upload token lifetime (default: server policy)
@@ -79,7 +79,15 @@ export async function runAdmin(args, opts, help = false) {
79
79
  throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
80
80
  const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
81
81
  const webUrl = flagString(parsed.flags, "--web-url");
82
- const workspace = flagString(parsed.flags, "--workspace") ?? "default";
82
+ // No default. An invite grants files:read/files:write on the workspace it
83
+ // names, so omitting the flag used to silently target the shared `default`
84
+ // workspace — and enrollment redemption mints a token without an org
85
+ // membership, so the recipient would not appear in any member list while
86
+ // still reading and writing its files.
87
+ const workspace = flagString(parsed.flags, "--workspace");
88
+ if (!workspace) {
89
+ throw new UsageError("--workspace is required (the workspace the invite grants access to)");
90
+ }
83
91
  const label = flagString(parsed.flags, "--label");
84
92
  const email = flagString(parsed.flags, "--email");
85
93
  const result = await createEnrollment(apiUrl, adminToken, {
@@ -149,9 +149,13 @@ Examples:
149
149
  keys.UPLOADS_WORKSPACE = workspace;
150
150
  if (token)
151
151
  keys.UPLOADS_TOKEN = token;
152
+ // Seed only the API URL. A `UPLOADS_WORKSPACE` written here outranks the
153
+ // workspace encoded in the token itself (see `resolveConfig`'s precedence),
154
+ // so seeding one would pin every later `uploads login` to whatever this
155
+ // wrote — historically `default` — no matter which workspace the user's
156
+ // token was actually minted for.
152
157
  if (Object.keys(keys).length === 0) {
153
158
  keys.UPLOADS_API_URL = DEFAULT_API_URL;
154
- keys.UPLOADS_WORKSPACE = DEFAULT_WORKSPACE;
155
159
  }
156
160
  try {
157
161
  const result = writeConfigKeys(path, keys, { force });
@@ -4,7 +4,7 @@ export declare const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
4
4
  export interface StepResult {
5
5
  command: string[];
6
6
  ok: boolean;
7
- skipped?: "dry-run" | "sign-in";
7
+ skipped?: "dry-run" | "sign-in" | "already-configured";
8
8
  error?: string;
9
9
  output?: string;
10
10
  }
@@ -17,6 +17,10 @@ infers your workspace from the bearer token, so only the token is needed.
17
17
  Claude Code and Codex ship the same reminder via their plugins (same command:
18
18
  \`${HOOK_COMMAND}\`) — install those plugins instead of relying on this step.
19
19
 
20
+ Safe to re-run. An MCP server already registered under this name is reported
21
+ as \`already configured\` and left as-is — including the token it was created
22
+ with. To point it at a new token: \`claude mcp remove <name>\` first.
23
+
20
24
  Usage:
21
25
  uploads install [skill|mcp|hooks|all] (default: all)
22
26
 
@@ -73,6 +77,16 @@ function skillCommand(skill) {
73
77
  // -g global, -y non-interactive, -a '*' every agent (skips the multi-select TUI)
74
78
  return ["npx", "-y", "skills", "add", SKILL_SOURCE, "--skill", skill, "-g", "-y", "-a", "*"];
75
79
  }
80
+ /**
81
+ * `claude mcp add` refuses to overwrite an existing entry and exits non-zero
82
+ * ("MCP server uploads already exists in local config"). That is the steady
83
+ * state for anyone re-running `uploads install`, not a failure — recognize it
84
+ * so the run stays green and the footer tells them how to re-add if they want
85
+ * a fresh token in the header.
86
+ */
87
+ function alreadyConfigured(result) {
88
+ return !result.ok && /already exists/i.test(result.error ?? "");
89
+ }
76
90
  function mcpCommand(name, url, bearer) {
77
91
  return [
78
92
  "claude",
@@ -101,7 +115,7 @@ function peekToken(globals) {
101
115
  return undefined;
102
116
  }
103
117
  }
104
- function printHumanSteps(results, redact, verbose) {
118
+ function printHumanSteps(results, redact, verbose, mcpName) {
105
119
  for (const [step, r] of Object.entries(results)) {
106
120
  const cmd = redact(r.command.join(" "));
107
121
  if (r.skipped === "dry-run") {
@@ -110,6 +124,10 @@ function printHumanSteps(results, redact, verbose) {
110
124
  else if (r.skipped === "sign-in") {
111
125
  process.stdout.write(`${step}: skipped — ${redact(r.error ?? "needs sign-in")}\n`);
112
126
  }
127
+ else if (r.skipped === "already-configured") {
128
+ process.stdout.write(`${step}: already configured — "${mcpName}" is registered in Claude Code (nothing to do)\n` +
129
+ ` To re-register (e.g. with a new token): claude mcp remove ${mcpName} && uploads install mcp\n`);
130
+ }
113
131
  else if (r.ok) {
114
132
  process.stdout.write(`${step}: ok\n`);
115
133
  if (verbose && r.output) {
@@ -189,7 +207,12 @@ export async function runInstall(args, opts, help = false) {
189
207
  const command = mcpCommand(name, url, token || "<token>");
190
208
  if (human)
191
209
  process.stdout.write("Installing MCP server…\n");
192
- results.mcp = dryRun ? { command, ok: true, skipped: "dry-run" } : runStep(run, command);
210
+ const step = dryRun
211
+ ? { command, ok: true, skipped: "dry-run" }
212
+ : runStep(run, command);
213
+ results.mcp = alreadyConfigured(step)
214
+ ? { command, ok: true, skipped: "already-configured", output: step.error }
215
+ : step;
193
216
  }
194
217
  }
195
218
  if (target === "hooks" || target === "all") {
@@ -223,7 +246,7 @@ export async function runInstall(args, opts, help = false) {
223
246
  process.stdout.write(JSON.stringify({ ok: !failed, steps }, null, 2) + "\n");
224
247
  return failed ? 1 : 0;
225
248
  }
226
- printHumanSteps(results, redact, verbose);
249
+ printHumanSteps(results, redact, verbose, name);
227
250
  // Path-level detail for hooks (printHumanSteps only shows the synthetic step).
228
251
  if ((target === "hooks" || target === "all") &&
229
252
  (dryRun || (human && (verbose || hookWrites.some((w) => w.action !== "skipped"))))) {
@@ -30,8 +30,13 @@ export interface DeviceLoginIo {
30
30
  write: (text: string) => void;
31
31
  /** Whether the CLI can prompt the user (a real TTY, not a script/CI pipe). */
32
32
  isTTY: boolean;
33
- /** Prompt for a new workspace name when the account has zero. */
34
- promptWorkspaceName: () => Promise<string>;
33
+ /**
34
+ * Prompt for a new workspace name when the account has zero. `suggestion`
35
+ * is a server-derived default (the user's GitHub login, when it makes a
36
+ * valid and available slug) offered as a bracketed default that Enter
37
+ * accepts; it is always overridable and never auto-submitted.
38
+ */
39
+ promptWorkspaceName: (suggestion?: string) => Promise<string>;
35
40
  }
36
41
  export declare const defaultDeviceIo: DeviceLoginIo;
37
42
  /** A completed device authorization: the session bearer plus the (possibly rewritten) scope. */
@@ -185,10 +185,14 @@ function openUrl(url) {
185
185
  // ignore — the URL is printed for manual navigation.
186
186
  }
187
187
  }
188
- async function promptWorkspaceName() {
188
+ async function promptWorkspaceName(suggestion) {
189
189
  const rl = createInterface({ input: stdin, output: process.stderr });
190
+ const hint = suggestion ? ` [${suggestion}]` : "";
190
191
  try {
191
- return (await rl.question("no workspaces yet — enter a name to create one (lowercase, hyphens): ")).trim();
192
+ const answer = (await rl.question(`no workspaces yet — enter a name to create one (lowercase, hyphens)${hint}: `)).trim();
193
+ // Empty input accepts the offered default; with no suggestion it stays
194
+ // empty and the caller's "cancelled" guard fires as before.
195
+ return answer || (suggestion ?? "");
192
196
  }
193
197
  finally {
194
198
  rl.close();
@@ -347,14 +351,14 @@ async function resolveMintWorkspace(apiUrl, accessToken, requested, io, create =
347
351
  io.write(`created workspace "${created.name}" — files will get public URLs under ${created.publicBaseUrl}/\n`);
348
352
  return created.name;
349
353
  }
350
- const { workspaces } = await listMintWorkspaces(apiUrl, accessToken);
354
+ const { workspaces, suggestedWorkspace } = await listMintWorkspaces(apiUrl, accessToken);
351
355
  if (workspaces.length === 1)
352
356
  return workspaces[0].workspace;
353
357
  if (workspaces.length === 0) {
354
358
  if (!io.isTTY) {
355
359
  throw new UsageError("your account has no workspace access yet — pass `--workspace <name> --create` to provision one, run `uploads login` interactively, or ask an administrator for an invitation");
356
360
  }
357
- const name = (await io.promptWorkspaceName()).trim();
361
+ const name = (await io.promptWorkspaceName(suggestedWorkspace)).trim();
358
362
  if (!name)
359
363
  throw new UsageError("workspace creation cancelled");
360
364
  const created = await createWorkspaceRequest(apiUrl, accessToken, name);
@@ -9,9 +9,10 @@ import { runInstall, runStep } from "./install.js";
9
9
  const UPDATE_HELP = `uploads update — update the CLI and refresh agent integrations
10
10
 
11
11
  Upgrades the globally installed npm package, then re-runs \`uploads install\` so
12
- the agent skills and the MCP registration match the new version. Skills and the
13
- MCP registration drift on their own, so this refreshes them even when the CLI is
14
- already current.
12
+ the agent skills match the new version. Skills drift on their own, so this
13
+ refreshes them even when the CLI is already current. An MCP server already
14
+ registered is left as-is (\`already configured\`) — \`claude mcp add\` never
15
+ overwrites an existing entry.
15
16
 
16
17
  Usage:
17
18
  uploads update [options]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,