drop2run 0.6.0 → 0.8.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 +8 -3
- package/dist/index.js +917 -86
- package/dist/{package-bzaCMW_E.js → package-BycuI1VJ.js} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ function configPath() {
|
|
|
10
10
|
return join(homedir(), ".config", "drop2run", "config.json");
|
|
11
11
|
}
|
|
12
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";
|
|
13
15
|
function loadCredentials(env = process.env, read = (path) => readFileSync(path, "utf8")) {
|
|
14
16
|
let stored = {};
|
|
15
17
|
try {
|
|
@@ -33,7 +35,8 @@ function resolveApiBaseUrl(env = process.env, read = (path) => readFileSync(path
|
|
|
33
35
|
return url.replace(/\/+$/, "");
|
|
34
36
|
}
|
|
35
37
|
function dashboardUrlFor(apiBaseUrl) {
|
|
36
|
-
|
|
38
|
+
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
39
|
+
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
37
40
|
}
|
|
38
41
|
function saveToken(token, path = configPath()) {
|
|
39
42
|
let existing = {};
|
|
@@ -76,14 +79,14 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
76
79
|
] : ["Run `drop2run login` to sign in through a browser, which stores one for you."];
|
|
77
80
|
const byHand = surface === "mcp" ? [
|
|
78
81
|
"To set one without signing in here, create a token at",
|
|
79
|
-
"https://dropto.run/account/tokens and either:",
|
|
82
|
+
"https://app.dropto.run/account/tokens and either:",
|
|
80
83
|
` - put it in ${configPath()} as {"token": "d2r_..."}, which this`,
|
|
81
84
|
" server reads on every call and needs no restart, or",
|
|
82
85
|
` - set ${TOKEN_VARIABLE} in the environment and restart this server, since it reads`,
|
|
83
86
|
" the environment it was started in."
|
|
84
87
|
] : [
|
|
85
88
|
"For CI, or where no browser can be opened, create a token at",
|
|
86
|
-
"https://dropto.run/account/tokens and either:",
|
|
89
|
+
"https://app.dropto.run/account/tokens and either:",
|
|
87
90
|
` - set ${TOKEN_VARIABLE} in the environment, or`,
|
|
88
91
|
` - put it in ${configPath()} as {"token": "d2r_..."}`
|
|
89
92
|
];
|
|
@@ -115,7 +118,9 @@ async function call(credentials, path, init2 = {}) {
|
|
|
115
118
|
async function findSite(credentials, site) {
|
|
116
119
|
const found = await lookUpSite(credentials, site);
|
|
117
120
|
if (found === void 0) {
|
|
118
|
-
throw new Error(
|
|
121
|
+
throw new Error(
|
|
122
|
+
`No site of yours is called "${site}". \`drop2run ls\` or the list_sites tool shows what exists.`
|
|
123
|
+
);
|
|
119
124
|
}
|
|
120
125
|
return found;
|
|
121
126
|
}
|
|
@@ -159,6 +164,108 @@ async function promoteDeploy(credentials, siteId, deployId) {
|
|
|
159
164
|
{ method: "POST" }
|
|
160
165
|
);
|
|
161
166
|
}
|
|
167
|
+
function enumName(value) {
|
|
168
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
function actionOf(expiresAt, action) {
|
|
171
|
+
return expiresAt === null ? null : enumName(action);
|
|
172
|
+
}
|
|
173
|
+
async function readInvitedOnly(credentials, siteId) {
|
|
174
|
+
try {
|
|
175
|
+
const page = await call(
|
|
176
|
+
credentials,
|
|
177
|
+
`sites/${encodeURIComponent(siteId)}/viewers?pageSize=1`
|
|
178
|
+
);
|
|
179
|
+
return typeof page.invitedOnly === "boolean" ? page.invitedOnly : null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function folderFromListing(credentials, siteId, subdomain) {
|
|
185
|
+
const { sites } = await call(
|
|
186
|
+
credentials,
|
|
187
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(subdomain)}`
|
|
188
|
+
);
|
|
189
|
+
return sites.find((candidate) => candidate.siteId === siteId)?.folderId ?? null;
|
|
190
|
+
}
|
|
191
|
+
async function getSite(credentials, siteId) {
|
|
192
|
+
const [site, invitedOnly] = await Promise.all([
|
|
193
|
+
call(
|
|
194
|
+
credentials,
|
|
195
|
+
`sites/${encodeURIComponent(siteId)}`
|
|
196
|
+
),
|
|
197
|
+
readInvitedOnly(credentials, siteId)
|
|
198
|
+
]);
|
|
199
|
+
const folderId = site.folderId !== void 0 ? site.folderId : await folderFromListing(credentials, site.siteId, site.subdomain);
|
|
200
|
+
return {
|
|
201
|
+
siteId: site.siteId,
|
|
202
|
+
subdomain: site.subdomain,
|
|
203
|
+
url: site.url,
|
|
204
|
+
name: site.name,
|
|
205
|
+
status: enumName(site.status),
|
|
206
|
+
invitedOnly,
|
|
207
|
+
liveDeployId: site.liveDeployId,
|
|
208
|
+
spaMode: site.spaMode,
|
|
209
|
+
docsMode: site.docsMode,
|
|
210
|
+
modeIsManual: site.modeIsManual,
|
|
211
|
+
passwordProtected: site.passwordProtected,
|
|
212
|
+
passwordProtectionAvailable: site.passwordProtectionAvailable,
|
|
213
|
+
formsEnabled: site.formsEnabled,
|
|
214
|
+
formsAvailable: site.formsAvailable,
|
|
215
|
+
expiresAt: site.expiresAt,
|
|
216
|
+
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
217
|
+
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
218
|
+
folderId,
|
|
219
|
+
deploys: site.deploys.map((deploy2) => ({
|
|
220
|
+
deployId: deploy2.deployId,
|
|
221
|
+
status: deploy2.status,
|
|
222
|
+
fileCount: deploy2.fileCount,
|
|
223
|
+
totalBytes: deploy2.totalBytes,
|
|
224
|
+
createdAt: deploy2.createdAt,
|
|
225
|
+
completedAt: deploy2.completedAt,
|
|
226
|
+
filesKept: deploy2.filesKept
|
|
227
|
+
}))
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function updateSiteSettings(credentials, siteId, change) {
|
|
231
|
+
const body = {};
|
|
232
|
+
if (change.name !== void 0) body.name = change.name;
|
|
233
|
+
if (change.mode !== void 0) {
|
|
234
|
+
body.spaMode = change.mode === "spa";
|
|
235
|
+
body.docsMode = change.mode === "docs";
|
|
236
|
+
}
|
|
237
|
+
if (change.password !== void 0) body.password = change.password;
|
|
238
|
+
if (change.expiresAt !== void 0) body.expiresAt = change.expiresAt;
|
|
239
|
+
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
240
|
+
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
241
|
+
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
242
|
+
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
243
|
+
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
244
|
+
const settings = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
245
|
+
method: "PATCH",
|
|
246
|
+
body: JSON.stringify(body)
|
|
247
|
+
});
|
|
248
|
+
const invitedOnly = typeof settings.invitedOnly === "boolean" ? settings.invitedOnly : null;
|
|
249
|
+
return {
|
|
250
|
+
...settings,
|
|
251
|
+
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
252
|
+
invitedOnly,
|
|
253
|
+
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function setSitePaused(credentials, siteId, action) {
|
|
257
|
+
const state = await call(
|
|
258
|
+
credentials,
|
|
259
|
+
`sites/${encodeURIComponent(siteId)}/${action}`,
|
|
260
|
+
{ method: "POST" }
|
|
261
|
+
);
|
|
262
|
+
return {
|
|
263
|
+
siteId: state.siteId,
|
|
264
|
+
subdomain: state.subdomain,
|
|
265
|
+
status: enumName(state.status),
|
|
266
|
+
ownerPaused: state.ownerPaused
|
|
267
|
+
};
|
|
268
|
+
}
|
|
162
269
|
async function listTokens(credentials) {
|
|
163
270
|
const body = await call(credentials, "tokens");
|
|
164
271
|
return body.tokens;
|
|
@@ -233,6 +340,46 @@ async function resolveFolder(credentials, folder) {
|
|
|
233
340
|
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
234
341
|
);
|
|
235
342
|
}
|
|
343
|
+
const ROOT_FOLDER = "root";
|
|
344
|
+
async function resolveFolderTarget(credentials, folder) {
|
|
345
|
+
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
346
|
+
}
|
|
347
|
+
const VERSIONS_LISTED = 10;
|
|
348
|
+
function servingModeOf$1(site) {
|
|
349
|
+
if (site.spaMode) return "spa";
|
|
350
|
+
if (site.docsMode) return "docs";
|
|
351
|
+
return "static";
|
|
352
|
+
}
|
|
353
|
+
function accessOf(site) {
|
|
354
|
+
if (site.passwordProtected) return "password";
|
|
355
|
+
if (site.invitedOnly === true) return "invite-only";
|
|
356
|
+
if (site.invitedOnly === false) return "public";
|
|
357
|
+
return "no password (whether it is invite-only could not be read)";
|
|
358
|
+
}
|
|
359
|
+
function describeSite(site, folderPath) {
|
|
360
|
+
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
361
|
+
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
362
|
+
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
363
|
+
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
364
|
+
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
365
|
+
});
|
|
366
|
+
const more = site.deploys.length - versions.length;
|
|
367
|
+
return [
|
|
368
|
+
`${site.subdomain} — ${site.url}`,
|
|
369
|
+
` name ${site.name ?? "(none)"}`,
|
|
370
|
+
` status ${site.status}`,
|
|
371
|
+
` access ${accessOf(site)}`,
|
|
372
|
+
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
373
|
+
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
374
|
+
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
375
|
+
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
376
|
+
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
377
|
+
"",
|
|
378
|
+
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
379
|
+
...versions,
|
|
380
|
+
...more > 0 ? [` … and ${more} older`] : []
|
|
381
|
+
].join("\n");
|
|
382
|
+
}
|
|
236
383
|
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
237
384
|
function newAttempt() {
|
|
238
385
|
const verifier = randomBytes(32).toString("base64url");
|
|
@@ -409,6 +556,12 @@ function writeProject(project, directory = process.cwd()) {
|
|
|
409
556
|
`);
|
|
410
557
|
return path;
|
|
411
558
|
}
|
|
559
|
+
function sizeOf(content) {
|
|
560
|
+
return ArrayBuffer.isView(content) ? content.byteLength : content.size;
|
|
561
|
+
}
|
|
562
|
+
async function bytesOf(content) {
|
|
563
|
+
return ArrayBuffer.isView(content) ? content : new Uint8Array(await content.arrayBuffer());
|
|
564
|
+
}
|
|
412
565
|
class DeployError extends Error {
|
|
413
566
|
/**
|
|
414
567
|
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
@@ -445,20 +598,55 @@ const ClientErrorCode = {
|
|
|
445
598
|
/** The user cancelled. */
|
|
446
599
|
Cancelled: "cancelled",
|
|
447
600
|
/** An upload kept failing after every retry. */
|
|
448
|
-
UploadFailed: "upload_failed"
|
|
601
|
+
UploadFailed: "upload_failed",
|
|
602
|
+
/**
|
|
603
|
+
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
604
|
+
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
605
|
+
* names memory.
|
|
606
|
+
*/
|
|
607
|
+
OutOfMemory: "out_of_memory",
|
|
608
|
+
/**
|
|
609
|
+
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
610
|
+
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
611
|
+
*/
|
|
612
|
+
ReadFailed: "read_failed",
|
|
613
|
+
/**
|
|
614
|
+
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
615
|
+
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
616
|
+
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
617
|
+
*/
|
|
618
|
+
NetworkFailed: "network_failed"
|
|
449
619
|
};
|
|
620
|
+
function describeError(error) {
|
|
621
|
+
const parts = [];
|
|
622
|
+
let current = error;
|
|
623
|
+
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
624
|
+
if (current instanceof Error) {
|
|
625
|
+
const code = current.code;
|
|
626
|
+
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
627
|
+
current = current.cause;
|
|
628
|
+
} else {
|
|
629
|
+
parts.push(String(current));
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return parts.join(" ← ");
|
|
634
|
+
}
|
|
450
635
|
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
451
636
|
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
637
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
638
|
+
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
452
639
|
async function prepareDeploy(siteId, files, options, signal) {
|
|
453
640
|
const manifest = files.map((file) => ({
|
|
454
641
|
path: file.path,
|
|
455
642
|
sha256: file.sha256,
|
|
456
|
-
size: file.bytes
|
|
643
|
+
size: sizeOf(file.bytes)
|
|
457
644
|
}));
|
|
458
645
|
const prepared = await request(
|
|
459
646
|
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
460
647
|
{ files: manifest },
|
|
461
|
-
options
|
|
648
|
+
options,
|
|
649
|
+
signal
|
|
462
650
|
);
|
|
463
651
|
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
464
652
|
}
|
|
@@ -468,31 +656,65 @@ function absolute(uploadUrl, baseUrl) {
|
|
|
468
656
|
return new URL(uploadUrl, baseUrl).toString();
|
|
469
657
|
}
|
|
470
658
|
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
659
|
+
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
660
|
+
const body = name === null || name === void 0 ? void 0 : { name };
|
|
661
|
+
const delay = options.delay ?? wait;
|
|
662
|
+
for (let attempt = 0; ; attempt++) {
|
|
663
|
+
try {
|
|
664
|
+
return await request(path, body, options, signal);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
667
|
+
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
668
|
+
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function wait(ms) {
|
|
673
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
479
674
|
}
|
|
480
675
|
async function request(path, body, options, signal) {
|
|
481
676
|
const doFetch = options.fetch ?? globalFetch$1();
|
|
482
677
|
const base = options.baseUrl ?? "/api";
|
|
678
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
483
679
|
const headers = {};
|
|
484
680
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
485
681
|
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
486
682
|
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
487
683
|
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
|
|
684
|
+
const controller = new AbortController();
|
|
685
|
+
let timedOut = false;
|
|
686
|
+
const timer = setTimeout(() => {
|
|
687
|
+
timedOut = true;
|
|
688
|
+
controller.abort();
|
|
689
|
+
}, timeoutMs);
|
|
690
|
+
try {
|
|
691
|
+
const response = await doFetch(`${base}/${path}`, {
|
|
692
|
+
method: "POST",
|
|
693
|
+
headers,
|
|
694
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
695
|
+
signal: controller.signal
|
|
696
|
+
});
|
|
697
|
+
if (!response.ok) throw await problemToError(response);
|
|
698
|
+
return await response.json();
|
|
699
|
+
} catch (error) {
|
|
700
|
+
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
701
|
+
if (timedOut) {
|
|
702
|
+
throw new DeployError(
|
|
703
|
+
ClientErrorCode.NetworkFailed,
|
|
704
|
+
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
705
|
+
{ path, cause: describeError(error) }
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (error instanceof TypeError) {
|
|
709
|
+
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
710
|
+
path,
|
|
711
|
+
cause: describeError(error)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
throw error;
|
|
715
|
+
} finally {
|
|
716
|
+
clearTimeout(timer);
|
|
717
|
+
}
|
|
496
718
|
}
|
|
497
719
|
function globalFetch$1() {
|
|
498
720
|
return globalThis.fetch.bind(globalThis);
|
|
@@ -991,17 +1213,29 @@ function extensionOf(path) {
|
|
|
991
1213
|
return dot <= 0 ? "" : name.slice(dot).toLowerCase();
|
|
992
1214
|
}
|
|
993
1215
|
async function sha256Hex(bytes) {
|
|
994
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes
|
|
1216
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
995
1217
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
996
1218
|
}
|
|
997
1219
|
async function hashAll(files, onProgress, signal) {
|
|
998
1220
|
const hashed = [];
|
|
999
1221
|
for (const [index, file] of files.entries()) {
|
|
1000
|
-
hashed.push({ ...file, sha256: await sha256Hex(file
|
|
1222
|
+
hashed.push({ ...file, sha256: await sha256Hex(await readForHash(file)) });
|
|
1001
1223
|
onProgress?.(index + 1, files.length);
|
|
1002
1224
|
}
|
|
1003
1225
|
return hashed;
|
|
1004
1226
|
}
|
|
1227
|
+
async function readForHash(file) {
|
|
1228
|
+
try {
|
|
1229
|
+
return await bytesOf(file.bytes);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (error instanceof RangeError) throw error;
|
|
1232
|
+
throw new DeployError(
|
|
1233
|
+
ClientErrorCode.ReadFailed,
|
|
1234
|
+
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
1235
|
+
{ path: file.path, cause: describeError(error) }
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1005
1239
|
const IGNORED_EXACT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".gitignore", ".gitkeep"]);
|
|
1006
1240
|
const IGNORED_DIRECTORIES = [
|
|
1007
1241
|
"__MACOSX",
|
|
@@ -1011,7 +1245,11 @@ const IGNORED_DIRECTORIES = [
|
|
|
1011
1245
|
".hg",
|
|
1012
1246
|
".idea",
|
|
1013
1247
|
".vscode",
|
|
1014
|
-
".claude"
|
|
1248
|
+
".claude",
|
|
1249
|
+
".agents",
|
|
1250
|
+
".cursor",
|
|
1251
|
+
".codex",
|
|
1252
|
+
".gemini"
|
|
1015
1253
|
];
|
|
1016
1254
|
function shouldIgnore(path) {
|
|
1017
1255
|
const segments = path.split("/");
|
|
@@ -1046,6 +1284,24 @@ function canPublish(paths) {
|
|
|
1046
1284
|
if (isImageFolder(paths)) return true;
|
|
1047
1285
|
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
1048
1286
|
}
|
|
1287
|
+
function servingModeOf(flags) {
|
|
1288
|
+
if (flags.docsMode) return "docs";
|
|
1289
|
+
if (flags.spaMode) return "spa";
|
|
1290
|
+
return "static";
|
|
1291
|
+
}
|
|
1292
|
+
function predictedModeOf(paths) {
|
|
1293
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
|
|
1294
|
+
if (paths.some(isDocumentPath)) return "docs";
|
|
1295
|
+
if (isImageFolder(paths)) return "docs";
|
|
1296
|
+
if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
|
|
1297
|
+
return "static";
|
|
1298
|
+
}
|
|
1299
|
+
function nestedIndexOf(paths) {
|
|
1300
|
+
if (predictedModeOf(paths) !== "docs") return null;
|
|
1301
|
+
const suffix = `/${REQUIRED_INDEX}`;
|
|
1302
|
+
const folders2 = paths.filter((path) => path.endsWith(suffix)).map((path) => path.slice(0, -suffix.length)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
|
|
1303
|
+
return folders2[0] ?? null;
|
|
1304
|
+
}
|
|
1049
1305
|
function renameOfLoneHtmlPage(paths) {
|
|
1050
1306
|
if (paths.length !== 1) return null;
|
|
1051
1307
|
const [only] = paths;
|
|
@@ -1073,7 +1329,7 @@ function checkLimits(files, limits) {
|
|
|
1073
1329
|
);
|
|
1074
1330
|
}
|
|
1075
1331
|
const refusal = sizeRefusal(
|
|
1076
|
-
files.map((file) => ({ path: file.path, size: file.bytes
|
|
1332
|
+
files.map((file) => ({ path: file.path, size: sizeOf(file.bytes) })),
|
|
1077
1333
|
limits
|
|
1078
1334
|
);
|
|
1079
1335
|
if (refusal) throw refusal;
|
|
@@ -1114,13 +1370,24 @@ const HEAD_BYTES = 64 * 1024;
|
|
|
1114
1370
|
const TITLE_SEPARATORS = ["|", "·", "—", "–", " - "];
|
|
1115
1371
|
function suggestSiteName(files) {
|
|
1116
1372
|
const index = files.find((file) => file.path === "index.html");
|
|
1117
|
-
if (index === void 0) return null;
|
|
1373
|
+
if (index === void 0 || !ArrayBuffer.isView(index.bytes)) return null;
|
|
1118
1374
|
try {
|
|
1119
1375
|
return nameFromHtml(decodeHead(index.bytes));
|
|
1120
1376
|
} catch {
|
|
1121
1377
|
return null;
|
|
1122
1378
|
}
|
|
1123
1379
|
}
|
|
1380
|
+
async function readSiteName(files) {
|
|
1381
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1382
|
+
if (index === void 0) return null;
|
|
1383
|
+
if (ArrayBuffer.isView(index.bytes)) return suggestSiteName(files);
|
|
1384
|
+
try {
|
|
1385
|
+
const head = new Uint8Array(await index.bytes.slice(0, HEAD_BYTES).arrayBuffer());
|
|
1386
|
+
return suggestSiteName([{ path: index.path, bytes: head }]);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1124
1391
|
function decodeHead(bytes) {
|
|
1125
1392
|
return new TextDecoder("utf-8", { fatal: false }).decode(
|
|
1126
1393
|
bytes.subarray(0, HEAD_BYTES)
|
|
@@ -1152,7 +1419,7 @@ function matchTag(html, tag) {
|
|
|
1152
1419
|
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(
|
|
1153
1420
|
html
|
|
1154
1421
|
);
|
|
1155
|
-
return match?.[1]
|
|
1422
|
+
return match?.[1] === void 0 ? null : decodeEntities(match[1]);
|
|
1156
1423
|
}
|
|
1157
1424
|
function matchOgSiteName(html) {
|
|
1158
1425
|
const patterns = [
|
|
@@ -1161,10 +1428,56 @@ function matchOgSiteName(html) {
|
|
|
1161
1428
|
];
|
|
1162
1429
|
for (const pattern of patterns) {
|
|
1163
1430
|
const match = pattern.exec(html);
|
|
1164
|
-
if (match?.[1] !== void 0) return match[1];
|
|
1431
|
+
if (match?.[1] !== void 0) return decodeEntities(match[1]);
|
|
1165
1432
|
}
|
|
1166
1433
|
return null;
|
|
1167
1434
|
}
|
|
1435
|
+
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(" ");
|
|
1436
|
+
const NAMED_ENTITIES = new Map([
|
|
1437
|
+
...LATIN1_ENTITIES.map((name, index) => [name, 160 + index]),
|
|
1438
|
+
["amp", 38],
|
|
1439
|
+
["lt", 60],
|
|
1440
|
+
["gt", 62],
|
|
1441
|
+
["quot", 34],
|
|
1442
|
+
["apos", 39],
|
|
1443
|
+
["ensp", 8194],
|
|
1444
|
+
["emsp", 8195],
|
|
1445
|
+
["thinsp", 8201],
|
|
1446
|
+
["ndash", 8211],
|
|
1447
|
+
["mdash", 8212],
|
|
1448
|
+
["lsquo", 8216],
|
|
1449
|
+
["rsquo", 8217],
|
|
1450
|
+
["sbquo", 8218],
|
|
1451
|
+
["ldquo", 8220],
|
|
1452
|
+
["rdquo", 8221],
|
|
1453
|
+
["bdquo", 8222],
|
|
1454
|
+
["dagger", 8224],
|
|
1455
|
+
["Dagger", 8225],
|
|
1456
|
+
["bull", 8226],
|
|
1457
|
+
["hellip", 8230],
|
|
1458
|
+
["prime", 8242],
|
|
1459
|
+
["lsaquo", 8249],
|
|
1460
|
+
["rsaquo", 8250],
|
|
1461
|
+
["euro", 8364],
|
|
1462
|
+
["trade", 8482],
|
|
1463
|
+
["larr", 8592],
|
|
1464
|
+
["rarr", 8594],
|
|
1465
|
+
["hearts", 9829]
|
|
1466
|
+
]);
|
|
1467
|
+
function decodeEntities(text) {
|
|
1468
|
+
return text.replace(
|
|
1469
|
+
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1470
|
+
(whole, decimal, hex, name) => {
|
|
1471
|
+
if (name !== void 0) {
|
|
1472
|
+
const code2 = NAMED_ENTITIES.get(name);
|
|
1473
|
+
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1474
|
+
}
|
|
1475
|
+
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1476
|
+
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1477
|
+
return String.fromCodePoint(valid ? code : 65533);
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1168
1481
|
const INVISIBLE = new RegExp(
|
|
1169
1482
|
`[${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)}]`,
|
|
1170
1483
|
"g"
|
|
@@ -1206,46 +1519,105 @@ function trimSuffix(title) {
|
|
|
1206
1519
|
return title;
|
|
1207
1520
|
}
|
|
1208
1521
|
const DEFAULT_CONCURRENCY = 8;
|
|
1522
|
+
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1209
1523
|
const DEFAULT_RETRIES = 3;
|
|
1210
1524
|
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1211
|
-
|
|
1212
|
-
|
|
1525
|
+
const DEFAULT_STALL_MS = 3e4;
|
|
1526
|
+
const PROGRESS_INTERVAL_MS = 200;
|
|
1527
|
+
class StalledError extends Error {
|
|
1528
|
+
/** @param seconds How long nothing moved. */
|
|
1529
|
+
constructor(seconds) {
|
|
1530
|
+
super(`No data moved for ${seconds}s.`);
|
|
1531
|
+
this.name = "StalledError";
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {}, onRetry) {
|
|
1535
|
+
const put = deps.put ?? defaultTransport(deps.fetch);
|
|
1213
1536
|
const delay = deps.delay ?? defaultDelay;
|
|
1214
1537
|
const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1215
1538
|
const retries = deps.retries ?? DEFAULT_RETRIES;
|
|
1539
|
+
const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
|
|
1540
|
+
const now = deps.now ?? Date.now;
|
|
1541
|
+
const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
|
|
1216
1542
|
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1217
1543
|
let nextIndex = 0;
|
|
1218
1544
|
let done = 0;
|
|
1219
|
-
let
|
|
1545
|
+
let failed = false;
|
|
1546
|
+
let reserved = 0;
|
|
1547
|
+
let waiting = [];
|
|
1548
|
+
const acquire = async (size) => {
|
|
1549
|
+
while (reserved > 0 && reserved + size > maxBytesInFlight) {
|
|
1550
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
1551
|
+
}
|
|
1552
|
+
reserved += size;
|
|
1553
|
+
};
|
|
1554
|
+
const release = (size) => {
|
|
1555
|
+
reserved -= size;
|
|
1556
|
+
const woken = waiting;
|
|
1557
|
+
waiting = [];
|
|
1558
|
+
for (const wake of woken) wake();
|
|
1559
|
+
};
|
|
1560
|
+
let landedBytes = 0;
|
|
1561
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1562
|
+
let lastReport = Number.NEGATIVE_INFINITY;
|
|
1563
|
+
const report = (path) => {
|
|
1564
|
+
if (onProgress === void 0) return;
|
|
1565
|
+
const at = now();
|
|
1566
|
+
if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
|
|
1567
|
+
lastReport = at;
|
|
1568
|
+
let bytes = landedBytes;
|
|
1569
|
+
for (const sent of inFlight.values()) bytes += sent;
|
|
1570
|
+
onProgress(done, targets.length, bytes, path);
|
|
1571
|
+
};
|
|
1220
1572
|
const worker = async () => {
|
|
1221
1573
|
for (; ; ) {
|
|
1574
|
+
if (failed) return;
|
|
1222
1575
|
const index = nextIndex++;
|
|
1223
1576
|
const target = targets[index];
|
|
1224
1577
|
if (target === void 0) return;
|
|
1225
1578
|
const file = byPath.get(target.path);
|
|
1226
1579
|
if (file === void 0) {
|
|
1580
|
+
failed = true;
|
|
1227
1581
|
throw new DeployError(
|
|
1228
1582
|
ClientErrorCode.UploadFailed,
|
|
1229
1583
|
`The server asked for ${target.path}, which is not in the manifest.`,
|
|
1230
1584
|
{ path: target.path }
|
|
1231
1585
|
);
|
|
1232
1586
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
signal
|
|
1238
|
-
|
|
1239
|
-
|
|
1587
|
+
const size = sizeOf(file.bytes);
|
|
1588
|
+
await acquire(size);
|
|
1589
|
+
try {
|
|
1590
|
+
if (failed) return;
|
|
1591
|
+
signal?.throwIfAborted();
|
|
1592
|
+
await uploadOne(target, file, {
|
|
1593
|
+
put,
|
|
1594
|
+
delay,
|
|
1595
|
+
retries,
|
|
1596
|
+
signal,
|
|
1597
|
+
uploadUrl,
|
|
1598
|
+
stallMs,
|
|
1599
|
+
onSent: (sent) => {
|
|
1600
|
+
inFlight.set(index, sent);
|
|
1601
|
+
report();
|
|
1602
|
+
},
|
|
1603
|
+
onRetry: (attempt, stalled) => {
|
|
1604
|
+
inFlight.set(index, 0);
|
|
1605
|
+
onRetry?.(target.path, attempt, retries + 1, stalled);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
failed = true;
|
|
1610
|
+
throw error;
|
|
1611
|
+
} finally {
|
|
1612
|
+
release(size);
|
|
1613
|
+
}
|
|
1614
|
+
inFlight.delete(index);
|
|
1240
1615
|
done += 1;
|
|
1241
|
-
|
|
1242
|
-
|
|
1616
|
+
landedBytes += size;
|
|
1617
|
+
report(file.path);
|
|
1243
1618
|
}
|
|
1244
1619
|
};
|
|
1245
|
-
const workers = Array.from(
|
|
1246
|
-
{ length: Math.min(concurrency, targets.length) },
|
|
1247
|
-
worker
|
|
1248
|
-
);
|
|
1620
|
+
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, worker);
|
|
1249
1621
|
try {
|
|
1250
1622
|
await Promise.all(workers);
|
|
1251
1623
|
} catch (error) {
|
|
@@ -1257,39 +1629,122 @@ async function uploadOne(target, file, context) {
|
|
|
1257
1629
|
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1258
1630
|
context.signal?.throwIfAborted();
|
|
1259
1631
|
if (attempt > 0) {
|
|
1632
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1260
1633
|
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1261
1634
|
context.signal?.throwIfAborted();
|
|
1262
1635
|
}
|
|
1263
1636
|
try {
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
...context.signal ? { signal: context.signal } : {}
|
|
1637
|
+
const status = await context.put({
|
|
1638
|
+
url: context.uploadUrl,
|
|
1639
|
+
token: target.token,
|
|
1640
|
+
body: untyped(file.bytes),
|
|
1641
|
+
signal: context.signal,
|
|
1642
|
+
onSent: context.onSent,
|
|
1643
|
+
stallMs: context.stallMs
|
|
1272
1644
|
});
|
|
1273
|
-
if (
|
|
1274
|
-
if (
|
|
1645
|
+
if (status >= 200 && status < 300) return;
|
|
1646
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
1275
1647
|
throw new DeployError(
|
|
1276
1648
|
ClientErrorCode.UploadFailed,
|
|
1277
1649
|
`Upload of ${target.path} was refused. The upload window may have expired — try deploying again.`,
|
|
1278
|
-
{ path: target.path, status
|
|
1650
|
+
{ path: target.path, status }
|
|
1279
1651
|
);
|
|
1280
1652
|
}
|
|
1281
|
-
|
|
1653
|
+
if (status === 413) {
|
|
1654
|
+
throw new DeployError(
|
|
1655
|
+
ClientErrorCode.UploadFailed,
|
|
1656
|
+
`${target.path} is too large to upload in one request. Make it smaller, or leave it out of this publish.`,
|
|
1657
|
+
{ path: target.path, status }
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1282
1661
|
} catch (error) {
|
|
1283
1662
|
if (error instanceof DeployError) throw error;
|
|
1284
1663
|
if (isAbort(error)) throw cancelled();
|
|
1285
1664
|
lastError = error;
|
|
1286
1665
|
}
|
|
1287
1666
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
);
|
|
1667
|
+
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.`;
|
|
1668
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1669
|
+
path: target.path,
|
|
1670
|
+
cause: describeError(lastError)
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
function untyped(content) {
|
|
1674
|
+
if (ArrayBuffer.isView(content) || content.type === "") return content;
|
|
1675
|
+
return content.slice(0, content.size);
|
|
1676
|
+
}
|
|
1677
|
+
function defaultTransport(doFetch) {
|
|
1678
|
+
if (doFetch !== void 0) return fetchTransport(doFetch);
|
|
1679
|
+
const Xhr = globalThis.XMLHttpRequest;
|
|
1680
|
+
return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
|
|
1681
|
+
}
|
|
1682
|
+
function fetchTransport(doFetch) {
|
|
1683
|
+
return async ({ url, token, body, signal }) => {
|
|
1684
|
+
const response = await doFetch(url, {
|
|
1685
|
+
method: "PUT",
|
|
1686
|
+
// The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
|
|
1687
|
+
// full-size copies of every file in flight — eight at once, so up to 400 MB on a drop of
|
|
1688
|
+
// 50 MB files. `fetch` sends exactly the range a view covers. The cast is TypeScript's
|
|
1689
|
+
// SharedArrayBuffer distinction again; see `sha256Hex`. A `Blob` goes as it is, and the runtime
|
|
1690
|
+
// streams it from disk.
|
|
1691
|
+
body,
|
|
1692
|
+
// The permit is the whole request. `Content-Length` is required by the Worker and cannot be
|
|
1693
|
+
// set here — it is a forbidden header name — but the runtime derives it from the body, which
|
|
1694
|
+
// is exactly the length the token names.
|
|
1695
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1696
|
+
...signal ? { signal } : {}
|
|
1697
|
+
});
|
|
1698
|
+
return response.status;
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function xhrTransport(Xhr) {
|
|
1702
|
+
return ({ url, token, body, signal, onSent, stallMs }) => new Promise((resolve2, reject) => {
|
|
1703
|
+
const xhr = new Xhr();
|
|
1704
|
+
let settled = false;
|
|
1705
|
+
let timer;
|
|
1706
|
+
const settle = (finish) => {
|
|
1707
|
+
if (settled) return;
|
|
1708
|
+
settled = true;
|
|
1709
|
+
clearTimeout(timer);
|
|
1710
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1711
|
+
finish();
|
|
1712
|
+
};
|
|
1713
|
+
const arm = () => {
|
|
1714
|
+
clearTimeout(timer);
|
|
1715
|
+
timer = setTimeout(() => {
|
|
1716
|
+
xhr.abort();
|
|
1717
|
+
settle(() => reject(new StalledError(Math.round(stallMs / 1e3))));
|
|
1718
|
+
}, stallMs);
|
|
1719
|
+
};
|
|
1720
|
+
function onAbort() {
|
|
1721
|
+
xhr.abort();
|
|
1722
|
+
settle(() => reject(abortReason(signal)));
|
|
1723
|
+
}
|
|
1724
|
+
if (signal?.aborted) {
|
|
1725
|
+
reject(abortReason(signal));
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1729
|
+
xhr.open("PUT", url);
|
|
1730
|
+
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
|
|
1731
|
+
xhr.upload.onprogress = (event) => {
|
|
1732
|
+
arm();
|
|
1733
|
+
onSent(event.loaded);
|
|
1734
|
+
};
|
|
1735
|
+
xhr.onprogress = arm;
|
|
1736
|
+
xhr.onload = () => settle(() => resolve2(xhr.status));
|
|
1737
|
+
xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
|
|
1738
|
+
arm();
|
|
1739
|
+
xhr.send(body);
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
function abortReason(signal) {
|
|
1743
|
+
const reason = signal?.reason;
|
|
1744
|
+
if (reason instanceof Error) return reason;
|
|
1745
|
+
const error = new Error("Aborted.");
|
|
1746
|
+
error.name = "AbortError";
|
|
1747
|
+
return error;
|
|
1293
1748
|
}
|
|
1294
1749
|
function globalFetch() {
|
|
1295
1750
|
return globalThis.fetch.bind(globalThis);
|
|
@@ -1303,9 +1758,17 @@ function isAbort(error) {
|
|
|
1303
1758
|
function cancelled() {
|
|
1304
1759
|
return new DeployError(ClientErrorCode.Cancelled, "Deploy cancelled.");
|
|
1305
1760
|
}
|
|
1761
|
+
function outOfMemory(detail) {
|
|
1762
|
+
return new DeployError(
|
|
1763
|
+
ClientErrorCode.OutOfMemory,
|
|
1764
|
+
"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.",
|
|
1765
|
+
detail
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1306
1768
|
function asDeployError(error) {
|
|
1307
1769
|
if (error instanceof DeployError) return error;
|
|
1308
1770
|
if (isAbort(error)) return cancelled();
|
|
1771
|
+
if (error instanceof RangeError) return outOfMemory(error.message);
|
|
1309
1772
|
return new DeployError(
|
|
1310
1773
|
ClientErrorCode.UploadFailed,
|
|
1311
1774
|
error instanceof Error ? error.message : "Upload failed.",
|
|
@@ -1314,7 +1777,11 @@ function asDeployError(error) {
|
|
|
1314
1777
|
}
|
|
1315
1778
|
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
1316
1779
|
try {
|
|
1317
|
-
|
|
1780
|
+
onProgress({ type: "reading", bytes: 0, total: 0 });
|
|
1781
|
+
const collected = await source(
|
|
1782
|
+
signal,
|
|
1783
|
+
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
1784
|
+
);
|
|
1318
1785
|
onProgress({ type: "collecting", files: collected.length });
|
|
1319
1786
|
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
1320
1787
|
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
@@ -1330,6 +1797,21 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1330
1797
|
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1331
1798
|
return;
|
|
1332
1799
|
}
|
|
1800
|
+
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1801
|
+
const totalBytes = prepared.upload.reduce(
|
|
1802
|
+
(sum, target) => sum + (sizes.get(target.path) ?? 0),
|
|
1803
|
+
0
|
|
1804
|
+
);
|
|
1805
|
+
if (prepared.upload.length > 0) {
|
|
1806
|
+
onProgress({
|
|
1807
|
+
type: "uploading",
|
|
1808
|
+
done: prepared.reused,
|
|
1809
|
+
total: prepared.total,
|
|
1810
|
+
bytes: 0,
|
|
1811
|
+
totalBytes,
|
|
1812
|
+
reused: prepared.reused
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1333
1815
|
await uploadAll(
|
|
1334
1816
|
prepared.upload,
|
|
1335
1817
|
prepared.uploadUrl,
|
|
@@ -1339,11 +1821,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1339
1821
|
done: prepared.reused + done,
|
|
1340
1822
|
total: prepared.total,
|
|
1341
1823
|
bytes,
|
|
1824
|
+
totalBytes,
|
|
1342
1825
|
reused: prepared.reused,
|
|
1343
|
-
path
|
|
1826
|
+
...path === void 0 ? {} : { path }
|
|
1344
1827
|
}),
|
|
1345
1828
|
signal,
|
|
1346
|
-
options.upload
|
|
1829
|
+
options.upload,
|
|
1830
|
+
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1347
1831
|
);
|
|
1348
1832
|
onProgress({ type: "completing", reused: prepared.reused });
|
|
1349
1833
|
const completed = await completeDeploy(
|
|
@@ -1351,9 +1835,15 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1351
1835
|
prepared.deployId,
|
|
1352
1836
|
options,
|
|
1353
1837
|
signal,
|
|
1354
|
-
|
|
1838
|
+
await readSiteName(files)
|
|
1355
1839
|
);
|
|
1356
|
-
onProgress({
|
|
1840
|
+
onProgress({
|
|
1841
|
+
type: "done",
|
|
1842
|
+
url: completed.url,
|
|
1843
|
+
name: completed.name,
|
|
1844
|
+
mode: servingModeOf(completed),
|
|
1845
|
+
modeIsManual: completed.modeIsManual
|
|
1846
|
+
});
|
|
1357
1847
|
} catch (error) {
|
|
1358
1848
|
const failure2 = asDeployError(error);
|
|
1359
1849
|
onProgress({
|
|
@@ -1406,6 +1896,15 @@ function escapesRoot(root, candidate) {
|
|
|
1406
1896
|
const inside = relative(root, candidate);
|
|
1407
1897
|
return inside === "" || inside.startsWith("..");
|
|
1408
1898
|
}
|
|
1899
|
+
function describeMode(result) {
|
|
1900
|
+
if (result.mode === null) return null;
|
|
1901
|
+
if (result.nestedIndex !== null) {
|
|
1902
|
+
return `Served as documents: there is no index.html at the top, but ${result.nestedIndex}/index.html is there. To publish the website, publish the ${result.nestedIndex} folder instead.`;
|
|
1903
|
+
}
|
|
1904
|
+
if (result.mode === "docs") return "Served as documents, in the documents reader.";
|
|
1905
|
+
if (result.mode === "spa") return "Served as a single-page app.";
|
|
1906
|
+
return "Served as a static site.";
|
|
1907
|
+
}
|
|
1409
1908
|
async function resolveSite(credentials, site, newSite) {
|
|
1410
1909
|
const { subdomain, folder } = newSite;
|
|
1411
1910
|
if (site !== void 0 && subdomain !== void 0) {
|
|
@@ -1415,7 +1914,7 @@ async function resolveSite(credentials, site, newSite) {
|
|
|
1415
1914
|
}
|
|
1416
1915
|
if (site !== void 0 && folder !== void 0) {
|
|
1417
1916
|
throw new Error(
|
|
1418
|
-
"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
|
|
1917
|
+
"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 with `drop2run set folder` or the `update_site` tool's `folder`."
|
|
1419
1918
|
);
|
|
1420
1919
|
}
|
|
1421
1920
|
if (site === void 0) {
|
|
@@ -1435,6 +1934,8 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1435
1934
|
let files = 0;
|
|
1436
1935
|
let url = target.url;
|
|
1437
1936
|
let unchanged = false;
|
|
1937
|
+
let mode = null;
|
|
1938
|
+
let paths = [];
|
|
1438
1939
|
let failure2 = null;
|
|
1439
1940
|
let failureDetail;
|
|
1440
1941
|
const record = (event) => {
|
|
@@ -1443,14 +1944,22 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1443
1944
|
unchanged = true;
|
|
1444
1945
|
url = event.url || target.url;
|
|
1445
1946
|
}
|
|
1446
|
-
if (event.type === "done")
|
|
1947
|
+
if (event.type === "done") {
|
|
1948
|
+
url = event.url;
|
|
1949
|
+
mode = event.mode;
|
|
1950
|
+
}
|
|
1447
1951
|
if (event.type === "error") {
|
|
1448
1952
|
failure2 = event.message;
|
|
1449
1953
|
failureDetail = event.detail;
|
|
1450
1954
|
}
|
|
1451
1955
|
onProgress?.(event);
|
|
1452
1956
|
};
|
|
1453
|
-
|
|
1957
|
+
const watched = async (signal, onRead) => {
|
|
1958
|
+
const collected = await source(signal, onRead);
|
|
1959
|
+
paths = collected.map((file) => file.path);
|
|
1960
|
+
return collected;
|
|
1961
|
+
};
|
|
1962
|
+
await deploy(watched, target.siteId, record, void 0, {
|
|
1454
1963
|
baseUrl: credentials.apiBaseUrl,
|
|
1455
1964
|
token: credentials.token
|
|
1456
1965
|
}).catch((error) => {
|
|
@@ -1460,7 +1969,15 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1460
1969
|
thrown.detail = failureDetail ?? error?.detail;
|
|
1461
1970
|
throw thrown;
|
|
1462
1971
|
});
|
|
1463
|
-
return {
|
|
1972
|
+
return {
|
|
1973
|
+
url,
|
|
1974
|
+
siteId: target.siteId,
|
|
1975
|
+
subdomain: target.subdomain,
|
|
1976
|
+
files,
|
|
1977
|
+
unchanged,
|
|
1978
|
+
mode,
|
|
1979
|
+
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
1980
|
+
};
|
|
1464
1981
|
}
|
|
1465
1982
|
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
1466
1983
|
const found = await stat(directory).catch(() => null);
|
|
@@ -1488,6 +2005,10 @@ function shortenPath(path, width) {
|
|
|
1488
2005
|
}
|
|
1489
2006
|
function describe(event, width = 80) {
|
|
1490
2007
|
switch (event.type) {
|
|
2008
|
+
case "reading":
|
|
2009
|
+
return event.total > 0 ? `Reading ${formatBytes(event.bytes)}/${formatBytes(event.total)}` : "Reading files";
|
|
2010
|
+
case "retrying":
|
|
2011
|
+
return `${event.stalled ? "Upload stalled" : "Upload failed"}, retrying ${shortenPath(event.path, Math.max(12, width - 40))} (attempt ${event.attempt}/${event.attempts})`;
|
|
1491
2012
|
case "collecting":
|
|
1492
2013
|
return `Collected ${event.files.toLocaleString()} ${event.files === 1 ? "file" : "files"}`;
|
|
1493
2014
|
case "hashing":
|
|
@@ -1535,6 +2056,82 @@ function progressWriter(write, isTerminal, width = 80) {
|
|
|
1535
2056
|
};
|
|
1536
2057
|
return writer;
|
|
1537
2058
|
}
|
|
2059
|
+
async function readSecret(prompt, input = process.stdin, output = process.stderr) {
|
|
2060
|
+
if (input.isTTY !== true || input.setRawMode === void 0) {
|
|
2061
|
+
const parts = [];
|
|
2062
|
+
for await (const chunk of input)
|
|
2063
|
+
parts.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
2064
|
+
return parts.join("").replace(/\r?\n$/, "");
|
|
2065
|
+
}
|
|
2066
|
+
const setRawMode = input.setRawMode.bind(input);
|
|
2067
|
+
output.write(prompt);
|
|
2068
|
+
setRawMode(true);
|
|
2069
|
+
input.setEncoding("utf8");
|
|
2070
|
+
input.resume();
|
|
2071
|
+
return await new Promise((resolve2, reject) => {
|
|
2072
|
+
const line = new HiddenLine();
|
|
2073
|
+
const finish = () => {
|
|
2074
|
+
setRawMode(false);
|
|
2075
|
+
input.pause();
|
|
2076
|
+
input.off("data", onData);
|
|
2077
|
+
output.write("\n");
|
|
2078
|
+
};
|
|
2079
|
+
function onData(chunk) {
|
|
2080
|
+
const outcome = line.type(chunk);
|
|
2081
|
+
if (outcome === "more") return;
|
|
2082
|
+
finish();
|
|
2083
|
+
if (outcome === "cancelled") reject(new Error("Cancelled."));
|
|
2084
|
+
else resolve2(line.value);
|
|
2085
|
+
}
|
|
2086
|
+
input.on("data", onData);
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
class HiddenLine {
|
|
2090
|
+
/** What has been typed, one entry per code point, so Backspace never splits a surrogate pair. */
|
|
2091
|
+
characters = [];
|
|
2092
|
+
/** Where an escape sequence has got to: none, just after ESC, or inside a `CSI`/`SS3` sequence. */
|
|
2093
|
+
escape = "none";
|
|
2094
|
+
/** The line so far. */
|
|
2095
|
+
get value() {
|
|
2096
|
+
return this.characters.join("");
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Takes the characters one chunk of input carries.
|
|
2100
|
+
*
|
|
2101
|
+
* @param chunk What arrived.
|
|
2102
|
+
* @returns `done` on Enter or Ctrl+D, `cancelled` on Ctrl+C, `more` otherwise.
|
|
2103
|
+
*/
|
|
2104
|
+
type(chunk) {
|
|
2105
|
+
for (const character of chunk) {
|
|
2106
|
+
const code = character.codePointAt(0) ?? 0;
|
|
2107
|
+
if (this.escape === "started") {
|
|
2108
|
+
this.escape = character === "[" || character === "O" ? "sequence" : "none";
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
if (this.escape === "sequence") {
|
|
2112
|
+
if (code >= 64 && code <= 126) this.escape = "none";
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (character === "\r" || character === "\n" || character === "") return "done";
|
|
2116
|
+
if (character === "") return "cancelled";
|
|
2117
|
+
if (character === "\x1B") {
|
|
2118
|
+
this.escape = "started";
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
if (character === "" || character === "\b") {
|
|
2122
|
+
this.characters.pop();
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
if (character === "") {
|
|
2126
|
+
this.characters.length = 0;
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
if (code < 32) continue;
|
|
2130
|
+
this.characters.push(character);
|
|
2131
|
+
}
|
|
2132
|
+
return "more";
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
1538
2135
|
function failure(text) {
|
|
1539
2136
|
return { text, json: { error: text }, code: 1 };
|
|
1540
2137
|
}
|
|
@@ -1646,9 +2243,9 @@ async function whoami() {
|
|
|
1646
2243
|
const response = await fetch(`${found.credentials.apiBaseUrl}/me`, {
|
|
1647
2244
|
headers: { Authorization: `Bearer ${found.credentials.token}` }
|
|
1648
2245
|
});
|
|
1649
|
-
if (!response.ok) {
|
|
2246
|
+
if (!response.ok || response.status === 204) {
|
|
1650
2247
|
return failure(
|
|
1651
|
-
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}.`
|
|
2248
|
+
response.status === 401 || response.status === 204 ? "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}.`
|
|
1652
2249
|
);
|
|
1653
2250
|
}
|
|
1654
2251
|
const me = await response.json();
|
|
@@ -1714,7 +2311,7 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
|
|
|
1714
2311
|
const target = newSite.subdomain === void 0 ? site ?? project?.siteId : void 0;
|
|
1715
2312
|
if (newSite.folder !== void 0 && target !== void 0 && project !== null) {
|
|
1716
2313
|
return failure(
|
|
1717
|
-
`This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site
|
|
2314
|
+
`This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site with \`drop2run set folder <path>\`, or add \`--subdomain\` to create a new one in the folder.`
|
|
1718
2315
|
);
|
|
1719
2316
|
}
|
|
1720
2317
|
try {
|
|
@@ -1726,9 +2323,11 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
|
|
|
1726
2323
|
newSite
|
|
1727
2324
|
);
|
|
1728
2325
|
const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
|
|
2326
|
+
const mode = describeMode(result);
|
|
1729
2327
|
return {
|
|
1730
2328
|
text: `${what}
|
|
1731
|
-
${result.url}
|
|
2329
|
+
${result.url}${mode === null ? "" : `
|
|
2330
|
+
${mode}`}`,
|
|
1732
2331
|
json: result,
|
|
1733
2332
|
code: 0
|
|
1734
2333
|
};
|
|
@@ -1843,6 +2442,197 @@ async function open(site, launch = openBrowser) {
|
|
|
1843
2442
|
return failure(error instanceof Error ? error.message : String(error));
|
|
1844
2443
|
}
|
|
1845
2444
|
}
|
|
2445
|
+
function siteOr(site, usage) {
|
|
2446
|
+
const named = site ?? readProject()?.siteId;
|
|
2447
|
+
return named === void 0 ? {
|
|
2448
|
+
result: failure(
|
|
2449
|
+
`Name the site: \`${usage}\`, or run this in a folder with a ${PROJECT_FILE}.`
|
|
2450
|
+
)
|
|
2451
|
+
} : { named };
|
|
2452
|
+
}
|
|
2453
|
+
async function info(site) {
|
|
2454
|
+
const found = credentialsOr();
|
|
2455
|
+
if ("result" in found) return found.result;
|
|
2456
|
+
const target = siteOr(site, "drop2run info <subdomain>");
|
|
2457
|
+
if ("result" in target) return target.result;
|
|
2458
|
+
try {
|
|
2459
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2460
|
+
const detail = await getSite(found.credentials, summary.siteId);
|
|
2461
|
+
const folderPath = detail.folderId === null ? void 0 : (await listFolders(found.credentials)).find(
|
|
2462
|
+
(folder) => folder.folderId === detail.folderId
|
|
2463
|
+
)?.path;
|
|
2464
|
+
return {
|
|
2465
|
+
text: describeSite(detail, folderPath),
|
|
2466
|
+
json: { ...detail, mode: servingModeOf$1(detail), folderPath: folderPath ?? null },
|
|
2467
|
+
code: 0
|
|
2468
|
+
};
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
async function pauseOrResume(action, site) {
|
|
2474
|
+
const found = credentialsOr();
|
|
2475
|
+
if ("result" in found) return found.result;
|
|
2476
|
+
const target = siteOr(site, `drop2run ${action} <subdomain>`);
|
|
2477
|
+
if ("result" in target) return target.result;
|
|
2478
|
+
try {
|
|
2479
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2480
|
+
const state = await setSitePaused(found.credentials, summary.siteId, action);
|
|
2481
|
+
return {
|
|
2482
|
+
text: action === "resume" ? `${state.subdomain} is back on the air.
|
|
2483
|
+
${summary.url}` : state.status === "paused" ? `Paused ${state.subdomain}. Its files and subdomain are kept; \`drop2run resume\` puts it back.` : (
|
|
2484
|
+
// A site Drop2Run suspended stays suspended: the pause is recorded for when that lifts,
|
|
2485
|
+
// and promising `resume` here would be promising something the API refuses.
|
|
2486
|
+
`${state.subdomain} is ${state.status}, not paused by you, so \`drop2run resume\` cannot bring it back. The pause is recorded and applies if that is lifted.`
|
|
2487
|
+
),
|
|
2488
|
+
json: state,
|
|
2489
|
+
code: 0
|
|
2490
|
+
};
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const SETTINGS_USAGE = [
|
|
2496
|
+
" name <text> What the dashboard calls the site",
|
|
2497
|
+
" mode static|spa|docs How it serves files",
|
|
2498
|
+
" password Read from stdin, or typed at a hidden prompt",
|
|
2499
|
+
" forms on|off Whether its forms accept submissions",
|
|
2500
|
+
" expires <ISO 8601 instant> When it comes down; add --then delete to delete rather than pause",
|
|
2501
|
+
" folder <path|id|root> Which folder it is filed in"
|
|
2502
|
+
].join("\n");
|
|
2503
|
+
function settingChange(setting, values, then) {
|
|
2504
|
+
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
2505
|
+
const needs = (example) => ({
|
|
2506
|
+
refusal: `\`set ${setting}\` needs a value, for example \`drop2run set ${setting} ${example}\`.`
|
|
2507
|
+
});
|
|
2508
|
+
if (then !== void 0 && setting !== "expires") {
|
|
2509
|
+
return { refusal: "`--then` only applies to `set expires`." };
|
|
2510
|
+
}
|
|
2511
|
+
if (setting !== "name" && setting !== "password" && values.length > 1) {
|
|
2512
|
+
return {
|
|
2513
|
+
refusal: `\`set ${setting}\` takes one value, and "${values.slice(1).join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
switch (setting) {
|
|
2517
|
+
case "name":
|
|
2518
|
+
return value ? { change: { name: value } } : needs('"Launch notes"');
|
|
2519
|
+
case "mode":
|
|
2520
|
+
if (value === "static" || value === "spa" || value === "docs") {
|
|
2521
|
+
return { change: { mode: value } };
|
|
2522
|
+
}
|
|
2523
|
+
return { refusal: "`set mode` takes `static`, `spa` or `docs`." };
|
|
2524
|
+
case "forms":
|
|
2525
|
+
if (value === "on" || value === "off") return { change: { formsEnabled: value === "on" } };
|
|
2526
|
+
return { refusal: "`set forms` takes `on` or `off`." };
|
|
2527
|
+
case "password": {
|
|
2528
|
+
if (value !== void 0) {
|
|
2529
|
+
return {
|
|
2530
|
+
refusal: '`set password` does not take the password as an argument, where it would stay in your shell history. Run `drop2run set password` and type it, or pipe it in: `printf %s "$SITE_PASSWORD" | drop2run set password`.'
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
return { password: true };
|
|
2534
|
+
}
|
|
2535
|
+
case "expires": {
|
|
2536
|
+
if (value === void 0) return needs("2026-12-31T09:00:00Z");
|
|
2537
|
+
if (then !== void 0 && then !== "pause" && then !== "delete") {
|
|
2538
|
+
return { refusal: "`--then` takes `pause` or `delete`." };
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
change: {
|
|
2542
|
+
expiresAt: value,
|
|
2543
|
+
...then === void 0 ? {} : { expiryAction: then }
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
case "folder":
|
|
2548
|
+
return value ? { folder: value } : needs("Clients/Acme");
|
|
2549
|
+
default:
|
|
2550
|
+
return {
|
|
2551
|
+
refusal: `${setting === void 0 ? "Name a setting." : `\`${setting}\` is not a setting.`} Settings:
|
|
2552
|
+
${SETTINGS_USAGE}`
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
async function set(site, setting, values, options, readPassword = () => readSecret("Password for visitors: ")) {
|
|
2557
|
+
const parsed = settingChange(setting, values, options.then);
|
|
2558
|
+
if ("refusal" in parsed) return failure(parsed.refusal);
|
|
2559
|
+
const found = credentialsOr();
|
|
2560
|
+
if ("result" in found) return found.result;
|
|
2561
|
+
const target = siteOr(site, `drop2run set ${setting} … --site <subdomain>`);
|
|
2562
|
+
if ("result" in target) return target.result;
|
|
2563
|
+
try {
|
|
2564
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2565
|
+
let change;
|
|
2566
|
+
if ("folder" in parsed) {
|
|
2567
|
+
change = { folderId: await resolveFolderTarget(found.credentials, parsed.folder) };
|
|
2568
|
+
} else if ("password" in parsed) {
|
|
2569
|
+
const password = await readPassword();
|
|
2570
|
+
if (password === "") {
|
|
2571
|
+
return failure("No password was given. To remove one, run `drop2run unset password`.");
|
|
2572
|
+
}
|
|
2573
|
+
change = { password };
|
|
2574
|
+
} else {
|
|
2575
|
+
change = parsed.change;
|
|
2576
|
+
}
|
|
2577
|
+
if (change.expiryAction === "delete" && !options.confirmed) {
|
|
2578
|
+
return failure(
|
|
2579
|
+
`This would delete ${summary.subdomain} and every version published to it when ${change.expiresAt} arrives, with no way back then.
|
|
2580
|
+
Add --yes if that is what you want, or leave out --then to pause it instead.`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
return await applied(found.credentials, summary.siteId, summary.subdomain, change);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
async function unset(site, setting, extra = []) {
|
|
2589
|
+
if (extra.length > 0) {
|
|
2590
|
+
return failure(
|
|
2591
|
+
`\`unset ${setting}\` takes nothing after it, and "${extra.join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
const clear = /* @__PURE__ */ new Map([
|
|
2595
|
+
["name", { name: "" }],
|
|
2596
|
+
["password", { password: "" }],
|
|
2597
|
+
["expires", { expiresAt: "" }]
|
|
2598
|
+
]);
|
|
2599
|
+
const change = setting === void 0 ? void 0 : clear.get(setting);
|
|
2600
|
+
if (change === void 0) {
|
|
2601
|
+
return failure(
|
|
2602
|
+
"`unset` clears `name`, `password` or `expires`. To file the site at the top level, run `drop2run set folder root`."
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
const found = credentialsOr();
|
|
2606
|
+
if ("result" in found) return found.result;
|
|
2607
|
+
const target = siteOr(site, `drop2run unset ${setting} --site <subdomain>`);
|
|
2608
|
+
if ("result" in target) return target.result;
|
|
2609
|
+
try {
|
|
2610
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2611
|
+
return await applied(found.credentials, summary.siteId, summary.subdomain, change);
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
async function applied(credentials, siteId, subdomain, change) {
|
|
2617
|
+
const settings = await updateSiteSettings(credentials, siteId, change);
|
|
2618
|
+
const takedown = settings.expiresAt === null ? "none" : `${settings.expiresAt} (${settings.expiryAction})`;
|
|
2619
|
+
return {
|
|
2620
|
+
text: [
|
|
2621
|
+
`Updated ${subdomain}.`,
|
|
2622
|
+
` name ${settings.name ?? "(none)"}`,
|
|
2623
|
+
` access ${accessOf(settings)}`,
|
|
2624
|
+
` mode ${servingModeOf$1(settings)}`,
|
|
2625
|
+
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2626
|
+
` takedown ${takedown}`,
|
|
2627
|
+
...settings.replacedInviteOnly ? [
|
|
2628
|
+
"The password replaced invite-only: the people on its list can no longer get in without it. The list is kept for if you switch back in the dashboard."
|
|
2629
|
+
] : [],
|
|
2630
|
+
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2631
|
+
].join("\n"),
|
|
2632
|
+
json: { ...settings, mode: servingModeOf$1(settings) },
|
|
2633
|
+
code: 0
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
1846
2636
|
async function tokens() {
|
|
1847
2637
|
const found = credentialsOr();
|
|
1848
2638
|
if ("result" in found) return found.result;
|
|
@@ -1894,6 +2684,11 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
1894
2684
|
drop2run ls List your sites
|
|
1895
2685
|
drop2run folders List your folders, as --folder takes them
|
|
1896
2686
|
drop2run open [site] Open a site in a browser
|
|
2687
|
+
drop2run info [site] Show a site's settings and versions
|
|
2688
|
+
drop2run set <setting> <value> [--site X] Change one setting (see Settings below)
|
|
2689
|
+
drop2run unset name|password|expires Clear a name, a password or a takedown
|
|
2690
|
+
drop2run pause [site] Take a site off the air, keeping its files
|
|
2691
|
+
drop2run resume [site] Put a paused site back on the air
|
|
1897
2692
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
1898
2693
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
1899
2694
|
drop2run token list List your access tokens
|
|
@@ -1909,6 +2704,23 @@ Flags
|
|
|
1909
2704
|
or a folder id (init and deploy only)
|
|
1910
2705
|
--device Sign in by approving a code on another machine
|
|
1911
2706
|
--yes Confirm a deletion, which cannot be undone
|
|
2707
|
+
--then X What \`set expires\` does when the date arrives: pause
|
|
2708
|
+
(the default, keeps the files) or delete (needs --yes)
|
|
2709
|
+
|
|
2710
|
+
Settings
|
|
2711
|
+
set name <text> What the dashboard calls the site; put a
|
|
2712
|
+
name with words starting "-" after --
|
|
2713
|
+
set mode static|spa|docs static answers 404 for a missing path, spa
|
|
2714
|
+
falls back to index.html, docs uses the viewer
|
|
2715
|
+
set password Typed at a hidden prompt, or piped in:
|
|
2716
|
+
printf %s "$PW" | drop2run set password
|
|
2717
|
+
set forms on|off Whether its forms accept submissions
|
|
2718
|
+
set expires <ISO 8601> Take it down then, e.g. 2026-12-31T09:00:00Z
|
|
2719
|
+
set folder <path|id|root> Move it to a folder, or root for the top level
|
|
2720
|
+
|
|
2721
|
+
\`set\` and \`unset\` act on --site, else the site in drop2run.json. A
|
|
2722
|
+
setting your plan does not include is refused with the reason, and
|
|
2723
|
+
\`info\` says which ones those are.
|
|
1912
2724
|
|
|
1913
2725
|
Where a site comes from
|
|
1914
2726
|
--subdomain creates one under the name you give. Otherwise: --site, then
|
|
@@ -1925,10 +2737,10 @@ Where a site comes from
|
|
|
1925
2737
|
Signing in
|
|
1926
2738
|
\`login\` opens a browser and listens on 127.0.0.1, so it needs both on this
|
|
1927
2739
|
machine. Over SSH, in a container or under WSL, use \`login --device\`: it
|
|
1928
|
-
prints a short code to enter at https://dropto.run/device from anywhere.
|
|
2740
|
+
prints a short code to enter at https://app.dropto.run/device from anywhere.
|
|
1929
2741
|
|
|
1930
2742
|
For CI, neither flow applies — create a token at
|
|
1931
|
-
https://dropto.run/account/tokens and either set DROP2RUN_TOKEN in your
|
|
2743
|
+
https://app.dropto.run/account/tokens and either set DROP2RUN_TOKEN in your
|
|
1932
2744
|
environment or put it in ~/.config/drop2run/config.json as {"token": "d2r_..."}.
|
|
1933
2745
|
`;
|
|
1934
2746
|
function flagValue(args, flag) {
|
|
@@ -1937,7 +2749,7 @@ function flagValue(args, flag) {
|
|
|
1937
2749
|
const value = args[at + 1];
|
|
1938
2750
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
1939
2751
|
}
|
|
1940
|
-
const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
|
|
2752
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then"];
|
|
1941
2753
|
function positionals(argv) {
|
|
1942
2754
|
const found = [];
|
|
1943
2755
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -1961,7 +2773,7 @@ const SUBDOMAIN_FLAG = {
|
|
|
1961
2773
|
const FOLDER_FLAG = {
|
|
1962
2774
|
flag: "--folder",
|
|
1963
2775
|
example: "Clients/Acme",
|
|
1964
|
-
effect: "`--folder` only files a new one — move an existing site
|
|
2776
|
+
effect: "`--folder` only files a new one — move an existing site with `drop2run set folder`",
|
|
1965
2777
|
elsewhere: (command) => command === "ls" || command === "list" ? " `drop2run folders` lists them." : ""
|
|
1966
2778
|
};
|
|
1967
2779
|
function newSiteFlagProblem(argv, command, site, value, described) {
|
|
@@ -1978,12 +2790,15 @@ function newSiteFlagProblem(argv, command, site, value, described) {
|
|
|
1978
2790
|
}
|
|
1979
2791
|
return void 0;
|
|
1980
2792
|
}
|
|
1981
|
-
async function run(
|
|
2793
|
+
async function run(commandLine) {
|
|
2794
|
+
const end = commandLine.indexOf("--");
|
|
2795
|
+
const argv = end === -1 ? commandLine : commandLine.slice(0, end);
|
|
2796
|
+
const literal = end === -1 ? [] : commandLine.slice(end + 1);
|
|
1982
2797
|
const json = argv.includes("--json");
|
|
1983
|
-
const positional = positionals(argv);
|
|
2798
|
+
const positional = [...positionals(argv), ...literal];
|
|
1984
2799
|
const [command, ...rest] = positional;
|
|
1985
2800
|
if (argv.includes("--version")) {
|
|
1986
|
-
const { version } = await import("./package-
|
|
2801
|
+
const { version } = await import("./package-BycuI1VJ.js").then(
|
|
1987
2802
|
(module) => module.default
|
|
1988
2803
|
);
|
|
1989
2804
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -1999,6 +2814,12 @@ async function run(argv) {
|
|
|
1999
2814
|
const failed = { text: refusal, json: { error: refusal }, code: 1 };
|
|
2000
2815
|
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
2001
2816
|
}
|
|
2817
|
+
const then = flagValue(argv, "--then");
|
|
2818
|
+
if (argv.includes("--then") && then === void 0) {
|
|
2819
|
+
const message = "`--then` needs a value: `--then pause` or `--then delete`.";
|
|
2820
|
+
const failed = { text: message, json: { error: message }, code: 1 };
|
|
2821
|
+
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
2822
|
+
}
|
|
2002
2823
|
const result = await dispatch(
|
|
2003
2824
|
command,
|
|
2004
2825
|
rest,
|
|
@@ -2006,11 +2827,12 @@ async function run(argv) {
|
|
|
2006
2827
|
json,
|
|
2007
2828
|
argv.includes("--device"),
|
|
2008
2829
|
argv.includes("--yes"),
|
|
2009
|
-
{ subdomain, folder }
|
|
2830
|
+
{ subdomain, folder },
|
|
2831
|
+
then
|
|
2010
2832
|
);
|
|
2011
2833
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
2012
2834
|
}
|
|
2013
|
-
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
|
|
2835
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0) {
|
|
2014
2836
|
switch (command) {
|
|
2015
2837
|
case "login": {
|
|
2016
2838
|
const print = json ? () => {
|
|
@@ -2036,6 +2858,15 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
|
|
|
2036
2858
|
return await folders();
|
|
2037
2859
|
case "open":
|
|
2038
2860
|
return await open(rest[0] ?? site);
|
|
2861
|
+
case "info":
|
|
2862
|
+
return await info(rest[0] ?? site);
|
|
2863
|
+
case "set":
|
|
2864
|
+
return await set(site, rest[0], rest.slice(1), { then, confirmed });
|
|
2865
|
+
case "unset":
|
|
2866
|
+
return await unset(site, rest[0], rest.slice(1));
|
|
2867
|
+
case "pause":
|
|
2868
|
+
case "resume":
|
|
2869
|
+
return await pauseOrResume(command, rest[0] ?? site);
|
|
2039
2870
|
case "rollback":
|
|
2040
2871
|
return await rollback(rest[0], site);
|
|
2041
2872
|
case "rm":
|
|
@@ -2043,7 +2874,7 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
|
|
|
2043
2874
|
case "token":
|
|
2044
2875
|
case "tokens":
|
|
2045
2876
|
return rest[0] === void 0 || rest[0] === "list" ? await tokens() : {
|
|
2046
|
-
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.`,
|
|
2877
|
+
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.`,
|
|
2047
2878
|
json: { error: `Unknown token subcommand "${rest[0]}".` },
|
|
2048
2879
|
code: 1
|
|
2049
2880
|
};
|