drop2run 0.5.0 → 0.7.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 +16 -3
- package/dist/index.js +636 -173
- package/dist/{package-DXmy47q4.js → package-D8mWS-nC.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ drop2run logout Remove the stored token
|
|
|
18
18
|
drop2run init [dir] [--site <subdomain>] Tie this folder to a site
|
|
19
19
|
drop2run deploy [dir] [--site <subdomain>] Publish a folder
|
|
20
20
|
drop2run init|deploy --subdomain <name> Create a new site under a name you pick
|
|
21
|
+
drop2run init|deploy --folder <path> File the new site in one of your folders
|
|
21
22
|
drop2run ls List your sites
|
|
23
|
+
drop2run folders List your folders, as --folder takes them
|
|
22
24
|
drop2run open [site] Open a site in a browser
|
|
23
25
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
24
26
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
@@ -52,6 +54,17 @@ which is not undoable.
|
|
|
52
54
|
`--site` never creates anything: it finds a site you already have, and fails when nothing matches. That
|
|
53
55
|
is what keeps a subdomain typed one letter wrong from quietly becoming a second site.
|
|
54
56
|
|
|
57
|
+
`--folder` files the site being created in one of your dashboard folders — by path, as `drop2run folders`
|
|
58
|
+
prints it, or by folder id:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
drop2run init dist --folder Clients/Acme
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Case does not matter. It never creates a folder and never moves an existing site, so it cannot be given
|
|
65
|
+
with `--site`, or to `deploy` in a project whose `drop2run.json` already names a site; a folder that does
|
|
66
|
+
not exist is refused with the list of those that do, before any site is made.
|
|
67
|
+
|
|
55
68
|
`deploy` never creates a second site while a `drop2run.json` sits next to it — that would leave the real
|
|
56
69
|
site untouched and you looking at a URL you did not expect.
|
|
57
70
|
|
|
@@ -69,7 +82,7 @@ server on `127.0.0.1`, which is what keeps it out of clipboards and scrollback.
|
|
|
69
82
|
|
|
70
83
|
### `--device`, for a machine with no browser
|
|
71
84
|
|
|
72
|
-
`drop2run login --device` prints a short code and waits. Enter it at <https://dropto.run/device> from any
|
|
85
|
+
`drop2run login --device` prints a short code and waits. Enter it at <https://app.dropto.run/device> from any
|
|
73
86
|
machine you are already signed in on — a phone will do — and the terminal picks up its token.
|
|
74
87
|
|
|
75
88
|
The short code is not the credential; it names the pending request, so somebody reading it over your
|
|
@@ -79,7 +92,7 @@ works once.
|
|
|
79
92
|
### CI
|
|
80
93
|
|
|
81
94
|
Neither flow works in CI — nothing there can open a browser or approve anything. Create a token at
|
|
82
|
-
<https://dropto.run/account/tokens> and set it in the environment:
|
|
95
|
+
<https://app.dropto.run/account/tokens> and set it in the environment:
|
|
83
96
|
|
|
84
97
|
```
|
|
85
98
|
DROP2RUN_TOKEN=d2r_...
|
|
@@ -97,7 +110,7 @@ account" is answerable in an issue report or a CI log.
|
|
|
97
110
|
|
|
98
111
|
Both need a browser session, and that is deliberate rather than missing. A token that can mint tokens is
|
|
99
112
|
not a leaked credential but a permanent one: whoever takes it makes a second, and revoking the first
|
|
100
|
-
changes nothing. Make and revoke tokens at <https://dropto.run/account/tokens>.
|
|
113
|
+
changes nothing. Make and revoke tokens at <https://app.dropto.run/account/tokens>.
|
|
101
114
|
|
|
102
115
|
`token list` does exist and answers what a terminal can answer — which machines hold a credential, and
|
|
103
116
|
which of them has not used it since it was made. It prints prefixes, never secrets.
|
package/dist/index.js
CHANGED
|
@@ -5,75 +5,13 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
8
|
-
async function call(credentials, path, init2 = {}) {
|
|
9
|
-
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
10
|
-
...init2,
|
|
11
|
-
headers: {
|
|
12
|
-
...init2.headers,
|
|
13
|
-
Authorization: `Bearer ${credentials.token}`,
|
|
14
|
-
...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
const problem = await response.json().catch(() => null);
|
|
19
|
-
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
20
|
-
}
|
|
21
|
-
return await response.json();
|
|
22
|
-
}
|
|
23
|
-
async function findSite(credentials, site) {
|
|
24
|
-
const wanted = site.trim().toLowerCase();
|
|
25
|
-
const found = (await listSites(credentials)).find(
|
|
26
|
-
(candidate) => candidate.siteId.toLowerCase() === wanted || candidate.subdomain.toLowerCase() === wanted
|
|
27
|
-
);
|
|
28
|
-
if (found === void 0) {
|
|
29
|
-
throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
|
|
30
|
-
}
|
|
31
|
-
return found;
|
|
32
|
-
}
|
|
33
|
-
async function deleteSite(credentials, siteId) {
|
|
34
|
-
const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
|
|
35
|
-
method: "DELETE",
|
|
36
|
-
headers: { Authorization: `Bearer ${credentials.token}` }
|
|
37
|
-
});
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
const problem = await response.json().catch(() => null);
|
|
40
|
-
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function promoteDeploy(credentials, siteId, deployId) {
|
|
44
|
-
return await call(
|
|
45
|
-
credentials,
|
|
46
|
-
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
47
|
-
{ method: "POST" }
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
async function listTokens(credentials) {
|
|
51
|
-
const body = await call(credentials, "tokens");
|
|
52
|
-
return body.tokens;
|
|
53
|
-
}
|
|
54
|
-
async function listSites(credentials) {
|
|
55
|
-
const body = await call(credentials, "sites");
|
|
56
|
-
return body.sites;
|
|
57
|
-
}
|
|
58
|
-
async function createSite(credentials, subdomain) {
|
|
59
|
-
const created = await call(
|
|
60
|
-
credentials,
|
|
61
|
-
"sites",
|
|
62
|
-
{
|
|
63
|
-
method: "POST",
|
|
64
|
-
// An absent field rather than a null one, because the API reads null, empty and missing the
|
|
65
|
-
// same way and a body carrying only what was asked for is the one that reads correctly in a
|
|
66
|
-
// log.
|
|
67
|
-
body: JSON.stringify(subdomain === void 0 ? {} : { subdomain })
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
return { ...created, name: null };
|
|
71
|
-
}
|
|
72
8
|
const TOKEN_VARIABLE = "DROP2RUN_TOKEN";
|
|
73
9
|
function configPath() {
|
|
74
10
|
return join(homedir(), ".config", "drop2run", "config.json");
|
|
75
11
|
}
|
|
76
12
|
const DEFAULT_API_BASE_URL = "https://dropto.run/api";
|
|
13
|
+
const PRODUCTION_SITE_ORIGIN = "https://dropto.run";
|
|
14
|
+
const PRODUCTION_DASHBOARD_ORIGIN = "https://app.dropto.run";
|
|
77
15
|
function loadCredentials(env = process.env, read = (path) => readFileSync(path, "utf8")) {
|
|
78
16
|
let stored = {};
|
|
79
17
|
try {
|
|
@@ -97,7 +35,8 @@ function resolveApiBaseUrl(env = process.env, read = (path) => readFileSync(path
|
|
|
97
35
|
return url.replace(/\/+$/, "");
|
|
98
36
|
}
|
|
99
37
|
function dashboardUrlFor(apiBaseUrl) {
|
|
100
|
-
|
|
38
|
+
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
39
|
+
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
101
40
|
}
|
|
102
41
|
function saveToken(token, path = configPath()) {
|
|
103
42
|
let existing = {};
|
|
@@ -140,14 +79,14 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
140
79
|
] : ["Run `drop2run login` to sign in through a browser, which stores one for you."];
|
|
141
80
|
const byHand = surface === "mcp" ? [
|
|
142
81
|
"To set one without signing in here, create a token at",
|
|
143
|
-
"https://dropto.run/account/tokens and either:",
|
|
82
|
+
"https://app.dropto.run/account/tokens and either:",
|
|
144
83
|
` - put it in ${configPath()} as {"token": "d2r_..."}, which this`,
|
|
145
84
|
" server reads on every call and needs no restart, or",
|
|
146
85
|
` - set ${TOKEN_VARIABLE} in the environment and restart this server, since it reads`,
|
|
147
86
|
" the environment it was started in."
|
|
148
87
|
] : [
|
|
149
88
|
"For CI, or where no browser can be opened, create a token at",
|
|
150
|
-
"https://dropto.run/account/tokens and either:",
|
|
89
|
+
"https://app.dropto.run/account/tokens and either:",
|
|
151
90
|
` - set ${TOKEN_VARIABLE} in the environment, or`,
|
|
152
91
|
` - put it in ${configPath()} as {"token": "d2r_..."}`
|
|
153
92
|
];
|
|
@@ -161,6 +100,142 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
161
100
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
162
101
|
].join("\n");
|
|
163
102
|
}
|
|
103
|
+
async function call(credentials, path, init2 = {}) {
|
|
104
|
+
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
105
|
+
...init2,
|
|
106
|
+
headers: {
|
|
107
|
+
...init2.headers,
|
|
108
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
109
|
+
...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const problem = await response.json().catch(() => null);
|
|
114
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
115
|
+
}
|
|
116
|
+
return await response.json();
|
|
117
|
+
}
|
|
118
|
+
async function findSite(credentials, site) {
|
|
119
|
+
const found = await lookUpSite(credentials, site);
|
|
120
|
+
if (found === void 0) {
|
|
121
|
+
throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
|
|
126
|
+
async function lookUpSite(credentials, site) {
|
|
127
|
+
const wanted = site.trim().toLowerCase();
|
|
128
|
+
if (wanted === "") return void 0;
|
|
129
|
+
const { sites } = await call(
|
|
130
|
+
credentials,
|
|
131
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(wanted)}`
|
|
132
|
+
);
|
|
133
|
+
const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
|
|
134
|
+
if (bySubdomain !== void 0) return bySubdomain;
|
|
135
|
+
if (!SITE_ID_SHAPE.test(wanted)) return void 0;
|
|
136
|
+
const response = await fetch(
|
|
137
|
+
`${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
|
|
138
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` } }
|
|
139
|
+
);
|
|
140
|
+
if (response.status === 404) return void 0;
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const problem = await response.json().catch(() => null);
|
|
143
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
144
|
+
}
|
|
145
|
+
const detail = await response.json();
|
|
146
|
+
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
147
|
+
}
|
|
148
|
+
async function deleteSite(credentials, siteId) {
|
|
149
|
+
const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
|
|
150
|
+
method: "DELETE",
|
|
151
|
+
headers: { Authorization: `Bearer ${credentials.token}` }
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const problem = await response.json().catch(() => null);
|
|
155
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function promoteDeploy(credentials, siteId, deployId) {
|
|
159
|
+
return await call(
|
|
160
|
+
credentials,
|
|
161
|
+
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
162
|
+
{ method: "POST" }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
async function listTokens(credentials) {
|
|
166
|
+
const body = await call(credentials, "tokens");
|
|
167
|
+
return body.tokens;
|
|
168
|
+
}
|
|
169
|
+
const SITES_PER_REQUEST = 100;
|
|
170
|
+
async function listSites(credentials) {
|
|
171
|
+
const body = await call(
|
|
172
|
+
credentials,
|
|
173
|
+
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
174
|
+
);
|
|
175
|
+
return { sites: body.sites, total: body.total };
|
|
176
|
+
}
|
|
177
|
+
async function createSite(credentials, subdomain, folderId) {
|
|
178
|
+
const created = await call(
|
|
179
|
+
credentials,
|
|
180
|
+
"sites",
|
|
181
|
+
{
|
|
182
|
+
method: "POST",
|
|
183
|
+
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
184
|
+
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
185
|
+
body: JSON.stringify({
|
|
186
|
+
...subdomain === void 0 ? {} : { subdomain },
|
|
187
|
+
...folderId === void 0 ? {} : { folderId }
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
return { ...created, name: null };
|
|
192
|
+
}
|
|
193
|
+
async function listFolders(credentials) {
|
|
194
|
+
const body = await call(credentials, "site-folders");
|
|
195
|
+
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
196
|
+
const pathOf = (folderId) => {
|
|
197
|
+
const names = [];
|
|
198
|
+
let cursor = byId.get(folderId);
|
|
199
|
+
while (cursor !== void 0 && names.length <= byId.size) {
|
|
200
|
+
names.unshift(cursor.name);
|
|
201
|
+
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
202
|
+
}
|
|
203
|
+
return names.join("/");
|
|
204
|
+
};
|
|
205
|
+
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
206
|
+
}
|
|
207
|
+
function comparablePath(path) {
|
|
208
|
+
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
const FOLDERS_LISTED_ON_MISS = 20;
|
|
211
|
+
async function resolveFolder(credentials, folder) {
|
|
212
|
+
const wanted = comparablePath(folder);
|
|
213
|
+
if (wanted === "") {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const folders2 = await listFolders(credentials);
|
|
219
|
+
const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
220
|
+
if (byId !== void 0) return byId.folderId;
|
|
221
|
+
const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
222
|
+
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
223
|
+
if (matches.length > 1) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (folders2.length === 0) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
234
|
+
const more = folders2.length - listed.length;
|
|
235
|
+
throw new Error(
|
|
236
|
+
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
164
239
|
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
165
240
|
function newAttempt() {
|
|
166
241
|
const verifier = randomBytes(32).toString("base64url");
|
|
@@ -337,6 +412,12 @@ function writeProject(project, directory = process.cwd()) {
|
|
|
337
412
|
`);
|
|
338
413
|
return path;
|
|
339
414
|
}
|
|
415
|
+
function sizeOf(content) {
|
|
416
|
+
return ArrayBuffer.isView(content) ? content.byteLength : content.size;
|
|
417
|
+
}
|
|
418
|
+
async function bytesOf(content) {
|
|
419
|
+
return ArrayBuffer.isView(content) ? content : new Uint8Array(await content.arrayBuffer());
|
|
420
|
+
}
|
|
340
421
|
class DeployError extends Error {
|
|
341
422
|
/**
|
|
342
423
|
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
@@ -373,20 +454,55 @@ const ClientErrorCode = {
|
|
|
373
454
|
/** The user cancelled. */
|
|
374
455
|
Cancelled: "cancelled",
|
|
375
456
|
/** An upload kept failing after every retry. */
|
|
376
|
-
UploadFailed: "upload_failed"
|
|
457
|
+
UploadFailed: "upload_failed",
|
|
458
|
+
/**
|
|
459
|
+
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
460
|
+
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
461
|
+
* names memory.
|
|
462
|
+
*/
|
|
463
|
+
OutOfMemory: "out_of_memory",
|
|
464
|
+
/**
|
|
465
|
+
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
466
|
+
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
467
|
+
*/
|
|
468
|
+
ReadFailed: "read_failed",
|
|
469
|
+
/**
|
|
470
|
+
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
471
|
+
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
472
|
+
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
473
|
+
*/
|
|
474
|
+
NetworkFailed: "network_failed"
|
|
377
475
|
};
|
|
476
|
+
function describeError(error) {
|
|
477
|
+
const parts = [];
|
|
478
|
+
let current = error;
|
|
479
|
+
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
480
|
+
if (current instanceof Error) {
|
|
481
|
+
const code = current.code;
|
|
482
|
+
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
483
|
+
current = current.cause;
|
|
484
|
+
} else {
|
|
485
|
+
parts.push(String(current));
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return parts.join(" ← ");
|
|
490
|
+
}
|
|
378
491
|
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
379
492
|
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
493
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
494
|
+
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
380
495
|
async function prepareDeploy(siteId, files, options, signal) {
|
|
381
496
|
const manifest = files.map((file) => ({
|
|
382
497
|
path: file.path,
|
|
383
498
|
sha256: file.sha256,
|
|
384
|
-
size: file.bytes
|
|
499
|
+
size: sizeOf(file.bytes)
|
|
385
500
|
}));
|
|
386
501
|
const prepared = await request(
|
|
387
502
|
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
388
503
|
{ files: manifest },
|
|
389
|
-
options
|
|
504
|
+
options,
|
|
505
|
+
signal
|
|
390
506
|
);
|
|
391
507
|
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
392
508
|
}
|
|
@@ -396,31 +512,65 @@ function absolute(uploadUrl, baseUrl) {
|
|
|
396
512
|
return new URL(uploadUrl, baseUrl).toString();
|
|
397
513
|
}
|
|
398
514
|
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
515
|
+
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
516
|
+
const body = name === null || name === void 0 ? void 0 : { name };
|
|
517
|
+
const delay = options.delay ?? wait;
|
|
518
|
+
for (let attempt = 0; ; attempt++) {
|
|
519
|
+
try {
|
|
520
|
+
return await request(path, body, options, signal);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
523
|
+
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
524
|
+
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function wait(ms) {
|
|
529
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
407
530
|
}
|
|
408
531
|
async function request(path, body, options, signal) {
|
|
409
532
|
const doFetch = options.fetch ?? globalFetch$1();
|
|
410
533
|
const base = options.baseUrl ?? "/api";
|
|
534
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
411
535
|
const headers = {};
|
|
412
536
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
413
537
|
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
414
538
|
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
415
539
|
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
|
|
540
|
+
const controller = new AbortController();
|
|
541
|
+
let timedOut = false;
|
|
542
|
+
const timer = setTimeout(() => {
|
|
543
|
+
timedOut = true;
|
|
544
|
+
controller.abort();
|
|
545
|
+
}, timeoutMs);
|
|
546
|
+
try {
|
|
547
|
+
const response = await doFetch(`${base}/${path}`, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers,
|
|
550
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
551
|
+
signal: controller.signal
|
|
552
|
+
});
|
|
553
|
+
if (!response.ok) throw await problemToError(response);
|
|
554
|
+
return await response.json();
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
557
|
+
if (timedOut) {
|
|
558
|
+
throw new DeployError(
|
|
559
|
+
ClientErrorCode.NetworkFailed,
|
|
560
|
+
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
561
|
+
{ path, cause: describeError(error) }
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if (error instanceof TypeError) {
|
|
565
|
+
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
566
|
+
path,
|
|
567
|
+
cause: describeError(error)
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
throw error;
|
|
571
|
+
} finally {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
}
|
|
424
574
|
}
|
|
425
575
|
function globalFetch$1() {
|
|
426
576
|
return globalThis.fetch.bind(globalThis);
|
|
@@ -919,17 +1069,29 @@ function extensionOf(path) {
|
|
|
919
1069
|
return dot <= 0 ? "" : name.slice(dot).toLowerCase();
|
|
920
1070
|
}
|
|
921
1071
|
async function sha256Hex(bytes) {
|
|
922
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes
|
|
1072
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
923
1073
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
924
1074
|
}
|
|
925
1075
|
async function hashAll(files, onProgress, signal) {
|
|
926
1076
|
const hashed = [];
|
|
927
1077
|
for (const [index, file] of files.entries()) {
|
|
928
|
-
hashed.push({ ...file, sha256: await sha256Hex(file
|
|
1078
|
+
hashed.push({ ...file, sha256: await sha256Hex(await readForHash(file)) });
|
|
929
1079
|
onProgress?.(index + 1, files.length);
|
|
930
1080
|
}
|
|
931
1081
|
return hashed;
|
|
932
1082
|
}
|
|
1083
|
+
async function readForHash(file) {
|
|
1084
|
+
try {
|
|
1085
|
+
return await bytesOf(file.bytes);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
if (error instanceof RangeError) throw error;
|
|
1088
|
+
throw new DeployError(
|
|
1089
|
+
ClientErrorCode.ReadFailed,
|
|
1090
|
+
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
1091
|
+
{ path: file.path, cause: describeError(error) }
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
933
1095
|
const IGNORED_EXACT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".gitignore", ".gitkeep"]);
|
|
934
1096
|
const IGNORED_DIRECTORIES = [
|
|
935
1097
|
"__MACOSX",
|
|
@@ -939,7 +1101,11 @@ const IGNORED_DIRECTORIES = [
|
|
|
939
1101
|
".hg",
|
|
940
1102
|
".idea",
|
|
941
1103
|
".vscode",
|
|
942
|
-
".claude"
|
|
1104
|
+
".claude",
|
|
1105
|
+
".agents",
|
|
1106
|
+
".cursor",
|
|
1107
|
+
".codex",
|
|
1108
|
+
".gemini"
|
|
943
1109
|
];
|
|
944
1110
|
function shouldIgnore(path) {
|
|
945
1111
|
const segments = path.split("/");
|
|
@@ -1001,7 +1167,7 @@ function checkLimits(files, limits) {
|
|
|
1001
1167
|
);
|
|
1002
1168
|
}
|
|
1003
1169
|
const refusal = sizeRefusal(
|
|
1004
|
-
files.map((file) => ({ path: file.path, size: file.bytes
|
|
1170
|
+
files.map((file) => ({ path: file.path, size: sizeOf(file.bytes) })),
|
|
1005
1171
|
limits
|
|
1006
1172
|
);
|
|
1007
1173
|
if (refusal) throw refusal;
|
|
@@ -1042,13 +1208,24 @@ const HEAD_BYTES = 64 * 1024;
|
|
|
1042
1208
|
const TITLE_SEPARATORS = ["|", "·", "—", "–", " - "];
|
|
1043
1209
|
function suggestSiteName(files) {
|
|
1044
1210
|
const index = files.find((file) => file.path === "index.html");
|
|
1045
|
-
if (index === void 0) return null;
|
|
1211
|
+
if (index === void 0 || !ArrayBuffer.isView(index.bytes)) return null;
|
|
1046
1212
|
try {
|
|
1047
1213
|
return nameFromHtml(decodeHead(index.bytes));
|
|
1048
1214
|
} catch {
|
|
1049
1215
|
return null;
|
|
1050
1216
|
}
|
|
1051
1217
|
}
|
|
1218
|
+
async function readSiteName(files) {
|
|
1219
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1220
|
+
if (index === void 0) return null;
|
|
1221
|
+
if (ArrayBuffer.isView(index.bytes)) return suggestSiteName(files);
|
|
1222
|
+
try {
|
|
1223
|
+
const head = new Uint8Array(await index.bytes.slice(0, HEAD_BYTES).arrayBuffer());
|
|
1224
|
+
return suggestSiteName([{ path: index.path, bytes: head }]);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1052
1229
|
function decodeHead(bytes) {
|
|
1053
1230
|
return new TextDecoder("utf-8", { fatal: false }).decode(
|
|
1054
1231
|
bytes.subarray(0, HEAD_BYTES)
|
|
@@ -1080,7 +1257,7 @@ function matchTag(html, tag) {
|
|
|
1080
1257
|
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(
|
|
1081
1258
|
html
|
|
1082
1259
|
);
|
|
1083
|
-
return match?.[1]
|
|
1260
|
+
return match?.[1] === void 0 ? null : decodeEntities(match[1]);
|
|
1084
1261
|
}
|
|
1085
1262
|
function matchOgSiteName(html) {
|
|
1086
1263
|
const patterns = [
|
|
@@ -1089,10 +1266,56 @@ function matchOgSiteName(html) {
|
|
|
1089
1266
|
];
|
|
1090
1267
|
for (const pattern of patterns) {
|
|
1091
1268
|
const match = pattern.exec(html);
|
|
1092
|
-
if (match?.[1] !== void 0) return match[1];
|
|
1269
|
+
if (match?.[1] !== void 0) return decodeEntities(match[1]);
|
|
1093
1270
|
}
|
|
1094
1271
|
return null;
|
|
1095
1272
|
}
|
|
1273
|
+
const LATIN1_ENTITIES = "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml".split(" ");
|
|
1274
|
+
const NAMED_ENTITIES = new Map([
|
|
1275
|
+
...LATIN1_ENTITIES.map((name, index) => [name, 160 + index]),
|
|
1276
|
+
["amp", 38],
|
|
1277
|
+
["lt", 60],
|
|
1278
|
+
["gt", 62],
|
|
1279
|
+
["quot", 34],
|
|
1280
|
+
["apos", 39],
|
|
1281
|
+
["ensp", 8194],
|
|
1282
|
+
["emsp", 8195],
|
|
1283
|
+
["thinsp", 8201],
|
|
1284
|
+
["ndash", 8211],
|
|
1285
|
+
["mdash", 8212],
|
|
1286
|
+
["lsquo", 8216],
|
|
1287
|
+
["rsquo", 8217],
|
|
1288
|
+
["sbquo", 8218],
|
|
1289
|
+
["ldquo", 8220],
|
|
1290
|
+
["rdquo", 8221],
|
|
1291
|
+
["bdquo", 8222],
|
|
1292
|
+
["dagger", 8224],
|
|
1293
|
+
["Dagger", 8225],
|
|
1294
|
+
["bull", 8226],
|
|
1295
|
+
["hellip", 8230],
|
|
1296
|
+
["prime", 8242],
|
|
1297
|
+
["lsaquo", 8249],
|
|
1298
|
+
["rsaquo", 8250],
|
|
1299
|
+
["euro", 8364],
|
|
1300
|
+
["trade", 8482],
|
|
1301
|
+
["larr", 8592],
|
|
1302
|
+
["rarr", 8594],
|
|
1303
|
+
["hearts", 9829]
|
|
1304
|
+
]);
|
|
1305
|
+
function decodeEntities(text) {
|
|
1306
|
+
return text.replace(
|
|
1307
|
+
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1308
|
+
(whole, decimal, hex, name) => {
|
|
1309
|
+
if (name !== void 0) {
|
|
1310
|
+
const code2 = NAMED_ENTITIES.get(name);
|
|
1311
|
+
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1312
|
+
}
|
|
1313
|
+
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1314
|
+
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1315
|
+
return String.fromCodePoint(valid ? code : 65533);
|
|
1316
|
+
}
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1096
1319
|
const INVISIBLE = new RegExp(
|
|
1097
1320
|
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}${String.fromCharCode(8203)}-${String.fromCharCode(8207)}${String.fromCharCode(8232)}-${String.fromCharCode(8238)}${String.fromCharCode(8288)}-${String.fromCharCode(8292)}${String.fromCharCode(8294)}-${String.fromCharCode(8303)}${String.fromCharCode(65279)}]`,
|
|
1098
1321
|
"g"
|
|
@@ -1134,46 +1357,105 @@ function trimSuffix(title) {
|
|
|
1134
1357
|
return title;
|
|
1135
1358
|
}
|
|
1136
1359
|
const DEFAULT_CONCURRENCY = 8;
|
|
1360
|
+
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1137
1361
|
const DEFAULT_RETRIES = 3;
|
|
1138
1362
|
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1139
|
-
|
|
1140
|
-
|
|
1363
|
+
const DEFAULT_STALL_MS = 3e4;
|
|
1364
|
+
const PROGRESS_INTERVAL_MS = 200;
|
|
1365
|
+
class StalledError extends Error {
|
|
1366
|
+
/** @param seconds How long nothing moved. */
|
|
1367
|
+
constructor(seconds) {
|
|
1368
|
+
super(`No data moved for ${seconds}s.`);
|
|
1369
|
+
this.name = "StalledError";
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {}, onRetry) {
|
|
1373
|
+
const put = deps.put ?? defaultTransport(deps.fetch);
|
|
1141
1374
|
const delay = deps.delay ?? defaultDelay;
|
|
1142
1375
|
const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1143
1376
|
const retries = deps.retries ?? DEFAULT_RETRIES;
|
|
1377
|
+
const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
|
|
1378
|
+
const now = deps.now ?? Date.now;
|
|
1379
|
+
const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
|
|
1144
1380
|
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1145
1381
|
let nextIndex = 0;
|
|
1146
1382
|
let done = 0;
|
|
1147
|
-
let
|
|
1383
|
+
let failed = false;
|
|
1384
|
+
let reserved = 0;
|
|
1385
|
+
let waiting = [];
|
|
1386
|
+
const acquire = async (size) => {
|
|
1387
|
+
while (reserved > 0 && reserved + size > maxBytesInFlight) {
|
|
1388
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
1389
|
+
}
|
|
1390
|
+
reserved += size;
|
|
1391
|
+
};
|
|
1392
|
+
const release = (size) => {
|
|
1393
|
+
reserved -= size;
|
|
1394
|
+
const woken = waiting;
|
|
1395
|
+
waiting = [];
|
|
1396
|
+
for (const wake of woken) wake();
|
|
1397
|
+
};
|
|
1398
|
+
let landedBytes = 0;
|
|
1399
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1400
|
+
let lastReport = Number.NEGATIVE_INFINITY;
|
|
1401
|
+
const report = (path) => {
|
|
1402
|
+
if (onProgress === void 0) return;
|
|
1403
|
+
const at = now();
|
|
1404
|
+
if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
|
|
1405
|
+
lastReport = at;
|
|
1406
|
+
let bytes = landedBytes;
|
|
1407
|
+
for (const sent of inFlight.values()) bytes += sent;
|
|
1408
|
+
onProgress(done, targets.length, bytes, path);
|
|
1409
|
+
};
|
|
1148
1410
|
const worker = async () => {
|
|
1149
1411
|
for (; ; ) {
|
|
1412
|
+
if (failed) return;
|
|
1150
1413
|
const index = nextIndex++;
|
|
1151
1414
|
const target = targets[index];
|
|
1152
1415
|
if (target === void 0) return;
|
|
1153
1416
|
const file = byPath.get(target.path);
|
|
1154
1417
|
if (file === void 0) {
|
|
1418
|
+
failed = true;
|
|
1155
1419
|
throw new DeployError(
|
|
1156
1420
|
ClientErrorCode.UploadFailed,
|
|
1157
1421
|
`The server asked for ${target.path}, which is not in the manifest.`,
|
|
1158
1422
|
{ path: target.path }
|
|
1159
1423
|
);
|
|
1160
1424
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
signal
|
|
1166
|
-
|
|
1167
|
-
|
|
1425
|
+
const size = sizeOf(file.bytes);
|
|
1426
|
+
await acquire(size);
|
|
1427
|
+
try {
|
|
1428
|
+
if (failed) return;
|
|
1429
|
+
signal?.throwIfAborted();
|
|
1430
|
+
await uploadOne(target, file, {
|
|
1431
|
+
put,
|
|
1432
|
+
delay,
|
|
1433
|
+
retries,
|
|
1434
|
+
signal,
|
|
1435
|
+
uploadUrl,
|
|
1436
|
+
stallMs,
|
|
1437
|
+
onSent: (sent) => {
|
|
1438
|
+
inFlight.set(index, sent);
|
|
1439
|
+
report();
|
|
1440
|
+
},
|
|
1441
|
+
onRetry: (attempt, stalled) => {
|
|
1442
|
+
inFlight.set(index, 0);
|
|
1443
|
+
onRetry?.(target.path, attempt, retries + 1, stalled);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
failed = true;
|
|
1448
|
+
throw error;
|
|
1449
|
+
} finally {
|
|
1450
|
+
release(size);
|
|
1451
|
+
}
|
|
1452
|
+
inFlight.delete(index);
|
|
1168
1453
|
done += 1;
|
|
1169
|
-
|
|
1170
|
-
|
|
1454
|
+
landedBytes += size;
|
|
1455
|
+
report(file.path);
|
|
1171
1456
|
}
|
|
1172
1457
|
};
|
|
1173
|
-
const workers = Array.from(
|
|
1174
|
-
{ length: Math.min(concurrency, targets.length) },
|
|
1175
|
-
worker
|
|
1176
|
-
);
|
|
1458
|
+
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, worker);
|
|
1177
1459
|
try {
|
|
1178
1460
|
await Promise.all(workers);
|
|
1179
1461
|
} catch (error) {
|
|
@@ -1185,39 +1467,122 @@ async function uploadOne(target, file, context) {
|
|
|
1185
1467
|
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1186
1468
|
context.signal?.throwIfAborted();
|
|
1187
1469
|
if (attempt > 0) {
|
|
1470
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1188
1471
|
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1189
1472
|
context.signal?.throwIfAborted();
|
|
1190
1473
|
}
|
|
1191
1474
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
...context.signal ? { signal: context.signal } : {}
|
|
1475
|
+
const status = await context.put({
|
|
1476
|
+
url: context.uploadUrl,
|
|
1477
|
+
token: target.token,
|
|
1478
|
+
body: untyped(file.bytes),
|
|
1479
|
+
signal: context.signal,
|
|
1480
|
+
onSent: context.onSent,
|
|
1481
|
+
stallMs: context.stallMs
|
|
1200
1482
|
});
|
|
1201
|
-
if (
|
|
1202
|
-
if (
|
|
1483
|
+
if (status >= 200 && status < 300) return;
|
|
1484
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
1203
1485
|
throw new DeployError(
|
|
1204
1486
|
ClientErrorCode.UploadFailed,
|
|
1205
1487
|
`Upload of ${target.path} was refused. The upload window may have expired — try deploying again.`,
|
|
1206
|
-
{ path: target.path, status
|
|
1488
|
+
{ path: target.path, status }
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
if (status === 413) {
|
|
1492
|
+
throw new DeployError(
|
|
1493
|
+
ClientErrorCode.UploadFailed,
|
|
1494
|
+
`${target.path} is too large to upload in one request. Make it smaller, or leave it out of this publish.`,
|
|
1495
|
+
{ path: target.path, status }
|
|
1207
1496
|
);
|
|
1208
1497
|
}
|
|
1209
|
-
lastError = new Error(`HTTP ${
|
|
1498
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1210
1499
|
} catch (error) {
|
|
1211
1500
|
if (error instanceof DeployError) throw error;
|
|
1212
1501
|
if (isAbort(error)) throw cancelled();
|
|
1213
1502
|
lastError = error;
|
|
1214
1503
|
}
|
|
1215
1504
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
);
|
|
1505
|
+
const message = lastError instanceof StalledError ? `Upload of ${target.path} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${target.path} after ${context.retries + 1} attempts.`;
|
|
1506
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1507
|
+
path: target.path,
|
|
1508
|
+
cause: describeError(lastError)
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
function untyped(content) {
|
|
1512
|
+
if (ArrayBuffer.isView(content) || content.type === "") return content;
|
|
1513
|
+
return content.slice(0, content.size);
|
|
1514
|
+
}
|
|
1515
|
+
function defaultTransport(doFetch) {
|
|
1516
|
+
if (doFetch !== void 0) return fetchTransport(doFetch);
|
|
1517
|
+
const Xhr = globalThis.XMLHttpRequest;
|
|
1518
|
+
return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
|
|
1519
|
+
}
|
|
1520
|
+
function fetchTransport(doFetch) {
|
|
1521
|
+
return async ({ url, token, body, signal }) => {
|
|
1522
|
+
const response = await doFetch(url, {
|
|
1523
|
+
method: "PUT",
|
|
1524
|
+
// The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
|
|
1525
|
+
// full-size copies of every file in flight — eight at once, so up to 400 MB on a drop of
|
|
1526
|
+
// 50 MB files. `fetch` sends exactly the range a view covers. The cast is TypeScript's
|
|
1527
|
+
// SharedArrayBuffer distinction again; see `sha256Hex`. A `Blob` goes as it is, and the runtime
|
|
1528
|
+
// streams it from disk.
|
|
1529
|
+
body,
|
|
1530
|
+
// The permit is the whole request. `Content-Length` is required by the Worker and cannot be
|
|
1531
|
+
// set here — it is a forbidden header name — but the runtime derives it from the body, which
|
|
1532
|
+
// is exactly the length the token names.
|
|
1533
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1534
|
+
...signal ? { signal } : {}
|
|
1535
|
+
});
|
|
1536
|
+
return response.status;
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function xhrTransport(Xhr) {
|
|
1540
|
+
return ({ url, token, body, signal, onSent, stallMs }) => new Promise((resolve2, reject) => {
|
|
1541
|
+
const xhr = new Xhr();
|
|
1542
|
+
let settled = false;
|
|
1543
|
+
let timer;
|
|
1544
|
+
const settle = (finish) => {
|
|
1545
|
+
if (settled) return;
|
|
1546
|
+
settled = true;
|
|
1547
|
+
clearTimeout(timer);
|
|
1548
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1549
|
+
finish();
|
|
1550
|
+
};
|
|
1551
|
+
const arm = () => {
|
|
1552
|
+
clearTimeout(timer);
|
|
1553
|
+
timer = setTimeout(() => {
|
|
1554
|
+
xhr.abort();
|
|
1555
|
+
settle(() => reject(new StalledError(Math.round(stallMs / 1e3))));
|
|
1556
|
+
}, stallMs);
|
|
1557
|
+
};
|
|
1558
|
+
function onAbort() {
|
|
1559
|
+
xhr.abort();
|
|
1560
|
+
settle(() => reject(abortReason(signal)));
|
|
1561
|
+
}
|
|
1562
|
+
if (signal?.aborted) {
|
|
1563
|
+
reject(abortReason(signal));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1567
|
+
xhr.open("PUT", url);
|
|
1568
|
+
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
|
|
1569
|
+
xhr.upload.onprogress = (event) => {
|
|
1570
|
+
arm();
|
|
1571
|
+
onSent(event.loaded);
|
|
1572
|
+
};
|
|
1573
|
+
xhr.onprogress = arm;
|
|
1574
|
+
xhr.onload = () => settle(() => resolve2(xhr.status));
|
|
1575
|
+
xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
|
|
1576
|
+
arm();
|
|
1577
|
+
xhr.send(body);
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
function abortReason(signal) {
|
|
1581
|
+
const reason = signal?.reason;
|
|
1582
|
+
if (reason instanceof Error) return reason;
|
|
1583
|
+
const error = new Error("Aborted.");
|
|
1584
|
+
error.name = "AbortError";
|
|
1585
|
+
return error;
|
|
1221
1586
|
}
|
|
1222
1587
|
function globalFetch() {
|
|
1223
1588
|
return globalThis.fetch.bind(globalThis);
|
|
@@ -1231,9 +1596,17 @@ function isAbort(error) {
|
|
|
1231
1596
|
function cancelled() {
|
|
1232
1597
|
return new DeployError(ClientErrorCode.Cancelled, "Deploy cancelled.");
|
|
1233
1598
|
}
|
|
1599
|
+
function outOfMemory(detail) {
|
|
1600
|
+
return new DeployError(
|
|
1601
|
+
ClientErrorCode.OutOfMemory,
|
|
1602
|
+
"This browser ran out of memory reading what you dropped. Drop the unzipped folder instead, or try again in a desktop browser with fewer tabs open.",
|
|
1603
|
+
detail
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1234
1606
|
function asDeployError(error) {
|
|
1235
1607
|
if (error instanceof DeployError) return error;
|
|
1236
1608
|
if (isAbort(error)) return cancelled();
|
|
1609
|
+
if (error instanceof RangeError) return outOfMemory(error.message);
|
|
1237
1610
|
return new DeployError(
|
|
1238
1611
|
ClientErrorCode.UploadFailed,
|
|
1239
1612
|
error instanceof Error ? error.message : "Upload failed.",
|
|
@@ -1242,7 +1615,11 @@ function asDeployError(error) {
|
|
|
1242
1615
|
}
|
|
1243
1616
|
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
1244
1617
|
try {
|
|
1245
|
-
|
|
1618
|
+
onProgress({ type: "reading", bytes: 0, total: 0 });
|
|
1619
|
+
const collected = await source(
|
|
1620
|
+
signal,
|
|
1621
|
+
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
1622
|
+
);
|
|
1246
1623
|
onProgress({ type: "collecting", files: collected.length });
|
|
1247
1624
|
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
1248
1625
|
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
@@ -1258,6 +1635,21 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1258
1635
|
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1259
1636
|
return;
|
|
1260
1637
|
}
|
|
1638
|
+
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1639
|
+
const totalBytes = prepared.upload.reduce(
|
|
1640
|
+
(sum, target) => sum + (sizes.get(target.path) ?? 0),
|
|
1641
|
+
0
|
|
1642
|
+
);
|
|
1643
|
+
if (prepared.upload.length > 0) {
|
|
1644
|
+
onProgress({
|
|
1645
|
+
type: "uploading",
|
|
1646
|
+
done: prepared.reused,
|
|
1647
|
+
total: prepared.total,
|
|
1648
|
+
bytes: 0,
|
|
1649
|
+
totalBytes,
|
|
1650
|
+
reused: prepared.reused
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1261
1653
|
await uploadAll(
|
|
1262
1654
|
prepared.upload,
|
|
1263
1655
|
prepared.uploadUrl,
|
|
@@ -1267,11 +1659,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1267
1659
|
done: prepared.reused + done,
|
|
1268
1660
|
total: prepared.total,
|
|
1269
1661
|
bytes,
|
|
1662
|
+
totalBytes,
|
|
1270
1663
|
reused: prepared.reused,
|
|
1271
|
-
path
|
|
1664
|
+
...path === void 0 ? {} : { path }
|
|
1272
1665
|
}),
|
|
1273
1666
|
signal,
|
|
1274
|
-
options.upload
|
|
1667
|
+
options.upload,
|
|
1668
|
+
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1275
1669
|
);
|
|
1276
1670
|
onProgress({ type: "completing", reused: prepared.reused });
|
|
1277
1671
|
const completed = await completeDeploy(
|
|
@@ -1279,7 +1673,7 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1279
1673
|
prepared.deployId,
|
|
1280
1674
|
options,
|
|
1281
1675
|
signal,
|
|
1282
|
-
|
|
1676
|
+
await readSiteName(files)
|
|
1283
1677
|
);
|
|
1284
1678
|
onProgress({ type: "done", url: completed.url, name: completed.name });
|
|
1285
1679
|
} catch (error) {
|
|
@@ -1334,20 +1728,23 @@ function escapesRoot(root, candidate) {
|
|
|
1334
1728
|
const inside = relative(root, candidate);
|
|
1335
1729
|
return inside === "" || inside.startsWith("..");
|
|
1336
1730
|
}
|
|
1337
|
-
async function resolveSite(credentials, site,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
return await createSite(credentials, subdomain);
|
|
1731
|
+
async function resolveSite(credentials, site, newSite) {
|
|
1732
|
+
const { subdomain, folder } = newSite;
|
|
1733
|
+
if (site !== void 0 && subdomain !== void 0) {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
"Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
|
|
1736
|
+
);
|
|
1345
1737
|
}
|
|
1346
|
-
if (site
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1738
|
+
if (site !== void 0 && folder !== void 0) {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
"A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site from the dashboard."
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (site === void 0) {
|
|
1744
|
+
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
1745
|
+
return await createSite(credentials, subdomain, folderId);
|
|
1746
|
+
}
|
|
1747
|
+
const found = await lookUpSite(credentials, site);
|
|
1351
1748
|
if (found === void 0) {
|
|
1352
1749
|
throw new Error(
|
|
1353
1750
|
`No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
|
|
@@ -1355,8 +1752,8 @@ async function resolveSite(credentials, site, subdomain) {
|
|
|
1355
1752
|
}
|
|
1356
1753
|
return found;
|
|
1357
1754
|
}
|
|
1358
|
-
async function publish(credentials, source, site, onProgress,
|
|
1359
|
-
const target = await resolveSite(credentials, site,
|
|
1755
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
1756
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
1360
1757
|
let files = 0;
|
|
1361
1758
|
let url = target.url;
|
|
1362
1759
|
let unchanged = false;
|
|
@@ -1387,11 +1784,11 @@ async function publish(credentials, source, site, onProgress, subdomain) {
|
|
|
1387
1784
|
});
|
|
1388
1785
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
1389
1786
|
}
|
|
1390
|
-
async function publishDirectory(credentials, directory, site, onProgress,
|
|
1787
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
1391
1788
|
const found = await stat(directory).catch(() => null);
|
|
1392
1789
|
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
1393
1790
|
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
1394
|
-
return await publish(credentials, directorySource(directory), site, onProgress,
|
|
1791
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
1395
1792
|
}
|
|
1396
1793
|
const silentProgress = Object.assign(() => {
|
|
1397
1794
|
}, { done: () => {
|
|
@@ -1413,6 +1810,10 @@ function shortenPath(path, width) {
|
|
|
1413
1810
|
}
|
|
1414
1811
|
function describe(event, width = 80) {
|
|
1415
1812
|
switch (event.type) {
|
|
1813
|
+
case "reading":
|
|
1814
|
+
return event.total > 0 ? `Reading ${formatBytes(event.bytes)}/${formatBytes(event.total)}` : "Reading files";
|
|
1815
|
+
case "retrying":
|
|
1816
|
+
return `${event.stalled ? "Upload stalled" : "Upload failed"}, retrying ${shortenPath(event.path, Math.max(12, width - 40))} (attempt ${event.attempt}/${event.attempts})`;
|
|
1416
1817
|
case "collecting":
|
|
1417
1818
|
return `Collected ${event.files.toLocaleString()} ${event.files === 1 ? "file" : "files"}`;
|
|
1418
1819
|
case "hashing":
|
|
@@ -1573,7 +1974,7 @@ async function whoami() {
|
|
|
1573
1974
|
});
|
|
1574
1975
|
if (!response.ok) {
|
|
1575
1976
|
return failure(
|
|
1576
|
-
response.status === 401 ? "This token is not valid any more. It may have been revoked or expired — create another at https://dropto.run/account/tokens." : `The API answered ${response.status}.`
|
|
1977
|
+
response.status === 401 ? "This token is not valid any more. It may have been revoked or expired — create another at https://app.dropto.run/account/tokens." : `The API answered ${response.status}.`
|
|
1577
1978
|
);
|
|
1578
1979
|
}
|
|
1579
1980
|
const me = await response.json();
|
|
@@ -1588,33 +1989,67 @@ API: ${found.credentials.apiBaseUrl}`,
|
|
|
1588
1989
|
async function list() {
|
|
1589
1990
|
const found = credentialsOr();
|
|
1590
1991
|
if ("result" in found) return found.result;
|
|
1591
|
-
const sites = await listSites(found.credentials);
|
|
1992
|
+
const { sites, total } = await listSites(found.credentials);
|
|
1592
1993
|
if (sites.length === 0) {
|
|
1593
1994
|
return {
|
|
1594
1995
|
text: "No sites yet. `drop2run deploy` publishes one.",
|
|
1595
|
-
json: { sites: [] },
|
|
1996
|
+
json: { sites: [], total: 0 },
|
|
1596
1997
|
code: 0
|
|
1597
1998
|
};
|
|
1598
1999
|
}
|
|
2000
|
+
const lines = sites.map(
|
|
2001
|
+
(site) => `${site.subdomain} ${site.url}${site.name ? ` ${site.name}` : ""}`
|
|
2002
|
+
);
|
|
2003
|
+
if (total > sites.length) {
|
|
2004
|
+
lines.push(
|
|
2005
|
+
`… and ${(total - sites.length).toLocaleString()} more — this lists the newest ${sites.length.toLocaleString()} of ${total.toLocaleString()}.`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
1599
2008
|
return {
|
|
1600
|
-
text:
|
|
1601
|
-
json: { sites },
|
|
2009
|
+
text: lines.join("\n"),
|
|
2010
|
+
json: { sites, total },
|
|
1602
2011
|
code: 0
|
|
1603
2012
|
};
|
|
1604
2013
|
}
|
|
1605
|
-
async function
|
|
2014
|
+
async function folders() {
|
|
2015
|
+
const found = credentialsOr();
|
|
2016
|
+
if ("result" in found) return found.result;
|
|
2017
|
+
try {
|
|
2018
|
+
const all = await listFolders(found.credentials);
|
|
2019
|
+
if (all.length === 0) {
|
|
2020
|
+
return {
|
|
2021
|
+
text: `No folders yet. Make one in the dashboard at ${dashboardUrlFor(found.credentials.apiBaseUrl)}, then file a new site in it with \`--folder\`.`,
|
|
2022
|
+
json: { folders: [] },
|
|
2023
|
+
code: 0
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
text: all.map((folder) => `${folder.path} ${folder.folderId}`).join("\n"),
|
|
2028
|
+
json: { folders: all },
|
|
2029
|
+
code: 0
|
|
2030
|
+
};
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
async function deployCommand(directory, site, report = silentProgress, newSite = {}) {
|
|
1606
2036
|
const found = credentialsOr();
|
|
1607
2037
|
if ("result" in found) return found.result;
|
|
1608
2038
|
const project = readProject();
|
|
1609
2039
|
const folder = directory ?? project?.dir ?? ".";
|
|
1610
|
-
const target = subdomain === void 0 ? site ?? project?.siteId : void 0;
|
|
2040
|
+
const target = newSite.subdomain === void 0 ? site ?? project?.siteId : void 0;
|
|
2041
|
+
if (newSite.folder !== void 0 && target !== void 0 && project !== null) {
|
|
2042
|
+
return failure(
|
|
2043
|
+
`This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site from the dashboard, or add \`--subdomain\` to create a new one in the folder.`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
1611
2046
|
try {
|
|
1612
2047
|
const result = await publishDirectory(
|
|
1613
2048
|
found.credentials,
|
|
1614
2049
|
resolve(folder),
|
|
1615
2050
|
target,
|
|
1616
2051
|
report,
|
|
1617
|
-
|
|
2052
|
+
newSite
|
|
1618
2053
|
);
|
|
1619
2054
|
const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
|
|
1620
2055
|
return {
|
|
@@ -1629,7 +2064,7 @@ ${result.url}`,
|
|
|
1629
2064
|
report.done();
|
|
1630
2065
|
}
|
|
1631
2066
|
}
|
|
1632
|
-
async function init(directory, site,
|
|
2067
|
+
async function init(directory, site, newSite = {}) {
|
|
1633
2068
|
const found = credentialsOr();
|
|
1634
2069
|
if ("result" in found) return found.result;
|
|
1635
2070
|
if (readProject() !== null) {
|
|
@@ -1638,7 +2073,11 @@ async function init(directory, site, subdomain) {
|
|
|
1638
2073
|
);
|
|
1639
2074
|
}
|
|
1640
2075
|
try {
|
|
1641
|
-
const target = site === void 0 ? await createSite(
|
|
2076
|
+
const target = site === void 0 ? await createSite(
|
|
2077
|
+
found.credentials,
|
|
2078
|
+
newSite.subdomain,
|
|
2079
|
+
newSite.folder === void 0 ? void 0 : await resolveFolder(found.credentials, newSite.folder)
|
|
2080
|
+
) : await findSite(found.credentials, site);
|
|
1642
2081
|
const project = {
|
|
1643
2082
|
siteId: target.siteId,
|
|
1644
2083
|
subdomain: target.subdomain,
|
|
@@ -1777,7 +2216,9 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
1777
2216
|
drop2run init [dir] [--site <subdomain>] Tie this folder to a site (writes drop2run.json)
|
|
1778
2217
|
drop2run deploy [dir] [--site <subdomain>] Publish a folder (default: . or drop2run.json)
|
|
1779
2218
|
drop2run init|deploy --subdomain <name> Create a new site under a name you pick
|
|
2219
|
+
drop2run init|deploy --folder <path> File the new site in one of your folders
|
|
1780
2220
|
drop2run ls List your sites
|
|
2221
|
+
drop2run folders List your folders, as --folder takes them
|
|
1781
2222
|
drop2run open [site] Open a site in a browser
|
|
1782
2223
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
1783
2224
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
@@ -1790,6 +2231,8 @@ Flags
|
|
|
1790
2231
|
--json Print machine-readable output instead of text
|
|
1791
2232
|
--site X Act on an existing site rather than the one in drop2run.json
|
|
1792
2233
|
--subdomain X Create a new site under this name (init and deploy only)
|
|
2234
|
+
--folder X File the new site in this folder: a path such as Clients/Acme,
|
|
2235
|
+
or a folder id (init and deploy only)
|
|
1793
2236
|
--device Sign in by approving a code on another machine
|
|
1794
2237
|
--yes Confirm a deletion, which cannot be undone
|
|
1795
2238
|
|
|
@@ -1801,13 +2244,17 @@ Where a site comes from
|
|
|
1801
2244
|
--site only ever finds a site you already have; it never creates one, so a
|
|
1802
2245
|
subdomain typed wrong fails instead of quietly becoming a second site.
|
|
1803
2246
|
|
|
2247
|
+
--folder only applies to a site being created. It never creates a folder
|
|
2248
|
+
and never moves an existing site; make and rearrange folders in the
|
|
2249
|
+
dashboard.
|
|
2250
|
+
|
|
1804
2251
|
Signing in
|
|
1805
2252
|
\`login\` opens a browser and listens on 127.0.0.1, so it needs both on this
|
|
1806
2253
|
machine. Over SSH, in a container or under WSL, use \`login --device\`: it
|
|
1807
|
-
prints a short code to enter at https://dropto.run/device from anywhere.
|
|
2254
|
+
prints a short code to enter at https://app.dropto.run/device from anywhere.
|
|
1808
2255
|
|
|
1809
2256
|
For CI, neither flow applies — create a token at
|
|
1810
|
-
https://dropto.run/account/tokens and either set DROP2RUN_TOKEN in your
|
|
2257
|
+
https://app.dropto.run/account/tokens and either set DROP2RUN_TOKEN in your
|
|
1811
2258
|
environment or put it in ~/.config/drop2run/config.json as {"token": "d2r_..."}.
|
|
1812
2259
|
`;
|
|
1813
2260
|
function flagValue(args, flag) {
|
|
@@ -1816,7 +2263,7 @@ function flagValue(args, flag) {
|
|
|
1816
2263
|
const value = args[at + 1];
|
|
1817
2264
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
1818
2265
|
}
|
|
1819
|
-
const VALUE_FLAGS = ["--site", "--subdomain"];
|
|
2266
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
|
|
1820
2267
|
function positionals(argv) {
|
|
1821
2268
|
const found = [];
|
|
1822
2269
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -1831,16 +2278,29 @@ function positionals(argv) {
|
|
|
1831
2278
|
}
|
|
1832
2279
|
return found;
|
|
1833
2280
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
2281
|
+
const SUBDOMAIN_FLAG = {
|
|
2282
|
+
flag: "--subdomain",
|
|
2283
|
+
example: "my-docs",
|
|
2284
|
+
effect: "`--subdomain` creates a new one",
|
|
2285
|
+
elsewhere: (command) => ` Use \`--site\` to tell \`${command}\` which of your sites to act on.`
|
|
2286
|
+
};
|
|
2287
|
+
const FOLDER_FLAG = {
|
|
2288
|
+
flag: "--folder",
|
|
2289
|
+
example: "Clients/Acme",
|
|
2290
|
+
effect: "`--folder` only files a new one — move an existing site from the dashboard",
|
|
2291
|
+
elsewhere: (command) => command === "ls" || command === "list" ? " `drop2run folders` lists them." : ""
|
|
2292
|
+
};
|
|
2293
|
+
function newSiteFlagProblem(argv, command, site, value, described) {
|
|
2294
|
+
const { flag, example, effect, elsewhere } = described;
|
|
2295
|
+
if (!argv.includes(flag)) return void 0;
|
|
2296
|
+
if (value === void 0) {
|
|
2297
|
+
return `\`${flag}\` needs a value after it, for example \`${flag} ${example}\`.`;
|
|
1838
2298
|
}
|
|
1839
2299
|
if (site !== void 0) {
|
|
1840
|
-
return
|
|
2300
|
+
return `\`--site\` and \`${flag}\` cannot both be given: \`--site\` publishes to a site you already have, and ${effect}. Drop whichever is not what you meant.`;
|
|
1841
2301
|
}
|
|
1842
2302
|
if (command !== "init" && command !== "deploy") {
|
|
1843
|
-
return
|
|
2303
|
+
return `\`${flag}\` only applies to \`init\` and \`deploy\`, which can create a site.` + elsewhere(command);
|
|
1844
2304
|
}
|
|
1845
2305
|
return void 0;
|
|
1846
2306
|
}
|
|
@@ -1849,7 +2309,7 @@ async function run(argv) {
|
|
|
1849
2309
|
const positional = positionals(argv);
|
|
1850
2310
|
const [command, ...rest] = positional;
|
|
1851
2311
|
if (argv.includes("--version")) {
|
|
1852
|
-
const { version } = await import("./package-
|
|
2312
|
+
const { version } = await import("./package-D8mWS-nC.js").then(
|
|
1853
2313
|
(module) => module.default
|
|
1854
2314
|
);
|
|
1855
2315
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -1859,7 +2319,8 @@ async function run(argv) {
|
|
|
1859
2319
|
if (command === void 0) return { text: HELP, json: { help: HELP }, code: 1 };
|
|
1860
2320
|
const site = flagValue(argv, "--site");
|
|
1861
2321
|
const subdomain = flagValue(argv, "--subdomain");
|
|
1862
|
-
const
|
|
2322
|
+
const folder = flagValue(argv, "--folder");
|
|
2323
|
+
const refusal = newSiteFlagProblem(argv, command, site, subdomain, SUBDOMAIN_FLAG) ?? newSiteFlagProblem(argv, command, site, folder, FOLDER_FLAG);
|
|
1863
2324
|
if (refusal !== void 0) {
|
|
1864
2325
|
const failed = { text: refusal, json: { error: refusal }, code: 1 };
|
|
1865
2326
|
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
@@ -1871,11 +2332,11 @@ async function run(argv) {
|
|
|
1871
2332
|
json,
|
|
1872
2333
|
argv.includes("--device"),
|
|
1873
2334
|
argv.includes("--yes"),
|
|
1874
|
-
subdomain
|
|
2335
|
+
{ subdomain, folder }
|
|
1875
2336
|
);
|
|
1876
2337
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
1877
2338
|
}
|
|
1878
|
-
async function dispatch(command, rest, site, json, device, confirmed,
|
|
2339
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
|
|
1879
2340
|
switch (command) {
|
|
1880
2341
|
case "login": {
|
|
1881
2342
|
const print = json ? () => {
|
|
@@ -1885,18 +2346,20 @@ async function dispatch(command, rest, site, json, device, confirmed, subdomain)
|
|
|
1885
2346
|
case "logout":
|
|
1886
2347
|
return logout();
|
|
1887
2348
|
case "init":
|
|
1888
|
-
return await init(rest[0] ?? ".", site,
|
|
2349
|
+
return await init(rest[0] ?? ".", site, newSite);
|
|
1889
2350
|
case "deploy": {
|
|
1890
2351
|
const report = json ? silentProgress : progressWriter(
|
|
1891
2352
|
(text) => process.stderr.write(text),
|
|
1892
2353
|
process.stderr.isTTY === true,
|
|
1893
2354
|
process.stderr.columns ?? 80
|
|
1894
2355
|
);
|
|
1895
|
-
return await deployCommand(rest[0], site, report,
|
|
2356
|
+
return await deployCommand(rest[0], site, report, newSite);
|
|
1896
2357
|
}
|
|
1897
2358
|
case "ls":
|
|
1898
2359
|
case "list":
|
|
1899
2360
|
return await list();
|
|
2361
|
+
case "folders":
|
|
2362
|
+
return await folders();
|
|
1900
2363
|
case "open":
|
|
1901
2364
|
return await open(rest[0] ?? site);
|
|
1902
2365
|
case "rollback":
|
|
@@ -1906,7 +2369,7 @@ async function dispatch(command, rest, site, json, device, confirmed, subdomain)
|
|
|
1906
2369
|
case "token":
|
|
1907
2370
|
case "tokens":
|
|
1908
2371
|
return rest[0] === void 0 || rest[0] === "list" ? await tokens() : {
|
|
1909
|
-
text: `\`token ${rest[0]}\` does not exist. Only \`token list\` does: creating and revoking a token need a browser session, at https://dropto.run/account/tokens.`,
|
|
2372
|
+
text: `\`token ${rest[0]}\` does not exist. Only \`token list\` does: creating and revoking a token need a browser session, at https://app.dropto.run/account/tokens.`,
|
|
1910
2373
|
json: { error: `Unknown token subcommand "${rest[0]}".` },
|
|
1911
2374
|
code: 1
|
|
1912
2375
|
};
|