@dropthis/cli 0.11.0 → 0.12.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
@@ -202,10 +202,25 @@ function normalizeApiErrorCode(error2) {
202
202
  if (error2.statusCode === 404) return "not_found";
203
203
  return "api_error";
204
204
  }
205
- function nextActionForApiError(error2) {
205
+ function nextActionForApiError(error2, mode = "json") {
206
+ const cliHint = cliNextAction(error2);
207
+ if (mode === "human" && cliHint) return cliHint;
206
208
  if (error2.suggestion) return error2.suggestion;
209
+ if (cliHint) return cliHint;
210
+ if (error2.statusCode === 422) {
211
+ return "Fix the input shown in the error detail and retry.";
212
+ }
213
+ if (error2.statusCode !== void 0 && error2.statusCode !== null && error2.statusCode >= 500) {
214
+ return "Retry the request with the same idempotency key, or contact support with the request id.";
215
+ }
216
+ return "Fix the request or retry after checking the drop state.";
217
+ }
218
+ function cliNextAction(error2) {
207
219
  const uploadNextAction = error2.code ? UPLOAD_NEXT_ACTIONS[error2.code] : void 0;
208
220
  if (uploadNextAction) return uploadNextAction;
221
+ if (error2.code === "network_error") {
222
+ return "Could not reach the dropthis API \u2014 check your network or DROPTHIS_API_URL.";
223
+ }
209
224
  if (error2.code === "revision_conflict") {
210
225
  return "Re-read the drop with dropthis get <drop-id>, merge your changes, and retry with --if-revision set to the current revision.";
211
226
  }
@@ -218,13 +233,7 @@ function nextActionForApiError(error2) {
218
233
  if (error2.statusCode === 413 || error2.code === "quota_exceeded") {
219
234
  return "Reduce the upload size or upgrade the account limit.";
220
235
  }
221
- if (error2.statusCode === 422) {
222
- return "Fix the input shown in the error detail and retry.";
223
- }
224
- if (error2.statusCode !== void 0 && error2.statusCode !== null && error2.statusCode >= 500) {
225
- return "Retry the request with the same idempotency key, or contact support with the request id.";
226
- }
227
- return "Fix the request or retry after checking the drop state.";
236
+ return void 0;
228
237
  }
229
238
  function exitCodeFor(code) {
230
239
  if (code === "usage_error") return 2;
@@ -239,7 +248,7 @@ function exitCodeFor(code) {
239
248
 
240
249
  // src/program.ts
241
250
  var import_commander = require("commander");
242
- var import_picocolors5 = __toESM(require("picocolors"), 1);
251
+ var import_picocolors6 = __toESM(require("picocolors"), 1);
243
252
 
244
253
  // src/auth.ts
245
254
  async function resolveCredential(input) {
@@ -318,6 +327,10 @@ function writeResult(deps, link, verb, data) {
318
327
  `${hint(`Vanity slug was taken \u2014 served at: ${w.detail ?? "see URL"}`)}
319
328
  `
320
329
  );
330
+ } else {
331
+ const text2 = typeof w.detail === "string" ? w.detail : typeof w.message === "string" ? w.message : typeof w.code === "string" ? w.code : void 0;
332
+ if (text2) deps.stderr(`${hint(text2)}
333
+ `);
321
334
  }
322
335
  }
323
336
  }
@@ -339,6 +352,13 @@ function formatDropDetails(data) {
339
352
  if (parts.length === 0) return "";
340
353
  return ` ${import_picocolors2.default.dim(parts.join(" \xB7 "))}`;
341
354
  }
355
+ function formatDate(iso) {
356
+ return iso.split("T")[0] ?? iso;
357
+ }
358
+ function formatDateTime(iso) {
359
+ const match = iso.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
360
+ return match ? `${match[1]} ${match[2]}` : iso;
361
+ }
342
362
  function formatBytes(bytes) {
343
363
  if (bytes < 1024) return `${bytes} B`;
344
364
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -384,7 +404,7 @@ function writeApiError(deps, details) {
384
404
  if (deps.outputMode === "human") {
385
405
  deps.stderr(`${error(details.message)}
386
406
  `);
387
- const nextAction = nextActionForApiError(details);
407
+ const nextAction = nextActionForApiError(details, "human");
388
408
  if (nextAction) deps.stderr(`${hint(nextAction)}
389
409
  `);
390
410
  if (details.currentRevision !== void 0)
@@ -399,7 +419,12 @@ function writeApiError(deps, details) {
399
419
  deps.stderr(`${JSON.stringify(apiErrorEnvelope(details))}
400
420
  `);
401
421
  }
402
- return { exitCode: exitCodeFor("api_error") };
422
+ return { exitCode: exitCodeFor(cliErrorCodeFor(details)) };
423
+ }
424
+ function cliErrorCodeFor(details) {
425
+ if (details.code === "network_error") return "network_error";
426
+ if (details.code === "file_not_found") return "local_input_error";
427
+ return "api_error";
403
428
  }
404
429
  function writeApiErrorSimple(deps, message) {
405
430
  if (deps.outputMode === "human") {
@@ -507,6 +532,14 @@ async function runAccountDelete(input, deps) {
507
532
  var prompts = __toESM(require("@clack/prompts"), 1);
508
533
 
509
534
  // src/types.ts
535
+ function extractPagination(data) {
536
+ if (!data || typeof data !== "object") return void 0;
537
+ const rec = data;
538
+ if (!("nextCursor" in rec) && !("hasMore" in rec)) return void 0;
539
+ const nextCursor = typeof rec.nextCursor === "string" ? rec.nextCursor : null;
540
+ const hasMore = typeof rec.hasMore === "boolean" ? rec.hasMore : nextCursor !== null;
541
+ return { nextCursor, hasMore };
542
+ }
510
543
  function unwrapListData(data, ...fallbackKeys) {
511
544
  if (data && typeof data === "object") {
512
545
  if ("data" in data) {
@@ -647,7 +680,7 @@ var COMMAND_ANNOTATIONS = {
647
680
  },
648
681
  deployments: {
649
682
  auth: "required",
650
- examples: ["dropthis deployments drop_abc --json"]
683
+ examples: ["dropthis deployments list drop_abc --json"]
651
684
  },
652
685
  "deployments list": {
653
686
  auth: "required",
@@ -657,6 +690,36 @@ var COMMAND_ANNOTATIONS = {
657
690
  auth: "required",
658
691
  examples: ["dropthis deployments get drop_abc dep_abc --json"]
659
692
  },
693
+ domains: {
694
+ auth: "required",
695
+ examples: ["dropthis domains list --json"]
696
+ },
697
+ "domains connect": {
698
+ auth: "required",
699
+ examples: [
700
+ "dropthis domains connect reports.example.com --mode path --json"
701
+ ]
702
+ },
703
+ "domains list": {
704
+ auth: "required",
705
+ examples: ["dropthis domains list --json"]
706
+ },
707
+ "domains status": {
708
+ auth: "required",
709
+ examples: ["dropthis domains status reports.example.com --json"]
710
+ },
711
+ "domains verify": {
712
+ auth: "required",
713
+ examples: ["dropthis domains verify reports.example.com --wait --json"]
714
+ },
715
+ "domains update": {
716
+ auth: "required",
717
+ examples: ["dropthis domains update reports.example.com --default --json"]
718
+ },
719
+ "domains remove": {
720
+ auth: "required",
721
+ examples: ["dropthis domains remove reports.example.com --yes --json"]
722
+ },
660
723
  "api-keys": {
661
724
  auth: "required",
662
725
  examples: ["dropthis api-keys create --label CI --json"]
@@ -738,6 +801,7 @@ async function runDeploymentsList(dropId, input, deps) {
738
801
  return writeApiError(deps, result.error);
739
802
  }
740
803
  const spread = spreadData(result.data);
804
+ const page = extractPagination(result.data);
741
805
  const items = spread.deployments ?? spread.data ?? result.data;
742
806
  if (deps.outputMode === "human") {
743
807
  if (!items || Array.isArray(items) && items.length === 0) {
@@ -747,8 +811,15 @@ async function runDeploymentsList(dropId, input, deps) {
747
811
  });
748
812
  } else if (Array.isArray(items)) {
749
813
  for (const item of items) {
814
+ const created = typeof item.createdAt === "string" ? formatDateTime(item.createdAt) : "";
750
815
  deps.stdout(
751
- `${item.id ?? ""} rev ${item.revision ?? "?"} ${item.createdAt ?? ""}
816
+ `${item.id ?? ""} rev ${item.revision ?? "?"} ${created}
817
+ `
818
+ );
819
+ }
820
+ if (page?.hasMore && page.nextCursor) {
821
+ deps.stdout(
822
+ `${hint(`More deployments: dropthis deployments list ${dropId} --cursor ${page.nextCursor}`)}
752
823
  `
753
824
  );
754
825
  }
@@ -756,7 +827,11 @@ async function runDeploymentsList(dropId, input, deps) {
756
827
  writeJson(deps, { ok: true, ...spread });
757
828
  }
758
829
  } else {
759
- writeJson(deps, { ok: true, ...spread });
830
+ writeJson(deps, {
831
+ ok: true,
832
+ ...spread,
833
+ ...page ? { next_cursor: page.nextCursor, has_more: page.hasMore } : {}
834
+ });
760
835
  }
761
836
  return { exitCode: 0 };
762
837
  }
@@ -779,7 +854,8 @@ async function runDeploymentsGet(dropId, deploymentId, _input, deps) {
779
854
  if (data.id) pairs.push(["ID", String(data.id)]);
780
855
  if (data.revision !== void 0)
781
856
  pairs.push(["Revision", String(data.revision)]);
782
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
857
+ if (data.createdAt)
858
+ pairs.push(["Created", formatDateTime(String(data.createdAt))]);
783
859
  writeKv(deps, pairs, { ok: true, deployment: result.data });
784
860
  return { exitCode: 0 };
785
861
  }
@@ -797,13 +873,13 @@ async function runDoctor(_input, deps) {
797
873
  const authSource = credential?.source ?? "missing";
798
874
  const storageBackend = stored?.storage ?? "none";
799
875
  const pairs = [
800
- ["Version", "0.11.0"],
876
+ ["Version", "0.12.0"],
801
877
  ["Auth", authSource],
802
878
  ["Storage", storageBackend]
803
879
  ];
804
880
  writeKv(deps, pairs, {
805
881
  ok: true,
806
- version: "0.11.0",
882
+ version: "0.12.0",
807
883
  auth: { source: authSource },
808
884
  storage: { backend: storageBackend }
809
885
  });
@@ -811,6 +887,7 @@ async function runDoctor(_input, deps) {
811
887
  }
812
888
 
813
889
  // src/commands/domains.ts
890
+ var prompts2 = __toESM(require("@clack/prompts"), 1);
814
891
  var import_picocolors3 = __toESM(require("picocolors"), 1);
815
892
  function formatDnsTable(records) {
816
893
  if (records.length === 0) return "";
@@ -848,11 +925,32 @@ function formatDnsTable(records) {
848
925
  }
849
926
  return rows.join("\n");
850
927
  }
851
- function printNextHints(deps, hints) {
852
- if (hints.length === 0) return;
928
+ function renderNextHint(h, hostname, maxRetryAfter) {
929
+ if (h.action === "verify") {
930
+ const recheck = maxRetryAfter > 0 ? ` (re-checks every ${maxRetryAfter}s)` : "";
931
+ return `Run: dropthis domains verify ${hostname} --wait${recheck}`;
932
+ }
933
+ if (h.action === "publish") {
934
+ return `Run: dropthis publish <file> --domain ${hostname}`;
935
+ }
936
+ return h.message;
937
+ }
938
+ function duplicatesRecordHint(h, dns) {
939
+ if (h.action !== "dns") return false;
940
+ return dns.some(
941
+ (r) => r.hint && h.message.includes(r.name) && h.message.includes(r.value)
942
+ );
943
+ }
944
+ function printNextHints(deps, hints, hostname, dns) {
945
+ const lines = hints.filter((h) => !duplicatesRecordHint(h, dns));
946
+ if (lines.length === 0) return;
947
+ const maxRetryAfter = dns.reduce(
948
+ (max, r) => Math.max(max, r.retryAfter ?? 0),
949
+ 0
950
+ );
853
951
  deps.stdout("\n");
854
- for (const h of hints) {
855
- deps.stdout(`${hint(h.message)}
952
+ for (const h of lines) {
953
+ deps.stdout(`${hint(renderNextHint(h, hostname, maxRetryAfter))}
856
954
  `);
857
955
  }
858
956
  }
@@ -889,7 +987,7 @@ async function runDomainsConnect(input, deps) {
889
987
  deps.stdout(`${formatDnsTable(domain.dns)}
890
988
  `);
891
989
  }
892
- printNextHints(deps, domain.next);
990
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
893
991
  } else {
894
992
  writeJson(deps, { ok: true, domain });
895
993
  }
@@ -957,17 +1055,19 @@ async function runDomainsStatus(idOrHostname, _input, deps) {
957
1055
  `);
958
1056
  }
959
1057
  if (domain.verifiedAt) {
960
- deps.stdout(` ${import_picocolors3.default.dim("Verified:")} ${domain.verifiedAt}
961
- `);
1058
+ deps.stdout(
1059
+ ` ${import_picocolors3.default.dim("Verified:")} ${formatDate(domain.verifiedAt)}
1060
+ `
1061
+ );
962
1062
  }
963
- deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${domain.createdAt}
1063
+ deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${formatDate(domain.createdAt)}
964
1064
  `);
965
1065
  if (domain.dns.length > 0) {
966
1066
  deps.stdout("\nDNS records:\n\n");
967
1067
  deps.stdout(`${formatDnsTable(domain.dns)}
968
1068
  `);
969
1069
  }
970
- printNextHints(deps, domain.next);
1070
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
971
1071
  } else {
972
1072
  writeJson(deps, { ok: true, domain });
973
1073
  }
@@ -1000,7 +1100,7 @@ async function runDomainsVerify(idOrHostname, input, deps, sleep = defaultSleep)
1000
1100
  deps.stdout(`${formatDnsTable(domain.dns)}
1001
1101
  `);
1002
1102
  }
1003
- printNextHints(deps, domain.next);
1103
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
1004
1104
  } else {
1005
1105
  writeJson(deps, { ok: true, domain });
1006
1106
  }
@@ -1081,7 +1181,22 @@ async function runDomainsUpdate(idOrHostname, input, deps) {
1081
1181
  });
1082
1182
  return { exitCode: 0 };
1083
1183
  }
1084
- async function runDomainsRemove(idOrHostname, _input, deps) {
1184
+ async function runDomainsRemove(idOrHostname, input, deps) {
1185
+ if (!input.yes && input.interactive === false) {
1186
+ return writeError(
1187
+ deps,
1188
+ "invalid_usage",
1189
+ "Pass --yes to remove the domain in non-interactive mode."
1190
+ );
1191
+ }
1192
+ if (!input.yes && input.interactive !== false) {
1193
+ const confirmed = await prompts2.confirm({
1194
+ message: `Remove ${idOrHostname}? This removes the domain and unmounts its drops.`
1195
+ });
1196
+ if (prompts2.isCancel(confirmed) || !confirmed) {
1197
+ return { exitCode: 0 };
1198
+ }
1199
+ }
1085
1200
  try {
1086
1201
  await requireCredential({
1087
1202
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1114,7 +1229,30 @@ async function runDomainsRemove(idOrHostname, _input, deps) {
1114
1229
  }
1115
1230
 
1116
1231
  // src/commands/drops.ts
1117
- var prompts2 = __toESM(require("@clack/prompts"), 1);
1232
+ var prompts3 = __toESM(require("@clack/prompts"), 1);
1233
+ var import_picocolors4 = __toESM(require("picocolors"), 1);
1234
+ async function resolveDropTarget(target, deps) {
1235
+ if (target.startsWith("drop_")) return { ok: true, dropId: target };
1236
+ const resolved = await deps.client.drops.resolve(target);
1237
+ if (resolved.error) {
1238
+ return { ok: false, ...writeApiError(deps, resolved.error) };
1239
+ }
1240
+ if (resolved.data === null) {
1241
+ return {
1242
+ ok: false,
1243
+ ...writeApiError(deps, {
1244
+ code: "not_found",
1245
+ message: `No drop matching "${target}" found in your account.`,
1246
+ suggestion: "It may belong to another account or have been deleted. Run dropthis list to see your drops, or pass the full drop_\u2026 id."
1247
+ })
1248
+ };
1249
+ }
1250
+ return {
1251
+ ok: true,
1252
+ dropId: resolved.data.id,
1253
+ ...resolved.data.slug ? { slug: resolved.data.slug } : {}
1254
+ };
1255
+ }
1118
1256
  async function runDropsList(input, deps) {
1119
1257
  try {
1120
1258
  await requireCredential({
@@ -1132,24 +1270,40 @@ async function runDropsList(input, deps) {
1132
1270
  const result = await deps.client.drops.list(params);
1133
1271
  if (result.error) return writeApiError(deps, result.error);
1134
1272
  const raw = unwrapListData(result.data, "drops");
1273
+ const page = extractPagination(result.data);
1135
1274
  const items = Array.isArray(raw) ? raw : void 0;
1136
1275
  if (deps.outputMode === "human") {
1137
1276
  if (!items || items.length === 0) {
1138
1277
  writeEmpty(deps, "No drops found.", { ok: true, drops: [] });
1139
1278
  } else {
1140
1279
  for (const item of items) {
1280
+ const created = typeof item.createdAt === "string" ? import_picocolors4.default.dim(formatDate(item.createdAt)) : "";
1141
1281
  deps.stdout(
1142
- `${item.id ?? ""} ${item.title ?? ""} ${item.url ? url(String(item.url)) : ""}
1282
+ `${item.id ?? ""} ${created} ${item.title ?? ""} ${item.url ? url(String(item.url)) : ""}
1283
+ `
1284
+ );
1285
+ }
1286
+ deps.stdout(
1287
+ `${import_picocolors4.default.dim(`${items.length} ${items.length === 1 ? "drop" : "drops"}`)}
1288
+ `
1289
+ );
1290
+ if (page?.hasMore && page.nextCursor) {
1291
+ deps.stdout(
1292
+ `${hint(`More drops: dropthis list --cursor ${page.nextCursor}`)}
1143
1293
  `
1144
1294
  );
1145
1295
  }
1146
1296
  }
1147
1297
  } else {
1148
- writeJson(deps, { ok: true, drops: items ?? raw });
1298
+ writeJson(deps, {
1299
+ ok: true,
1300
+ drops: items ?? raw,
1301
+ ...page ? { next_cursor: page.nextCursor, has_more: page.hasMore } : {}
1302
+ });
1149
1303
  }
1150
1304
  return { exitCode: 0 };
1151
1305
  }
1152
- async function runDropsGet(dropId, _input, deps) {
1306
+ async function runDropsGet(target, _input, deps) {
1153
1307
  try {
1154
1308
  await requireCredential({
1155
1309
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1159,7 +1313,9 @@ async function runDropsGet(dropId, _input, deps) {
1159
1313
  } catch {
1160
1314
  return writeAuthError(deps);
1161
1315
  }
1162
- const result = await deps.client.drops.get(dropId);
1316
+ const resolved = await resolveDropTarget(target, deps);
1317
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1318
+ const result = await deps.client.drops.get(resolved.dropId);
1163
1319
  if (result.error) return writeApiError(deps, result.error);
1164
1320
  const data = result.data;
1165
1321
  const pairs = [];
@@ -1169,7 +1325,8 @@ async function runDropsGet(dropId, _input, deps) {
1169
1325
  if (data.visibility) pairs.push(["Visibility", String(data.visibility)]);
1170
1326
  if (data.revision !== void 0)
1171
1327
  pairs.push(["Revision", String(data.revision)]);
1172
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
1328
+ if (data.createdAt)
1329
+ pairs.push(["Created", formatDate(String(data.createdAt))]);
1173
1330
  if (data.accessible !== void 0)
1174
1331
  pairs.push(["Accessible", String(data.accessible)]);
1175
1332
  if (data.persistent !== void 0)
@@ -1179,7 +1336,7 @@ async function runDropsGet(dropId, _input, deps) {
1179
1336
  writeKv(deps, pairs, { ok: true, drop: result.data });
1180
1337
  return { exitCode: 0 };
1181
1338
  }
1182
- async function runDropsDelete(dropId, input, deps) {
1339
+ async function runDropsDelete(target, input, deps) {
1183
1340
  if (!input.yes && input.interactive === false) {
1184
1341
  return writeError(
1185
1342
  deps,
@@ -1187,14 +1344,6 @@ async function runDropsDelete(dropId, input, deps) {
1187
1344
  "Pass --yes to delete in non-interactive mode."
1188
1345
  );
1189
1346
  }
1190
- if (!input.yes && input.interactive !== false) {
1191
- const confirmed = await prompts2.confirm({
1192
- message: `Delete ${dropId}?`
1193
- });
1194
- if (prompts2.isCancel(confirmed) || !confirmed) {
1195
- return { exitCode: 0 };
1196
- }
1197
- }
1198
1347
  try {
1199
1348
  await requireCredential({
1200
1349
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1204,6 +1353,17 @@ async function runDropsDelete(dropId, input, deps) {
1204
1353
  } catch {
1205
1354
  return writeAuthError(deps);
1206
1355
  }
1356
+ const resolved = await resolveDropTarget(target, deps);
1357
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1358
+ const dropId = resolved.dropId;
1359
+ if (!input.yes && input.interactive !== false) {
1360
+ const confirmed = await prompts3.confirm({
1361
+ message: `Delete ${dropId}${dropId === target ? "" : ` (${target})`}?`
1362
+ });
1363
+ if (prompts3.isCancel(confirmed) || !confirmed) {
1364
+ return { exitCode: 0 };
1365
+ }
1366
+ }
1207
1367
  const result = await deps.client.drops.delete(dropId);
1208
1368
  if (result?.error) return writeApiError(deps, result.error);
1209
1369
  writeHumanOrJson(deps, success(`Deleted ${dropId}`), {
@@ -1215,31 +1375,31 @@ async function runDropsDelete(dropId, input, deps) {
1215
1375
  }
1216
1376
 
1217
1377
  // src/commands/login.ts
1218
- var prompts3 = __toESM(require("@clack/prompts"), 1);
1378
+ var prompts4 = __toESM(require("@clack/prompts"), 1);
1219
1379
  async function runLoginInteractive(deps) {
1220
- prompts3.intro("dropthis login");
1221
- const email = await prompts3.text({
1380
+ prompts4.intro("dropthis login");
1381
+ const email = await prompts4.text({
1222
1382
  message: "Email address",
1223
1383
  validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
1224
1384
  });
1225
- if (prompts3.isCancel(email)) {
1226
- prompts3.cancel("Login cancelled.");
1385
+ if (prompts4.isCancel(email)) {
1386
+ prompts4.cancel("Login cancelled.");
1227
1387
  return { exitCode: 0 };
1228
1388
  }
1229
1389
  const requestResult = await deps.client.auth.requestEmailOtp({ email });
1230
1390
  if (requestResult.error) {
1231
- prompts3.cancel(requestResult.error.message);
1391
+ prompts4.cancel(requestResult.error.message);
1232
1392
  return { exitCode: exitCodeFor("api_error") };
1233
1393
  }
1234
1394
  const maxAttempts = 3;
1235
1395
  let session;
1236
1396
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1237
- const otp = await prompts3.text({
1397
+ const otp = await prompts4.text({
1238
1398
  message: attempt === 1 ? "Paste the code from your email" : "Try again \u2014 paste the code from your email",
1239
1399
  validate: (v) => v && v.length >= 4 && v.length <= 8 ? void 0 : "Code must be 4\u20138 characters."
1240
1400
  });
1241
- if (prompts3.isCancel(otp)) {
1242
- prompts3.cancel("Login cancelled.");
1401
+ if (prompts4.isCancel(otp)) {
1402
+ prompts4.cancel("Login cancelled.");
1243
1403
  return { exitCode: 0 };
1244
1404
  }
1245
1405
  const result = await deps.client.auth.verifyEmailOtp({
@@ -1251,20 +1411,20 @@ async function runLoginInteractive(deps) {
1251
1411
  break;
1252
1412
  }
1253
1413
  if (attempt < maxAttempts) {
1254
- prompts3.log.warning(result.error.message);
1414
+ prompts4.log.warning(result.error.message);
1255
1415
  } else {
1256
- prompts3.cancel(result.error.message);
1416
+ prompts4.cancel(result.error.message);
1257
1417
  return { exitCode: exitCodeFor("api_error") };
1258
1418
  }
1259
1419
  }
1260
1420
  if (!session) {
1261
- prompts3.cancel("Login failed.");
1421
+ prompts4.cancel("Login failed.");
1262
1422
  return { exitCode: exitCodeFor("api_error") };
1263
1423
  }
1264
1424
  const authedClient = deps.createClient(session.data.token);
1265
1425
  const apiKey = await authedClient.apiKeys.create({ label: "CLI" });
1266
1426
  if (apiKey.error) {
1267
- prompts3.cancel(apiKey.error.message);
1427
+ prompts4.cancel(apiKey.error.message);
1268
1428
  return { exitCode: exitCodeFor("api_error") };
1269
1429
  }
1270
1430
  await deps.store.save({
@@ -1275,7 +1435,7 @@ async function runLoginInteractive(deps) {
1275
1435
  email,
1276
1436
  storage: "secure"
1277
1437
  });
1278
- prompts3.outro("Logged in successfully.");
1438
+ prompts4.outro("Logged in successfully.");
1279
1439
  return { exitCode: 0 };
1280
1440
  }
1281
1441
  async function runLoginRequest(input, deps) {
@@ -1464,12 +1624,12 @@ function parseJsonObject(value, label) {
1464
1624
  }
1465
1625
 
1466
1626
  // src/spinner.ts
1467
- var import_picocolors4 = __toESM(require("picocolors"), 1);
1627
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
1468
1628
  var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1469
1629
  function createSpinner(message, stderr) {
1470
1630
  let i = 0;
1471
1631
  const timer = setInterval(() => {
1472
- stderr(`\r\x1B[2K${import_picocolors4.default.cyan(frames[i % frames.length])} ${message}`);
1632
+ stderr(`\r\x1B[2K${import_picocolors5.default.cyan(frames[i % frames.length])} ${message}`);
1473
1633
  i++;
1474
1634
  }, 80);
1475
1635
  return {
@@ -1620,24 +1780,10 @@ async function runPull(target, input, deps) {
1620
1780
  } catch {
1621
1781
  return writeAuthError(deps);
1622
1782
  }
1623
- let dropId;
1624
- let defaultDirName;
1625
- if (target.startsWith("drop_")) {
1626
- dropId = target;
1627
- defaultDirName = target;
1628
- } else {
1629
- const resolved = await deps.client.drops.resolve(target);
1630
- if (resolved.error) return writeApiError(deps, resolved.error);
1631
- if (resolved.data === null) {
1632
- return writeApiError(deps, {
1633
- code: "not_found",
1634
- message: `No drop matching "${target}" found in your account.`,
1635
- suggestion: "It may belong to another account or have been deleted. Run dropthis list to see your drops, or pass the full drop_\u2026 id."
1636
- });
1637
- }
1638
- dropId = resolved.data.id;
1639
- defaultDirName = resolved.data.slug || resolved.data.id;
1640
- }
1783
+ const resolved = await resolveDropTarget(target, deps);
1784
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1785
+ const dropId = resolved.dropId;
1786
+ const defaultDirName = resolved.slug || resolved.dropId;
1641
1787
  const spin = shouldSpin(deps.outputMode) ? createSpinner("Pulling content\u2026", deps.stderr) : void 0;
1642
1788
  const manifestResult = await deps.client.drops.getContent(dropId);
1643
1789
  if (manifestResult.error) {
@@ -1820,7 +1966,7 @@ function assertDropId(target) {
1820
1966
  }
1821
1967
 
1822
1968
  // src/commands/update-settings.ts
1823
- var prompts4 = __toESM(require("@clack/prompts"), 1);
1969
+ var prompts5 = __toESM(require("@clack/prompts"), 1);
1824
1970
  var NO_SETTINGS_MESSAGE = "Nothing to update. Provide at least one settings option.";
1825
1971
  var NO_SETTINGS_NEXT_ACTION = "Run dropthis update-settings --help, or retry with one of: --title, --visibility, --password, --no-password, --noindex, --index, --expires-at, --metadata, or --metadata-file. To replace the content at the URL instead, use dropthis update-content <dropId> ./dist.";
1826
1972
  async function runUpdateSettings(dropId, raw, deps) {
@@ -1844,7 +1990,7 @@ async function runUpdateSettings(dropId, raw, deps) {
1844
1990
  if (shouldPrompt(deps)) {
1845
1991
  const prompted = await promptForSettings(
1846
1992
  dropId,
1847
- deps.prompts ?? prompts4
1993
+ deps.prompts ?? prompts5
1848
1994
  );
1849
1995
  if (!prompted) return { exitCode: 0 };
1850
1996
  parsed = { ...parsed, ...prompted };
@@ -2233,6 +2379,55 @@ async function writeCredentialFile(path, credential) {
2233
2379
  await (0, import_promises3.rename)(tmpPath, path);
2234
2380
  }
2235
2381
 
2382
+ // src/typo-guard.ts
2383
+ var import_promises4 = require("fs/promises");
2384
+ function levenshtein(a, b) {
2385
+ if (a === b) return 0;
2386
+ if (a.length === 0) return b.length;
2387
+ if (b.length === 0) return a.length;
2388
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
2389
+ for (let i = 1; i <= a.length; i++) {
2390
+ const curr = [i];
2391
+ for (let j = 1; j <= b.length; j++) {
2392
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2393
+ const deletion = (prev[j] ?? 0) + 1;
2394
+ const insertion = (curr[j - 1] ?? 0) + 1;
2395
+ const substitution = (prev[j - 1] ?? 0) + cost;
2396
+ curr[j] = Math.min(deletion, insertion, substitution);
2397
+ }
2398
+ prev = curr;
2399
+ }
2400
+ return prev[b.length] ?? 0;
2401
+ }
2402
+ function suggestCommand(token, names) {
2403
+ const lower = token.toLowerCase();
2404
+ let best = null;
2405
+ for (const name of names) {
2406
+ if (lower.length >= 2 && name.startsWith(lower)) return name;
2407
+ const dist = levenshtein(lower, name);
2408
+ if (best === null || dist < best.dist) best = { name, dist };
2409
+ }
2410
+ if (best !== null && best.dist <= 2 && best.dist < lower.length) {
2411
+ return best.name;
2412
+ }
2413
+ return null;
2414
+ }
2415
+ function isBareWordToken(token) {
2416
+ if (token === "" || token === "-") return false;
2417
+ if (/\s/.test(token)) return false;
2418
+ if (token.includes("/") || token.includes("\\")) return false;
2419
+ if (token.includes(".")) return false;
2420
+ return true;
2421
+ }
2422
+ async function pathExists(path) {
2423
+ try {
2424
+ await (0, import_promises4.stat)(path);
2425
+ return true;
2426
+ } catch {
2427
+ return false;
2428
+ }
2429
+ }
2430
+
2236
2431
  // src/program.ts
2237
2432
  function buildProgram(options = {}) {
2238
2433
  const program = new import_commander.Command();
@@ -2243,6 +2438,7 @@ function buildProgram(options = {}) {
2243
2438
  return argv.includes("--json") || argv.includes("--quiet") || argv.includes("-q") || options.env?.CI === "true" || options.env === void 0 && process.env.CI === "true" || options.stdoutIsTTY === false || options.stdoutIsTTY === void 0 && process.stdout.isTTY !== true;
2244
2439
  };
2245
2440
  program.exitOverride();
2441
+ program.showSuggestionAfterError();
2246
2442
  program.configureOutput({
2247
2443
  writeErr,
2248
2444
  // In JSON mode, suppress commander's default plain-text error so the
@@ -2253,13 +2449,13 @@ function buildProgram(options = {}) {
2253
2449
  });
2254
2450
  program.name("dropthis").description(
2255
2451
  [
2256
- import_picocolors5.default.dim("Publish anything online and get a URL back."),
2452
+ import_picocolors6.default.dim("Publish anything online and get a URL back."),
2257
2453
  "",
2258
- `${import_picocolors5.default.bold("Quick start:")}`,
2259
- ` ${import_picocolors5.default.cyan("dropthis login")}`,
2260
- ` ${import_picocolors5.default.cyan("dropthis publish ./dist")}`
2454
+ `${import_picocolors6.default.bold("Quick start:")}`,
2455
+ ` ${import_picocolors6.default.cyan("dropthis login")}`,
2456
+ ` ${import_picocolors6.default.cyan("dropthis publish ./dist")}`
2261
2457
  ].join("\n")
2262
- ).version("0.11.0").configureHelp({
2458
+ ).version("0.12.0").configureHelp({
2263
2459
  subcommandTerm(cmd) {
2264
2460
  const args = cmd.registeredArguments.map((a) => {
2265
2461
  const name = a.name();
@@ -2268,16 +2464,28 @@ function buildProgram(options = {}) {
2268
2464
  }).join(" ");
2269
2465
  return args ? `${cmd.name()} ${args}` : cmd.name();
2270
2466
  },
2271
- styleTitle: (title) => import_picocolors5.default.bold(title),
2272
- styleCommandText: (str) => import_picocolors5.default.cyan(str),
2273
- styleSubcommandText: (str) => import_picocolors5.default.cyan(str),
2274
- styleOptionText: (str) => import_picocolors5.default.yellow(str)
2467
+ styleTitle: (title) => import_picocolors6.default.bold(title),
2468
+ styleCommandText: (str) => import_picocolors6.default.cyan(str),
2469
+ styleSubcommandText: (str) => import_picocolors6.default.cyan(str),
2470
+ styleOptionText: (str) => import_picocolors6.default.yellow(str)
2275
2471
  });
2276
2472
  program.option("--api-key <key>", "Override API key for this invocation");
2277
2473
  program.option("--api-url <url>", "Override API base URL");
2278
2474
  program.option("--json", "Force JSON output");
2279
2475
  program.option("-q, --quiet", "Suppress status output and imply JSON");
2280
2476
  program.option("--no-interactive", "Disable interactive prompts");
2477
+ program.addHelpText(
2478
+ "after",
2479
+ [
2480
+ "",
2481
+ "Output is JSON when piped/CI/--json; human tables in a terminal.",
2482
+ "Docs: https://dropthis.app"
2483
+ ].join("\n")
2484
+ );
2485
+ const examplesBlock = (lines) => `
2486
+ ${import_picocolors6.default.bold("Examples:")}
2487
+ ${lines.map((l) => ` ${l}`).join("\n")}
2488
+ `;
2281
2489
  program.command("publish [input...]", { isDefault: true }).description(
2282
2490
  "Publish content to a permanent public URL (also: share, post, put online, make public, get a link).\nFiles, folders, URLs, strings, or stdin; multiple files bundle into one drop. Use - for stdin, or pipe without args.\nCreates a NEW drop each run \u2014 to change one you already published, use update-content (the files) or update-settings (title/visibility/password/expiry/metadata) with its drop_\u2026 id, not publish again."
2283
2491
  ).option("--title <title>", "Drop title").option("--visibility <v>", "public or unlisted (default: public)").option("--password <password>", "Require password to view").option("--noindex", "Prevent search-engine indexing").option("--expires-at <datetime>", "Auto-delete after this ISO 8601 date").option(
@@ -2301,6 +2509,15 @@ function buildProgram(options = {}) {
2301
2509
  ).option("--json", "Force JSON output").option(
2302
2510
  "--idempotency-key <key>",
2303
2511
  "Prevent duplicate publishes on retry (auto-generated)"
2512
+ ).addHelpText(
2513
+ "after",
2514
+ examplesBlock([
2515
+ "dropthis publish ./report.html",
2516
+ "dropthis publish ./dist --title 'Launch page'",
2517
+ "cat report.md | dropthis publish - --content-type text/markdown --path report.md",
2518
+ "dropthis publish ./dist --domain reports.example.com --slug launch",
2519
+ "URL=$(dropthis publish ./dist --url)"
2520
+ ])
2304
2521
  ).action(async (inputs, commandOptions) => {
2305
2522
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2306
2523
  if (inputs.length === 0 && stdinIsTTY) {
@@ -2326,6 +2543,29 @@ function buildProgram(options = {}) {
2326
2543
  process.exitCode = writeError(deps, "local_input_error", msg).exitCode;
2327
2544
  return;
2328
2545
  }
2546
+ if (deps.outputMode === "human" && deps.interactive && inputs.length === 1 && typeof publishInput === "string" && isBareWordToken(publishInput) && !await pathExists(publishInput)) {
2547
+ const suggestion = suggestCommand(
2548
+ publishInput,
2549
+ program.commands.map((c) => c.name())
2550
+ );
2551
+ if (suggestion) {
2552
+ process.exitCode = writeError(
2553
+ deps,
2554
+ "invalid_usage",
2555
+ `Unknown command '${publishInput}' \u2014 did you mean '${suggestion}'?`,
2556
+ `To publish the literal text, pipe it: echo '${publishInput}' | dropthis publish`
2557
+ ).exitCode;
2558
+ return;
2559
+ }
2560
+ const prompts6 = await import("@clack/prompts");
2561
+ const confirmed = await prompts6.confirm({
2562
+ message: `Publish '${publishInput}' as inline text?`,
2563
+ initialValue: false
2564
+ });
2565
+ if (prompts6.isCancel(confirmed) || !confirmed) {
2566
+ return;
2567
+ }
2568
+ }
2329
2569
  let dropOpts;
2330
2570
  try {
2331
2571
  dropOpts = toDropOptions(commandOptions);
@@ -2429,7 +2669,14 @@ function buildProgram(options = {}) {
2429
2669
  );
2430
2670
  program.command("pull <target>").description(
2431
2671
  "Download a drop's files into a local directory (accepts the drop_\u2026 id, the drop URL, or its slug).\nFetches the current deployment's file manifest and writes every file. Pull, edit, then update-content to ship the changes back to the same URL \u2014 also the rollback path."
2432
- ).option("-o, --output <dir>", "Output directory (default: ./<slug-or-id>)").option("--json", "Force JSON output").action(
2672
+ ).option("-o, --output <dir>", "Output directory (default: ./<slug-or-id>)").option("--json", "Force JSON output").addHelpText(
2673
+ "after",
2674
+ examplesBlock([
2675
+ "dropthis pull drop_abc123 -o ./site # download the drop's files",
2676
+ "$EDITOR ./site/index.html # edit locally",
2677
+ "dropthis update-content drop_abc123 ./site # ship back to the same URL"
2678
+ ])
2679
+ ).action(
2433
2680
  async (target, commandOptions) => {
2434
2681
  const deps = await commandDeps(
2435
2682
  program,
@@ -2441,19 +2688,29 @@ function buildProgram(options = {}) {
2441
2688
  process.exitCode = result.exitCode;
2442
2689
  }
2443
2690
  );
2444
- program.command("get <dropId>").description("Show drop details (pass the full drop_\u2026 id)").option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
2691
+ program.command("get <dropId>").description(
2692
+ "Show drop details (accepts the drop_\u2026 id, the drop URL, or its slug)"
2693
+ ).option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
2445
2694
  const deps = await commandDeps(program, options, store, commandOptions);
2446
2695
  const result = await runDropsGet(dropId, commandOptions, deps);
2447
2696
  process.exitCode = result.exitCode;
2448
2697
  });
2449
- program.command("list").description("List your drops").option("--limit <n>", "Page size", parseInteger).option("--cursor <cursor>", "Pagination cursor").option("--json", "Force JSON output").action(
2698
+ program.command("list").description("List your drops").option("--limit <n>", "Page size", parseInteger).option("--cursor <cursor>", "Pagination cursor").option("--json", "Force JSON output").addHelpText(
2699
+ "after",
2700
+ examplesBlock([
2701
+ "dropthis list",
2702
+ "dropthis list --json | jq -r '.drops[] | [.id, .url] | @tsv' # recover drop ids"
2703
+ ])
2704
+ ).action(
2450
2705
  async (commandOptions) => {
2451
2706
  const deps = await commandDeps(program, options, store, commandOptions);
2452
2707
  const result = await runDropsList(commandOptions, deps);
2453
2708
  process.exitCode = result.exitCode;
2454
2709
  }
2455
2710
  );
2456
- program.command("delete <dropId>").description("Delete a drop (pass the full drop_\u2026 id)").option("--yes", "Confirm deletion").option("--json", "Force JSON output").action(
2711
+ program.command("delete <dropId>").description(
2712
+ "Delete a drop (accepts the drop_\u2026 id, the drop URL, or its slug)"
2713
+ ).option("--yes", "Confirm deletion").option("--json", "Force JSON output").action(
2457
2714
  async (dropId, commandOptions) => {
2458
2715
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2459
2716
  const global = globalOptions(program, commandOptions);
@@ -2497,7 +2754,15 @@ function buildProgram(options = {}) {
2497
2754
  ).requiredOption(
2498
2755
  "--mode <mode>",
2499
2756
  "Mount mode: path (many drops at hostname/{slug}/) or dedicated (one drop at root)"
2500
- ).option("--json", "Force JSON output").action(
2757
+ ).option("--json", "Force JSON output").addHelpText(
2758
+ "after",
2759
+ examplesBlock([
2760
+ "dropthis domains connect reports.example.com --mode path",
2761
+ "# add the CNAME shown above at your DNS provider, then:",
2762
+ "dropthis domains verify reports.example.com --wait",
2763
+ "dropthis publish ./report.html --domain reports.example.com"
2764
+ ])
2765
+ ).action(
2501
2766
  async (hostname, commandOptions) => {
2502
2767
  if (commandOptions.mode !== "path" && commandOptions.mode !== "dedicated") {
2503
2768
  process.stderr.write(`error: --mode must be "path" or "dedicated"
@@ -2529,7 +2794,13 @@ function buildProgram(options = {}) {
2529
2794
  "--timeout <secs>",
2530
2795
  "Max seconds to wait (default: 300)",
2531
2796
  parseInteger
2532
- ).option("--json", "Force JSON output").action(
2797
+ ).option("--json", "Force JSON output").addHelpText(
2798
+ "after",
2799
+ examplesBlock([
2800
+ "dropthis domains verify reports.example.com --wait",
2801
+ "dropthis domains verify reports.example.com --wait --timeout 600"
2802
+ ])
2803
+ ).action(
2533
2804
  async (hostname, commandOptions) => {
2534
2805
  const deps = await commandDeps(program, options, store, commandOptions);
2535
2806
  const result = await runDomainsVerify(hostname, commandOptions, deps);
@@ -2553,12 +2824,23 @@ function buildProgram(options = {}) {
2553
2824
  }
2554
2825
  );
2555
2826
  domains.command("remove <hostname>").description(
2556
- "Remove a custom domain. IMPORTANT: also delete the DNS CNAME at your provider to prevent DNS hijacking."
2557
- ).option("--json", "Force JSON output").action(async (hostname, commandOptions) => {
2558
- const deps = await commandDeps(program, options, store, commandOptions);
2559
- const result = await runDomainsRemove(hostname, commandOptions, deps);
2560
- process.exitCode = result.exitCode;
2561
- });
2827
+ "Remove a custom domain (unmounts its drops). IMPORTANT: also delete the DNS CNAME at your provider to prevent DNS hijacking."
2828
+ ).option("--yes", "Confirm removal").option("--json", "Force JSON output").action(
2829
+ async (hostname, commandOptions) => {
2830
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2831
+ const global = globalOptions(program, commandOptions);
2832
+ const deps = await commandDeps(program, options, store, commandOptions);
2833
+ const result = await runDomainsRemove(
2834
+ hostname,
2835
+ {
2836
+ ...commandOptions,
2837
+ interactive: isInteractive(stdinIsTTY, deps.env, global)
2838
+ },
2839
+ deps
2840
+ );
2841
+ process.exitCode = result.exitCode;
2842
+ }
2843
+ );
2562
2844
  const apiKeys = program.command("api-keys").description("Manage API keys");
2563
2845
  apiKeys.command("create").description("Create an API key").option("--label <label>", "API key label", "CLI").option("--json", "Force JSON output").action(async (commandOptions) => {
2564
2846
  const deps = await commandDeps(program, options, store, commandOptions);
@@ -2591,6 +2873,16 @@ function buildProgram(options = {}) {
2591
2873
  if (!commandOptions.email || !commandOptions.otp) {
2592
2874
  const deps2 = await commandDeps(program, options, store, commandOptions);
2593
2875
  const global2 = globalOptions(program, commandOptions);
2876
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2877
+ if (!isInteractive(stdinIsTTY, deps2.env, global2)) {
2878
+ process.exitCode = writeError(
2879
+ deps2,
2880
+ "invalid_usage",
2881
+ "dropthis login is interactive and needs a terminal.",
2882
+ "Set DROPTHIS_API_KEY, or run: dropthis login request --email <email>, then dropthis login verify --email <email> --otp <code>."
2883
+ ).exitCode;
2884
+ return;
2885
+ }
2594
2886
  const context = createContext({ global: global2 });
2595
2887
  const result2 = await runLoginInteractive({
2596
2888
  ...deps2,
@@ -2849,9 +3141,6 @@ function writeUsageErrorEnvelope(input) {
2849
3141
  `${JSON.stringify(errorEnvelope("usage_error", input.message))}
2850
3142
  `
2851
3143
  );
2852
- } else {
2853
- input.stderr(`${input.message}
2854
- `);
2855
3144
  }
2856
3145
  return 2;
2857
3146
  }