@dropthis/cli 0.11.0 → 0.13.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,13 +202,31 @@ 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
  }
227
+ if (error2.code === "publish_conflict") {
228
+ return "The drop changed state mid-publish (likely deleted). Run dropthis publish again to create a fresh drop.";
229
+ }
212
230
  if (error2.statusCode === 404 || error2.code === "not_found") {
213
231
  return "Check the drop id or slug and retry; list your drops with dropthis list.";
214
232
  }
@@ -218,13 +236,7 @@ function nextActionForApiError(error2) {
218
236
  if (error2.statusCode === 413 || error2.code === "quota_exceeded") {
219
237
  return "Reduce the upload size or upgrade the account limit.";
220
238
  }
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.";
239
+ return void 0;
228
240
  }
229
241
  function exitCodeFor(code) {
230
242
  if (code === "usage_error") return 2;
@@ -239,7 +251,7 @@ function exitCodeFor(code) {
239
251
 
240
252
  // src/program.ts
241
253
  var import_commander = require("commander");
242
- var import_picocolors5 = __toESM(require("picocolors"), 1);
254
+ var import_picocolors6 = __toESM(require("picocolors"), 1);
243
255
 
244
256
  // src/auth.ts
245
257
  async function resolveCredential(input) {
@@ -318,6 +330,10 @@ function writeResult(deps, link, verb, data) {
318
330
  `${hint(`Vanity slug was taken \u2014 served at: ${w.detail ?? "see URL"}`)}
319
331
  `
320
332
  );
333
+ } else {
334
+ const text2 = typeof w.detail === "string" ? w.detail : typeof w.message === "string" ? w.message : typeof w.code === "string" ? w.code : void 0;
335
+ if (text2) deps.stderr(`${hint(text2)}
336
+ `);
321
337
  }
322
338
  }
323
339
  }
@@ -339,6 +355,13 @@ function formatDropDetails(data) {
339
355
  if (parts.length === 0) return "";
340
356
  return ` ${import_picocolors2.default.dim(parts.join(" \xB7 "))}`;
341
357
  }
358
+ function formatDate(iso) {
359
+ return iso.split("T")[0] ?? iso;
360
+ }
361
+ function formatDateTime(iso) {
362
+ const match = iso.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
363
+ return match ? `${match[1]} ${match[2]}` : iso;
364
+ }
342
365
  function formatBytes(bytes) {
343
366
  if (bytes < 1024) return `${bytes} B`;
344
367
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -384,7 +407,7 @@ function writeApiError(deps, details) {
384
407
  if (deps.outputMode === "human") {
385
408
  deps.stderr(`${error(details.message)}
386
409
  `);
387
- const nextAction = nextActionForApiError(details);
410
+ const nextAction = nextActionForApiError(details, "human");
388
411
  if (nextAction) deps.stderr(`${hint(nextAction)}
389
412
  `);
390
413
  if (details.currentRevision !== void 0)
@@ -399,7 +422,12 @@ function writeApiError(deps, details) {
399
422
  deps.stderr(`${JSON.stringify(apiErrorEnvelope(details))}
400
423
  `);
401
424
  }
402
- return { exitCode: exitCodeFor("api_error") };
425
+ return { exitCode: exitCodeFor(cliErrorCodeFor(details)) };
426
+ }
427
+ function cliErrorCodeFor(details) {
428
+ if (details.code === "network_error") return "network_error";
429
+ if (details.code === "file_not_found") return "local_input_error";
430
+ return "api_error";
403
431
  }
404
432
  function writeApiErrorSimple(deps, message) {
405
433
  if (deps.outputMode === "human") {
@@ -507,6 +535,14 @@ async function runAccountDelete(input, deps) {
507
535
  var prompts = __toESM(require("@clack/prompts"), 1);
508
536
 
509
537
  // src/types.ts
538
+ function extractPagination(data) {
539
+ if (!data || typeof data !== "object") return void 0;
540
+ const rec = data;
541
+ if (!("nextCursor" in rec) && !("hasMore" in rec)) return void 0;
542
+ const nextCursor = typeof rec.nextCursor === "string" ? rec.nextCursor : null;
543
+ const hasMore = typeof rec.hasMore === "boolean" ? rec.hasMore : nextCursor !== null;
544
+ return { nextCursor, hasMore };
545
+ }
510
546
  function unwrapListData(data, ...fallbackKeys) {
511
547
  if (data && typeof data === "object") {
512
548
  if ("data" in data) {
@@ -639,7 +675,10 @@ var COMMAND_ANNOTATIONS = {
639
675
  },
640
676
  list: {
641
677
  auth: "required",
642
- examples: ["dropthis list --json"]
678
+ examples: [
679
+ "dropthis list --json",
680
+ "dropthis list --domain reports.example.com --json"
681
+ ]
643
682
  },
644
683
  delete: {
645
684
  auth: "required",
@@ -647,7 +686,7 @@ var COMMAND_ANNOTATIONS = {
647
686
  },
648
687
  deployments: {
649
688
  auth: "required",
650
- examples: ["dropthis deployments drop_abc --json"]
689
+ examples: ["dropthis deployments list drop_abc --json"]
651
690
  },
652
691
  "deployments list": {
653
692
  auth: "required",
@@ -657,6 +696,36 @@ var COMMAND_ANNOTATIONS = {
657
696
  auth: "required",
658
697
  examples: ["dropthis deployments get drop_abc dep_abc --json"]
659
698
  },
699
+ domains: {
700
+ auth: "required",
701
+ examples: ["dropthis domains list --json"]
702
+ },
703
+ "domains connect": {
704
+ auth: "required",
705
+ examples: [
706
+ "dropthis domains connect reports.example.com --mode path --json"
707
+ ]
708
+ },
709
+ "domains list": {
710
+ auth: "required",
711
+ examples: ["dropthis domains list --json"]
712
+ },
713
+ "domains status": {
714
+ auth: "required",
715
+ examples: ["dropthis domains status reports.example.com --json"]
716
+ },
717
+ "domains verify": {
718
+ auth: "required",
719
+ examples: ["dropthis domains verify reports.example.com --wait --json"]
720
+ },
721
+ "domains update": {
722
+ auth: "required",
723
+ examples: ["dropthis domains update reports.example.com --default --json"]
724
+ },
725
+ "domains remove": {
726
+ auth: "required",
727
+ examples: ["dropthis domains remove reports.example.com --yes --json"]
728
+ },
660
729
  "api-keys": {
661
730
  auth: "required",
662
731
  examples: ["dropthis api-keys create --label CI --json"]
@@ -679,6 +748,7 @@ var COMMAND_ANNOTATIONS = {
679
748
  examples: ["dropthis account delete --yes --json"]
680
749
  },
681
750
  doctor: { examples: ["dropthis doctor --json"] },
751
+ upgrade: { examples: ["dropthis upgrade", "dropthis upgrade --json"] },
682
752
  commands: { examples: ["dropthis commands --json"] }
683
753
  };
684
754
  function describeArgs(cmd) {
@@ -738,6 +808,7 @@ async function runDeploymentsList(dropId, input, deps) {
738
808
  return writeApiError(deps, result.error);
739
809
  }
740
810
  const spread = spreadData(result.data);
811
+ const page = extractPagination(result.data);
741
812
  const items = spread.deployments ?? spread.data ?? result.data;
742
813
  if (deps.outputMode === "human") {
743
814
  if (!items || Array.isArray(items) && items.length === 0) {
@@ -747,8 +818,15 @@ async function runDeploymentsList(dropId, input, deps) {
747
818
  });
748
819
  } else if (Array.isArray(items)) {
749
820
  for (const item of items) {
821
+ const created = typeof item.createdAt === "string" ? formatDateTime(item.createdAt) : "";
750
822
  deps.stdout(
751
- `${item.id ?? ""} rev ${item.revision ?? "?"} ${item.createdAt ?? ""}
823
+ `${item.id ?? ""} rev ${item.revision ?? "?"} ${created}
824
+ `
825
+ );
826
+ }
827
+ if (page?.hasMore && page.nextCursor) {
828
+ deps.stdout(
829
+ `${hint(`More deployments: dropthis deployments list ${dropId} --cursor ${page.nextCursor}`)}
752
830
  `
753
831
  );
754
832
  }
@@ -756,7 +834,11 @@ async function runDeploymentsList(dropId, input, deps) {
756
834
  writeJson(deps, { ok: true, ...spread });
757
835
  }
758
836
  } else {
759
- writeJson(deps, { ok: true, ...spread });
837
+ writeJson(deps, {
838
+ ok: true,
839
+ ...spread,
840
+ ...page ? { next_cursor: page.nextCursor, has_more: page.hasMore } : {}
841
+ });
760
842
  }
761
843
  return { exitCode: 0 };
762
844
  }
@@ -779,7 +861,8 @@ async function runDeploymentsGet(dropId, deploymentId, _input, deps) {
779
861
  if (data.id) pairs.push(["ID", String(data.id)]);
780
862
  if (data.revision !== void 0)
781
863
  pairs.push(["Revision", String(data.revision)]);
782
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
864
+ if (data.createdAt)
865
+ pairs.push(["Created", formatDateTime(String(data.createdAt))]);
783
866
  writeKv(deps, pairs, { ok: true, deployment: result.data });
784
867
  return { exitCode: 0 };
785
868
  }
@@ -797,13 +880,13 @@ async function runDoctor(_input, deps) {
797
880
  const authSource = credential?.source ?? "missing";
798
881
  const storageBackend = stored?.storage ?? "none";
799
882
  const pairs = [
800
- ["Version", "0.11.0"],
883
+ ["Version", "0.13.0"],
801
884
  ["Auth", authSource],
802
885
  ["Storage", storageBackend]
803
886
  ];
804
887
  writeKv(deps, pairs, {
805
888
  ok: true,
806
- version: "0.11.0",
889
+ version: "0.13.0",
807
890
  auth: { source: authSource },
808
891
  storage: { backend: storageBackend }
809
892
  });
@@ -811,6 +894,7 @@ async function runDoctor(_input, deps) {
811
894
  }
812
895
 
813
896
  // src/commands/domains.ts
897
+ var prompts2 = __toESM(require("@clack/prompts"), 1);
814
898
  var import_picocolors3 = __toESM(require("picocolors"), 1);
815
899
  function formatDnsTable(records) {
816
900
  if (records.length === 0) return "";
@@ -848,11 +932,32 @@ function formatDnsTable(records) {
848
932
  }
849
933
  return rows.join("\n");
850
934
  }
851
- function printNextHints(deps, hints) {
852
- if (hints.length === 0) return;
935
+ function renderNextHint(h, hostname, maxRetryAfter) {
936
+ if (h.action === "verify") {
937
+ const recheck = maxRetryAfter > 0 ? ` (re-checks every ${maxRetryAfter}s)` : "";
938
+ return `Run: dropthis domains verify ${hostname} --wait${recheck}`;
939
+ }
940
+ if (h.action === "publish") {
941
+ return `Run: dropthis publish <file> --domain ${hostname}`;
942
+ }
943
+ return h.message;
944
+ }
945
+ function duplicatesRecordHint(h, dns) {
946
+ if (h.action !== "dns") return false;
947
+ return dns.some(
948
+ (r) => r.hint && h.message.includes(r.name) && h.message.includes(r.value)
949
+ );
950
+ }
951
+ function printNextHints(deps, hints, hostname, dns) {
952
+ const lines = hints.filter((h) => !duplicatesRecordHint(h, dns));
953
+ if (lines.length === 0) return;
954
+ const maxRetryAfter = dns.reduce(
955
+ (max, r) => Math.max(max, r.retryAfter ?? 0),
956
+ 0
957
+ );
853
958
  deps.stdout("\n");
854
- for (const h of hints) {
855
- deps.stdout(`${hint(h.message)}
959
+ for (const h of lines) {
960
+ deps.stdout(`${hint(renderNextHint(h, hostname, maxRetryAfter))}
856
961
  `);
857
962
  }
858
963
  }
@@ -889,7 +994,7 @@ async function runDomainsConnect(input, deps) {
889
994
  deps.stdout(`${formatDnsTable(domain.dns)}
890
995
  `);
891
996
  }
892
- printNextHints(deps, domain.next);
997
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
893
998
  } else {
894
999
  writeJson(deps, { ok: true, domain });
895
1000
  }
@@ -957,17 +1062,19 @@ async function runDomainsStatus(idOrHostname, _input, deps) {
957
1062
  `);
958
1063
  }
959
1064
  if (domain.verifiedAt) {
960
- deps.stdout(` ${import_picocolors3.default.dim("Verified:")} ${domain.verifiedAt}
961
- `);
1065
+ deps.stdout(
1066
+ ` ${import_picocolors3.default.dim("Verified:")} ${formatDate(domain.verifiedAt)}
1067
+ `
1068
+ );
962
1069
  }
963
- deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${domain.createdAt}
1070
+ deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${formatDate(domain.createdAt)}
964
1071
  `);
965
1072
  if (domain.dns.length > 0) {
966
1073
  deps.stdout("\nDNS records:\n\n");
967
1074
  deps.stdout(`${formatDnsTable(domain.dns)}
968
1075
  `);
969
1076
  }
970
- printNextHints(deps, domain.next);
1077
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
971
1078
  } else {
972
1079
  writeJson(deps, { ok: true, domain });
973
1080
  }
@@ -1000,7 +1107,7 @@ async function runDomainsVerify(idOrHostname, input, deps, sleep = defaultSleep)
1000
1107
  deps.stdout(`${formatDnsTable(domain.dns)}
1001
1108
  `);
1002
1109
  }
1003
- printNextHints(deps, domain.next);
1110
+ printNextHints(deps, domain.next, domain.hostname, domain.dns);
1004
1111
  } else {
1005
1112
  writeJson(deps, { ok: true, domain });
1006
1113
  }
@@ -1081,7 +1188,22 @@ async function runDomainsUpdate(idOrHostname, input, deps) {
1081
1188
  });
1082
1189
  return { exitCode: 0 };
1083
1190
  }
1084
- async function runDomainsRemove(idOrHostname, _input, deps) {
1191
+ async function runDomainsRemove(idOrHostname, input, deps) {
1192
+ if (!input.yes && input.interactive === false) {
1193
+ return writeError(
1194
+ deps,
1195
+ "invalid_usage",
1196
+ "Pass --yes to remove the domain in non-interactive mode."
1197
+ );
1198
+ }
1199
+ if (!input.yes && input.interactive !== false) {
1200
+ const confirmed = await prompts2.confirm({
1201
+ message: `Remove ${idOrHostname}? This removes the domain and unmounts its drops.`
1202
+ });
1203
+ if (prompts2.isCancel(confirmed) || !confirmed) {
1204
+ return { exitCode: 0 };
1205
+ }
1206
+ }
1085
1207
  try {
1086
1208
  await requireCredential({
1087
1209
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1114,7 +1236,30 @@ async function runDomainsRemove(idOrHostname, _input, deps) {
1114
1236
  }
1115
1237
 
1116
1238
  // src/commands/drops.ts
1117
- var prompts2 = __toESM(require("@clack/prompts"), 1);
1239
+ var prompts3 = __toESM(require("@clack/prompts"), 1);
1240
+ var import_picocolors4 = __toESM(require("picocolors"), 1);
1241
+ async function resolveDropTarget(target, deps) {
1242
+ if (target.startsWith("drop_")) return { ok: true, dropId: target };
1243
+ const resolved = await deps.client.drops.resolve(target);
1244
+ if (resolved.error) {
1245
+ return { ok: false, ...writeApiError(deps, resolved.error) };
1246
+ }
1247
+ if (resolved.data === null) {
1248
+ return {
1249
+ ok: false,
1250
+ ...writeApiError(deps, {
1251
+ code: "not_found",
1252
+ message: `No drop matching "${target}" found in your account.`,
1253
+ 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."
1254
+ })
1255
+ };
1256
+ }
1257
+ return {
1258
+ ok: true,
1259
+ dropId: resolved.data.id,
1260
+ ...resolved.data.slug ? { slug: resolved.data.slug } : {}
1261
+ };
1262
+ }
1118
1263
  async function runDropsList(input, deps) {
1119
1264
  try {
1120
1265
  await requireCredential({
@@ -1127,29 +1272,46 @@ async function runDropsList(input, deps) {
1127
1272
  }
1128
1273
  const params = {
1129
1274
  ...input.limit !== void 0 ? { limit: input.limit } : {},
1130
- ...input.cursor ? { cursor: input.cursor } : {}
1275
+ ...input.cursor ? { cursor: input.cursor } : {},
1276
+ ...input.domain ? { domain: input.domain } : {}
1131
1277
  };
1132
1278
  const result = await deps.client.drops.list(params);
1133
1279
  if (result.error) return writeApiError(deps, result.error);
1134
1280
  const raw = unwrapListData(result.data, "drops");
1281
+ const page = extractPagination(result.data);
1135
1282
  const items = Array.isArray(raw) ? raw : void 0;
1136
1283
  if (deps.outputMode === "human") {
1137
1284
  if (!items || items.length === 0) {
1138
1285
  writeEmpty(deps, "No drops found.", { ok: true, drops: [] });
1139
1286
  } else {
1140
1287
  for (const item of items) {
1288
+ const created = typeof item.createdAt === "string" ? import_picocolors4.default.dim(formatDate(item.createdAt)) : "";
1141
1289
  deps.stdout(
1142
- `${item.id ?? ""} ${item.title ?? ""} ${item.url ? url(String(item.url)) : ""}
1290
+ `${item.id ?? ""} ${created} ${item.title ?? ""} ${item.url ? url(String(item.url)) : ""}
1291
+ `
1292
+ );
1293
+ }
1294
+ deps.stdout(
1295
+ `${import_picocolors4.default.dim(`${items.length} ${items.length === 1 ? "drop" : "drops"}`)}
1296
+ `
1297
+ );
1298
+ if (page?.hasMore && page.nextCursor) {
1299
+ deps.stdout(
1300
+ `${hint(`More drops: dropthis list --cursor ${page.nextCursor}`)}
1143
1301
  `
1144
1302
  );
1145
1303
  }
1146
1304
  }
1147
1305
  } else {
1148
- writeJson(deps, { ok: true, drops: items ?? raw });
1306
+ writeJson(deps, {
1307
+ ok: true,
1308
+ drops: items ?? raw,
1309
+ ...page ? { next_cursor: page.nextCursor, has_more: page.hasMore } : {}
1310
+ });
1149
1311
  }
1150
1312
  return { exitCode: 0 };
1151
1313
  }
1152
- async function runDropsGet(dropId, _input, deps) {
1314
+ async function runDropsGet(target, _input, deps) {
1153
1315
  try {
1154
1316
  await requireCredential({
1155
1317
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1159,7 +1321,9 @@ async function runDropsGet(dropId, _input, deps) {
1159
1321
  } catch {
1160
1322
  return writeAuthError(deps);
1161
1323
  }
1162
- const result = await deps.client.drops.get(dropId);
1324
+ const resolved = await resolveDropTarget(target, deps);
1325
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1326
+ const result = await deps.client.drops.get(resolved.dropId);
1163
1327
  if (result.error) return writeApiError(deps, result.error);
1164
1328
  const data = result.data;
1165
1329
  const pairs = [];
@@ -1169,7 +1333,8 @@ async function runDropsGet(dropId, _input, deps) {
1169
1333
  if (data.visibility) pairs.push(["Visibility", String(data.visibility)]);
1170
1334
  if (data.revision !== void 0)
1171
1335
  pairs.push(["Revision", String(data.revision)]);
1172
- if (data.createdAt) pairs.push(["Created", String(data.createdAt)]);
1336
+ if (data.createdAt)
1337
+ pairs.push(["Created", formatDate(String(data.createdAt))]);
1173
1338
  if (data.accessible !== void 0)
1174
1339
  pairs.push(["Accessible", String(data.accessible)]);
1175
1340
  if (data.persistent !== void 0)
@@ -1179,7 +1344,7 @@ async function runDropsGet(dropId, _input, deps) {
1179
1344
  writeKv(deps, pairs, { ok: true, drop: result.data });
1180
1345
  return { exitCode: 0 };
1181
1346
  }
1182
- async function runDropsDelete(dropId, input, deps) {
1347
+ async function runDropsDelete(target, input, deps) {
1183
1348
  if (!input.yes && input.interactive === false) {
1184
1349
  return writeError(
1185
1350
  deps,
@@ -1187,14 +1352,6 @@ async function runDropsDelete(dropId, input, deps) {
1187
1352
  "Pass --yes to delete in non-interactive mode."
1188
1353
  );
1189
1354
  }
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
1355
  try {
1199
1356
  await requireCredential({
1200
1357
  ...deps.apiKey ? { apiKey: deps.apiKey } : {},
@@ -1204,6 +1361,17 @@ async function runDropsDelete(dropId, input, deps) {
1204
1361
  } catch {
1205
1362
  return writeAuthError(deps);
1206
1363
  }
1364
+ const resolved = await resolveDropTarget(target, deps);
1365
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1366
+ const dropId = resolved.dropId;
1367
+ if (!input.yes && input.interactive !== false) {
1368
+ const confirmed = await prompts3.confirm({
1369
+ message: `Delete ${dropId}${dropId === target ? "" : ` (${target})`}?`
1370
+ });
1371
+ if (prompts3.isCancel(confirmed) || !confirmed) {
1372
+ return { exitCode: 0 };
1373
+ }
1374
+ }
1207
1375
  const result = await deps.client.drops.delete(dropId);
1208
1376
  if (result?.error) return writeApiError(deps, result.error);
1209
1377
  writeHumanOrJson(deps, success(`Deleted ${dropId}`), {
@@ -1215,31 +1383,31 @@ async function runDropsDelete(dropId, input, deps) {
1215
1383
  }
1216
1384
 
1217
1385
  // src/commands/login.ts
1218
- var prompts3 = __toESM(require("@clack/prompts"), 1);
1386
+ var prompts4 = __toESM(require("@clack/prompts"), 1);
1219
1387
  async function runLoginInteractive(deps) {
1220
- prompts3.intro("dropthis login");
1221
- const email = await prompts3.text({
1388
+ prompts4.intro("dropthis login");
1389
+ const email = await prompts4.text({
1222
1390
  message: "Email address",
1223
1391
  validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
1224
1392
  });
1225
- if (prompts3.isCancel(email)) {
1226
- prompts3.cancel("Login cancelled.");
1393
+ if (prompts4.isCancel(email)) {
1394
+ prompts4.cancel("Login cancelled.");
1227
1395
  return { exitCode: 0 };
1228
1396
  }
1229
1397
  const requestResult = await deps.client.auth.requestEmailOtp({ email });
1230
1398
  if (requestResult.error) {
1231
- prompts3.cancel(requestResult.error.message);
1399
+ prompts4.cancel(requestResult.error.message);
1232
1400
  return { exitCode: exitCodeFor("api_error") };
1233
1401
  }
1234
1402
  const maxAttempts = 3;
1235
1403
  let session;
1236
1404
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1237
- const otp = await prompts3.text({
1405
+ const otp = await prompts4.text({
1238
1406
  message: attempt === 1 ? "Paste the code from your email" : "Try again \u2014 paste the code from your email",
1239
1407
  validate: (v) => v && v.length >= 4 && v.length <= 8 ? void 0 : "Code must be 4\u20138 characters."
1240
1408
  });
1241
- if (prompts3.isCancel(otp)) {
1242
- prompts3.cancel("Login cancelled.");
1409
+ if (prompts4.isCancel(otp)) {
1410
+ prompts4.cancel("Login cancelled.");
1243
1411
  return { exitCode: 0 };
1244
1412
  }
1245
1413
  const result = await deps.client.auth.verifyEmailOtp({
@@ -1251,20 +1419,20 @@ async function runLoginInteractive(deps) {
1251
1419
  break;
1252
1420
  }
1253
1421
  if (attempt < maxAttempts) {
1254
- prompts3.log.warning(result.error.message);
1422
+ prompts4.log.warning(result.error.message);
1255
1423
  } else {
1256
- prompts3.cancel(result.error.message);
1424
+ prompts4.cancel(result.error.message);
1257
1425
  return { exitCode: exitCodeFor("api_error") };
1258
1426
  }
1259
1427
  }
1260
1428
  if (!session) {
1261
- prompts3.cancel("Login failed.");
1429
+ prompts4.cancel("Login failed.");
1262
1430
  return { exitCode: exitCodeFor("api_error") };
1263
1431
  }
1264
1432
  const authedClient = deps.createClient(session.data.token);
1265
1433
  const apiKey = await authedClient.apiKeys.create({ label: "CLI" });
1266
1434
  if (apiKey.error) {
1267
- prompts3.cancel(apiKey.error.message);
1435
+ prompts4.cancel(apiKey.error.message);
1268
1436
  return { exitCode: exitCodeFor("api_error") };
1269
1437
  }
1270
1438
  await deps.store.save({
@@ -1275,7 +1443,7 @@ async function runLoginInteractive(deps) {
1275
1443
  email,
1276
1444
  storage: "secure"
1277
1445
  });
1278
- prompts3.outro("Logged in successfully.");
1446
+ prompts4.outro("Logged in successfully.");
1279
1447
  return { exitCode: 0 };
1280
1448
  }
1281
1449
  async function runLoginRequest(input, deps) {
@@ -1464,12 +1632,12 @@ function parseJsonObject(value, label) {
1464
1632
  }
1465
1633
 
1466
1634
  // src/spinner.ts
1467
- var import_picocolors4 = __toESM(require("picocolors"), 1);
1635
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
1468
1636
  var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1469
1637
  function createSpinner(message, stderr) {
1470
1638
  let i = 0;
1471
1639
  const timer = setInterval(() => {
1472
- stderr(`\r\x1B[2K${import_picocolors4.default.cyan(frames[i % frames.length])} ${message}`);
1640
+ stderr(`\r\x1B[2K${import_picocolors5.default.cyan(frames[i % frames.length])} ${message}`);
1473
1641
  i++;
1474
1642
  }, 80);
1475
1643
  return {
@@ -1620,24 +1788,10 @@ async function runPull(target, input, deps) {
1620
1788
  } catch {
1621
1789
  return writeAuthError(deps);
1622
1790
  }
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
- }
1791
+ const resolved = await resolveDropTarget(target, deps);
1792
+ if (!resolved.ok) return { exitCode: resolved.exitCode };
1793
+ const dropId = resolved.dropId;
1794
+ const defaultDirName = resolved.slug || resolved.dropId;
1641
1795
  const spin = shouldSpin(deps.outputMode) ? createSpinner("Pulling content\u2026", deps.stderr) : void 0;
1642
1796
  const manifestResult = await deps.client.drops.getContent(dropId);
1643
1797
  if (manifestResult.error) {
@@ -1820,7 +1974,7 @@ function assertDropId(target) {
1820
1974
  }
1821
1975
 
1822
1976
  // src/commands/update-settings.ts
1823
- var prompts4 = __toESM(require("@clack/prompts"), 1);
1977
+ var prompts5 = __toESM(require("@clack/prompts"), 1);
1824
1978
  var NO_SETTINGS_MESSAGE = "Nothing to update. Provide at least one settings option.";
1825
1979
  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
1980
  async function runUpdateSettings(dropId, raw, deps) {
@@ -1844,7 +1998,7 @@ async function runUpdateSettings(dropId, raw, deps) {
1844
1998
  if (shouldPrompt(deps)) {
1845
1999
  const prompted = await promptForSettings(
1846
2000
  dropId,
1847
- deps.prompts ?? prompts4
2001
+ deps.prompts ?? prompts5
1848
2002
  );
1849
2003
  if (!prompted) return { exitCode: 0 };
1850
2004
  parsed = { ...parsed, ...prompted };
@@ -2054,6 +2208,109 @@ function assertDropId2(target) {
2054
2208
  }
2055
2209
  }
2056
2210
 
2211
+ // src/commands/upgrade.ts
2212
+ var import_node_child_process = require("child_process");
2213
+ var import_node_fs = require("fs");
2214
+ var import_node_util = require("util");
2215
+
2216
+ // src/semver.ts
2217
+ function compareSemver(a, b) {
2218
+ const parse = (v) => (v.split("-")[0] ?? v).split(".").map(Number);
2219
+ const pa = parse(a);
2220
+ const pb = parse(b);
2221
+ for (let i = 0; i < 3; i++) {
2222
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
2223
+ if (d !== 0) return d < 0 ? -1 : 1;
2224
+ }
2225
+ return 0;
2226
+ }
2227
+
2228
+ // src/commands/upgrade.ts
2229
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
2230
+ var REGISTRY_LATEST = "https://registry.npmjs.org/@dropthis%2fcli/latest";
2231
+ async function fetchLatestFromRegistry() {
2232
+ try {
2233
+ const res = await fetch(REGISTRY_LATEST, {
2234
+ signal: AbortSignal.timeout(1e4)
2235
+ });
2236
+ if (!res.ok) return null;
2237
+ const body = await res.json();
2238
+ return typeof body.version === "string" ? body.version : null;
2239
+ } catch {
2240
+ return null;
2241
+ }
2242
+ }
2243
+ async function detectNpmGlobalInstall() {
2244
+ try {
2245
+ const argv1 = process.argv[1];
2246
+ if (!argv1) return false;
2247
+ const entry = (0, import_node_fs.realpathSync)(argv1);
2248
+ const { stdout } = await execFileAsync("npm", ["root", "-g"]);
2249
+ const root = (0, import_node_fs.realpathSync)(stdout.trim());
2250
+ return entry.startsWith(root);
2251
+ } catch {
2252
+ return false;
2253
+ }
2254
+ }
2255
+ async function npmInstallLatest() {
2256
+ try {
2257
+ await execFileAsync("npm", ["install", "-g", "@dropthis/cli@latest"]);
2258
+ return { ok: true };
2259
+ } catch (err) {
2260
+ return {
2261
+ ok: false,
2262
+ message: err instanceof Error ? err.message : String(err)
2263
+ };
2264
+ }
2265
+ }
2266
+ async function runUpgrade(_input, deps) {
2267
+ const fetchLatest = deps.fetchLatest ?? fetchLatestFromRegistry;
2268
+ const isNpmGlobal = deps.isNpmGlobalInstall ?? detectNpmGlobalInstall;
2269
+ const install = deps.runNpmInstall ?? npmInstallLatest;
2270
+ const latest = await fetchLatest();
2271
+ if (!latest) {
2272
+ return writeError(
2273
+ deps,
2274
+ "upgrade_check_failed",
2275
+ "Could not reach the npm registry to check for updates. Try again, or run: npm install -g @dropthis/cli@latest"
2276
+ );
2277
+ }
2278
+ if (compareSemver(latest, deps.currentVersion) <= 0) {
2279
+ writeHumanOrJson(
2280
+ deps,
2281
+ `Already on latest version (${deps.currentVersion})`,
2282
+ { ok: true, status: "already-latest", version: deps.currentVersion }
2283
+ );
2284
+ return { exitCode: 0 };
2285
+ }
2286
+ if (!await isNpmGlobal()) {
2287
+ return writeError(
2288
+ deps,
2289
+ "unsupported_install",
2290
+ `Update available (${deps.currentVersion} \u2192 ${latest}), but this install was not made with \`npm install -g\`. Upgrade with the installer you used, e.g.: npm install -g @dropthis/cli@latest`
2291
+ );
2292
+ }
2293
+ if (deps.outputMode === "human") {
2294
+ deps.stdout(`Updating ${deps.currentVersion} \u2192 ${latest}...
2295
+ `);
2296
+ }
2297
+ const result = await install();
2298
+ if (!result.ok) {
2299
+ return writeError(
2300
+ deps,
2301
+ "upgrade_failed",
2302
+ `npm install failed${result.message ? `: ${result.message}` : ""}. Try: npm install -g @dropthis/cli@latest`
2303
+ );
2304
+ }
2305
+ writeHumanOrJson(deps, `Updated ${deps.currentVersion} \u2192 ${latest}`, {
2306
+ ok: true,
2307
+ status: "upgraded",
2308
+ from: deps.currentVersion,
2309
+ to: latest
2310
+ });
2311
+ return { exitCode: 0 };
2312
+ }
2313
+
2057
2314
  // src/commands/whoami.ts
2058
2315
  async function runWhoami(_input, deps) {
2059
2316
  const credential = await resolveCredential({
@@ -2233,6 +2490,55 @@ async function writeCredentialFile(path, credential) {
2233
2490
  await (0, import_promises3.rename)(tmpPath, path);
2234
2491
  }
2235
2492
 
2493
+ // src/typo-guard.ts
2494
+ var import_promises4 = require("fs/promises");
2495
+ function levenshtein(a, b) {
2496
+ if (a === b) return 0;
2497
+ if (a.length === 0) return b.length;
2498
+ if (b.length === 0) return a.length;
2499
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
2500
+ for (let i = 1; i <= a.length; i++) {
2501
+ const curr = [i];
2502
+ for (let j = 1; j <= b.length; j++) {
2503
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2504
+ const deletion = (prev[j] ?? 0) + 1;
2505
+ const insertion = (curr[j - 1] ?? 0) + 1;
2506
+ const substitution = (prev[j - 1] ?? 0) + cost;
2507
+ curr[j] = Math.min(deletion, insertion, substitution);
2508
+ }
2509
+ prev = curr;
2510
+ }
2511
+ return prev[b.length] ?? 0;
2512
+ }
2513
+ function suggestCommand(token, names) {
2514
+ const lower = token.toLowerCase();
2515
+ let best = null;
2516
+ for (const name of names) {
2517
+ if (lower.length >= 2 && name.startsWith(lower)) return name;
2518
+ const dist = levenshtein(lower, name);
2519
+ if (best === null || dist < best.dist) best = { name, dist };
2520
+ }
2521
+ if (best !== null && best.dist <= 2 && best.dist < lower.length) {
2522
+ return best.name;
2523
+ }
2524
+ return null;
2525
+ }
2526
+ function isBareWordToken(token) {
2527
+ if (token === "" || token === "-") return false;
2528
+ if (/\s/.test(token)) return false;
2529
+ if (token.includes("/") || token.includes("\\")) return false;
2530
+ if (token.includes(".")) return false;
2531
+ return true;
2532
+ }
2533
+ async function pathExists(path) {
2534
+ try {
2535
+ await (0, import_promises4.stat)(path);
2536
+ return true;
2537
+ } catch {
2538
+ return false;
2539
+ }
2540
+ }
2541
+
2236
2542
  // src/program.ts
2237
2543
  function buildProgram(options = {}) {
2238
2544
  const program = new import_commander.Command();
@@ -2243,6 +2549,7 @@ function buildProgram(options = {}) {
2243
2549
  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
2550
  };
2245
2551
  program.exitOverride();
2552
+ program.showSuggestionAfterError();
2246
2553
  program.configureOutput({
2247
2554
  writeErr,
2248
2555
  // In JSON mode, suppress commander's default plain-text error so the
@@ -2253,13 +2560,13 @@ function buildProgram(options = {}) {
2253
2560
  });
2254
2561
  program.name("dropthis").description(
2255
2562
  [
2256
- import_picocolors5.default.dim("Publish anything online and get a URL back."),
2563
+ import_picocolors6.default.dim("Publish anything online and get a URL back."),
2257
2564
  "",
2258
- `${import_picocolors5.default.bold("Quick start:")}`,
2259
- ` ${import_picocolors5.default.cyan("dropthis login")}`,
2260
- ` ${import_picocolors5.default.cyan("dropthis publish ./dist")}`
2565
+ `${import_picocolors6.default.bold("Quick start:")}`,
2566
+ ` ${import_picocolors6.default.cyan("dropthis login")}`,
2567
+ ` ${import_picocolors6.default.cyan("dropthis publish ./dist")}`
2261
2568
  ].join("\n")
2262
- ).version("0.11.0").configureHelp({
2569
+ ).version("0.13.0").configureHelp({
2263
2570
  subcommandTerm(cmd) {
2264
2571
  const args = cmd.registeredArguments.map((a) => {
2265
2572
  const name = a.name();
@@ -2268,16 +2575,28 @@ function buildProgram(options = {}) {
2268
2575
  }).join(" ");
2269
2576
  return args ? `${cmd.name()} ${args}` : cmd.name();
2270
2577
  },
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)
2578
+ styleTitle: (title) => import_picocolors6.default.bold(title),
2579
+ styleCommandText: (str) => import_picocolors6.default.cyan(str),
2580
+ styleSubcommandText: (str) => import_picocolors6.default.cyan(str),
2581
+ styleOptionText: (str) => import_picocolors6.default.yellow(str)
2275
2582
  });
2276
2583
  program.option("--api-key <key>", "Override API key for this invocation");
2277
2584
  program.option("--api-url <url>", "Override API base URL");
2278
2585
  program.option("--json", "Force JSON output");
2279
2586
  program.option("-q, --quiet", "Suppress status output and imply JSON");
2280
2587
  program.option("--no-interactive", "Disable interactive prompts");
2588
+ program.addHelpText(
2589
+ "after",
2590
+ [
2591
+ "",
2592
+ "Output is JSON when piped/CI/--json; human tables in a terminal.",
2593
+ "Docs: https://dropthis.app"
2594
+ ].join("\n")
2595
+ );
2596
+ const examplesBlock = (lines) => `
2597
+ ${import_picocolors6.default.bold("Examples:")}
2598
+ ${lines.map((l) => ` ${l}`).join("\n")}
2599
+ `;
2281
2600
  program.command("publish [input...]", { isDefault: true }).description(
2282
2601
  "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
2602
  ).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 +2620,15 @@ function buildProgram(options = {}) {
2301
2620
  ).option("--json", "Force JSON output").option(
2302
2621
  "--idempotency-key <key>",
2303
2622
  "Prevent duplicate publishes on retry (auto-generated)"
2623
+ ).addHelpText(
2624
+ "after",
2625
+ examplesBlock([
2626
+ "dropthis publish ./report.html",
2627
+ "dropthis publish ./dist --title 'Launch page'",
2628
+ "cat report.md | dropthis publish - --content-type text/markdown --path report.md",
2629
+ "dropthis publish ./dist --domain reports.example.com --slug launch",
2630
+ "URL=$(dropthis publish ./dist --url)"
2631
+ ])
2304
2632
  ).action(async (inputs, commandOptions) => {
2305
2633
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2306
2634
  if (inputs.length === 0 && stdinIsTTY) {
@@ -2326,6 +2654,29 @@ function buildProgram(options = {}) {
2326
2654
  process.exitCode = writeError(deps, "local_input_error", msg).exitCode;
2327
2655
  return;
2328
2656
  }
2657
+ if (deps.outputMode === "human" && deps.interactive && inputs.length === 1 && typeof publishInput === "string" && isBareWordToken(publishInput) && !await pathExists(publishInput)) {
2658
+ const suggestion = suggestCommand(
2659
+ publishInput,
2660
+ program.commands.map((c) => c.name())
2661
+ );
2662
+ if (suggestion) {
2663
+ process.exitCode = writeError(
2664
+ deps,
2665
+ "invalid_usage",
2666
+ `Unknown command '${publishInput}' \u2014 did you mean '${suggestion}'?`,
2667
+ `To publish the literal text, pipe it: echo '${publishInput}' | dropthis publish`
2668
+ ).exitCode;
2669
+ return;
2670
+ }
2671
+ const prompts6 = await import("@clack/prompts");
2672
+ const confirmed = await prompts6.confirm({
2673
+ message: `Publish '${publishInput}' as inline text?`,
2674
+ initialValue: false
2675
+ });
2676
+ if (prompts6.isCancel(confirmed) || !confirmed) {
2677
+ return;
2678
+ }
2679
+ }
2329
2680
  let dropOpts;
2330
2681
  try {
2331
2682
  dropOpts = toDropOptions(commandOptions);
@@ -2429,7 +2780,14 @@ function buildProgram(options = {}) {
2429
2780
  );
2430
2781
  program.command("pull <target>").description(
2431
2782
  "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(
2783
+ ).option("-o, --output <dir>", "Output directory (default: ./<slug-or-id>)").option("--json", "Force JSON output").addHelpText(
2784
+ "after",
2785
+ examplesBlock([
2786
+ "dropthis pull drop_abc123 -o ./site # download the drop's files",
2787
+ "$EDITOR ./site/index.html # edit locally",
2788
+ "dropthis update-content drop_abc123 ./site # ship back to the same URL"
2789
+ ])
2790
+ ).action(
2433
2791
  async (target, commandOptions) => {
2434
2792
  const deps = await commandDeps(
2435
2793
  program,
@@ -2441,19 +2799,30 @@ function buildProgram(options = {}) {
2441
2799
  process.exitCode = result.exitCode;
2442
2800
  }
2443
2801
  );
2444
- program.command("get <dropId>").description("Show drop details (pass the full drop_\u2026 id)").option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
2802
+ program.command("get <dropId>").description(
2803
+ "Show drop details (accepts the drop_\u2026 id, the drop URL, or its slug)"
2804
+ ).option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
2445
2805
  const deps = await commandDeps(program, options, store, commandOptions);
2446
2806
  const result = await runDropsGet(dropId, commandOptions, deps);
2447
2807
  process.exitCode = result.exitCode;
2448
2808
  });
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(
2809
+ program.command("list").description("List your drops").option("--limit <n>", "Page size", parseInteger).option("--cursor <cursor>", "Pagination cursor").option("--domain <hostname>", "Only drops mounted on this custom domain").option("--json", "Force JSON output").addHelpText(
2810
+ "after",
2811
+ examplesBlock([
2812
+ "dropthis list",
2813
+ "dropthis list --json | jq -r '.drops[] | [.id, .url] | @tsv' # recover drop ids",
2814
+ "dropthis list --domain reports.example.com --json # drops on a custom domain"
2815
+ ])
2816
+ ).action(
2450
2817
  async (commandOptions) => {
2451
2818
  const deps = await commandDeps(program, options, store, commandOptions);
2452
2819
  const result = await runDropsList(commandOptions, deps);
2453
2820
  process.exitCode = result.exitCode;
2454
2821
  }
2455
2822
  );
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(
2823
+ program.command("delete <dropId>").description(
2824
+ "Delete a drop (accepts the drop_\u2026 id, the drop URL, or its slug)"
2825
+ ).option("--yes", "Confirm deletion").option("--json", "Force JSON output").action(
2457
2826
  async (dropId, commandOptions) => {
2458
2827
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2459
2828
  const global = globalOptions(program, commandOptions);
@@ -2497,7 +2866,15 @@ function buildProgram(options = {}) {
2497
2866
  ).requiredOption(
2498
2867
  "--mode <mode>",
2499
2868
  "Mount mode: path (many drops at hostname/{slug}/) or dedicated (one drop at root)"
2500
- ).option("--json", "Force JSON output").action(
2869
+ ).option("--json", "Force JSON output").addHelpText(
2870
+ "after",
2871
+ examplesBlock([
2872
+ "dropthis domains connect reports.example.com --mode path",
2873
+ "# add the CNAME shown above at your DNS provider, then:",
2874
+ "dropthis domains verify reports.example.com --wait",
2875
+ "dropthis publish ./report.html --domain reports.example.com"
2876
+ ])
2877
+ ).action(
2501
2878
  async (hostname, commandOptions) => {
2502
2879
  if (commandOptions.mode !== "path" && commandOptions.mode !== "dedicated") {
2503
2880
  process.stderr.write(`error: --mode must be "path" or "dedicated"
@@ -2529,7 +2906,13 @@ function buildProgram(options = {}) {
2529
2906
  "--timeout <secs>",
2530
2907
  "Max seconds to wait (default: 300)",
2531
2908
  parseInteger
2532
- ).option("--json", "Force JSON output").action(
2909
+ ).option("--json", "Force JSON output").addHelpText(
2910
+ "after",
2911
+ examplesBlock([
2912
+ "dropthis domains verify reports.example.com --wait",
2913
+ "dropthis domains verify reports.example.com --wait --timeout 600"
2914
+ ])
2915
+ ).action(
2533
2916
  async (hostname, commandOptions) => {
2534
2917
  const deps = await commandDeps(program, options, store, commandOptions);
2535
2918
  const result = await runDomainsVerify(hostname, commandOptions, deps);
@@ -2553,12 +2936,23 @@ function buildProgram(options = {}) {
2553
2936
  }
2554
2937
  );
2555
2938
  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
- });
2939
+ "Remove a custom domain (unmounts its drops). IMPORTANT: also delete the DNS CNAME at your provider to prevent DNS hijacking."
2940
+ ).option("--yes", "Confirm removal").option("--json", "Force JSON output").action(
2941
+ async (hostname, commandOptions) => {
2942
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2943
+ const global = globalOptions(program, commandOptions);
2944
+ const deps = await commandDeps(program, options, store, commandOptions);
2945
+ const result = await runDomainsRemove(
2946
+ hostname,
2947
+ {
2948
+ ...commandOptions,
2949
+ interactive: isInteractive(stdinIsTTY, deps.env, global)
2950
+ },
2951
+ deps
2952
+ );
2953
+ process.exitCode = result.exitCode;
2954
+ }
2955
+ );
2562
2956
  const apiKeys = program.command("api-keys").description("Manage API keys");
2563
2957
  apiKeys.command("create").description("Create an API key").option("--label <label>", "API key label", "CLI").option("--json", "Force JSON output").action(async (commandOptions) => {
2564
2958
  const deps = await commandDeps(program, options, store, commandOptions);
@@ -2591,6 +2985,16 @@ function buildProgram(options = {}) {
2591
2985
  if (!commandOptions.email || !commandOptions.otp) {
2592
2986
  const deps2 = await commandDeps(program, options, store, commandOptions);
2593
2987
  const global2 = globalOptions(program, commandOptions);
2988
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2989
+ if (!isInteractive(stdinIsTTY, deps2.env, global2)) {
2990
+ process.exitCode = writeError(
2991
+ deps2,
2992
+ "invalid_usage",
2993
+ "dropthis login is interactive and needs a terminal.",
2994
+ "Set DROPTHIS_API_KEY, or run: dropthis login request --email <email>, then dropthis login verify --email <email> --otp <code>."
2995
+ ).exitCode;
2996
+ return;
2997
+ }
2594
2998
  const context = createContext({ global: global2 });
2595
2999
  const result2 = await runLoginInteractive({
2596
3000
  ...deps2,
@@ -2694,6 +3098,19 @@ function buildProgram(options = {}) {
2694
3098
  const result = await runDoctor(commandOptions, deps);
2695
3099
  process.exitCode = result.exitCode;
2696
3100
  });
3101
+ program.command("upgrade").description("Update the CLI to the latest version from npm").option("--json", "Force JSON output").action(async (commandOptions) => {
3102
+ const deps = await commandDeps(
3103
+ program,
3104
+ options,
3105
+ store,
3106
+ commandOptions
3107
+ );
3108
+ const result = await runUpgrade(commandOptions, {
3109
+ ...deps,
3110
+ currentVersion: "0.13.0"
3111
+ });
3112
+ process.exitCode = result.exitCode;
3113
+ });
2697
3114
  program.command("commands").description("Print machine-readable command metadata").option("--json", "Force JSON output").action(async (commandOptions) => {
2698
3115
  const deps = await commandDeps(
2699
3116
  program,
@@ -2842,6 +3259,72 @@ async function readStdinBytes(stdin) {
2842
3259
  return Buffer.concat(chunks);
2843
3260
  }
2844
3261
 
3262
+ // src/update-check.ts
3263
+ var import_node_child_process2 = require("child_process");
3264
+ var import_node_fs2 = require("fs");
3265
+ var import_node_os3 = require("os");
3266
+ var import_node_path3 = require("path");
3267
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3268
+ var REGISTRY_LATEST2 = "https://registry.npmjs.org/@dropthis%2fcli/latest";
3269
+ function updateCachePath(configDir) {
3270
+ return (0, import_node_path3.join)(
3271
+ configDir ?? (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".config", "dropthis"),
3272
+ "update-check.json"
3273
+ );
3274
+ }
3275
+ function readCache(path) {
3276
+ try {
3277
+ const value = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
3278
+ if (typeof value.lastCheckedAt !== "string" || typeof value.latestVersion !== "string")
3279
+ return null;
3280
+ return {
3281
+ lastCheckedAt: value.lastCheckedAt,
3282
+ latestVersion: value.latestVersion
3283
+ };
3284
+ } catch {
3285
+ return null;
3286
+ }
3287
+ }
3288
+ function shouldShowNotice(input) {
3289
+ if (input.env.CI) return false;
3290
+ if (input.env.DROPTHIS_NO_UPDATE_NOTIFIER) return false;
3291
+ if (!input.stderrIsTTY) return false;
3292
+ if (input.argv.includes("--json") || input.argv.includes("--quiet") || input.argv.includes("-q"))
3293
+ return false;
3294
+ return true;
3295
+ }
3296
+ function noticeFromCache(input) {
3297
+ if (!input.cache) return null;
3298
+ if (compareSemver(input.cache.latestVersion, input.currentVersion) <= 0)
3299
+ return null;
3300
+ return `Update available ${input.currentVersion} \u2192 ${input.cache.latestVersion} \xB7 run \`dropthis upgrade\``;
3301
+ }
3302
+ function shouldRefresh(cache, now) {
3303
+ if (!cache) return true;
3304
+ const last = Date.parse(cache.lastCheckedAt);
3305
+ if (Number.isNaN(last)) return true;
3306
+ return now.getTime() - last >= CHECK_INTERVAL_MS;
3307
+ }
3308
+ function scheduleBackgroundRefresh(cachePath, spawnFn = import_node_child_process2.spawn) {
3309
+ const script = `
3310
+ const [, cachePath] = process.argv;
3311
+ fetch(${JSON.stringify(REGISTRY_LATEST2)}, { signal: AbortSignal.timeout(5000) })
3312
+ .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
3313
+ .then((d) => {
3314
+ const fs = require("node:fs");
3315
+ const path = require("node:path");
3316
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3317
+ fs.writeFileSync(cachePath, JSON.stringify({ lastCheckedAt: new Date().toISOString(), latestVersion: String(d.version) }));
3318
+ })
3319
+ .catch(() => {});
3320
+ `;
3321
+ const child = spawnFn(process.execPath, ["-e", script, cachePath], {
3322
+ detached: true,
3323
+ stdio: "ignore"
3324
+ });
3325
+ child.unref();
3326
+ }
3327
+
2845
3328
  // src/cli.ts
2846
3329
  function writeUsageErrorEnvelope(input) {
2847
3330
  if (input.json) {
@@ -2849,9 +3332,6 @@ function writeUsageErrorEnvelope(input) {
2849
3332
  `${JSON.stringify(errorEnvelope("usage_error", input.message))}
2850
3333
  `
2851
3334
  );
2852
- } else {
2853
- input.stderr(`${input.message}
2854
- `);
2855
3335
  }
2856
3336
  return 2;
2857
3337
  }
@@ -2861,7 +3341,29 @@ function isCommanderError(err) {
2861
3341
  function wantsJson(argv) {
2862
3342
  return argv.includes("--json") || argv.includes("--quiet") || argv.includes("-q") || process.env.CI === "true" || process.stdout.isTTY !== true;
2863
3343
  }
2864
- buildProgram({ store: createCredentialStore() }).parseAsync(process.argv).catch((error2) => {
3344
+ var updateCache = readCache(updateCachePath());
3345
+ var showNotice = shouldShowNotice({
3346
+ env: process.env,
3347
+ stderrIsTTY: process.stderr.isTTY === true,
3348
+ argv: process.argv.slice(2)
3349
+ });
3350
+ if (showNotice) {
3351
+ const notice = noticeFromCache({
3352
+ cache: updateCache,
3353
+ currentVersion: "0.13.0"
3354
+ });
3355
+ if (notice) {
3356
+ process.stderr.write(`
3357
+ ${hint(notice)}
3358
+
3359
+ `);
3360
+ }
3361
+ }
3362
+ buildProgram({ store: createCredentialStore() }).parseAsync(process.argv).finally(() => {
3363
+ if (showNotice && shouldRefresh(updateCache, /* @__PURE__ */ new Date())) {
3364
+ scheduleBackgroundRefresh(updateCachePath());
3365
+ }
3366
+ }).catch((error2) => {
2865
3367
  if (isCommanderError(error2)) {
2866
3368
  if (error2.code === "commander.help" || error2.code === "commander.helpDisplayed" || error2.code === "commander.version") {
2867
3369
  process.exitCode = 0;