@dropthis/cli 0.20.0 → 0.21.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/README.md CHANGED
@@ -94,6 +94,57 @@ dropthis publish ./dist \
94
94
 
95
95
  `publish` always creates a NEW drop with a new URL. To change a drop you already published, use `update-content` or `update-settings` with its `drop_…` id — never publish again.
96
96
 
97
+ ### Canonical vs raw URLs
98
+
99
+ Every drop has a canonical `url` — an always-branded human view (badge guaranteed). For a single non-HTML file (`.pdf`, `.png`, `.json`, a URL you asked the server to fetch, etc.), publish also prints a `Raw:` line: the file's exact bytes at a natural path, no wrapper. Hand the canonical `url` to people; hand the raw URL to agents or downstream tooling that needs the real bytes.
100
+
101
+ ### Publishing a bundle with remote assets (`--manifest`)
102
+
103
+ When an AI agent generates an HTML page that references external images or other files, use `--manifest` to publish everything as one drop without base64-encoding the assets. The server fetches remote files during publish so bytes never pass through your process.
104
+
105
+ ```bash
106
+ dropthis publish --manifest bundle.json
107
+ ```
108
+
109
+ `bundle.json` shape:
110
+
111
+ ```json
112
+ {
113
+ "files": [
114
+ {
115
+ "path": "index.html",
116
+ "content": "<html>...</html>"
117
+ },
118
+ {
119
+ "path": "hero.jpg",
120
+ "source_url": "https://cdn.example.com/hero.jpg"
121
+ },
122
+ {
123
+ "path": "data.json",
124
+ "content_base64": "eyJrZXkiOiJ2YWx1ZSJ9"
125
+ }
126
+ ]
127
+ }
128
+ ```
129
+
130
+ Each file entry must have a `path` and exactly one content key:
131
+
132
+ | Key | Description |
133
+ |-----|-------------|
134
+ | `content` | UTF-8 text content (HTML, CSS, JSON, markdown, …) |
135
+ | `source_url` | Public `https://` URL — the server fetches the bytes server-side. Use this for images and other binary assets instead of base64-encoding them. |
136
+ | `content_base64` | Base64-encoded bytes (for binary files you already have in memory) |
137
+
138
+ Optional per-file keys: `content_type` (MIME type override).
139
+
140
+ The `--manifest` flag also works on `update-content`:
141
+
142
+ ```bash
143
+ dropthis update-content drop_abc123 --manifest bundle-v2.json
144
+ ```
145
+
146
+ Cannot be combined with a positional file/folder/URL argument.
147
+
97
148
  All publish flags:
98
149
 
99
150
  | Flag | Description |
@@ -105,6 +156,7 @@ All publish flags:
105
156
  | `--entry <path>` | Entry file for directories |
106
157
  | `--content-type <mime>` | MIME type (required for stdin) |
107
158
  | `--path <path>` | Filename (required for stdin) |
159
+ | `--manifest <file>` | Multi-file bundle JSON (see above) |
108
160
  | `--expires-at <datetime>` | Expiration datetime |
109
161
  | `--metadata <json>` | Metadata as JSON string |
110
162
  | `--metadata-file <path>` | Metadata from a JSON file |
@@ -133,6 +185,9 @@ Change an existing drop without creating a new URL. Content and settings are **s
133
185
  # Replace the content at the same URL (ships a new deployment)
134
186
  dropthis update-content drop_abc123 ./dist-v2
135
187
 
188
+ # Update from a manifest bundle (same --manifest format as publish)
189
+ dropthis update-content drop_abc123 --manifest bundle-v2.json
190
+
136
191
  # Change settings only — title, visibility, password, expiry, metadata
137
192
  dropthis update-settings drop_abc123 --title "New title"
138
193
 
package/dist/cli.cjs CHANGED
@@ -916,13 +916,13 @@ async function runDoctor(_input, deps) {
916
916
  const authSource = credential?.source ?? "missing";
917
917
  const storageBackend = stored?.storage ?? "none";
918
918
  const pairs = [
919
- ["Version", "0.20.0"],
919
+ ["Version", "0.21.0"],
920
920
  ["Auth", authSource],
921
921
  ["Storage", storageBackend]
922
922
  ];
923
923
  writeKv(deps, pairs, {
924
924
  ok: true,
925
- version: "0.20.0",
925
+ version: "0.21.0",
926
926
  auth: { source: authSource },
927
927
  storage: { backend: storageBackend }
928
928
  });
@@ -1615,6 +1615,7 @@ async function runLogout(input, deps) {
1615
1615
 
1616
1616
  // src/drop-operations.ts
1617
1617
  var import_node_crypto = require("crypto");
1618
+ var import_promises = require("fs/promises");
1618
1619
  var MAX_BUNDLE_FILE_COUNT = 200;
1619
1620
  function isFileSystemError(error2) {
1620
1621
  if (!(error2 instanceof Error)) return false;
@@ -1634,6 +1635,70 @@ function validateManifestFileCount(count) {
1634
1635
  );
1635
1636
  }
1636
1637
  }
1638
+ async function loadManifestFile(manifestPath) {
1639
+ let raw;
1640
+ try {
1641
+ raw = JSON.parse(await (0, import_promises.readFile)(manifestPath, "utf8"));
1642
+ } catch (err) {
1643
+ const msg = err instanceof Error ? err.message : "Could not read manifest file.";
1644
+ throw new Error(`Failed to read manifest file: ${msg}`);
1645
+ }
1646
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1647
+ throw new Error("Manifest must be a JSON object with a 'files' array.");
1648
+ }
1649
+ const obj = raw;
1650
+ if (!Array.isArray(obj.files)) {
1651
+ throw new Error("Manifest must have a 'files' array.");
1652
+ }
1653
+ const files = obj.files.map(
1654
+ (entry, i) => {
1655
+ if (!entry || typeof entry !== "object") {
1656
+ throw new Error(`Manifest files[${i}] must be an object.`);
1657
+ }
1658
+ const {
1659
+ path,
1660
+ content,
1661
+ content_base64,
1662
+ source_url,
1663
+ content_type,
1664
+ size_bytes,
1665
+ checksum_sha256
1666
+ } = entry;
1667
+ if (!path || typeof path !== "string") {
1668
+ throw new Error(`Manifest files[${i}] must have a 'path' string.`);
1669
+ }
1670
+ const contentKeys = [content, content_base64, source_url].filter(
1671
+ (v) => v !== void 0
1672
+ );
1673
+ if (contentKeys.length === 0) {
1674
+ throw new Error(
1675
+ `Manifest files[${i}] (path: "${path}") must have exactly one of: content, content_base64, source_url.`
1676
+ );
1677
+ }
1678
+ if (contentKeys.length > 1) {
1679
+ throw new Error(
1680
+ `Manifest files[${i}] (path: "${path}") must have only one of: content, content_base64, source_url.`
1681
+ );
1682
+ }
1683
+ if (source_url !== void 0 && !source_url.startsWith("http://") && !source_url.startsWith("https://")) {
1684
+ throw new Error(
1685
+ `Manifest files[${i}] (path: "${path}") has an invalid source_url "${source_url}": source_url must be an http(s) URL.`
1686
+ );
1687
+ }
1688
+ const file = { path };
1689
+ if (content !== void 0) file.content = content;
1690
+ if (content_base64 !== void 0) file.contentBase64 = content_base64;
1691
+ if (source_url !== void 0) file.sourceUrl = source_url;
1692
+ if (content_type !== void 0) file.contentType = content_type;
1693
+ if (size_bytes !== void 0)
1694
+ file.sizeBytes = size_bytes;
1695
+ if (checksum_sha256 !== void 0)
1696
+ file.checksumSha256 = checksum_sha256;
1697
+ return file;
1698
+ }
1699
+ );
1700
+ return { kind: "files", files };
1701
+ }
1637
1702
  function formatDryRunManifest(prepared, extra) {
1638
1703
  if (prepared.kind === "source") {
1639
1704
  return {
@@ -1646,8 +1711,11 @@ function formatDryRunManifest(prepared, extra) {
1646
1711
  ...prepared.metadata ? { metadata: prepared.metadata } : {}
1647
1712
  };
1648
1713
  }
1714
+ const remoteFileCount = prepared.manifest.files.filter(
1715
+ (f) => typeof f.sizeBytes !== "number"
1716
+ ).length;
1649
1717
  const totalBytes = prepared.manifest.files.reduce(
1650
- (sum, f) => sum + f.sizeBytes,
1718
+ (sum, f) => sum + (typeof f.sizeBytes === "number" ? f.sizeBytes : 0),
1651
1719
  0
1652
1720
  );
1653
1721
  return {
@@ -1661,12 +1729,15 @@ function formatDryRunManifest(prepared, extra) {
1661
1729
  },
1662
1730
  options: prepared.options,
1663
1731
  ...prepared.metadata ? { metadata: prepared.metadata } : {},
1664
- totalBytes
1732
+ totalBytes,
1733
+ // Only include remoteFileCount when there are remote files, to preserve
1734
+ // the existing output shape for all-inline bundles.
1735
+ ...remoteFileCount > 0 ? { remoteFileCount } : {}
1665
1736
  };
1666
1737
  }
1667
1738
 
1668
1739
  // src/options.ts
1669
- var import_promises = require("fs/promises");
1740
+ var import_promises2 = require("fs/promises");
1670
1741
  var import_node = require("@dropthis/node");
1671
1742
  async function parseDropOptions(raw) {
1672
1743
  if (raw.metadata && raw.metadataFile) {
@@ -1695,7 +1766,7 @@ async function parseDropOptions(raw) {
1695
1766
  }
1696
1767
  }
1697
1768
  const metadata = raw.metadata ? parseJsonObject(raw.metadata, "--metadata") : raw.metadataFile ? parseJsonObject(
1698
- await (0, import_promises.readFile)(raw.metadataFile, "utf8"),
1769
+ await (0, import_promises2.readFile)(raw.metadataFile, "utf8"),
1699
1770
  "--metadata-file"
1700
1771
  ) : void 0;
1701
1772
  return {
@@ -1777,6 +1848,25 @@ async function runPublish(input, raw, deps) {
1777
1848
  if (didInlineAuth && deps.createClient) {
1778
1849
  client = deps.createClient(credential.apiKey);
1779
1850
  }
1851
+ if (raw.manifest) {
1852
+ const hasInput2 = Array.isArray(input) ? input.length > 0 : input !== void 0;
1853
+ if (hasInput2) {
1854
+ return writeError(
1855
+ deps,
1856
+ "invalid_usage",
1857
+ "--manifest cannot be combined with a positional input.",
1858
+ "Remove the positional argument, or omit --manifest and pass your files directly."
1859
+ );
1860
+ }
1861
+ let manifestInput;
1862
+ try {
1863
+ manifestInput = await loadManifestFile(raw.manifest);
1864
+ } catch (err) {
1865
+ const msg = err instanceof Error ? err.message : "Invalid manifest.";
1866
+ return writeError(deps, "invalid_usage", msg);
1867
+ }
1868
+ input = manifestInput;
1869
+ }
1780
1870
  if (raw.dryRun) {
1781
1871
  return handlePublishDryRun(input, raw, deps, client);
1782
1872
  }
@@ -1845,7 +1935,8 @@ async function handlePublishDryRun(input, raw, deps, client) {
1845
1935
  );
1846
1936
  }
1847
1937
  try {
1848
- if (!input || Array.isArray(input) && input.length === 0) {
1938
+ const hasInput = Array.isArray(input) ? input.length > 0 : input !== void 0;
1939
+ if (!hasInput) {
1849
1940
  throw new Error("Publish requires <input>.");
1850
1941
  }
1851
1942
  const options = await parseDropOptions(raw);
@@ -1868,7 +1959,7 @@ async function handlePublishDryRun(input, raw, deps, client) {
1868
1959
  }
1869
1960
 
1870
1961
  // src/commands/pull.ts
1871
- var import_promises2 = require("fs/promises");
1962
+ var import_promises3 = require("fs/promises");
1872
1963
  var import_node_path = require("path");
1873
1964
  async function runPull(target, input, deps) {
1874
1965
  try {
@@ -1908,7 +1999,7 @@ async function runPull(target, input, deps) {
1908
1999
  return writeError(deps, "local_input_error", message);
1909
2000
  }
1910
2001
  try {
1911
- await (0, import_promises2.mkdir)(outDir, { recursive: true });
2002
+ await (0, import_promises3.mkdir)(outDir, { recursive: true });
1912
2003
  for (const { path, dest } of destinations) {
1913
2004
  const fileResult = await deps.client.drops.getContent(dropId, { path });
1914
2005
  if (fileResult.error) {
@@ -1919,8 +2010,8 @@ async function runPull(target, input, deps) {
1919
2010
  });
1920
2011
  }
1921
2012
  const file = fileResult.data;
1922
- await (0, import_promises2.mkdir)((0, import_node_path.dirname)(dest), { recursive: true });
1923
- await (0, import_promises2.writeFile)(dest, file.bytes);
2013
+ await (0, import_promises3.mkdir)((0, import_node_path.dirname)(dest), { recursive: true });
2014
+ await (0, import_promises3.writeFile)(dest, file.bytes);
1924
2015
  }
1925
2016
  } catch (error2) {
1926
2017
  spin?.fail();
@@ -1986,6 +2077,23 @@ async function runUpdateContent(target, input, raw, deps) {
1986
2077
  const resolved = await resolveDropTarget(target, resolveDeps);
1987
2078
  if (!resolved.ok) return { exitCode: resolved.exitCode };
1988
2079
  const dropId = resolved.dropId;
2080
+ if (raw.manifest) {
2081
+ const hasInput = Array.isArray(input) ? input.length > 0 : input !== void 0;
2082
+ if (hasInput) {
2083
+ return writeError(
2084
+ deps,
2085
+ "invalid_usage",
2086
+ "--manifest cannot be combined with a positional input.",
2087
+ "Remove the positional argument, or omit --manifest and pass your files directly."
2088
+ );
2089
+ }
2090
+ try {
2091
+ input = await loadManifestFile(raw.manifest);
2092
+ } catch (err) {
2093
+ const msg = err instanceof Error ? err.message : "Invalid manifest.";
2094
+ return writeError(deps, "invalid_usage", msg);
2095
+ }
2096
+ }
1989
2097
  if (raw.dryRun) {
1990
2098
  return handleDryRun(dropId, input, raw, deps);
1991
2099
  }
@@ -2478,7 +2586,7 @@ function createContext(input) {
2478
2586
  }
2479
2587
 
2480
2588
  // src/storage.ts
2481
- var import_promises3 = require("fs/promises");
2589
+ var import_promises4 = require("fs/promises");
2482
2590
  var import_node_os = require("os");
2483
2591
  var import_node_path2 = require("path");
2484
2592
  var import_keyring = require("@napi-rs/keyring");
@@ -2512,7 +2620,7 @@ var InsecureFileCredentialStore = class {
2512
2620
  });
2513
2621
  }
2514
2622
  async clear() {
2515
- await (0, import_promises3.rm)(this.filePath, { force: true });
2623
+ await (0, import_promises4.rm)(this.filePath, { force: true });
2516
2624
  }
2517
2625
  };
2518
2626
  var KeyringCredentialStore = class {
@@ -2539,7 +2647,7 @@ var KeyringCredentialStore = class {
2539
2647
  }
2540
2648
  async clear() {
2541
2649
  await this.keyring.deletePassword();
2542
- await (0, import_promises3.rm)(this.filePath, { force: true });
2650
+ await (0, import_promises4.rm)(this.filePath, { force: true });
2543
2651
  }
2544
2652
  };
2545
2653
  function createCredentialStore(options = {}) {
@@ -2564,7 +2672,7 @@ function credentialPath(configDir) {
2564
2672
  }
2565
2673
  async function readJsonFile(path) {
2566
2674
  try {
2567
- return JSON.parse(await (0, import_promises3.readFile)(path, "utf8"));
2675
+ return JSON.parse(await (0, import_promises4.readFile)(path, "utf8"));
2568
2676
  } catch (error2) {
2569
2677
  if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
2570
2678
  return null;
@@ -2573,18 +2681,18 @@ async function readJsonFile(path) {
2573
2681
  }
2574
2682
  }
2575
2683
  async function writeCredentialFile(path, credential) {
2576
- await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
2684
+ await (0, import_promises4.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
2577
2685
  const tmpPath = `${path}.${process.pid}.tmp`;
2578
- await (0, import_promises3.writeFile)(tmpPath, `${JSON.stringify(credential, null, 2)}
2686
+ await (0, import_promises4.writeFile)(tmpPath, `${JSON.stringify(credential, null, 2)}
2579
2687
  `, {
2580
2688
  mode: 384
2581
2689
  });
2582
- await (0, import_promises3.chmod)(tmpPath, 384);
2583
- await (0, import_promises3.rename)(tmpPath, path);
2690
+ await (0, import_promises4.chmod)(tmpPath, 384);
2691
+ await (0, import_promises4.rename)(tmpPath, path);
2584
2692
  }
2585
2693
 
2586
2694
  // src/typo-guard.ts
2587
- var import_promises4 = require("fs/promises");
2695
+ var import_promises5 = require("fs/promises");
2588
2696
  function levenshtein(a, b) {
2589
2697
  if (a === b) return 0;
2590
2698
  if (a.length === 0) return b.length;
@@ -2625,7 +2733,7 @@ function isBareWordToken(token) {
2625
2733
  }
2626
2734
  async function pathExists(path) {
2627
2735
  try {
2628
- await (0, import_promises4.stat)(path);
2736
+ await (0, import_promises5.stat)(path);
2629
2737
  return true;
2630
2738
  } catch {
2631
2739
  return false;
@@ -2659,7 +2767,7 @@ function buildProgram(options = {}) {
2659
2767
  ` ${import_picocolors6.default.cyan("dropthis login")}`,
2660
2768
  ` ${import_picocolors6.default.cyan("dropthis publish ./dist")}`
2661
2769
  ].join("\n")
2662
- ).version("0.20.0").configureHelp({
2770
+ ).version("0.21.0").configureHelp({
2663
2771
  subcommandTerm(cmd) {
2664
2772
  const args = cmd.registeredArguments.map((a) => {
2665
2773
  const name = a.name();
@@ -2710,6 +2818,9 @@ ${lines.map((l) => ` ${l}`).join("\n")}
2710
2818
  ).option(
2711
2819
  "--slug <vanity-slug>",
2712
2820
  "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."
2821
+ ).option(
2822
+ "--manifest <file>",
2823
+ "Publish a multi-file bundle defined in a JSON file (path, content/source_url/content_base64 per file). Cannot combine with a positional input."
2713
2824
  ).option("--url", "Print only the published URL (no JSON envelope)").option(
2714
2825
  "--dry-run",
2715
2826
  "Show what would be published without publishing (JSON; cannot combine with --url)"
@@ -2740,7 +2851,7 @@ ${import_picocolors6.default.bold("Canonical vs raw URLs:")}`,
2740
2851
  ].join("\n")
2741
2852
  ).action(async (inputs, commandOptions) => {
2742
2853
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2743
- if (inputs.length === 0 && stdinIsTTY) {
2854
+ if (inputs.length === 0 && stdinIsTTY && !commandOptions.manifest) {
2744
2855
  program.outputHelp();
2745
2856
  return;
2746
2857
  }
@@ -2752,16 +2863,22 @@ ${import_picocolors6.default.bold("Canonical vs raw URLs:")}`,
2752
2863
  stdinIsTTY
2753
2864
  );
2754
2865
  let publishInput;
2755
- try {
2756
- publishInput = await resolvePublishInputs(
2757
- inputs,
2758
- options.stdin,
2759
- stdinIsTTY
2760
- );
2761
- } catch (err) {
2762
- const msg = err instanceof Error ? err.message : "Invalid input.";
2763
- process.exitCode = writeError(deps, "local_input_error", msg).exitCode;
2764
- return;
2866
+ if (inputs.length > 0 || !commandOptions.manifest) {
2867
+ try {
2868
+ publishInput = await resolvePublishInputs(
2869
+ inputs,
2870
+ options.stdin,
2871
+ stdinIsTTY
2872
+ );
2873
+ } catch (err) {
2874
+ const msg = err instanceof Error ? err.message : "Invalid input.";
2875
+ process.exitCode = writeError(
2876
+ deps,
2877
+ "local_input_error",
2878
+ msg
2879
+ ).exitCode;
2880
+ return;
2881
+ }
2765
2882
  }
2766
2883
  if (deps.outputMode === "human" && deps.interactive && inputs.length === 1 && typeof publishInput === "string" && isBareWordToken(publishInput) && !await pathExists(publishInput)) {
2767
2884
  const suggestion = suggestCommand(
@@ -2806,6 +2923,9 @@ ${import_picocolors6.default.bold("Canonical vs raw URLs:")}`,
2806
2923
  "--content-type <mime>",
2807
2924
  "Override MIME type (auto-detected from extension)"
2808
2925
  ).option("--path <name>", "Set filename for stdin content").option(
2926
+ "--manifest <file>",
2927
+ "Update content from a multi-file bundle defined in a JSON file (path, content/source_url/content_base64 per file). Cannot combine with a positional input."
2928
+ ).option(
2809
2929
  "--if-revision <n>",
2810
2930
  "Fail if current revision doesn't match (optimistic lock)",
2811
2931
  parseInteger
@@ -2820,20 +2940,22 @@ ${import_picocolors6.default.bold("Canonical vs raw URLs:")}`,
2820
2940
  const deps = await commandDeps(program, options, store, commandOptions);
2821
2941
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? false;
2822
2942
  let updateInput;
2823
- try {
2824
- updateInput = await resolvePublishInputs(
2825
- inputs,
2826
- options.stdin,
2827
- stdinIsTTY
2828
- );
2829
- } catch (err) {
2830
- const msg = err instanceof Error ? err.message : "Invalid input.";
2831
- process.exitCode = writeError(
2832
- deps,
2833
- "local_input_error",
2834
- msg
2835
- ).exitCode;
2836
- return;
2943
+ if (inputs.length > 0 || !commandOptions.manifest) {
2944
+ try {
2945
+ updateInput = await resolvePublishInputs(
2946
+ inputs,
2947
+ options.stdin,
2948
+ stdinIsTTY
2949
+ );
2950
+ } catch (err) {
2951
+ const msg = err instanceof Error ? err.message : "Invalid input.";
2952
+ process.exitCode = writeError(
2953
+ deps,
2954
+ "local_input_error",
2955
+ msg
2956
+ ).exitCode;
2957
+ return;
2958
+ }
2837
2959
  }
2838
2960
  let dropOpts;
2839
2961
  try {
@@ -3232,7 +3354,7 @@ ${import_picocolors6.default.bold("Canonical vs raw URLs:")}`,
3232
3354
  );
3233
3355
  const result = await runUpgrade(commandOptions, {
3234
3356
  ...deps,
3235
- currentVersion: "0.20.0"
3357
+ currentVersion: "0.21.0"
3236
3358
  });
3237
3359
  process.exitCode = result.exitCode;
3238
3360
  });
@@ -3342,6 +3464,7 @@ function toDropOptions(options) {
3342
3464
  ...options.domain ? { domain: options.domain } : {},
3343
3465
  ...options.shared ? { shared: true } : {},
3344
3466
  ...options.slug ? { slug: options.slug } : {},
3467
+ ...options.manifest ? { manifest: options.manifest } : {},
3345
3468
  ...options.json !== void 0 ? { json: options.json } : {},
3346
3469
  ...options.quiet !== void 0 ? { quiet: options.quiet } : {},
3347
3470
  ...options.url !== void 0 ? { url: options.url } : {},
@@ -3476,7 +3599,7 @@ var showNotice = shouldShowNotice({
3476
3599
  if (showNotice) {
3477
3600
  const notice = noticeFromCache({
3478
3601
  cache: updateCache,
3479
- currentVersion: "0.20.0"
3602
+ currentVersion: "0.21.0"
3480
3603
  });
3481
3604
  if (notice) {
3482
3605
  process.stderr.write(`