@dropthis/cli 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -69,37 +69,37 @@ function errorEnvelope(code, message, nextAction) {
69
69
  }
70
70
  };
71
71
  }
72
- function apiErrorEnvelope(error) {
72
+ function apiErrorEnvelope(error2) {
73
73
  return {
74
74
  ok: false,
75
75
  error: {
76
- code: error.code ?? "api_error",
77
- message: error.message,
78
- ...error.statusCode !== void 0 ? { status: error.statusCode } : {},
79
- ...typeof error.detail === "string" ? { detail: error.detail } : {},
80
- ...error.param ? { param: error.param } : {},
81
- ...error.currentRevision !== void 0 ? { current_revision: error.currentRevision } : {},
82
- ...error.requestId ? { request_id: error.requestId } : {},
83
- next_action: nextActionForApiError(error)
76
+ code: error2.code ?? "api_error",
77
+ message: error2.message,
78
+ ...error2.statusCode !== void 0 ? { status: error2.statusCode } : {},
79
+ ...typeof error2.detail === "string" ? { detail: error2.detail } : {},
80
+ ...error2.param ? { param: error2.param } : {},
81
+ ...error2.currentRevision !== void 0 ? { current_revision: error2.currentRevision } : {},
82
+ ...error2.requestId ? { request_id: error2.requestId } : {},
83
+ next_action: nextActionForApiError(error2)
84
84
  }
85
85
  };
86
86
  }
87
- function nextActionForApiError(error) {
88
- const uploadNextAction = error.code ? UPLOAD_NEXT_ACTIONS[error.code] : void 0;
87
+ function nextActionForApiError(error2) {
88
+ const uploadNextAction = error2.code ? UPLOAD_NEXT_ACTIONS[error2.code] : void 0;
89
89
  if (uploadNextAction) return uploadNextAction;
90
- if (error.code === "revision_conflict") {
90
+ if (error2.code === "revision_conflict") {
91
91
  return "Fetch the drop, merge your changes, and retry with the current revision.";
92
92
  }
93
- if (error.statusCode === 401 || error.code === "missing_api_key") {
93
+ if (error2.statusCode === 401 || error2.code === "missing_api_key") {
94
94
  return "Authenticate with dropthis login or set DROPTHIS_API_KEY.";
95
95
  }
96
- if (error.statusCode === 413 || error.code === "quota_exceeded") {
96
+ if (error2.statusCode === 413 || error2.code === "quota_exceeded") {
97
97
  return "Reduce the upload size or upgrade the account limit.";
98
98
  }
99
- if (error.statusCode === 422) {
99
+ if (error2.statusCode === 422) {
100
100
  return "Fix the input shown in the error detail and retry.";
101
101
  }
102
- if (error.statusCode !== void 0 && error.statusCode !== null && error.statusCode >= 500) {
102
+ if (error2.statusCode !== void 0 && error2.statusCode !== null && error2.statusCode >= 500) {
103
103
  return "Retry the request with the same idempotency key, or contact support with the request id.";
104
104
  }
105
105
  return "Fix the request or retry after checking the drop state.";
@@ -112,56 +112,187 @@ function exitCodeFor(code) {
112
112
  return 1;
113
113
  }
114
114
 
115
- // src/commands/account.ts
116
- async function runAccountGet(_input, deps) {
117
- if (await resolveCredential({ env: deps.env, store: deps.store }) === null) {
118
- deps.stderr(
119
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
120
- `
121
- );
122
- return { exitCode: exitCodeFor("auth_error") };
115
+ // src/ui.ts
116
+ var import_picocolors = __toESM(require("picocolors"), 1);
117
+ var symbols = {
118
+ success: "\u2713",
119
+ error: "\u2717",
120
+ warning: "!"
121
+ };
122
+ function success(msg) {
123
+ return `${import_picocolors.default.green(symbols.success)} ${msg}`;
124
+ }
125
+ function error(msg) {
126
+ return `${import_picocolors.default.red(symbols.error)} ${msg}`;
127
+ }
128
+ function hint(msg) {
129
+ return ` ${import_picocolors.default.dim(msg)}`;
130
+ }
131
+ function url(u) {
132
+ return import_picocolors.default.cyan(u);
133
+ }
134
+ function kvLines(pairs) {
135
+ if (pairs.length === 0) return "";
136
+ const maxKey = Math.max(...pairs.map(([k]) => k.length));
137
+ return pairs.map(([k, v]) => ` ${import_picocolors.default.dim(`${k.padEnd(maxKey)}:`)} ${v}`).join("\n");
138
+ }
139
+
140
+ // src/fmt.ts
141
+ function writeResult(deps, link, verb, data) {
142
+ if (deps.outputMode === "human") {
143
+ deps.stdout(`${success(`${verb} ${url(link)}`)}
144
+ `);
145
+ } else {
146
+ deps.stdout(`${JSON.stringify({ ok: true, drop: data })}
147
+ `);
123
148
  }
124
- const result = await deps.client.account.get();
125
- if (result.error) {
149
+ }
150
+ function writeJson(deps, envelope) {
151
+ deps.stdout(`${JSON.stringify(envelope)}
152
+ `);
153
+ }
154
+ function writeKv(deps, pairs, json) {
155
+ if (deps.outputMode === "human") {
156
+ deps.stdout(`${kvLines(pairs)}
157
+ `);
158
+ } else {
159
+ deps.stdout(`${JSON.stringify(json)}
160
+ `);
161
+ }
162
+ }
163
+ function writeHumanOrJson(deps, humanText, json) {
164
+ if (deps.outputMode === "human") {
165
+ deps.stdout(`${humanText}
166
+ `);
167
+ } else {
168
+ deps.stdout(`${JSON.stringify(json)}
169
+ `);
170
+ }
171
+ }
172
+ function writeError(deps, code, message, nextAction) {
173
+ if (deps.outputMode === "human") {
174
+ deps.stderr(`${error(message)}
175
+ `);
176
+ if (nextAction) deps.stderr(`${hint(nextAction)}
177
+ `);
178
+ } else {
126
179
  deps.stderr(
127
- `${JSON.stringify(errorEnvelope("api_error", result.error.message))}
180
+ `${JSON.stringify(errorEnvelope(code, message, nextAction))}
128
181
  `
129
182
  );
130
- return { exitCode: exitCodeFor("api_error") };
131
183
  }
132
- deps.stdout(`${JSON.stringify({ ok: true, account: result.data })}
184
+ return { exitCode: exitCodeFor(code) };
185
+ }
186
+ function writeApiError(deps, details) {
187
+ if (deps.outputMode === "human") {
188
+ deps.stderr(`${error(details.message)}
189
+ `);
190
+ const nextAction = details.requestId ? `Request ID: ${details.requestId}` : void 0;
191
+ if (nextAction) deps.stderr(`${hint(nextAction)}
192
+ `);
193
+ } else {
194
+ deps.stderr(`${JSON.stringify(apiErrorEnvelope(details))}
195
+ `);
196
+ }
197
+ return { exitCode: exitCodeFor("api_error") };
198
+ }
199
+ function writeApiErrorSimple(deps, message) {
200
+ if (deps.outputMode === "human") {
201
+ deps.stderr(`${error(message)}
202
+ `);
203
+ } else {
204
+ deps.stderr(`${JSON.stringify(errorEnvelope("api_error", message))}
205
+ `);
206
+ }
207
+ return { exitCode: exitCodeFor("api_error") };
208
+ }
209
+ function writeAuthError(deps) {
210
+ return writeError(
211
+ deps,
212
+ "auth_error",
213
+ "No API key found.",
214
+ "Set DROPTHIS_API_KEY or run dropthis login."
215
+ );
216
+ }
217
+ function writeEmpty(deps, message, json) {
218
+ if (deps.outputMode === "human") {
219
+ deps.stdout(`${message}
220
+ `);
221
+ } else {
222
+ deps.stdout(`${JSON.stringify(json)}
133
223
  `);
224
+ }
225
+ }
226
+
227
+ // src/commands/account.ts
228
+ async function runAccountGet(_input, deps) {
229
+ if (await resolveCredential({ env: deps.env, store: deps.store }) === null) {
230
+ return writeAuthError(deps);
231
+ }
232
+ const result = await deps.client.account.get();
233
+ if (result.error) {
234
+ return writeApiErrorSimple(deps, result.error.message);
235
+ }
236
+ const data = result.data;
237
+ const pairs = [];
238
+ if (data?.id) pairs.push(["ID", String(data.id)]);
239
+ if (data?.email) pairs.push(["Email", String(data.email)]);
240
+ if (data?.plan) pairs.push(["Plan", String(data.plan)]);
241
+ writeKv(deps, pairs, { ok: true, account: result.data });
134
242
  return { exitCode: 0 };
135
243
  }
136
244
 
137
245
  // src/commands/api-keys.ts
138
246
  async function runApiKeysCreate(input, deps) {
139
- if (!await hasCredential(deps)) return authError(deps);
247
+ if (!await hasCredential(deps)) return writeAuthError(deps);
140
248
  const result = await deps.client.apiKeys.create({ label: input.label });
141
- if (result.error) return apiError(deps, result.error.message);
142
- deps.stdout(`${JSON.stringify({ ok: true, api_key: result.data })}
143
- `);
249
+ if (result.error) return writeApiErrorSimple(deps, result.error.message);
250
+ const data = result.data;
251
+ const pairs = [];
252
+ if (data.id) pairs.push(["ID", String(data.id)]);
253
+ if (data.key) pairs.push(["Key", String(data.key)]);
254
+ if (data.last4) pairs.push(["Last 4", String(data.last4)]);
255
+ writeKv(deps, pairs, { ok: true, api_key: result.data });
144
256
  return { exitCode: 0 };
145
257
  }
146
258
  async function runApiKeysList(_input, deps) {
147
- if (!await hasCredential(deps)) return authError(deps);
259
+ if (!await hasCredential(deps)) return writeAuthError(deps);
148
260
  const result = await deps.client.apiKeys.list();
149
- if (result.error) return apiError(deps, result.error.message);
150
- deps.stdout(
151
- `${JSON.stringify({ ok: true, api_keys: listItems(result.data) })}
261
+ if (result.error) return writeApiErrorSimple(deps, result.error.message);
262
+ const raw = listItems(result.data);
263
+ const items = Array.isArray(raw) ? raw : void 0;
264
+ if (deps.outputMode === "human") {
265
+ if (!items || items.length === 0) {
266
+ writeEmpty(deps, "No API keys found.", { ok: true, api_keys: [] });
267
+ } else {
268
+ for (const item of items) {
269
+ deps.stdout(
270
+ `${item.id ?? ""} ${item.label ?? ""} ...${item.last4 ?? ""}
152
271
  `
153
- );
272
+ );
273
+ }
274
+ }
275
+ } else {
276
+ writeJson(deps, { ok: true, api_keys: items ?? raw });
277
+ }
154
278
  return { exitCode: 0 };
155
279
  }
156
280
  async function runApiKeysDelete(keyId, input, deps) {
157
281
  if (!input.yes && input.interactive === false) {
158
- return invalidUsage(deps, "Pass --yes to delete in non-interactive mode.");
282
+ return writeError(
283
+ deps,
284
+ "invalid_usage",
285
+ "Pass --yes to delete in non-interactive mode."
286
+ );
159
287
  }
160
- if (!await hasCredential(deps)) return authError(deps);
288
+ if (!await hasCredential(deps)) return writeAuthError(deps);
161
289
  const result = await deps.client.apiKeys.delete(keyId);
162
- if (result?.error) return apiError(deps, result.error.message);
163
- deps.stdout(`${JSON.stringify({ ok: true, deleted: true, id: keyId })}
164
- `);
290
+ if (result?.error) return writeApiErrorSimple(deps, result.error.message);
291
+ writeHumanOrJson(deps, success(`Deleted ${keyId}`), {
292
+ ok: true,
293
+ deleted: true,
294
+ id: keyId
295
+ });
165
296
  return { exitCode: 0 };
166
297
  }
167
298
  async function hasCredential(deps) {
@@ -176,23 +307,6 @@ function listItems(data) {
176
307
  }
177
308
  return data;
178
309
  }
179
- function authError(deps) {
180
- deps.stderr(
181
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
182
- `
183
- );
184
- return { exitCode: exitCodeFor("auth_error") };
185
- }
186
- function apiError(deps, message) {
187
- deps.stderr(`${JSON.stringify(errorEnvelope("api_error", message))}
188
- `);
189
- return { exitCode: exitCodeFor("api_error") };
190
- }
191
- function invalidUsage(deps, message) {
192
- deps.stderr(`${JSON.stringify(errorEnvelope("invalid_usage", message))}
193
- `);
194
- return { exitCode: exitCodeFor("invalid_usage") };
195
- }
196
310
 
197
311
  // src/commands/commands.ts
198
312
  var COMMANDS = [
@@ -290,31 +404,51 @@ async function runCommands(_input, deps) {
290
404
 
291
405
  // src/commands/deployments.ts
292
406
  async function runDeploymentsList(dropId, input, deps) {
293
- if (!await hasCredential2(deps)) return authError2(deps);
407
+ if (!await hasCredential2(deps)) return writeAuthError(deps);
294
408
  const params = {
295
409
  ...input.limit !== void 0 ? { limit: input.limit } : {},
296
410
  ...input.cursor ? { cursor: input.cursor } : {}
297
411
  };
298
412
  const result = await deps.client.deployments.list(dropId, params);
299
413
  if (result.error) {
300
- deps.stderr(`${JSON.stringify(apiErrorEnvelope(result.error))}
301
- `);
302
- return { exitCode: exitCodeFor("api_error") };
414
+ return writeApiError(deps, result.error);
415
+ }
416
+ const spread = spreadData(result.data);
417
+ const items = spread.deployments ?? spread.data ?? result.data;
418
+ if (deps.outputMode === "human") {
419
+ if (!items || Array.isArray(items) && items.length === 0) {
420
+ writeEmpty(deps, "No deployments found.", {
421
+ ok: true,
422
+ ...spread
423
+ });
424
+ } else if (Array.isArray(items)) {
425
+ for (const item of items) {
426
+ deps.stdout(
427
+ `${item.id ?? ""} rev ${item.revision ?? "?"} ${item.created_at ?? ""}
428
+ `
429
+ );
430
+ }
431
+ } else {
432
+ writeJson(deps, { ok: true, ...spread });
433
+ }
434
+ } else {
435
+ writeJson(deps, { ok: true, ...spread });
303
436
  }
304
- deps.stdout(`${JSON.stringify({ ok: true, ...spreadData(result.data) })}
305
- `);
306
437
  return { exitCode: 0 };
307
438
  }
308
439
  async function runDeploymentsGet(dropId, deploymentId, _input, deps) {
309
- if (!await hasCredential2(deps)) return authError2(deps);
440
+ if (!await hasCredential2(deps)) return writeAuthError(deps);
310
441
  const result = await deps.client.deployments.get(dropId, deploymentId);
311
442
  if (result.error) {
312
- deps.stderr(`${JSON.stringify(apiErrorEnvelope(result.error))}
313
- `);
314
- return { exitCode: exitCodeFor("api_error") };
315
- }
316
- deps.stdout(`${JSON.stringify({ ok: true, deployment: result.data })}
317
- `);
443
+ return writeApiError(deps, result.error);
444
+ }
445
+ const data = result.data;
446
+ const pairs = [];
447
+ if (data.id) pairs.push(["ID", String(data.id)]);
448
+ if (data.revision !== void 0)
449
+ pairs.push(["Revision", String(data.revision)]);
450
+ if (data.created_at) pairs.push(["Created", String(data.created_at)]);
451
+ writeKv(deps, pairs, { ok: true, deployment: result.data });
318
452
  return { exitCode: 0 };
319
453
  }
320
454
  async function hasCredential2(deps) {
@@ -324,13 +458,6 @@ async function hasCredential2(deps) {
324
458
  store: deps.store
325
459
  }) !== null;
326
460
  }
327
- function authError2(deps) {
328
- deps.stderr(
329
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
330
- `
331
- );
332
- return { exitCode: exitCodeFor("auth_error") };
333
- }
334
461
  function spreadData(data) {
335
462
  return data && typeof data === "object" ? data : { data };
336
463
  }
@@ -342,15 +469,19 @@ async function runDoctor(_input, deps) {
342
469
  env: deps.env,
343
470
  store: deps.store
344
471
  });
345
- deps.stdout(
346
- `${JSON.stringify({
347
- ok: true,
348
- version: "0.1.0",
349
- auth: { source: credential?.source ?? "missing" },
350
- storage: { backend: stored?.storage ?? "none" }
351
- })}
352
- `
353
- );
472
+ const authSource = credential?.source ?? "missing";
473
+ const storageBackend = stored?.storage ?? "none";
474
+ const pairs = [
475
+ ["Version", "0.1.0"],
476
+ ["Auth", authSource],
477
+ ["Storage", storageBackend]
478
+ ];
479
+ writeKv(deps, pairs, {
480
+ ok: true,
481
+ version: "0.1.0",
482
+ auth: { source: authSource },
483
+ storage: { backend: storageBackend }
484
+ });
354
485
  return { exitCode: 0 };
355
486
  }
356
487
 
@@ -396,52 +527,82 @@ function parseJsonObject(value, label) {
396
527
 
397
528
  // src/commands/drops.ts
398
529
  async function runDropsList(input, deps) {
399
- if (!await hasCredential3(deps)) return authError3(deps);
530
+ if (!await hasCredential3(deps)) return writeAuthError(deps);
400
531
  const params = {
401
532
  ...input.limit !== void 0 ? { limit: input.limit } : {},
402
533
  ...input.cursor ? { cursor: input.cursor } : {}
403
534
  };
404
535
  const result = await deps.client.drops.list(params);
405
- if (result.error) return apiError2(deps, result.error.message);
406
- deps.stdout(
407
- `${JSON.stringify({ ok: true, drops: listItems2(result.data) })}
536
+ if (result.error) return writeApiErrorSimple(deps, result.error.message);
537
+ const raw = listItems2(result.data);
538
+ const items = Array.isArray(raw) ? raw : void 0;
539
+ if (deps.outputMode === "human") {
540
+ if (!items || items.length === 0) {
541
+ writeEmpty(deps, "No drops found.", { ok: true, drops: [] });
542
+ } else {
543
+ for (const item of items) {
544
+ deps.stdout(
545
+ `${item.id ?? ""} ${item.title ?? ""} ${item.url ? url(String(item.url)) : ""}
408
546
  `
409
- );
547
+ );
548
+ }
549
+ }
550
+ } else {
551
+ writeJson(deps, { ok: true, drops: items ?? raw });
552
+ }
410
553
  return { exitCode: 0 };
411
554
  }
412
555
  async function runDropsGet(dropId, _input, deps) {
413
- if (!await hasCredential3(deps)) return authError3(deps);
556
+ if (!await hasCredential3(deps)) return writeAuthError(deps);
414
557
  const result = await deps.client.drops.get(dropId);
415
- if (result.error) return apiError2(deps, result.error.message);
416
- deps.stdout(`${JSON.stringify({ ok: true, drop: result.data })}
417
- `);
558
+ if (result.error) return writeApiErrorSimple(deps, result.error.message);
559
+ const data = result.data;
560
+ const pairs = [];
561
+ if (data.id) pairs.push(["ID", String(data.id)]);
562
+ if (data.url) pairs.push(["URL", url(String(data.url))]);
563
+ if (data.title) pairs.push(["Title", String(data.title)]);
564
+ if (data.visibility) pairs.push(["Visibility", String(data.visibility)]);
565
+ if (data.revision !== void 0)
566
+ pairs.push(["Revision", String(data.revision)]);
567
+ if (data.created_at) pairs.push(["Created", String(data.created_at)]);
568
+ writeKv(deps, pairs, { ok: true, drop: result.data });
418
569
  return { exitCode: 0 };
419
570
  }
420
571
  async function runDropsUpdate(dropId, input, deps) {
421
- if (!await hasCredential3(deps)) return authError3(deps);
572
+ if (!await hasCredential3(deps)) return writeAuthError(deps);
422
573
  try {
423
574
  const options = await parseDropOptions(input);
424
575
  const result = await deps.client.drops.update(dropId, options);
425
- if (result.error) return apiError2(deps, result.error.message);
426
- deps.stdout(`${JSON.stringify({ ok: true, drop: result.data })}
427
- `);
576
+ if (result.error) return writeApiErrorSimple(deps, result.error.message);
577
+ writeHumanOrJson(deps, success(`Updated ${dropId}`), {
578
+ ok: true,
579
+ drop: result.data
580
+ });
428
581
  return { exitCode: 0 };
429
- } catch (error) {
430
- return invalidUsage2(
582
+ } catch (error2) {
583
+ return writeError(
431
584
  deps,
432
- error instanceof Error ? error.message : "Invalid drop options."
585
+ "invalid_usage",
586
+ error2 instanceof Error ? error2.message : "Invalid drop options."
433
587
  );
434
588
  }
435
589
  }
436
590
  async function runDropsDelete(dropId, input, deps) {
437
591
  if (!input.yes && input.interactive === false) {
438
- return invalidUsage2(deps, "Pass --yes to delete in non-interactive mode.");
592
+ return writeError(
593
+ deps,
594
+ "invalid_usage",
595
+ "Pass --yes to delete in non-interactive mode."
596
+ );
439
597
  }
440
- if (!await hasCredential3(deps)) return authError3(deps);
598
+ if (!await hasCredential3(deps)) return writeAuthError(deps);
441
599
  const result = await deps.client.drops.delete(dropId);
442
- if (result?.error) return apiError2(deps, result.error.message);
443
- deps.stdout(`${JSON.stringify({ ok: true, deleted: true, id: dropId })}
444
- `);
600
+ if (result?.error) return writeApiErrorSimple(deps, result.error.message);
601
+ writeHumanOrJson(deps, success(`Deleted ${dropId}`), {
602
+ ok: true,
603
+ deleted: true,
604
+ id: dropId
605
+ });
445
606
  return { exitCode: 0 };
446
607
  }
447
608
  async function hasCredential3(deps) {
@@ -456,23 +617,6 @@ function listItems2(data) {
456
617
  }
457
618
  return data;
458
619
  }
459
- function authError3(deps) {
460
- deps.stderr(
461
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
462
- `
463
- );
464
- return { exitCode: exitCodeFor("auth_error") };
465
- }
466
- function apiError2(deps, message) {
467
- deps.stderr(`${JSON.stringify(errorEnvelope("api_error", message))}
468
- `);
469
- return { exitCode: exitCodeFor("api_error") };
470
- }
471
- function invalidUsage2(deps, message) {
472
- deps.stderr(`${JSON.stringify(errorEnvelope("invalid_usage", message))}
473
- `);
474
- return { exitCode: exitCodeFor("invalid_usage") };
475
- }
476
620
 
477
621
  // src/commands/login.ts
478
622
  var prompts = __toESM(require("@clack/prompts"), 1);
@@ -592,12 +736,9 @@ async function runLogout(input, deps) {
592
736
  }
593
737
  await deps.store.clear();
594
738
  if (revokeError) {
595
- deps.stderr(`${JSON.stringify(errorEnvelope("api_error", revokeError))}
596
- `);
597
- return { exitCode: exitCodeFor("api_error") };
739
+ return writeApiErrorSimple(deps, revokeError);
598
740
  }
599
- deps.stdout(`${JSON.stringify({ ok: true })}
600
- `);
741
+ writeHumanOrJson(deps, success("Logged out."), { ok: true });
601
742
  return { exitCode: 0 };
602
743
  }
603
744
 
@@ -606,14 +747,40 @@ var import_node_crypto = require("crypto");
606
747
  var import_promises3 = require("fs/promises");
607
748
  var import_node_path = require("path");
608
749
 
750
+ // src/spinner.ts
751
+ var import_picocolors2 = __toESM(require("picocolors"), 1);
752
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
753
+ function createSpinner(message, stderr) {
754
+ let i = 0;
755
+ const timer = setInterval(() => {
756
+ stderr(`\r\x1B[2K${import_picocolors2.default.cyan(frames[i % frames.length])} ${message}`);
757
+ i++;
758
+ }, 80);
759
+ return {
760
+ stop(finalMessage) {
761
+ clearInterval(timer);
762
+ const text2 = finalMessage ?? message;
763
+ stderr(`\r\x1B[2K${import_picocolors2.default.green(symbols.success)} ${text2}
764
+ `);
765
+ },
766
+ fail() {
767
+ clearInterval(timer);
768
+ stderr("\r\x1B[2K");
769
+ }
770
+ };
771
+ }
772
+ function shouldSpin(outputMode) {
773
+ return outputMode === "human" && process.stderr.isTTY === true && !process.env.CI;
774
+ }
775
+
609
776
  // src/commands/json_body.ts
610
777
  var import_promises2 = require("fs/promises");
611
778
  async function readJsonObjectFile(path) {
612
779
  let parsed;
613
780
  try {
614
781
  parsed = JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
615
- } catch (error) {
616
- const message = error instanceof Error ? error.message : "Unknown parse error";
782
+ } catch (error2) {
783
+ const message = error2 instanceof Error ? error2.message : "Unknown parse error";
617
784
  throw new Error(`Invalid JSON body in ${path}: ${message}`);
618
785
  }
619
786
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -631,16 +798,13 @@ async function runPublish(input, raw, deps) {
631
798
  store: deps.store
632
799
  });
633
800
  if (!credential) {
634
- deps.stderr(
635
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
636
- `
637
- );
638
- return { exitCode: exitCodeFor("auth_error") };
801
+ return writeAuthError(deps);
639
802
  }
640
803
  if (raw.dryRun) {
641
804
  return handlePublishDryRun(input, raw, deps);
642
805
  }
643
806
  const singleInput = Array.isArray(input) ? input[0] : input;
807
+ const spin = shouldSpin(deps.outputMode) ? createSpinner("Publishing\u2026", deps.stderr) : void 0;
644
808
  try {
645
809
  if (raw.fromJson && singleInput) {
646
810
  throw new Error("Use either <input> or --from-json, not both.");
@@ -653,32 +817,28 @@ async function runPublish(input, raw, deps) {
653
817
  throw new Error("Publish requires <input> or --from-json.");
654
818
  })();
655
819
  if (result.error) {
656
- deps.stderr(`${JSON.stringify(apiErrorEnvelope(result.error))}
657
- `);
658
- return { exitCode: exitCodeFor("api_error") };
820
+ spin?.fail();
821
+ return writeApiError(deps, result.error);
659
822
  }
823
+ spin?.stop("Published");
660
824
  if (raw.url) deps.stdout(`${result.data.url}
661
825
  `);
662
- else if (deps.outputMode === "human")
663
- deps.stdout(`Published: ${result.data.url}
664
- `);
665
- else deps.stdout(`${JSON.stringify({ ok: true, drop: result.data })}
666
- `);
826
+ else writeResult(deps, result.data.url, "Published", result.data);
667
827
  return { exitCode: 0 };
668
- } catch (error) {
669
- const message = error instanceof Error ? error.message : "Publish failed.";
670
- deps.stderr(`${JSON.stringify(errorEnvelope("invalid_usage", message))}
671
- `);
672
- return { exitCode: exitCodeFor("invalid_usage") };
828
+ } catch (error2) {
829
+ const message = error2 instanceof Error ? error2.message : "Publish failed.";
830
+ spin?.fail();
831
+ return writeError(deps, "invalid_usage", message);
673
832
  }
674
833
  }
675
834
  async function handlePublishDryRun(input, raw, deps) {
676
835
  if (raw.url) {
677
- deps.stderr(
678
- `${JSON.stringify(errorEnvelope("invalid_usage", "--url and --dry-run cannot be combined.", "Remove --url; dry-run outputs JSON describing what would be published."))}
679
- `
836
+ return writeError(
837
+ deps,
838
+ "invalid_usage",
839
+ "--url and --dry-run cannot be combined.",
840
+ "Remove --url; dry-run outputs JSON describing what would be published."
680
841
  );
681
- return { exitCode: exitCodeFor("invalid_usage") };
682
842
  }
683
843
  try {
684
844
  if (raw.fromJson) {
@@ -709,12 +869,10 @@ async function handlePublishDryRun(input, raw, deps) {
709
869
  deps.stdout(`${JSON.stringify(output, null, 2)}
710
870
  `);
711
871
  return { exitCode: 0 };
712
- } catch (error) {
713
- const code = isFileSystemError(error) ? "local_input_error" : "invalid_usage";
714
- const message = error instanceof Error ? error.message : "Dry-run failed.";
715
- deps.stderr(`${JSON.stringify(errorEnvelope(code, message))}
716
- `);
717
- return { exitCode: exitCodeFor(code) };
872
+ } catch (error2) {
873
+ const code = isFileSystemError(error2) ? "local_input_error" : "invalid_usage";
874
+ const message = error2 instanceof Error ? error2.message : "Dry-run failed.";
875
+ return writeError(deps, code, message);
718
876
  }
719
877
  }
720
878
  async function dryRunMultiFile(inputs, options, deps) {
@@ -724,18 +882,18 @@ async function dryRunMultiFile(inputs, options, deps) {
724
882
  try {
725
883
  info = await (0, import_promises3.stat)(filePath);
726
884
  } catch {
727
- deps.stderr(
728
- `${JSON.stringify(errorEnvelope("local_input_error", `File not found: ${filePath}`))}
729
- `
885
+ return writeError(
886
+ deps,
887
+ "local_input_error",
888
+ `File not found: ${filePath}`
730
889
  );
731
- return { exitCode: exitCodeFor("local_input_error") };
732
890
  }
733
891
  if (info.isDirectory()) {
734
- deps.stderr(
735
- `${JSON.stringify(errorEnvelope("local_input_error", `Cannot mix directories with multiple file arguments. Use a single folder instead: dropthis publish ${filePath}`))}
736
- `
892
+ return writeError(
893
+ deps,
894
+ "local_input_error",
895
+ `Cannot mix directories with multiple file arguments. Use a single folder instead: dropthis publish ${filePath}`
737
896
  );
738
- return { exitCode: exitCodeFor("local_input_error") };
739
897
  }
740
898
  files.push({
741
899
  path: (0, import_node_path.basename)(filePath),
@@ -744,11 +902,11 @@ async function dryRunMultiFile(inputs, options, deps) {
744
902
  });
745
903
  }
746
904
  if (files.length > MAX_BUNDLE_FILE_COUNT) {
747
- deps.stderr(
748
- `${JSON.stringify(errorEnvelope("local_input_error", `Bundle contains ${files.length} files, exceeding the maximum of ${MAX_BUNDLE_FILE_COUNT}.`))}
749
- `
905
+ return writeError(
906
+ deps,
907
+ "local_input_error",
908
+ `Bundle contains ${files.length} files, exceeding the maximum of ${MAX_BUNDLE_FILE_COUNT}.`
750
909
  );
751
- return { exitCode: exitCodeFor("local_input_error") };
752
910
  }
753
911
  const entry = options.entry ?? files.find((f) => f.path === "index.html")?.path ?? files[0]?.path ?? null;
754
912
  const totalBytes = files.reduce((sum, f) => sum + f.sizeBytes, 0);
@@ -831,9 +989,9 @@ function mimeForPath(filePath) {
831
989
  };
832
990
  return types[ext] ?? "application/octet-stream";
833
991
  }
834
- function isFileSystemError(error) {
835
- if (!(error instanceof Error)) return false;
836
- const code = error.code;
992
+ function isFileSystemError(error2) {
993
+ if (!(error2 instanceof Error)) return false;
994
+ const code = error2.code;
837
995
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELIMIT";
838
996
  }
839
997
  function withPublishIdempotency(options) {
@@ -849,15 +1007,12 @@ async function runUpdate(target, input, raw, deps) {
849
1007
  store: deps.store
850
1008
  });
851
1009
  if (!credential) {
852
- deps.stderr(
853
- `${JSON.stringify(errorEnvelope("auth_error", "No API key found.", "Set DROPTHIS_API_KEY or run dropthis login."))}
854
- `
855
- );
856
- return { exitCode: exitCodeFor("auth_error") };
1010
+ return writeAuthError(deps);
857
1011
  }
858
1012
  if (raw.dryRun) {
859
1013
  return handleUpdateDryRun(target, input, raw, deps);
860
1014
  }
1015
+ const spin = shouldSpin(deps.outputMode) ? createSpinner("Updating\u2026", deps.stderr) : void 0;
861
1016
  try {
862
1017
  assertDropId(target);
863
1018
  if (raw.fromJson && input) {
@@ -881,32 +1036,28 @@ async function runUpdate(target, input, raw, deps) {
881
1036
  })
882
1037
  ) : await deps.client.update(target, parsed, revisionOptions);
883
1038
  if (result.error) {
884
- deps.stderr(`${JSON.stringify(apiErrorEnvelope(result.error))}
885
- `);
886
- return { exitCode: exitCodeFor("api_error") };
1039
+ spin?.fail();
1040
+ return writeApiError(deps, result.error);
887
1041
  }
1042
+ spin?.stop("Updated");
888
1043
  if (raw.url) deps.stdout(`${result.data.url}
889
1044
  `);
890
- else if (deps.outputMode === "human")
891
- deps.stdout(`Updated: ${result.data.url}
892
- `);
893
- else deps.stdout(`${JSON.stringify({ ok: true, drop: result.data })}
894
- `);
1045
+ else writeResult(deps, result.data.url, "Updated", result.data);
895
1046
  return { exitCode: 0 };
896
- } catch (error) {
897
- const message = error instanceof Error ? error.message : "Update failed.";
898
- deps.stderr(`${JSON.stringify(errorEnvelope("invalid_usage", message))}
899
- `);
900
- return { exitCode: exitCodeFor("invalid_usage") };
1047
+ } catch (error2) {
1048
+ const message = error2 instanceof Error ? error2.message : "Update failed.";
1049
+ spin?.fail();
1050
+ return writeError(deps, "invalid_usage", message);
901
1051
  }
902
1052
  }
903
1053
  async function handleUpdateDryRun(target, input, raw, deps) {
904
1054
  if (raw.url) {
905
- deps.stderr(
906
- `${JSON.stringify(errorEnvelope("invalid_usage", "--url and --dry-run cannot be combined.", "Remove --url; dry-run outputs JSON describing what would be published."))}
907
- `
1055
+ return writeError(
1056
+ deps,
1057
+ "invalid_usage",
1058
+ "--url and --dry-run cannot be combined.",
1059
+ "Remove --url; dry-run outputs JSON describing what would be published."
908
1060
  );
909
- return { exitCode: exitCodeFor("invalid_usage") };
910
1061
  }
911
1062
  try {
912
1063
  assertDropId(target);
@@ -947,12 +1098,10 @@ async function handleUpdateDryRun(target, input, raw, deps) {
947
1098
  deps.stdout(`${JSON.stringify(output, null, 2)}
948
1099
  `);
949
1100
  return { exitCode: 0 };
950
- } catch (error) {
951
- const code = isFileSystemError2(error) ? "local_input_error" : "invalid_usage";
952
- const message = error instanceof Error ? error.message : "Dry-run failed.";
953
- deps.stderr(`${JSON.stringify(errorEnvelope(code, message))}
954
- `);
955
- return { exitCode: exitCodeFor(code) };
1101
+ } catch (error2) {
1102
+ const code = isFileSystemError2(error2) ? "local_input_error" : "invalid_usage";
1103
+ const message = error2 instanceof Error ? error2.message : "Dry-run failed.";
1104
+ return writeError(deps, code, message);
956
1105
  }
957
1106
  }
958
1107
  var MAX_BUNDLE_FILE_COUNT2 = 200;
@@ -986,9 +1135,9 @@ function validateDryRunFileCount(output) {
986
1135
  );
987
1136
  }
988
1137
  }
989
- function isFileSystemError2(error) {
990
- if (!(error instanceof Error)) return false;
991
- const code = error.code;
1138
+ function isFileSystemError2(error2) {
1139
+ if (!(error2 instanceof Error)) return false;
1140
+ const code = error2.code;
992
1141
  return code === "ENOENT" || code === "ENOTDIR";
993
1142
  }
994
1143
  function withUpdateIdempotency(options) {
@@ -1009,23 +1158,29 @@ async function runWhoami(_input, deps) {
1009
1158
  store: deps.store
1010
1159
  });
1011
1160
  if (!credential) {
1012
- deps.stdout(`${JSON.stringify({ ok: true, authenticated: false })}
1013
- `);
1161
+ writeHumanOrJson(deps, "Not authenticated. Run dropthis login.", {
1162
+ ok: true,
1163
+ authenticated: false
1164
+ });
1014
1165
  return { exitCode: 0 };
1015
1166
  }
1016
- deps.stdout(
1017
- `${JSON.stringify({
1018
- ok: true,
1019
- authenticated: true,
1020
- source: credential.source,
1021
- masked_key: maskKey(credential.apiKey),
1022
- ...credential.keyId ? { key_id: credential.keyId } : {},
1023
- ...credential.keyLast4 ? { key_last4: credential.keyLast4 } : {},
1024
- ...credential.accountId ? { account_id: credential.accountId } : {},
1025
- ...credential.email ? { email: credential.email } : {}
1026
- })}
1027
- `
1028
- );
1167
+ const masked = maskKey(credential.apiKey);
1168
+ const pairs = [];
1169
+ if (credential.email) pairs.push(["Email", credential.email]);
1170
+ pairs.push(["Key", masked]);
1171
+ pairs.push(["Source", credential.source]);
1172
+ if (credential.accountId) pairs.push(["Account", credential.accountId]);
1173
+ const jsonEnvelope = {
1174
+ ok: true,
1175
+ authenticated: true,
1176
+ source: credential.source,
1177
+ masked_key: masked,
1178
+ ...credential.keyId ? { key_id: credential.keyId } : {},
1179
+ ...credential.keyLast4 ? { key_last4: credential.keyLast4 } : {},
1180
+ ...credential.accountId ? { account_id: credential.accountId } : {},
1181
+ ...credential.email ? { email: credential.email } : {}
1182
+ };
1183
+ writeKv(deps, pairs, jsonEnvelope);
1029
1184
  return { exitCode: 0 };
1030
1185
  }
1031
1186
 
@@ -1141,11 +1296,11 @@ function credentialPath(configDir) {
1141
1296
  async function readJsonFile(path) {
1142
1297
  try {
1143
1298
  return JSON.parse(await (0, import_promises4.readFile)(path, "utf8"));
1144
- } catch (error) {
1145
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1299
+ } catch (error2) {
1300
+ if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
1146
1301
  return null;
1147
1302
  }
1148
- throw error;
1303
+ throw error2;
1149
1304
  }
1150
1305
  }
1151
1306
  async function writeCredentialFile(path, credential) {
@@ -1161,7 +1316,7 @@ async function writeCredentialFile(path, credential) {
1161
1316
  function buildProgram(options = {}) {
1162
1317
  const program = new import_commander.Command();
1163
1318
  const store = options.store ?? new MemoryCredentialStore();
1164
- program.name("dropthis").description("Publish anything online and get a URL back.").version("0.1.5");
1319
+ program.name("dropthis").description("Publish anything online and get a URL back.").version("0.2.0");
1165
1320
  program.option("--api-key <key>", "Override API key for this invocation");
1166
1321
  program.option("--api-url <url>", "Override API base URL");
1167
1322
  program.option("--json", "Force JSON output");
@@ -1482,8 +1637,8 @@ async function readStdinBytes(stdin) {
1482
1637
  }
1483
1638
 
1484
1639
  // src/cli.ts
1485
- buildProgram({ store: createCredentialStore() }).parseAsync(process.argv).catch((error) => {
1486
- console.error(error);
1640
+ buildProgram({ store: createCredentialStore() }).parseAsync(process.argv).catch((error2) => {
1641
+ console.error(error2);
1487
1642
  process.exitCode = 1;
1488
1643
  });
1489
1644
  //# sourceMappingURL=cli.cjs.map