@dropthis/cli 0.10.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;
@@ -232,12 +241,14 @@ function exitCodeFor(code) {
232
241
  if (code === "auth_error") return 3;
233
242
  if (code === "local_input_error") return 4;
234
243
  if (code === "network_error") return 5;
244
+ if (code === "verify_pending") return 6;
245
+ if (code === "verify_timeout") return 7;
235
246
  return 1;
236
247
  }
237
248
 
238
249
  // src/program.ts
239
250
  var import_commander = require("commander");
240
- var import_picocolors4 = __toESM(require("picocolors"), 1);
251
+ var import_picocolors6 = __toESM(require("picocolors"), 1);
241
252
 
242
253
  // src/auth.ts
243
254
  async function resolveCredential(input) {
@@ -285,6 +296,9 @@ function success(msg) {
285
296
  function error(msg) {
286
297
  return `${import_picocolors.default.red(symbols.error)} ${msg}`;
287
298
  }
299
+ function warn(msg) {
300
+ return `${import_picocolors.default.yellow(symbols.warning)} ${msg}`;
301
+ }
288
302
  function hint(msg) {
289
303
  return ` ${import_picocolors.default.dim(msg)}`;
290
304
  }
@@ -305,6 +319,21 @@ function writeResult(deps, link, verb, data) {
305
319
  const details = formatDropDetails(data);
306
320
  if (details) deps.stdout(`${details}
307
321
  `);
322
+ const warnings = data.warnings;
323
+ if (Array.isArray(warnings)) {
324
+ for (const w of warnings) {
325
+ if (w.code === "slug_suffixed") {
326
+ deps.stderr(
327
+ `${hint(`Vanity slug was taken \u2014 served at: ${w.detail ?? "see URL"}`)}
328
+ `
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
+ `);
334
+ }
335
+ }
336
+ }
308
337
  } else {
309
338
  deps.stdout(`${JSON.stringify({ ok: true, drop: data })}
310
339
  `);
@@ -323,6 +352,13 @@ function formatDropDetails(data) {
323
352
  if (parts.length === 0) return "";
324
353
  return ` ${import_picocolors2.default.dim(parts.join(" \xB7 "))}`;
325
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
+ }
326
362
  function formatBytes(bytes) {
327
363
  if (bytes < 1024) return `${bytes} B`;
328
364
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -368,7 +404,7 @@ function writeApiError(deps, details) {
368
404
  if (deps.outputMode === "human") {
369
405
  deps.stderr(`${error(details.message)}
370
406
  `);
371
- const nextAction = nextActionForApiError(details);
407
+ const nextAction = nextActionForApiError(details, "human");
372
408
  if (nextAction) deps.stderr(`${hint(nextAction)}
373
409
  `);
374
410
  if (details.currentRevision !== void 0)
@@ -383,7 +419,12 @@ function writeApiError(deps, details) {
383
419
  deps.stderr(`${JSON.stringify(apiErrorEnvelope(details))}
384
420
  `);
385
421
  }
386
- 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";
387
428
  }
388
429
  function writeApiErrorSimple(deps, message) {
389
430
  if (deps.outputMode === "human") {
@@ -491,6 +532,14 @@ async function runAccountDelete(input, deps) {
491
532
  var prompts = __toESM(require("@clack/prompts"), 1);
492
533
 
493
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
+ }
494
543
  function unwrapListData(data, ...fallbackKeys) {
495
544
  if (data && typeof data === "object") {
496
545
  if ("data" in data) {
@@ -631,7 +680,7 @@ var COMMAND_ANNOTATIONS = {
631
680
  },
632
681
  deployments: {
633
682
  auth: "required",
634
- examples: ["dropthis deployments drop_abc --json"]
683
+ examples: ["dropthis deployments list drop_abc --json"]
635
684
  },
636
685
  "deployments list": {
637
686
  auth: "required",
@@ -641,6 +690,36 @@ var COMMAND_ANNOTATIONS = {
641
690
  auth: "required",
642
691
  examples: ["dropthis deployments get drop_abc dep_abc --json"]
643
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
+ },
644
723
  "api-keys": {
645
724
  auth: "required",
646
725
  examples: ["dropthis api-keys create --label CI --json"]
@@ -722,6 +801,7 @@ async function runDeploymentsList(dropId, input, deps) {
722
801
  return writeApiError(deps, result.error);
723
802
  }
724
803
  const spread = spreadData(result.data);
804
+ const page = extractPagination(result.data);
725
805
  const items = spread.deployments ?? spread.data ?? result.data;
726
806
  if (deps.outputMode === "human") {
727
807
  if (!items || Array.isArray(items) && items.length === 0) {
@@ -731,8 +811,15 @@ async function runDeploymentsList(dropId, input, deps) {
731
811
  });
732
812
  } else if (Array.isArray(items)) {
733
813
  for (const item of items) {
814
+ const created = typeof item.createdAt === "string" ? formatDateTime(item.createdAt) : "";
815
+ deps.stdout(
816
+ `${item.id ?? ""} rev ${item.revision ?? "?"} ${created}
817
+ `
818
+ );
819
+ }
820
+ if (page?.hasMore && page.nextCursor) {
734
821
  deps.stdout(
735
- `${item.id ?? ""} rev ${item.revision ?? "?"} ${item.createdAt ?? ""}
822
+ `${hint(`More deployments: dropthis deployments list ${dropId} --cursor ${page.nextCursor}`)}
736
823
  `
737
824
  );
738
825
  }
@@ -740,7 +827,11 @@ async function runDeploymentsList(dropId, input, deps) {
740
827
  writeJson(deps, { ok: true, ...spread });
741
828
  }
742
829
  } else {
743
- writeJson(deps, { ok: true, ...spread });
830
+ writeJson(deps, {
831
+ ok: true,
832
+ ...spread,
833
+ ...page ? { next_cursor: page.nextCursor, has_more: page.hasMore } : {}
834
+ });
744
835
  }
745
836
  return { exitCode: 0 };
746
837
  }
@@ -763,7 +854,8 @@ async function runDeploymentsGet(dropId, deploymentId, _input, deps) {
763
854
  if (data.id) pairs.push(["ID", String(data.id)]);
764
855
  if (data.revision !== void 0)
765
856
  pairs.push(["Revision", String(data.revision)]);
766
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
857
+ if (data.createdAt)
858
+ pairs.push(["Created", formatDateTime(String(data.createdAt))]);
767
859
  writeKv(deps, pairs, { ok: true, deployment: result.data });
768
860
  return { exitCode: 0 };
769
861
  }
@@ -781,21 +873,386 @@ async function runDoctor(_input, deps) {
781
873
  const authSource = credential?.source ?? "missing";
782
874
  const storageBackend = stored?.storage ?? "none";
783
875
  const pairs = [
784
- ["Version", "0.10.0"],
876
+ ["Version", "0.12.0"],
785
877
  ["Auth", authSource],
786
878
  ["Storage", storageBackend]
787
879
  ];
788
880
  writeKv(deps, pairs, {
789
881
  ok: true,
790
- version: "0.10.0",
882
+ version: "0.12.0",
791
883
  auth: { source: authSource },
792
884
  storage: { backend: storageBackend }
793
885
  });
794
886
  return { exitCode: 0 };
795
887
  }
796
888
 
797
- // src/commands/drops.ts
889
+ // src/commands/domains.ts
798
890
  var prompts2 = __toESM(require("@clack/prompts"), 1);
891
+ var import_picocolors3 = __toESM(require("picocolors"), 1);
892
+ function formatDnsTable(records) {
893
+ if (records.length === 0) return "";
894
+ const rows = [];
895
+ const colWidths = {
896
+ type: Math.max(4, ...records.map((r) => r.type.length)),
897
+ name: Math.max(4, ...records.map((r) => r.name.length)),
898
+ value: Math.max(5, ...records.map((r) => r.value.length)),
899
+ status: Math.max(6, ...records.map((r) => r.status.length))
900
+ };
901
+ const header = [
902
+ "Type".padEnd(colWidths.type),
903
+ "Name".padEnd(colWidths.name),
904
+ "Value".padEnd(colWidths.value),
905
+ "Status".padEnd(colWidths.status)
906
+ ].join(" ");
907
+ rows.push(import_picocolors3.default.dim(header));
908
+ rows.push(import_picocolors3.default.dim("\u2500".repeat(header.length)));
909
+ for (const rec of records) {
910
+ const statusColor = rec.status === "ok" ? import_picocolors3.default.green(rec.status.padEnd(colWidths.status)) : rec.status === "mismatch" ? import_picocolors3.default.yellow(rec.status.padEnd(colWidths.status)) : import_picocolors3.default.red(rec.status.padEnd(colWidths.status));
911
+ rows.push(
912
+ [
913
+ rec.type.padEnd(colWidths.type),
914
+ rec.name.padEnd(colWidths.name),
915
+ rec.value.padEnd(colWidths.value),
916
+ statusColor
917
+ ].join(" ")
918
+ );
919
+ if (rec.hint) {
920
+ rows.push(` ${import_picocolors3.default.dim(rec.hint)}`);
921
+ }
922
+ if (rec.observed) {
923
+ rows.push(` ${import_picocolors3.default.dim(`Observed: ${rec.observed}`)}`);
924
+ }
925
+ }
926
+ return rows.join("\n");
927
+ }
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
+ );
951
+ deps.stdout("\n");
952
+ for (const h of lines) {
953
+ deps.stdout(`${hint(renderNextHint(h, hostname, maxRetryAfter))}
954
+ `);
955
+ }
956
+ }
957
+ function computeSleepMs(records, elapsedMs, timeoutMs) {
958
+ const maxRetryAfter = records.reduce(
959
+ (max, r) => Math.max(max, r.retryAfter ?? 0),
960
+ 0
961
+ );
962
+ const defaultSeconds = maxRetryAfter > 0 ? maxRetryAfter : 15;
963
+ const desiredMs = defaultSeconds * 1e3;
964
+ const remainingMs = timeoutMs - elapsedMs;
965
+ return Math.min(desiredMs, Math.max(0, remainingMs));
966
+ }
967
+ async function runDomainsConnect(input, deps) {
968
+ try {
969
+ await requireCredential({
970
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
971
+ env: deps.env,
972
+ store: deps.store
973
+ });
974
+ } catch {
975
+ return writeAuthError(deps);
976
+ }
977
+ const result = await deps.client.domains.connect(input);
978
+ if (result.error) return writeApiError(deps, result.error);
979
+ const domain = result.data;
980
+ if (deps.outputMode === "human") {
981
+ deps.stdout(
982
+ `${success(`Connected ${domain.hostname} (${domain.mode} mode)`)}
983
+ `
984
+ );
985
+ if (domain.dns.length > 0) {
986
+ deps.stdout("\nDNS records to create at your provider:\n\n");
987
+ deps.stdout(`${formatDnsTable(domain.dns)}
988
+ `);
989
+ }
990
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
991
+ } else {
992
+ writeJson(deps, { ok: true, domain });
993
+ }
994
+ return { exitCode: 0 };
995
+ }
996
+ async function runDomainsList(_input, deps) {
997
+ try {
998
+ await requireCredential({
999
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1000
+ env: deps.env,
1001
+ store: deps.store
1002
+ });
1003
+ } catch {
1004
+ return writeAuthError(deps);
1005
+ }
1006
+ const result = await deps.client.domains.list();
1007
+ if (result.error) return writeApiError(deps, result.error);
1008
+ const domains = result.data?.domains ?? [];
1009
+ if (deps.outputMode === "human") {
1010
+ if (domains.length === 0) {
1011
+ writeEmpty(deps, "No domains connected yet.", { ok: true, domains: [] });
1012
+ } else {
1013
+ for (const d of domains) {
1014
+ const defaultMark = d.default ? import_picocolors3.default.green(" (default)") : "";
1015
+ const drop = d.dropId ? import_picocolors3.default.dim(` \u2192 ${d.dropId}`) : "";
1016
+ deps.stdout(
1017
+ ` ${d.hostname}${defaultMark} ${import_picocolors3.default.dim(d.mode)} ${import_picocolors3.default.dim(d.status)}${drop}
1018
+ `
1019
+ );
1020
+ }
1021
+ }
1022
+ } else {
1023
+ writeJson(deps, { ok: true, domains });
1024
+ }
1025
+ return { exitCode: 0 };
1026
+ }
1027
+ async function runDomainsStatus(idOrHostname, _input, deps) {
1028
+ try {
1029
+ await requireCredential({
1030
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1031
+ env: deps.env,
1032
+ store: deps.store
1033
+ });
1034
+ } catch {
1035
+ return writeAuthError(deps);
1036
+ }
1037
+ const result = await deps.client.domains.get(idOrHostname);
1038
+ if (result.error) return writeApiError(deps, result.error);
1039
+ const domain = result.data;
1040
+ if (deps.outputMode === "human") {
1041
+ deps.stdout(` ${import_picocolors3.default.dim("Hostname:")} ${domain.hostname}
1042
+ `);
1043
+ deps.stdout(` ${import_picocolors3.default.dim("Mode: ")} ${domain.mode}
1044
+ `);
1045
+ deps.stdout(` ${import_picocolors3.default.dim("Status: ")} ${domain.status}
1046
+ `);
1047
+ if (domain.failureReason) {
1048
+ deps.stdout(
1049
+ ` ${import_picocolors3.default.dim("Reason: ")} ${import_picocolors3.default.red(domain.failureReason)}
1050
+ `
1051
+ );
1052
+ }
1053
+ if (domain.dropId) {
1054
+ deps.stdout(` ${import_picocolors3.default.dim("Drop: ")} ${domain.dropId}
1055
+ `);
1056
+ }
1057
+ if (domain.verifiedAt) {
1058
+ deps.stdout(
1059
+ ` ${import_picocolors3.default.dim("Verified:")} ${formatDate(domain.verifiedAt)}
1060
+ `
1061
+ );
1062
+ }
1063
+ deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${formatDate(domain.createdAt)}
1064
+ `);
1065
+ if (domain.dns.length > 0) {
1066
+ deps.stdout("\nDNS records:\n\n");
1067
+ deps.stdout(`${formatDnsTable(domain.dns)}
1068
+ `);
1069
+ }
1070
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
1071
+ } else {
1072
+ writeJson(deps, { ok: true, domain });
1073
+ }
1074
+ return { exitCode: 0 };
1075
+ }
1076
+ var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1077
+ async function runDomainsVerify(idOrHostname, input, deps, sleep = defaultSleep) {
1078
+ try {
1079
+ await requireCredential({
1080
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1081
+ env: deps.env,
1082
+ store: deps.store
1083
+ });
1084
+ } catch {
1085
+ return writeAuthError(deps);
1086
+ }
1087
+ const timeoutMs = (input.timeout ?? 300) * 1e3;
1088
+ const doVerify = async (elapsedMs) => {
1089
+ const result = await deps.client.domains.verify(idOrHostname);
1090
+ if (result.error) return writeApiError(deps, result.error);
1091
+ const domain = result.data;
1092
+ if (!input.wait) {
1093
+ if (deps.outputMode === "human") {
1094
+ deps.stdout(
1095
+ `${domain.status === "live" ? success(`${idOrHostname} is live`) : warn(`${idOrHostname}: ${domain.status}`)}
1096
+ `
1097
+ );
1098
+ if (domain.dns.length > 0) {
1099
+ deps.stdout("\n");
1100
+ deps.stdout(`${formatDnsTable(domain.dns)}
1101
+ `);
1102
+ }
1103
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
1104
+ } else {
1105
+ writeJson(deps, { ok: true, domain });
1106
+ }
1107
+ return {
1108
+ exitCode: domain.status === "live" ? 0 : domain.status === "failed" ? 1 : 6
1109
+ };
1110
+ }
1111
+ if (domain.status === "live" || domain.status === "failed") {
1112
+ if (deps.outputMode === "human") {
1113
+ deps.stdout(
1114
+ `${domain.status === "live" ? success(`${idOrHostname} is live`) : warn(`${idOrHostname}: ${domain.status}`)}
1115
+ `
1116
+ );
1117
+ if (domain.dns.length > 0) {
1118
+ deps.stdout("\n");
1119
+ deps.stdout(`${formatDnsTable(domain.dns)}
1120
+ `);
1121
+ }
1122
+ } else {
1123
+ writeJson(deps, { ok: true, domain });
1124
+ }
1125
+ return { exitCode: domain.status === "live" ? 0 : 1 };
1126
+ }
1127
+ const sleepMs = computeSleepMs(domain.dns, elapsedMs, timeoutMs);
1128
+ const newElapsed = elapsedMs + sleepMs;
1129
+ if (newElapsed >= timeoutMs) {
1130
+ const timeoutSecs = input.timeout ?? 300;
1131
+ const message = `Timed out waiting for ${idOrHostname} to go live after ${timeoutSecs}s`;
1132
+ if (deps.outputMode === "human") {
1133
+ deps.stderr(
1134
+ ` ${import_picocolors3.default.red("Timed out waiting for")} ${idOrHostname} ${import_picocolors3.default.red("to go live")}
1135
+ `
1136
+ );
1137
+ } else {
1138
+ deps.stderr(
1139
+ `${JSON.stringify(errorEnvelope("verify_timeout", message))}
1140
+ `
1141
+ );
1142
+ }
1143
+ return { exitCode: 7 };
1144
+ }
1145
+ if (deps.outputMode === "human" && domain.dns.length > 0) {
1146
+ deps.stdout(`${formatDnsTable(domain.dns)}
1147
+ `);
1148
+ }
1149
+ await sleep(sleepMs);
1150
+ return doVerify(newElapsed);
1151
+ };
1152
+ return doVerify(0);
1153
+ }
1154
+ async function runDomainsUpdate(idOrHostname, input, deps) {
1155
+ if (input.drop === void 0 && input.setDefault === void 0) {
1156
+ return writeError(
1157
+ deps,
1158
+ "invalid_usage",
1159
+ "Provide at least one option: --drop <dropId> or --default"
1160
+ );
1161
+ }
1162
+ try {
1163
+ await requireCredential({
1164
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1165
+ env: deps.env,
1166
+ store: deps.store
1167
+ });
1168
+ } catch {
1169
+ return writeAuthError(deps);
1170
+ }
1171
+ const updateInput = {};
1172
+ if (input.drop !== void 0) updateInput.dropId = input.drop;
1173
+ if (input.setDefault !== void 0) updateInput.default = input.setDefault;
1174
+ const result = await deps.client.domains.update(idOrHostname, updateInput);
1175
+ if (result.error) return writeApiError(deps, result.error);
1176
+ const domain = result.data;
1177
+ writeHumanOrJson(deps, `${success(`Updated ${domain.hostname}`)}
1178
+ `, {
1179
+ ok: true,
1180
+ domain
1181
+ });
1182
+ return { exitCode: 0 };
1183
+ }
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
+ }
1200
+ try {
1201
+ await requireCredential({
1202
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1203
+ env: deps.env,
1204
+ store: deps.store
1205
+ });
1206
+ } catch {
1207
+ return writeAuthError(deps);
1208
+ }
1209
+ const result = await deps.client.domains.delete(idOrHostname);
1210
+ if (result.error) return writeApiError(deps, result.error);
1211
+ const deleted = result.data;
1212
+ if (deps.outputMode === "human") {
1213
+ deps.stdout(`${success(`Removed ${deleted.hostname}`)}
1214
+ `);
1215
+ if (deleted.warning) {
1216
+ deps.stderr(`${warn(deleted.warning)}
1217
+ `);
1218
+ }
1219
+ } else {
1220
+ writeJson(deps, {
1221
+ ok: true,
1222
+ deleted: true,
1223
+ id: deleted.id,
1224
+ hostname: deleted.hostname,
1225
+ warning: deleted.warning
1226
+ });
1227
+ }
1228
+ return { exitCode: 0 };
1229
+ }
1230
+
1231
+ // src/commands/drops.ts
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
+ }
799
1256
  async function runDropsList(input, deps) {
800
1257
  try {
801
1258
  await requireCredential({
@@ -813,24 +1270,40 @@ async function runDropsList(input, deps) {
813
1270
  const result = await deps.client.drops.list(params);
814
1271
  if (result.error) return writeApiError(deps, result.error);
815
1272
  const raw = unwrapListData(result.data, "drops");
1273
+ const page = extractPagination(result.data);
816
1274
  const items = Array.isArray(raw) ? raw : void 0;
817
1275
  if (deps.outputMode === "human") {
818
1276
  if (!items || items.length === 0) {
819
1277
  writeEmpty(deps, "No drops found.", { ok: true, drops: [] });
820
1278
  } else {
821
1279
  for (const item of items) {
1280
+ const created = typeof item.createdAt === "string" ? import_picocolors4.default.dim(formatDate(item.createdAt)) : "";
822
1281
  deps.stdout(
823
- `${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}`)}
824
1293
  `
825
1294
  );
826
1295
  }
827
1296
  }
828
1297
  } else {
829
- 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
+ });
830
1303
  }
831
1304
  return { exitCode: 0 };
832
1305
  }
833
- async function runDropsGet(dropId, _input, deps) {
1306
+ async function runDropsGet(target, _input, deps) {
834
1307
  try {
835
1308
  await requireCredential({
836
1309
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -840,7 +1313,9 @@ async function runDropsGet(dropId, _input, deps) {
840
1313
  } catch {
841
1314
  return writeAuthError(deps);
842
1315
  }
843
- 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);
844
1319
  if (result.error) return writeApiError(deps, result.error);
845
1320
  const data = result.data;
846
1321
  const pairs = [];
@@ -850,7 +1325,8 @@ async function runDropsGet(dropId, _input, deps) {
850
1325
  if (data.visibility) pairs.push(["Visibility", String(data.visibility)]);
851
1326
  if (data.revision !== void 0)
852
1327
  pairs.push(["Revision", String(data.revision)]);
853
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
1328
+ if (data.createdAt)
1329
+ pairs.push(["Created", formatDate(String(data.createdAt))]);
854
1330
  if (data.accessible !== void 0)
855
1331
  pairs.push(["Accessible", String(data.accessible)]);
856
1332
  if (data.persistent !== void 0)
@@ -860,7 +1336,7 @@ async function runDropsGet(dropId, _input, deps) {
860
1336
  writeKv(deps, pairs, { ok: true, drop: result.data });
861
1337
  return { exitCode: 0 };
862
1338
  }
863
- async function runDropsDelete(dropId, input, deps) {
1339
+ async function runDropsDelete(target, input, deps) {
864
1340
  if (!input.yes && input.interactive === false) {
865
1341
  return writeError(
866
1342
  deps,
@@ -868,14 +1344,6 @@ async function runDropsDelete(dropId, input, deps) {
868
1344
  "Pass --yes to delete in non-interactive mode."
869
1345
  );
870
1346
  }
871
- if (!input.yes && input.interactive !== false) {
872
- const confirmed = await prompts2.confirm({
873
- message: `Delete ${dropId}?`
874
- });
875
- if (prompts2.isCancel(confirmed) || !confirmed) {
876
- return { exitCode: 0 };
877
- }
878
- }
879
1347
  try {
880
1348
  await requireCredential({
881
1349
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -885,6 +1353,17 @@ async function runDropsDelete(dropId, input, deps) {
885
1353
  } catch {
886
1354
  return writeAuthError(deps);
887
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
+ }
888
1367
  const result = await deps.client.drops.delete(dropId);
889
1368
  if (result?.error) return writeApiError(deps, result.error);
890
1369
  writeHumanOrJson(deps, success(`Deleted ${dropId}`), {
@@ -896,31 +1375,31 @@ async function runDropsDelete(dropId, input, deps) {
896
1375
  }
897
1376
 
898
1377
  // src/commands/login.ts
899
- var prompts3 = __toESM(require("@clack/prompts"), 1);
1378
+ var prompts4 = __toESM(require("@clack/prompts"), 1);
900
1379
  async function runLoginInteractive(deps) {
901
- prompts3.intro("dropthis login");
902
- const email = await prompts3.text({
1380
+ prompts4.intro("dropthis login");
1381
+ const email = await prompts4.text({
903
1382
  message: "Email address",
904
1383
  validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
905
1384
  });
906
- if (prompts3.isCancel(email)) {
907
- prompts3.cancel("Login cancelled.");
1385
+ if (prompts4.isCancel(email)) {
1386
+ prompts4.cancel("Login cancelled.");
908
1387
  return { exitCode: 0 };
909
1388
  }
910
1389
  const requestResult = await deps.client.auth.requestEmailOtp({ email });
911
1390
  if (requestResult.error) {
912
- prompts3.cancel(requestResult.error.message);
1391
+ prompts4.cancel(requestResult.error.message);
913
1392
  return { exitCode: exitCodeFor("api_error") };
914
1393
  }
915
1394
  const maxAttempts = 3;
916
1395
  let session;
917
1396
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
918
- const otp = await prompts3.text({
1397
+ const otp = await prompts4.text({
919
1398
  message: attempt === 1 ? "Paste the code from your email" : "Try again \u2014 paste the code from your email",
920
1399
  validate: (v) => v && v.length >= 4 && v.length <= 8 ? void 0 : "Code must be 4\u20138 characters."
921
1400
  });
922
- if (prompts3.isCancel(otp)) {
923
- prompts3.cancel("Login cancelled.");
1401
+ if (prompts4.isCancel(otp)) {
1402
+ prompts4.cancel("Login cancelled.");
924
1403
  return { exitCode: 0 };
925
1404
  }
926
1405
  const result = await deps.client.auth.verifyEmailOtp({
@@ -932,20 +1411,20 @@ async function runLoginInteractive(deps) {
932
1411
  break;
933
1412
  }
934
1413
  if (attempt < maxAttempts) {
935
- prompts3.log.warning(result.error.message);
1414
+ prompts4.log.warning(result.error.message);
936
1415
  } else {
937
- prompts3.cancel(result.error.message);
1416
+ prompts4.cancel(result.error.message);
938
1417
  return { exitCode: exitCodeFor("api_error") };
939
1418
  }
940
1419
  }
941
1420
  if (!session) {
942
- prompts3.cancel("Login failed.");
1421
+ prompts4.cancel("Login failed.");
943
1422
  return { exitCode: exitCodeFor("api_error") };
944
1423
  }
945
1424
  const authedClient = deps.createClient(session.data.token);
946
1425
  const apiKey = await authedClient.apiKeys.create({ label: "CLI" });
947
1426
  if (apiKey.error) {
948
- prompts3.cancel(apiKey.error.message);
1427
+ prompts4.cancel(apiKey.error.message);
949
1428
  return { exitCode: exitCodeFor("api_error") };
950
1429
  }
951
1430
  await deps.store.save({
@@ -956,7 +1435,7 @@ async function runLoginInteractive(deps) {
956
1435
  email,
957
1436
  storage: "secure"
958
1437
  });
959
- prompts3.outro("Logged in successfully.");
1438
+ prompts4.outro("Logged in successfully.");
960
1439
  return { exitCode: 0 };
961
1440
  }
962
1441
  async function runLoginRequest(input, deps) {
@@ -1131,7 +1610,9 @@ async function parseDropOptions(raw) {
1131
1610
  ...raw.entry ? { entry: raw.entry } : {},
1132
1611
  ...raw.contentType ? { contentType: raw.contentType } : {},
1133
1612
  ...raw.path ? { path: raw.path } : {},
1134
- ...raw.idempotencyKey ? { idempotencyKey: raw.idempotencyKey } : {}
1613
+ ...raw.idempotencyKey ? { idempotencyKey: raw.idempotencyKey } : {},
1614
+ ...raw.domain ? { domain: raw.domain } : {},
1615
+ ...raw.slug ? { slug: raw.slug } : {}
1135
1616
  };
1136
1617
  }
1137
1618
  function parseJsonObject(value, label) {
@@ -1143,12 +1624,12 @@ function parseJsonObject(value, label) {
1143
1624
  }
1144
1625
 
1145
1626
  // src/spinner.ts
1146
- var import_picocolors3 = __toESM(require("picocolors"), 1);
1627
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
1147
1628
  var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1148
1629
  function createSpinner(message, stderr) {
1149
1630
  let i = 0;
1150
1631
  const timer = setInterval(() => {
1151
- stderr(`\r\x1B[2K${import_picocolors3.default.cyan(frames[i % frames.length])} ${message}`);
1632
+ stderr(`\r\x1B[2K${import_picocolors5.default.cyan(frames[i % frames.length])} ${message}`);
1152
1633
  i++;
1153
1634
  }, 80);
1154
1635
  return {
@@ -1299,24 +1780,10 @@ async function runPull(target, input, deps) {
1299
1780
  } catch {
1300
1781
  return writeAuthError(deps);
1301
1782
  }
1302
- let dropId;
1303
- let defaultDirName;
1304
- if (target.startsWith("drop_")) {
1305
- dropId = target;
1306
- defaultDirName = target;
1307
- } else {
1308
- const resolved = await deps.client.drops.resolve(target);
1309
- if (resolved.error) return writeApiError(deps, resolved.error);
1310
- if (resolved.data === null) {
1311
- return writeApiError(deps, {
1312
- code: "not_found",
1313
- message: `No drop matching "${target}" found in your account.`,
1314
- 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."
1315
- });
1316
- }
1317
- dropId = resolved.data.id;
1318
- defaultDirName = resolved.data.slug || resolved.data.id;
1319
- }
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;
1320
1787
  const spin = shouldSpin(deps.outputMode) ? createSpinner("Pulling content\u2026", deps.stderr) : void 0;
1321
1788
  const manifestResult = await deps.client.drops.getContent(dropId);
1322
1789
  if (manifestResult.error) {
@@ -1499,7 +1966,7 @@ function assertDropId(target) {
1499
1966
  }
1500
1967
 
1501
1968
  // src/commands/update-settings.ts
1502
- var prompts4 = __toESM(require("@clack/prompts"), 1);
1969
+ var prompts5 = __toESM(require("@clack/prompts"), 1);
1503
1970
  var NO_SETTINGS_MESSAGE = "Nothing to update. Provide at least one settings option.";
1504
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.";
1505
1972
  async function runUpdateSettings(dropId, raw, deps) {
@@ -1523,7 +1990,7 @@ async function runUpdateSettings(dropId, raw, deps) {
1523
1990
  if (shouldPrompt(deps)) {
1524
1991
  const prompted = await promptForSettings(
1525
1992
  dropId,
1526
- deps.prompts ?? prompts4
1993
+ deps.prompts ?? prompts5
1527
1994
  );
1528
1995
  if (!prompted) return { exitCode: 0 };
1529
1996
  parsed = { ...parsed, ...prompted };
@@ -1912,6 +2379,55 @@ async function writeCredentialFile(path, credential) {
1912
2379
  await (0, import_promises3.rename)(tmpPath, path);
1913
2380
  }
1914
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
+
1915
2431
  // src/program.ts
1916
2432
  function buildProgram(options = {}) {
1917
2433
  const program = new import_commander.Command();
@@ -1922,6 +2438,7 @@ function buildProgram(options = {}) {
1922
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;
1923
2439
  };
1924
2440
  program.exitOverride();
2441
+ program.showSuggestionAfterError();
1925
2442
  program.configureOutput({
1926
2443
  writeErr,
1927
2444
  // In JSON mode, suppress commander's default plain-text error so the
@@ -1932,13 +2449,13 @@ function buildProgram(options = {}) {
1932
2449
  });
1933
2450
  program.name("dropthis").description(
1934
2451
  [
1935
- import_picocolors4.default.dim("Publish anything online and get a URL back."),
2452
+ import_picocolors6.default.dim("Publish anything online and get a URL back."),
1936
2453
  "",
1937
- `${import_picocolors4.default.bold("Quick start:")}`,
1938
- ` ${import_picocolors4.default.cyan("dropthis login")}`,
1939
- ` ${import_picocolors4.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")}`
1940
2457
  ].join("\n")
1941
- ).version("0.10.0").configureHelp({
2458
+ ).version("0.12.0").configureHelp({
1942
2459
  subcommandTerm(cmd) {
1943
2460
  const args = cmd.registeredArguments.map((a) => {
1944
2461
  const name = a.name();
@@ -1947,16 +2464,28 @@ function buildProgram(options = {}) {
1947
2464
  }).join(" ");
1948
2465
  return args ? `${cmd.name()} ${args}` : cmd.name();
1949
2466
  },
1950
- styleTitle: (title) => import_picocolors4.default.bold(title),
1951
- styleCommandText: (str) => import_picocolors4.default.cyan(str),
1952
- styleSubcommandText: (str) => import_picocolors4.default.cyan(str),
1953
- styleOptionText: (str) => import_picocolors4.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)
1954
2471
  });
1955
2472
  program.option("--api-key <key>", "Override API key for this invocation");
1956
2473
  program.option("--api-url <url>", "Override API base URL");
1957
2474
  program.option("--json", "Force JSON output");
1958
2475
  program.option("-q, --quiet", "Suppress status output and imply JSON");
1959
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
+ `;
1960
2489
  program.command("publish [input...]", { isDefault: true }).description(
1961
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."
1962
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(
@@ -1968,12 +2497,27 @@ function buildProgram(options = {}) {
1968
2497
  ).option(
1969
2498
  "--metadata <json>",
1970
2499
  `Attach JSON key-value pairs, e.g. '{"source":"ci"}'`
1971
- ).option("--metadata-file <path>", "Read metadata JSON from a file").option("--path <name>", "Set filename when publishing from stdin").option("--url", "Print only the published URL (no JSON envelope)").option(
2500
+ ).option("--metadata-file <path>", "Read metadata JSON from a file").option("--path <name>", "Set filename when publishing from stdin").option(
2501
+ "--domain <hostname>",
2502
+ "Serve this drop under a connected custom domain (must be live \u2014 see: dropthis domains list). Path-mode: drop lives at https://domain/{slug}/. Dedicated: drop is at the root (409 if occupied). Omit to use the account's default path domain."
2503
+ ).option(
2504
+ "--slug <vanity-slug>",
2505
+ "Vanity slug for path-mode custom domains (1\u201363 lowercase letters/digits/hyphens). Auto-suffixed if taken. Only valid with --domain on a path-mode domain."
2506
+ ).option("--url", "Print only the published URL (no JSON envelope)").option(
1972
2507
  "--dry-run",
1973
2508
  "Show what would be published without publishing (JSON; cannot combine with --url)"
1974
2509
  ).option("--json", "Force JSON output").option(
1975
2510
  "--idempotency-key <key>",
1976
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
+ ])
1977
2521
  ).action(async (inputs, commandOptions) => {
1978
2522
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
1979
2523
  if (inputs.length === 0 && stdinIsTTY) {
@@ -1999,6 +2543,29 @@ function buildProgram(options = {}) {
1999
2543
  process.exitCode = writeError(deps, "local_input_error", msg).exitCode;
2000
2544
  return;
2001
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
+ }
2002
2569
  let dropOpts;
2003
2570
  try {
2004
2571
  dropOpts = toDropOptions(commandOptions);
@@ -2102,7 +2669,14 @@ function buildProgram(options = {}) {
2102
2669
  );
2103
2670
  program.command("pull <target>").description(
2104
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."
2105
- ).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(
2106
2680
  async (target, commandOptions) => {
2107
2681
  const deps = await commandDeps(
2108
2682
  program,
@@ -2114,19 +2688,29 @@ function buildProgram(options = {}) {
2114
2688
  process.exitCode = result.exitCode;
2115
2689
  }
2116
2690
  );
2117
- 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) => {
2118
2694
  const deps = await commandDeps(program, options, store, commandOptions);
2119
2695
  const result = await runDropsGet(dropId, commandOptions, deps);
2120
2696
  process.exitCode = result.exitCode;
2121
2697
  });
2122
- 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(
2123
2705
  async (commandOptions) => {
2124
2706
  const deps = await commandDeps(program, options, store, commandOptions);
2125
2707
  const result = await runDropsList(commandOptions, deps);
2126
2708
  process.exitCode = result.exitCode;
2127
2709
  }
2128
2710
  );
2129
- 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(
2130
2714
  async (dropId, commandOptions) => {
2131
2715
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2132
2716
  const global = globalOptions(program, commandOptions);
@@ -2162,6 +2746,101 @@ function buildProgram(options = {}) {
2162
2746
  process.exitCode = result.exitCode;
2163
2747
  }
2164
2748
  );
2749
+ const domains = program.command("domains").description(
2750
+ "Manage custom domains. Loop: connect \u2192 create the CNAME at your DNS provider \u2192 verify --wait \u2192 publish --domain"
2751
+ );
2752
+ domains.command("connect <hostname>").description(
2753
+ "Connect a custom domain. Returns DNS records to create at your provider.\nAfter adding them: dropthis domains verify <hostname> --wait"
2754
+ ).requiredOption(
2755
+ "--mode <mode>",
2756
+ "Mount mode: path (many drops at hostname/{slug}/) or dedicated (one drop at root)"
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(
2766
+ async (hostname, commandOptions) => {
2767
+ if (commandOptions.mode !== "path" && commandOptions.mode !== "dedicated") {
2768
+ process.stderr.write(`error: --mode must be "path" or "dedicated"
2769
+ `);
2770
+ process.exitCode = 2;
2771
+ return;
2772
+ }
2773
+ const deps = await commandDeps(program, options, store, commandOptions);
2774
+ const result = await runDomainsConnect(
2775
+ { hostname, mode: commandOptions.mode },
2776
+ deps
2777
+ );
2778
+ process.exitCode = result.exitCode;
2779
+ }
2780
+ );
2781
+ domains.command("list").description("List connected custom domains").option("--json", "Force JSON output").action(async (commandOptions) => {
2782
+ const deps = await commandDeps(program, options, store, commandOptions);
2783
+ const result = await runDomainsList(commandOptions, deps);
2784
+ process.exitCode = result.exitCode;
2785
+ });
2786
+ domains.command("status <hostname>").description("Show domain details and DNS record status").option("--json", "Force JSON output").action(async (hostname, commandOptions) => {
2787
+ const deps = await commandDeps(program, options, store, commandOptions);
2788
+ const result = await runDomainsStatus(hostname, commandOptions, deps);
2789
+ process.exitCode = result.exitCode;
2790
+ });
2791
+ domains.command("verify <hostname>").description(
2792
+ "Trigger a DNS verification check. With --wait, polls until live or failed.\nUse after adding the DNS records from: dropthis domains connect"
2793
+ ).option("--wait", "Poll until live (or failed/timeout)").option(
2794
+ "--timeout <secs>",
2795
+ "Max seconds to wait (default: 300)",
2796
+ parseInteger
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(
2804
+ async (hostname, commandOptions) => {
2805
+ const deps = await commandDeps(program, options, store, commandOptions);
2806
+ const result = await runDomainsVerify(hostname, commandOptions, deps);
2807
+ process.exitCode = result.exitCode;
2808
+ }
2809
+ );
2810
+ domains.command("update <hostname>").description(
2811
+ "Update a domain \u2014 repoint to a different drop (dedicated mode) or set/clear as account default (path mode). At least one option required."
2812
+ ).option("--drop <dropId>", "Repoint to a different drop (dedicated mode)").option("--default", "Set as account default publish domain (path mode)").option("--json", "Force JSON output").action(
2813
+ async (hostname, commandOptions) => {
2814
+ const deps = await commandDeps(program, options, store, commandOptions);
2815
+ const result = await runDomainsUpdate(
2816
+ hostname,
2817
+ {
2818
+ ...commandOptions.drop !== void 0 ? { drop: commandOptions.drop } : {},
2819
+ ...commandOptions.default !== void 0 ? { setDefault: commandOptions.default } : {}
2820
+ },
2821
+ deps
2822
+ );
2823
+ process.exitCode = result.exitCode;
2824
+ }
2825
+ );
2826
+ domains.command("remove <hostname>").description(
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
+ );
2165
2844
  const apiKeys = program.command("api-keys").description("Manage API keys");
2166
2845
  apiKeys.command("create").description("Create an API key").option("--label <label>", "API key label", "CLI").option("--json", "Force JSON output").action(async (commandOptions) => {
2167
2846
  const deps = await commandDeps(program, options, store, commandOptions);
@@ -2194,6 +2873,16 @@ function buildProgram(options = {}) {
2194
2873
  if (!commandOptions.email || !commandOptions.otp) {
2195
2874
  const deps2 = await commandDeps(program, options, store, commandOptions);
2196
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
+ }
2197
2886
  const context = createContext({ global: global2 });
2198
2887
  const result2 = await runLoginInteractive({
2199
2888
  ...deps2,
@@ -2400,6 +3089,8 @@ function toDropOptions(options) {
2400
3089
  ...options.contentType ? { contentType: options.contentType } : {},
2401
3090
  ...options.path ? { path: options.path } : {},
2402
3091
  ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {},
3092
+ ...options.domain ? { domain: options.domain } : {},
3093
+ ...options.slug ? { slug: options.slug } : {},
2403
3094
  ...options.json !== void 0 ? { json: options.json } : {},
2404
3095
  ...options.quiet !== void 0 ? { quiet: options.quiet } : {},
2405
3096
  ...options.url !== void 0 ? { url: options.url } : {},
@@ -2450,9 +3141,6 @@ function writeUsageErrorEnvelope(input) {
2450
3141
  `${JSON.stringify(errorEnvelope("usage_error", input.message))}
2451
3142
  `
2452
3143
  );
2453
- } else {
2454
- input.stderr(`${input.message}
2455
- `);
2456
3144
  }
2457
3145
  return 2;
2458
3146
  }