@dropthis/cli 0.9.3 → 0.10.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 +39 -2
- package/dist/cli.cjs +147 -17
- package/dist/cli.cjs.map +1 -1
- package/node_modules/@dropthis/node/README.md +99 -7
- package/node_modules/@dropthis/node/dist/{drops-BWA0D1IW.d.cts → drops-D0MLPPF9.d.cts} +106 -15
- package/node_modules/@dropthis/node/dist/{drops-BWA0D1IW.d.ts → drops-D0MLPPF9.d.ts} +106 -15
- package/node_modules/@dropthis/node/dist/edge.cjs +219 -38
- package/node_modules/@dropthis/node/dist/edge.cjs.map +1 -1
- package/node_modules/@dropthis/node/dist/edge.d.cts +1 -1
- package/node_modules/@dropthis/node/dist/edge.d.ts +1 -1
- package/node_modules/@dropthis/node/dist/edge.mjs +219 -38
- package/node_modules/@dropthis/node/dist/edge.mjs.map +1 -1
- package/node_modules/@dropthis/node/dist/index.cjs +226 -40
- package/node_modules/@dropthis/node/dist/index.cjs.map +1 -1
- package/node_modules/@dropthis/node/dist/index.d.cts +10 -7
- package/node_modules/@dropthis/node/dist/index.d.ts +10 -7
- package/node_modules/@dropthis/node/dist/index.mjs +226 -40
- package/node_modules/@dropthis/node/dist/index.mjs.map +1 -1
- package/node_modules/@dropthis/node/package.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -88,18 +88,19 @@ dropthis ./dist --url
|
|
|
88
88
|
dropthis publish ./dist \
|
|
89
89
|
--title "Launch page" \
|
|
90
90
|
--visibility unlisted \
|
|
91
|
-
--password s3cret \
|
|
92
91
|
--entry index.html \
|
|
93
92
|
--expires-at 2026-12-31T00:00:00Z
|
|
94
93
|
```
|
|
95
94
|
|
|
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
|
All publish flags:
|
|
97
98
|
|
|
98
99
|
| Flag | Description |
|
|
99
100
|
|------|-------------|
|
|
100
101
|
| `--title <title>` | Drop title |
|
|
101
102
|
| `--visibility <public\|unlisted>` | Drop visibility |
|
|
102
|
-
| `--password <password>` | Set password protection |
|
|
103
|
+
| `--password <password>` | Set password protection (ships with Pro; not enabled on any tier yet) |
|
|
103
104
|
| `--noindex` | Prevent search indexing |
|
|
104
105
|
| `--entry <path>` | Entry file for directories |
|
|
105
106
|
| `--content-type <mime>` | MIME type (required for stdin) |
|
|
@@ -141,6 +142,33 @@ dropthis update-content drop_abc123 ./dist-v2 --if-revision 1
|
|
|
141
142
|
|
|
142
143
|
`update-content` replaces the files at the URL; `update-settings` changes title, visibility, password, noindex, expiry, or metadata. Creating a new drop is always `publish` — neither update command makes a new URL.
|
|
143
144
|
|
|
145
|
+
With `--if-revision`, a concurrent edit fails with a 409 instead of clobbering. The error shows the drop's `current_revision` and how to retry:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
✗ Revision mismatch
|
|
149
|
+
The drop changed since it was last read. Re-read it with GET /v1/drops/{drop_id} and retry with if_revision: 2 (sent as the If-Revision header).
|
|
150
|
+
Current revision: 2 — retry with --if-revision 2
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In JSON mode the same error carries `current_revision` and a `next_action` field.
|
|
154
|
+
|
|
155
|
+
## Pull (read back)
|
|
156
|
+
|
|
157
|
+
Download what a drop is serving — by `drop_…` id, drop URL, or slug. URLs and slugs are resolved to your own drops; the local copy is also the rollback path (pull an old state, then `update-content` it back):
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# By id, into ./drop_abc123/
|
|
161
|
+
dropthis pull drop_abc123
|
|
162
|
+
|
|
163
|
+
# By URL — resolves the slug to your drop, writes into ./abc123/
|
|
164
|
+
dropthis pull https://abc123.dropthis.app
|
|
165
|
+
|
|
166
|
+
# Choose the output directory
|
|
167
|
+
dropthis pull drop_abc123 -o ./site
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Pull fetches the current deployment's file manifest and writes every file into the output directory. It is owner-side read-back via the API — it works regardless of any viewer password. Custom-domain URLs are not resolvable yet; use the `drop_…` id instead.
|
|
171
|
+
|
|
144
172
|
## Commands
|
|
145
173
|
|
|
146
174
|
```bash
|
|
@@ -149,6 +177,7 @@ dropthis publish [input...] # Same as above, explicit form
|
|
|
149
177
|
|
|
150
178
|
dropthis update-content <drop-id> [input] # Replace a drop's content (same URL)
|
|
151
179
|
dropthis update-settings <drop-id> # Change title/visibility/password/expiry/metadata
|
|
180
|
+
dropthis pull <id|url> [-o <dir>] # Download a drop's files to a local directory
|
|
152
181
|
dropthis get <drop-id> # Show drop details
|
|
153
182
|
dropthis list # List your drops
|
|
154
183
|
dropthis delete <drop-id> # Delete a drop (--yes to confirm)
|
|
@@ -233,6 +262,14 @@ Reports CLI version, auth source (`env`, `flag`, `storage`, or `missing`), and c
|
|
|
233
262
|
{"ok":true,"version":"0.4.1","auth":{"source":"env"},"storage":{"backend":"secure"}}
|
|
234
263
|
```
|
|
235
264
|
|
|
265
|
+
## Pricing tiers
|
|
266
|
+
|
|
267
|
+
- **Free ($0)** — drops expire after 7 days, 5 MB per drop, dropthis badge.
|
|
268
|
+
- **Personal ($5/mo)** — drops stay live while subscribed (no expiry), no badge, 100 MB per drop, 2 GB account storage.
|
|
269
|
+
- **Pro ($19/mo)** — everything in Personal plus password protection, custom domains, and analytics.
|
|
270
|
+
|
|
271
|
+
`dropthis account` shows your active tier and its limits. Note: password protection ships with Pro and is not enabled on any tier yet — the API rejects the `--password` option until then.
|
|
272
|
+
|
|
236
273
|
## Agent skills
|
|
237
274
|
|
|
238
275
|
For AI coding agents (Cursor, Claude Code, Windsurf, etc.), install the [dropthis-skills](https://github.com/dropthis-dev/dropthis-skills) package:
|
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((
|
|
138
|
+
return new Promise((resolve2) => {
|
|
139
139
|
rl.question(question, (answer) => {
|
|
140
140
|
rl.close();
|
|
141
|
-
|
|
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 "
|
|
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.";
|
|
@@ -371,6 +371,11 @@ function writeApiError(deps, details) {
|
|
|
371
371
|
const nextAction = nextActionForApiError(details);
|
|
372
372
|
if (nextAction) deps.stderr(`${hint(nextAction)}
|
|
373
373
|
`);
|
|
374
|
+
if (details.currentRevision !== void 0)
|
|
375
|
+
deps.stderr(
|
|
376
|
+
`${hint(`Current revision: ${details.currentRevision} \u2014 retry with --if-revision ${details.currentRevision}`)}
|
|
377
|
+
`
|
|
378
|
+
);
|
|
374
379
|
if (details.requestId)
|
|
375
380
|
deps.stderr(`${import_picocolors2.default.dim(`Request ID: ${details.requestId}`)}
|
|
376
381
|
`);
|
|
@@ -605,6 +610,13 @@ var COMMAND_ANNOTATIONS = {
|
|
|
605
610
|
auth: "required",
|
|
606
611
|
examples: ["dropthis update-settings drop_abc --title 'Launch' --json"]
|
|
607
612
|
},
|
|
613
|
+
pull: {
|
|
614
|
+
auth: "required",
|
|
615
|
+
examples: [
|
|
616
|
+
"dropthis pull drop_abc -o ./site",
|
|
617
|
+
"dropthis pull https://abc123.dropthis.app"
|
|
618
|
+
]
|
|
619
|
+
},
|
|
608
620
|
get: {
|
|
609
621
|
auth: "required",
|
|
610
622
|
examples: ["dropthis get drop_abc --json"]
|
|
@@ -769,13 +781,13 @@ async function runDoctor(_input, deps) {
|
|
|
769
781
|
const authSource = credential?.source ?? "missing";
|
|
770
782
|
const storageBackend = stored?.storage ?? "none";
|
|
771
783
|
const pairs = [
|
|
772
|
-
["Version", "0.
|
|
784
|
+
["Version", "0.10.0"],
|
|
773
785
|
["Auth", authSource],
|
|
774
786
|
["Storage", storageBackend]
|
|
775
787
|
];
|
|
776
788
|
writeKv(deps, pairs, {
|
|
777
789
|
ok: true,
|
|
778
|
-
version: "0.
|
|
790
|
+
version: "0.10.0",
|
|
779
791
|
auth: { source: authSource },
|
|
780
792
|
storage: { backend: storageBackend }
|
|
781
793
|
});
|
|
@@ -1274,6 +1286,110 @@ async function handlePublishDryRun(input, raw, deps, client) {
|
|
|
1274
1286
|
}
|
|
1275
1287
|
}
|
|
1276
1288
|
|
|
1289
|
+
// src/commands/pull.ts
|
|
1290
|
+
var import_promises2 = require("fs/promises");
|
|
1291
|
+
var import_node_path = require("path");
|
|
1292
|
+
async function runPull(target, input, deps) {
|
|
1293
|
+
try {
|
|
1294
|
+
await requireCredential({
|
|
1295
|
+
...deps.apiKey ? { apiKey: deps.apiKey } : {},
|
|
1296
|
+
env: deps.env,
|
|
1297
|
+
store: deps.store
|
|
1298
|
+
});
|
|
1299
|
+
} catch {
|
|
1300
|
+
return writeAuthError(deps);
|
|
1301
|
+
}
|
|
1302
|
+
let dropId;
|
|
1303
|
+
let defaultDirName;
|
|
1304
|
+
if (target.startsWith("drop_")) {
|
|
1305
|
+
dropId = target;
|
|
1306
|
+
defaultDirName = target;
|
|
1307
|
+
} else {
|
|
1308
|
+
const resolved = await deps.client.drops.resolve(target);
|
|
1309
|
+
if (resolved.error) return writeApiError(deps, resolved.error);
|
|
1310
|
+
if (resolved.data === null) {
|
|
1311
|
+
return writeApiError(deps, {
|
|
1312
|
+
code: "not_found",
|
|
1313
|
+
message: `No drop matching "${target}" found in your account.`,
|
|
1314
|
+
suggestion: "It may belong to another account or have been deleted. Run dropthis list to see your drops, or pass the full drop_\u2026 id."
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
dropId = resolved.data.id;
|
|
1318
|
+
defaultDirName = resolved.data.slug || resolved.data.id;
|
|
1319
|
+
}
|
|
1320
|
+
const spin = shouldSpin(deps.outputMode) ? createSpinner("Pulling content\u2026", deps.stderr) : void 0;
|
|
1321
|
+
const manifestResult = await deps.client.drops.getContent(dropId);
|
|
1322
|
+
if (manifestResult.error) {
|
|
1323
|
+
spin?.fail();
|
|
1324
|
+
return writeApiError(deps, manifestResult.error);
|
|
1325
|
+
}
|
|
1326
|
+
const manifest = manifestResult.data;
|
|
1327
|
+
const outDir = (0, import_node_path.resolve)(
|
|
1328
|
+
deps.cwd ?? process.cwd(),
|
|
1329
|
+
input.output ?? defaultDirName
|
|
1330
|
+
);
|
|
1331
|
+
let destinations;
|
|
1332
|
+
try {
|
|
1333
|
+
destinations = manifest.files.map((file) => ({
|
|
1334
|
+
path: file.path,
|
|
1335
|
+
dest: safeDestination(outDir, file.path)
|
|
1336
|
+
}));
|
|
1337
|
+
} catch (error2) {
|
|
1338
|
+
spin?.fail();
|
|
1339
|
+
const message = error2 instanceof Error ? error2.message : "Invalid manifest path.";
|
|
1340
|
+
return writeError(deps, "local_input_error", message);
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
await (0, import_promises2.mkdir)(outDir, { recursive: true });
|
|
1344
|
+
for (const { path, dest } of destinations) {
|
|
1345
|
+
const fileResult = await deps.client.drops.getContent(dropId, { path });
|
|
1346
|
+
if (fileResult.error) {
|
|
1347
|
+
spin?.fail();
|
|
1348
|
+
return writeApiError(deps, {
|
|
1349
|
+
...fileResult.error,
|
|
1350
|
+
message: `${path}: ${fileResult.error.message}`
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
const file = fileResult.data;
|
|
1354
|
+
await (0, import_promises2.mkdir)((0, import_node_path.dirname)(dest), { recursive: true });
|
|
1355
|
+
await (0, import_promises2.writeFile)(dest, file.bytes);
|
|
1356
|
+
}
|
|
1357
|
+
} catch (error2) {
|
|
1358
|
+
spin?.fail();
|
|
1359
|
+
const message = error2 instanceof Error ? error2.message : "Failed to write files.";
|
|
1360
|
+
return writeError(deps, "local_input_error", message);
|
|
1361
|
+
}
|
|
1362
|
+
spin?.stop();
|
|
1363
|
+
const count = manifest.files.length;
|
|
1364
|
+
writeHumanOrJson(
|
|
1365
|
+
deps,
|
|
1366
|
+
success(`Pulled ${count} ${count === 1 ? "file" : "files"} to ${outDir}`),
|
|
1367
|
+
{
|
|
1368
|
+
ok: true,
|
|
1369
|
+
pulled: {
|
|
1370
|
+
dropId: manifest.dropId,
|
|
1371
|
+
deploymentId: manifest.deploymentId,
|
|
1372
|
+
revision: manifest.revision,
|
|
1373
|
+
...manifest.entry !== void 0 ? { entry: manifest.entry } : {},
|
|
1374
|
+
dir: outDir,
|
|
1375
|
+
files: manifest.files
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
return { exitCode: 0 };
|
|
1380
|
+
}
|
|
1381
|
+
function safeDestination(outDir, filePath) {
|
|
1382
|
+
if (!filePath || (0, import_node_path.isAbsolute)(filePath)) {
|
|
1383
|
+
throw new Error(`Unsafe file path in content manifest: "${filePath}"`);
|
|
1384
|
+
}
|
|
1385
|
+
const dest = (0, import_node_path.join)(outDir, filePath);
|
|
1386
|
+
const rel = (0, import_node_path.relative)(outDir, dest);
|
|
1387
|
+
if (rel.startsWith("..") || (0, import_node_path.isAbsolute)(rel)) {
|
|
1388
|
+
throw new Error(`Unsafe file path in content manifest: "${filePath}"`);
|
|
1389
|
+
}
|
|
1390
|
+
return dest;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1277
1393
|
// src/commands/update-content.ts
|
|
1278
1394
|
var NO_CONTENT_MESSAGE = "update-content requires content. Provide a file, folder, URL, or text to ship a new deployment.";
|
|
1279
1395
|
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 +1807,9 @@ function createContext(input) {
|
|
|
1691
1807
|
}
|
|
1692
1808
|
|
|
1693
1809
|
// src/storage.ts
|
|
1694
|
-
var
|
|
1810
|
+
var import_promises3 = require("fs/promises");
|
|
1695
1811
|
var import_node_os = require("os");
|
|
1696
|
-
var
|
|
1812
|
+
var import_node_path2 = require("path");
|
|
1697
1813
|
var import_keyring = require("@napi-rs/keyring");
|
|
1698
1814
|
var MemoryCredentialStore = class {
|
|
1699
1815
|
credential = null;
|
|
@@ -1725,7 +1841,7 @@ var InsecureFileCredentialStore = class {
|
|
|
1725
1841
|
});
|
|
1726
1842
|
}
|
|
1727
1843
|
async clear() {
|
|
1728
|
-
await (0,
|
|
1844
|
+
await (0, import_promises3.rm)(this.filePath, { force: true });
|
|
1729
1845
|
}
|
|
1730
1846
|
};
|
|
1731
1847
|
var KeyringCredentialStore = class {
|
|
@@ -1752,7 +1868,7 @@ var KeyringCredentialStore = class {
|
|
|
1752
1868
|
}
|
|
1753
1869
|
async clear() {
|
|
1754
1870
|
await this.keyring.deletePassword();
|
|
1755
|
-
await (0,
|
|
1871
|
+
await (0, import_promises3.rm)(this.filePath, { force: true });
|
|
1756
1872
|
}
|
|
1757
1873
|
};
|
|
1758
1874
|
function createCredentialStore(options = {}) {
|
|
@@ -1770,14 +1886,14 @@ function defaultKeyringAdapter() {
|
|
|
1770
1886
|
};
|
|
1771
1887
|
}
|
|
1772
1888
|
function credentialPath(configDir) {
|
|
1773
|
-
return (0,
|
|
1774
|
-
configDir ?? (0,
|
|
1889
|
+
return (0, import_node_path2.join)(
|
|
1890
|
+
configDir ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".config", "dropthis"),
|
|
1775
1891
|
"credentials.json"
|
|
1776
1892
|
);
|
|
1777
1893
|
}
|
|
1778
1894
|
async function readJsonFile(path) {
|
|
1779
1895
|
try {
|
|
1780
|
-
return JSON.parse(await (0,
|
|
1896
|
+
return JSON.parse(await (0, import_promises3.readFile)(path, "utf8"));
|
|
1781
1897
|
} catch (error2) {
|
|
1782
1898
|
if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
|
|
1783
1899
|
return null;
|
|
@@ -1786,14 +1902,14 @@ async function readJsonFile(path) {
|
|
|
1786
1902
|
}
|
|
1787
1903
|
}
|
|
1788
1904
|
async function writeCredentialFile(path, credential) {
|
|
1789
|
-
await (0,
|
|
1905
|
+
await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
1790
1906
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
1791
|
-
await (0,
|
|
1907
|
+
await (0, import_promises3.writeFile)(tmpPath, `${JSON.stringify(credential, null, 2)}
|
|
1792
1908
|
`, {
|
|
1793
1909
|
mode: 384
|
|
1794
1910
|
});
|
|
1795
|
-
await (0,
|
|
1796
|
-
await (0,
|
|
1911
|
+
await (0, import_promises3.chmod)(tmpPath, 384);
|
|
1912
|
+
await (0, import_promises3.rename)(tmpPath, path);
|
|
1797
1913
|
}
|
|
1798
1914
|
|
|
1799
1915
|
// src/program.ts
|
|
@@ -1822,7 +1938,7 @@ function buildProgram(options = {}) {
|
|
|
1822
1938
|
` ${import_picocolors4.default.cyan("dropthis login")}`,
|
|
1823
1939
|
` ${import_picocolors4.default.cyan("dropthis publish ./dist")}`
|
|
1824
1940
|
].join("\n")
|
|
1825
|
-
).version("0.
|
|
1941
|
+
).version("0.10.0").configureHelp({
|
|
1826
1942
|
subcommandTerm(cmd) {
|
|
1827
1943
|
const args = cmd.registeredArguments.map((a) => {
|
|
1828
1944
|
const name = a.name();
|
|
@@ -1984,6 +2100,20 @@ function buildProgram(options = {}) {
|
|
|
1984
2100
|
process.exitCode = result.exitCode;
|
|
1985
2101
|
}
|
|
1986
2102
|
);
|
|
2103
|
+
program.command("pull <target>").description(
|
|
2104
|
+
"Download a drop's files into a local directory (accepts the drop_\u2026 id, the drop URL, or its slug).\nFetches the current deployment's file manifest and writes every file. Pull, edit, then update-content to ship the changes back to the same URL \u2014 also the rollback path."
|
|
2105
|
+
).option("-o, --output <dir>", "Output directory (default: ./<slug-or-id>)").option("--json", "Force JSON output").action(
|
|
2106
|
+
async (target, commandOptions) => {
|
|
2107
|
+
const deps = await commandDeps(
|
|
2108
|
+
program,
|
|
2109
|
+
options,
|
|
2110
|
+
store,
|
|
2111
|
+
commandOptions
|
|
2112
|
+
);
|
|
2113
|
+
const result = await runPull(target, commandOptions, deps);
|
|
2114
|
+
process.exitCode = result.exitCode;
|
|
2115
|
+
}
|
|
2116
|
+
);
|
|
1987
2117
|
program.command("get <dropId>").description("Show drop details (pass the full drop_\u2026 id)").option("--json", "Force JSON output").action(async (dropId, commandOptions) => {
|
|
1988
2118
|
const deps = await commandDeps(program, options, store, commandOptions);
|
|
1989
2119
|
const result = await runDropsGet(dropId, commandOptions, deps);
|