@dropthis/cli 0.9.3 → 0.11.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
@@ -135,10 +135,10 @@ function readlineQuestion(question) {
135
135
  input: process.stdin,
136
136
  output: process.stderr
137
137
  });
138
- return new Promise((resolve) => {
138
+ return new Promise((resolve2) => {
139
139
  rl.question(question, (answer) => {
140
140
  rl.close();
141
- resolve(answer);
141
+ resolve2(answer);
142
142
  });
143
143
  });
144
144
  }
@@ -207,7 +207,7 @@ function nextActionForApiError(error2) {
207
207
  const uploadNextAction = error2.code ? UPLOAD_NEXT_ACTIONS[error2.code] : void 0;
208
208
  if (uploadNextAction) return uploadNextAction;
209
209
  if (error2.code === "revision_conflict") {
210
- return "Fetch the drop, merge your changes, and retry with the current revision.";
210
+ return "Re-read the drop with dropthis get <drop-id>, merge your changes, and retry with --if-revision set to the current revision.";
211
211
  }
212
212
  if (error2.statusCode === 404 || error2.code === "not_found") {
213
213
  return "Check the drop id or slug and retry; list your drops with dropthis list.";
@@ -232,12 +232,14 @@ function exitCodeFor(code) {
232
232
  if (code === "auth_error") return 3;
233
233
  if (code === "local_input_error") return 4;
234
234
  if (code === "network_error") return 5;
235
+ if (code === "verify_pending") return 6;
236
+ if (code === "verify_timeout") return 7;
235
237
  return 1;
236
238
  }
237
239
 
238
240
  // src/program.ts
239
241
  var import_commander = require("commander");
240
- var import_picocolors4 = __toESM(require("picocolors"), 1);
242
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
241
243
 
242
244
  // src/auth.ts
243
245
  async function resolveCredential(input) {
@@ -285,6 +287,9 @@ function success(msg) {
285
287
  function error(msg) {
286
288
  return `${import_picocolors.default.red(symbols.error)} ${msg}`;
287
289
  }
290
+ function warn(msg) {
291
+ return `${import_picocolors.default.yellow(symbols.warning)} ${msg}`;
292
+ }
288
293
  function hint(msg) {
289
294
  return ` ${import_picocolors.default.dim(msg)}`;
290
295
  }
@@ -305,6 +310,17 @@ function writeResult(deps, link, verb, data) {
305
310
  const details = formatDropDetails(data);
306
311
  if (details) deps.stdout(`${details}
307
312
  `);
313
+ const warnings = data.warnings;
314
+ if (Array.isArray(warnings)) {
315
+ for (const w of warnings) {
316
+ if (w.code === "slug_suffixed") {
317
+ deps.stderr(
318
+ `${hint(`Vanity slug was taken \u2014 served at: ${w.detail ?? "see URL"}`)}
319
+ `
320
+ );
321
+ }
322
+ }
323
+ }
308
324
  } else {
309
325
  deps.stdout(`${JSON.stringify({ ok: true, drop: data })}
310
326
  `);
@@ -371,6 +387,11 @@ function writeApiError(deps, details) {
371
387
  const nextAction = nextActionForApiError(details);
372
388
  if (nextAction) deps.stderr(`${hint(nextAction)}
373
389
  `);
390
+ if (details.currentRevision !== void 0)
391
+ deps.stderr(
392
+ `${hint(`Current revision: ${details.currentRevision} \u2014 retry with --if-revision ${details.currentRevision}`)}
393
+ `
394
+ );
374
395
  if (details.requestId)
375
396
  deps.stderr(`${import_picocolors2.default.dim(`Request ID: ${details.requestId}`)}
376
397
  `);
@@ -605,6 +626,13 @@ var COMMAND_ANNOTATIONS = {
605
626
  auth: "required",
606
627
  examples: ["dropthis update-settings drop_abc --title 'Launch' --json"]
607
628
  },
629
+ pull: {
630
+ auth: "required",
631
+ examples: [
632
+ "dropthis pull drop_abc -o ./site",
633
+ "dropthis pull https://abc123.dropthis.app"
634
+ ]
635
+ },
608
636
  get: {
609
637
  auth: "required",
610
638
  examples: ["dropthis get drop_abc --json"]
@@ -769,19 +797,322 @@ async function runDoctor(_input, deps) {
769
797
  const authSource = credential?.source ?? "missing";
770
798
  const storageBackend = stored?.storage ?? "none";
771
799
  const pairs = [
772
- ["Version", "0.9.3"],
800
+ ["Version", "0.11.0"],
773
801
  ["Auth", authSource],
774
802
  ["Storage", storageBackend]
775
803
  ];
776
804
  writeKv(deps, pairs, {
777
805
  ok: true,
778
- version: "0.9.3",
806
+ version: "0.11.0",
779
807
  auth: { source: authSource },
780
808
  storage: { backend: storageBackend }
781
809
  });
782
810
  return { exitCode: 0 };
783
811
  }
784
812
 
813
+ // src/commands/domains.ts
814
+ var import_picocolors3 = __toESM(require("picocolors"), 1);
815
+ function formatDnsTable(records) {
816
+ if (records.length === 0) return "";
817
+ const rows = [];
818
+ const colWidths = {
819
+ type: Math.max(4, ...records.map((r) => r.type.length)),
820
+ name: Math.max(4, ...records.map((r) => r.name.length)),
821
+ value: Math.max(5, ...records.map((r) => r.value.length)),
822
+ status: Math.max(6, ...records.map((r) => r.status.length))
823
+ };
824
+ const header = [
825
+ "Type".padEnd(colWidths.type),
826
+ "Name".padEnd(colWidths.name),
827
+ "Value".padEnd(colWidths.value),
828
+ "Status".padEnd(colWidths.status)
829
+ ].join(" ");
830
+ rows.push(import_picocolors3.default.dim(header));
831
+ rows.push(import_picocolors3.default.dim("\u2500".repeat(header.length)));
832
+ for (const rec of records) {
833
+ 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));
834
+ rows.push(
835
+ [
836
+ rec.type.padEnd(colWidths.type),
837
+ rec.name.padEnd(colWidths.name),
838
+ rec.value.padEnd(colWidths.value),
839
+ statusColor
840
+ ].join(" ")
841
+ );
842
+ if (rec.hint) {
843
+ rows.push(` ${import_picocolors3.default.dim(rec.hint)}`);
844
+ }
845
+ if (rec.observed) {
846
+ rows.push(` ${import_picocolors3.default.dim(`Observed: ${rec.observed}`)}`);
847
+ }
848
+ }
849
+ return rows.join("\n");
850
+ }
851
+ function printNextHints(deps, hints) {
852
+ if (hints.length === 0) return;
853
+ deps.stdout("\n");
854
+ for (const h of hints) {
855
+ deps.stdout(`${hint(h.message)}
856
+ `);
857
+ }
858
+ }
859
+ function computeSleepMs(records, elapsedMs, timeoutMs) {
860
+ const maxRetryAfter = records.reduce(
861
+ (max, r) => Math.max(max, r.retryAfter ?? 0),
862
+ 0
863
+ );
864
+ const defaultSeconds = maxRetryAfter > 0 ? maxRetryAfter : 15;
865
+ const desiredMs = defaultSeconds * 1e3;
866
+ const remainingMs = timeoutMs - elapsedMs;
867
+ return Math.min(desiredMs, Math.max(0, remainingMs));
868
+ }
869
+ async function runDomainsConnect(input, deps) {
870
+ try {
871
+ await requireCredential({
872
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
873
+ env: deps.env,
874
+ store: deps.store
875
+ });
876
+ } catch {
877
+ return writeAuthError(deps);
878
+ }
879
+ const result = await deps.client.domains.connect(input);
880
+ if (result.error) return writeApiError(deps, result.error);
881
+ const domain = result.data;
882
+ if (deps.outputMode === "human") {
883
+ deps.stdout(
884
+ `${success(`Connected ${domain.hostname} (${domain.mode} mode)`)}
885
+ `
886
+ );
887
+ if (domain.dns.length > 0) {
888
+ deps.stdout("\nDNS records to create at your provider:\n\n");
889
+ deps.stdout(`${formatDnsTable(domain.dns)}
890
+ `);
891
+ }
892
+ printNextHints(deps, domain.next);
893
+ } else {
894
+ writeJson(deps, { ok: true, domain });
895
+ }
896
+ return { exitCode: 0 };
897
+ }
898
+ async function runDomainsList(_input, deps) {
899
+ try {
900
+ await requireCredential({
901
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
902
+ env: deps.env,
903
+ store: deps.store
904
+ });
905
+ } catch {
906
+ return writeAuthError(deps);
907
+ }
908
+ const result = await deps.client.domains.list();
909
+ if (result.error) return writeApiError(deps, result.error);
910
+ const domains = result.data?.domains ?? [];
911
+ if (deps.outputMode === "human") {
912
+ if (domains.length === 0) {
913
+ writeEmpty(deps, "No domains connected yet.", { ok: true, domains: [] });
914
+ } else {
915
+ for (const d of domains) {
916
+ const defaultMark = d.default ? import_picocolors3.default.green(" (default)") : "";
917
+ const drop = d.dropId ? import_picocolors3.default.dim(` \u2192 ${d.dropId}`) : "";
918
+ deps.stdout(
919
+ ` ${d.hostname}${defaultMark} ${import_picocolors3.default.dim(d.mode)} ${import_picocolors3.default.dim(d.status)}${drop}
920
+ `
921
+ );
922
+ }
923
+ }
924
+ } else {
925
+ writeJson(deps, { ok: true, domains });
926
+ }
927
+ return { exitCode: 0 };
928
+ }
929
+ async function runDomainsStatus(idOrHostname, _input, deps) {
930
+ try {
931
+ await requireCredential({
932
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
933
+ env: deps.env,
934
+ store: deps.store
935
+ });
936
+ } catch {
937
+ return writeAuthError(deps);
938
+ }
939
+ const result = await deps.client.domains.get(idOrHostname);
940
+ if (result.error) return writeApiError(deps, result.error);
941
+ const domain = result.data;
942
+ if (deps.outputMode === "human") {
943
+ deps.stdout(` ${import_picocolors3.default.dim("Hostname:")} ${domain.hostname}
944
+ `);
945
+ deps.stdout(` ${import_picocolors3.default.dim("Mode: ")} ${domain.mode}
946
+ `);
947
+ deps.stdout(` ${import_picocolors3.default.dim("Status: ")} ${domain.status}
948
+ `);
949
+ if (domain.failureReason) {
950
+ deps.stdout(
951
+ ` ${import_picocolors3.default.dim("Reason: ")} ${import_picocolors3.default.red(domain.failureReason)}
952
+ `
953
+ );
954
+ }
955
+ if (domain.dropId) {
956
+ deps.stdout(` ${import_picocolors3.default.dim("Drop: ")} ${domain.dropId}
957
+ `);
958
+ }
959
+ if (domain.verifiedAt) {
960
+ deps.stdout(` ${import_picocolors3.default.dim("Verified:")} ${domain.verifiedAt}
961
+ `);
962
+ }
963
+ deps.stdout(` ${import_picocolors3.default.dim("Created: ")} ${domain.createdAt}
964
+ `);
965
+ if (domain.dns.length > 0) {
966
+ deps.stdout("\nDNS records:\n\n");
967
+ deps.stdout(`${formatDnsTable(domain.dns)}
968
+ `);
969
+ }
970
+ printNextHints(deps, domain.next);
971
+ } else {
972
+ writeJson(deps, { ok: true, domain });
973
+ }
974
+ return { exitCode: 0 };
975
+ }
976
+ var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
977
+ async function runDomainsVerify(idOrHostname, input, deps, sleep = defaultSleep) {
978
+ try {
979
+ await requireCredential({
980
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
981
+ env: deps.env,
982
+ store: deps.store
983
+ });
984
+ } catch {
985
+ return writeAuthError(deps);
986
+ }
987
+ const timeoutMs = (input.timeout ?? 300) * 1e3;
988
+ const doVerify = async (elapsedMs) => {
989
+ const result = await deps.client.domains.verify(idOrHostname);
990
+ if (result.error) return writeApiError(deps, result.error);
991
+ const domain = result.data;
992
+ if (!input.wait) {
993
+ if (deps.outputMode === "human") {
994
+ deps.stdout(
995
+ `${domain.status === "live" ? success(`${idOrHostname} is live`) : warn(`${idOrHostname}: ${domain.status}`)}
996
+ `
997
+ );
998
+ if (domain.dns.length > 0) {
999
+ deps.stdout("\n");
1000
+ deps.stdout(`${formatDnsTable(domain.dns)}
1001
+ `);
1002
+ }
1003
+ printNextHints(deps, domain.next);
1004
+ } else {
1005
+ writeJson(deps, { ok: true, domain });
1006
+ }
1007
+ return {
1008
+ exitCode: domain.status === "live" ? 0 : domain.status === "failed" ? 1 : 6
1009
+ };
1010
+ }
1011
+ if (domain.status === "live" || domain.status === "failed") {
1012
+ if (deps.outputMode === "human") {
1013
+ deps.stdout(
1014
+ `${domain.status === "live" ? success(`${idOrHostname} is live`) : warn(`${idOrHostname}: ${domain.status}`)}
1015
+ `
1016
+ );
1017
+ if (domain.dns.length > 0) {
1018
+ deps.stdout("\n");
1019
+ deps.stdout(`${formatDnsTable(domain.dns)}
1020
+ `);
1021
+ }
1022
+ } else {
1023
+ writeJson(deps, { ok: true, domain });
1024
+ }
1025
+ return { exitCode: domain.status === "live" ? 0 : 1 };
1026
+ }
1027
+ const sleepMs = computeSleepMs(domain.dns, elapsedMs, timeoutMs);
1028
+ const newElapsed = elapsedMs + sleepMs;
1029
+ if (newElapsed >= timeoutMs) {
1030
+ const timeoutSecs = input.timeout ?? 300;
1031
+ const message = `Timed out waiting for ${idOrHostname} to go live after ${timeoutSecs}s`;
1032
+ if (deps.outputMode === "human") {
1033
+ deps.stderr(
1034
+ ` ${import_picocolors3.default.red("Timed out waiting for")} ${idOrHostname} ${import_picocolors3.default.red("to go live")}
1035
+ `
1036
+ );
1037
+ } else {
1038
+ deps.stderr(
1039
+ `${JSON.stringify(errorEnvelope("verify_timeout", message))}
1040
+ `
1041
+ );
1042
+ }
1043
+ return { exitCode: 7 };
1044
+ }
1045
+ if (deps.outputMode === "human" && domain.dns.length > 0) {
1046
+ deps.stdout(`${formatDnsTable(domain.dns)}
1047
+ `);
1048
+ }
1049
+ await sleep(sleepMs);
1050
+ return doVerify(newElapsed);
1051
+ };
1052
+ return doVerify(0);
1053
+ }
1054
+ async function runDomainsUpdate(idOrHostname, input, deps) {
1055
+ if (input.drop === void 0 && input.setDefault === void 0) {
1056
+ return writeError(
1057
+ deps,
1058
+ "invalid_usage",
1059
+ "Provide at least one option: --drop <dropId> or --default"
1060
+ );
1061
+ }
1062
+ try {
1063
+ await requireCredential({
1064
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1065
+ env: deps.env,
1066
+ store: deps.store
1067
+ });
1068
+ } catch {
1069
+ return writeAuthError(deps);
1070
+ }
1071
+ const updateInput = {};
1072
+ if (input.drop !== void 0) updateInput.dropId = input.drop;
1073
+ if (input.setDefault !== void 0) updateInput.default = input.setDefault;
1074
+ const result = await deps.client.domains.update(idOrHostname, updateInput);
1075
+ if (result.error) return writeApiError(deps, result.error);
1076
+ const domain = result.data;
1077
+ writeHumanOrJson(deps, `${success(`Updated ${domain.hostname}`)}
1078
+ `, {
1079
+ ok: true,
1080
+ domain
1081
+ });
1082
+ return { exitCode: 0 };
1083
+ }
1084
+ async function runDomainsRemove(idOrHostname, _input, deps) {
1085
+ try {
1086
+ await requireCredential({
1087
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1088
+ env: deps.env,
1089
+ store: deps.store
1090
+ });
1091
+ } catch {
1092
+ return writeAuthError(deps);
1093
+ }
1094
+ const result = await deps.client.domains.delete(idOrHostname);
1095
+ if (result.error) return writeApiError(deps, result.error);
1096
+ const deleted = result.data;
1097
+ if (deps.outputMode === "human") {
1098
+ deps.stdout(`${success(`Removed ${deleted.hostname}`)}
1099
+ `);
1100
+ if (deleted.warning) {
1101
+ deps.stderr(`${warn(deleted.warning)}
1102
+ `);
1103
+ }
1104
+ } else {
1105
+ writeJson(deps, {
1106
+ ok: true,
1107
+ deleted: true,
1108
+ id: deleted.id,
1109
+ hostname: deleted.hostname,
1110
+ warning: deleted.warning
1111
+ });
1112
+ }
1113
+ return { exitCode: 0 };
1114
+ }
1115
+
785
1116
  // src/commands/drops.ts
786
1117
  var prompts2 = __toESM(require("@clack/prompts"), 1);
787
1118
  async function runDropsList(input, deps) {
@@ -1119,7 +1450,9 @@ async function parseDropOptions(raw) {
1119
1450
  ...raw.entry ? { entry: raw.entry } : {},
1120
1451
  ...raw.contentType ? { contentType: raw.contentType } : {},
1121
1452
  ...raw.path ? { path: raw.path } : {},
1122
- ...raw.idempotencyKey ? { idempotencyKey: raw.idempotencyKey } : {}
1453
+ ...raw.idempotencyKey ? { idempotencyKey: raw.idempotencyKey } : {},
1454
+ ...raw.domain ? { domain: raw.domain } : {},
1455
+ ...raw.slug ? { slug: raw.slug } : {}
1123
1456
  };
1124
1457
  }
1125
1458
  function parseJsonObject(value, label) {
@@ -1131,12 +1464,12 @@ function parseJsonObject(value, label) {
1131
1464
  }
1132
1465
 
1133
1466
  // src/spinner.ts
1134
- var import_picocolors3 = __toESM(require("picocolors"), 1);
1467
+ var import_picocolors4 = __toESM(require("picocolors"), 1);
1135
1468
  var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1136
1469
  function createSpinner(message, stderr) {
1137
1470
  let i = 0;
1138
1471
  const timer = setInterval(() => {
1139
- stderr(`\r\x1B[2K${import_picocolors3.default.cyan(frames[i % frames.length])} ${message}`);
1472
+ stderr(`\r\x1B[2K${import_picocolors4.default.cyan(frames[i % frames.length])} ${message}`);
1140
1473
  i++;
1141
1474
  }, 80);
1142
1475
  return {
@@ -1274,6 +1607,110 @@ async function handlePublishDryRun(input, raw, deps, client) {
1274
1607
  }
1275
1608
  }
1276
1609
 
1610
+ // src/commands/pull.ts
1611
+ var import_promises2 = require("fs/promises");
1612
+ var import_node_path = require("path");
1613
+ async function runPull(target, input, deps) {
1614
+ try {
1615
+ await requireCredential({
1616
+ ...deps.apiKey ? { apiKey: deps.apiKey } : {},
1617
+ env: deps.env,
1618
+ store: deps.store
1619
+ });
1620
+ } catch {
1621
+ return writeAuthError(deps);
1622
+ }
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
+ }
1641
+ const spin = shouldSpin(deps.outputMode) ? createSpinner("Pulling content\u2026", deps.stderr) : void 0;
1642
+ const manifestResult = await deps.client.drops.getContent(dropId);
1643
+ if (manifestResult.error) {
1644
+ spin?.fail();
1645
+ return writeApiError(deps, manifestResult.error);
1646
+ }
1647
+ const manifest = manifestResult.data;
1648
+ const outDir = (0, import_node_path.resolve)(
1649
+ deps.cwd ?? process.cwd(),
1650
+ input.output ?? defaultDirName
1651
+ );
1652
+ let destinations;
1653
+ try {
1654
+ destinations = manifest.files.map((file) => ({
1655
+ path: file.path,
1656
+ dest: safeDestination(outDir, file.path)
1657
+ }));
1658
+ } catch (error2) {
1659
+ spin?.fail();
1660
+ const message = error2 instanceof Error ? error2.message : "Invalid manifest path.";
1661
+ return writeError(deps, "local_input_error", message);
1662
+ }
1663
+ try {
1664
+ await (0, import_promises2.mkdir)(outDir, { recursive: true });
1665
+ for (const { path, dest } of destinations) {
1666
+ const fileResult = await deps.client.drops.getContent(dropId, { path });
1667
+ if (fileResult.error) {
1668
+ spin?.fail();
1669
+ return writeApiError(deps, {
1670
+ ...fileResult.error,
1671
+ message: `${path}: ${fileResult.error.message}`
1672
+ });
1673
+ }
1674
+ const file = fileResult.data;
1675
+ await (0, import_promises2.mkdir)((0, import_node_path.dirname)(dest), { recursive: true });
1676
+ await (0, import_promises2.writeFile)(dest, file.bytes);
1677
+ }
1678
+ } catch (error2) {
1679
+ spin?.fail();
1680
+ const message = error2 instanceof Error ? error2.message : "Failed to write files.";
1681
+ return writeError(deps, "local_input_error", message);
1682
+ }
1683
+ spin?.stop();
1684
+ const count = manifest.files.length;
1685
+ writeHumanOrJson(
1686
+ deps,
1687
+ success(`Pulled ${count} ${count === 1 ? "file" : "files"} to ${outDir}`),
1688
+ {
1689
+ ok: true,
1690
+ pulled: {
1691
+ dropId: manifest.dropId,
1692
+ deploymentId: manifest.deploymentId,
1693
+ revision: manifest.revision,
1694
+ ...manifest.entry !== void 0 ? { entry: manifest.entry } : {},
1695
+ dir: outDir,
1696
+ files: manifest.files
1697
+ }
1698
+ }
1699
+ );
1700
+ return { exitCode: 0 };
1701
+ }
1702
+ function safeDestination(outDir, filePath) {
1703
+ if (!filePath || (0, import_node_path.isAbsolute)(filePath)) {
1704
+ throw new Error(`Unsafe file path in content manifest: "${filePath}"`);
1705
+ }
1706
+ const dest = (0, import_node_path.join)(outDir, filePath);
1707
+ const rel = (0, import_node_path.relative)(outDir, dest);
1708
+ if (rel.startsWith("..") || (0, import_node_path.isAbsolute)(rel)) {
1709
+ throw new Error(`Unsafe file path in content manifest: "${filePath}"`);
1710
+ }
1711
+ return dest;
1712
+ }
1713
+
1277
1714
  // src/commands/update-content.ts
1278
1715
  var NO_CONTENT_MESSAGE = "update-content requires content. Provide a file, folder, URL, or text to ship a new deployment.";
1279
1716
  var NO_CONTENT_NEXT_ACTION = "Pass an input, e.g. dropthis update-content <dropId> ./dist. To change title, visibility, password, expiry, or metadata, use dropthis update-settings <dropId> instead.";
@@ -1691,9 +2128,9 @@ function createContext(input) {
1691
2128
  }
1692
2129
 
1693
2130
  // src/storage.ts
1694
- var import_promises2 = require("fs/promises");
2131
+ var import_promises3 = require("fs/promises");
1695
2132
  var import_node_os = require("os");
1696
- var import_node_path = require("path");
2133
+ var import_node_path2 = require("path");
1697
2134
  var import_keyring = require("@napi-rs/keyring");
1698
2135
  var MemoryCredentialStore = class {
1699
2136
  credential = null;
@@ -1725,7 +2162,7 @@ var InsecureFileCredentialStore = class {
1725
2162
  });
1726
2163
  }
1727
2164
  async clear() {
1728
- await (0, import_promises2.rm)(this.filePath, { force: true });
2165
+ await (0, import_promises3.rm)(this.filePath, { force: true });
1729
2166
  }
1730
2167
  };
1731
2168
  var KeyringCredentialStore = class {
@@ -1752,7 +2189,7 @@ var KeyringCredentialStore = class {
1752
2189
  }
1753
2190
  async clear() {
1754
2191
  await this.keyring.deletePassword();
1755
- await (0, import_promises2.rm)(this.filePath, { force: true });
2192
+ await (0, import_promises3.rm)(this.filePath, { force: true });
1756
2193
  }
1757
2194
  };
1758
2195
  function createCredentialStore(options = {}) {
@@ -1770,14 +2207,14 @@ function defaultKeyringAdapter() {
1770
2207
  };
1771
2208
  }
1772
2209
  function credentialPath(configDir) {
1773
- return (0, import_node_path.join)(
1774
- configDir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "dropthis"),
2210
+ return (0, import_node_path2.join)(
2211
+ configDir ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".config", "dropthis"),
1775
2212
  "credentials.json"
1776
2213
  );
1777
2214
  }
1778
2215
  async function readJsonFile(path) {
1779
2216
  try {
1780
- return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
2217
+ return JSON.parse(await (0, import_promises3.readFile)(path, "utf8"));
1781
2218
  } catch (error2) {
1782
2219
  if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
1783
2220
  return null;
@@ -1786,14 +2223,14 @@ async function readJsonFile(path) {
1786
2223
  }
1787
2224
  }
1788
2225
  async function writeCredentialFile(path, credential) {
1789
- await (0, import_promises2.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
2226
+ await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
1790
2227
  const tmpPath = `${path}.${process.pid}.tmp`;
1791
- await (0, import_promises2.writeFile)(tmpPath, `${JSON.stringify(credential, null, 2)}
2228
+ await (0, import_promises3.writeFile)(tmpPath, `${JSON.stringify(credential, null, 2)}
1792
2229
  `, {
1793
2230
  mode: 384
1794
2231
  });
1795
- await (0, import_promises2.chmod)(tmpPath, 384);
1796
- await (0, import_promises2.rename)(tmpPath, path);
2232
+ await (0, import_promises3.chmod)(tmpPath, 384);
2233
+ await (0, import_promises3.rename)(tmpPath, path);
1797
2234
  }
1798
2235
 
1799
2236
  // src/program.ts
@@ -1816,13 +2253,13 @@ function buildProgram(options = {}) {
1816
2253
  });
1817
2254
  program.name("dropthis").description(
1818
2255
  [
1819
- import_picocolors4.default.dim("Publish anything online and get a URL back."),
2256
+ import_picocolors5.default.dim("Publish anything online and get a URL back."),
1820
2257
  "",
1821
- `${import_picocolors4.default.bold("Quick start:")}`,
1822
- ` ${import_picocolors4.default.cyan("dropthis login")}`,
1823
- ` ${import_picocolors4.default.cyan("dropthis publish ./dist")}`
2258
+ `${import_picocolors5.default.bold("Quick start:")}`,
2259
+ ` ${import_picocolors5.default.cyan("dropthis login")}`,
2260
+ ` ${import_picocolors5.default.cyan("dropthis publish ./dist")}`
1824
2261
  ].join("\n")
1825
- ).version("0.9.3").configureHelp({
2262
+ ).version("0.11.0").configureHelp({
1826
2263
  subcommandTerm(cmd) {
1827
2264
  const args = cmd.registeredArguments.map((a) => {
1828
2265
  const name = a.name();
@@ -1831,10 +2268,10 @@ function buildProgram(options = {}) {
1831
2268
  }).join(" ");
1832
2269
  return args ? `${cmd.name()} ${args}` : cmd.name();
1833
2270
  },
1834
- styleTitle: (title) => import_picocolors4.default.bold(title),
1835
- styleCommandText: (str) => import_picocolors4.default.cyan(str),
1836
- styleSubcommandText: (str) => import_picocolors4.default.cyan(str),
1837
- styleOptionText: (str) => import_picocolors4.default.yellow(str)
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)
1838
2275
  });
1839
2276
  program.option("--api-key <key>", "Override API key for this invocation");
1840
2277
  program.option("--api-url <url>", "Override API base URL");
@@ -1852,7 +2289,13 @@ function buildProgram(options = {}) {
1852
2289
  ).option(
1853
2290
  "--metadata <json>",
1854
2291
  `Attach JSON key-value pairs, e.g. '{"source":"ci"}'`
1855
- ).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(
2292
+ ).option("--metadata-file <path>", "Read metadata JSON from a file").option("--path <name>", "Set filename when publishing from stdin").option(
2293
+ "--domain <hostname>",
2294
+ "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."
2295
+ ).option(
2296
+ "--slug <vanity-slug>",
2297
+ "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."
2298
+ ).option("--url", "Print only the published URL (no JSON envelope)").option(
1856
2299
  "--dry-run",
1857
2300
  "Show what would be published without publishing (JSON; cannot combine with --url)"
1858
2301
  ).option("--json", "Force JSON output").option(
@@ -1984,6 +2427,20 @@ function buildProgram(options = {}) {
1984
2427
  process.exitCode = result.exitCode;
1985
2428
  }
1986
2429
  );
2430
+ program.command("pull <target>").description(
2431
+ "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(
2433
+ async (target, commandOptions) => {
2434
+ const deps = await commandDeps(
2435
+ program,
2436
+ options,
2437
+ store,
2438
+ commandOptions
2439
+ );
2440
+ const result = await runPull(target, commandOptions, deps);
2441
+ process.exitCode = result.exitCode;
2442
+ }
2443
+ );
1987
2444
  program.command("get <dropId>").description("Show drop details (pass the full drop_\u2026 id)").option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
1988
2445
  const deps = await commandDeps(program, options, store, commandOptions);
1989
2446
  const result = await runDropsGet(dropId, commandOptions, deps);
@@ -2032,6 +2489,76 @@ function buildProgram(options = {}) {
2032
2489
  process.exitCode = result.exitCode;
2033
2490
  }
2034
2491
  );
2492
+ const domains = program.command("domains").description(
2493
+ "Manage custom domains. Loop: connect \u2192 create the CNAME at your DNS provider \u2192 verify --wait \u2192 publish --domain"
2494
+ );
2495
+ domains.command("connect <hostname>").description(
2496
+ "Connect a custom domain. Returns DNS records to create at your provider.\nAfter adding them: dropthis domains verify <hostname> --wait"
2497
+ ).requiredOption(
2498
+ "--mode <mode>",
2499
+ "Mount mode: path (many drops at hostname/{slug}/) or dedicated (one drop at root)"
2500
+ ).option("--json", "Force JSON output").action(
2501
+ async (hostname, commandOptions) => {
2502
+ if (commandOptions.mode !== "path" && commandOptions.mode !== "dedicated") {
2503
+ process.stderr.write(`error: --mode must be "path" or "dedicated"
2504
+ `);
2505
+ process.exitCode = 2;
2506
+ return;
2507
+ }
2508
+ const deps = await commandDeps(program, options, store, commandOptions);
2509
+ const result = await runDomainsConnect(
2510
+ { hostname, mode: commandOptions.mode },
2511
+ deps
2512
+ );
2513
+ process.exitCode = result.exitCode;
2514
+ }
2515
+ );
2516
+ domains.command("list").description("List connected custom domains").option("--json", "Force JSON output").action(async (commandOptions) => {
2517
+ const deps = await commandDeps(program, options, store, commandOptions);
2518
+ const result = await runDomainsList(commandOptions, deps);
2519
+ process.exitCode = result.exitCode;
2520
+ });
2521
+ domains.command("status <hostname>").description("Show domain details and DNS record status").option("--json", "Force JSON output").action(async (hostname, commandOptions) => {
2522
+ const deps = await commandDeps(program, options, store, commandOptions);
2523
+ const result = await runDomainsStatus(hostname, commandOptions, deps);
2524
+ process.exitCode = result.exitCode;
2525
+ });
2526
+ domains.command("verify <hostname>").description(
2527
+ "Trigger a DNS verification check. With --wait, polls until live or failed.\nUse after adding the DNS records from: dropthis domains connect"
2528
+ ).option("--wait", "Poll until live (or failed/timeout)").option(
2529
+ "--timeout <secs>",
2530
+ "Max seconds to wait (default: 300)",
2531
+ parseInteger
2532
+ ).option("--json", "Force JSON output").action(
2533
+ async (hostname, commandOptions) => {
2534
+ const deps = await commandDeps(program, options, store, commandOptions);
2535
+ const result = await runDomainsVerify(hostname, commandOptions, deps);
2536
+ process.exitCode = result.exitCode;
2537
+ }
2538
+ );
2539
+ domains.command("update <hostname>").description(
2540
+ "Update a domain \u2014 repoint to a different drop (dedicated mode) or set/clear as account default (path mode). At least one option required."
2541
+ ).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(
2542
+ async (hostname, commandOptions) => {
2543
+ const deps = await commandDeps(program, options, store, commandOptions);
2544
+ const result = await runDomainsUpdate(
2545
+ hostname,
2546
+ {
2547
+ ...commandOptions.drop !== void 0 ? { drop: commandOptions.drop } : {},
2548
+ ...commandOptions.default !== void 0 ? { setDefault: commandOptions.default } : {}
2549
+ },
2550
+ deps
2551
+ );
2552
+ process.exitCode = result.exitCode;
2553
+ }
2554
+ );
2555
+ 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
+ });
2035
2562
  const apiKeys = program.command("api-keys").description("Manage API keys");
2036
2563
  apiKeys.command("create").description("Create an API key").option("--label <label>", "API key label", "CLI").option("--json", "Force JSON output").action(async (commandOptions) => {
2037
2564
  const deps = await commandDeps(program, options, store, commandOptions);
@@ -2270,6 +2797,8 @@ function toDropOptions(options) {
2270
2797
  ...options.contentType ? { contentType: options.contentType } : {},
2271
2798
  ...options.path ? { path: options.path } : {},
2272
2799
  ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {},
2800
+ ...options.domain ? { domain: options.domain } : {},
2801
+ ...options.slug ? { slug: options.slug } : {},
2273
2802
  ...options.json !== void 0 ? { json: options.json } : {},
2274
2803
  ...options.quiet !== void 0 ? { quiet: options.quiet } : {},
2275
2804
  ...options.url !== void 0 ? { url: options.url } : {},