drop2run 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +461 -92
- package/dist/{package-LdzYr7IV.js → package-B79z0E4v.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ drop2run rm <site> --yes Delete a site and everything on it
|
|
|
32
32
|
drop2run comments [site] Read the comments left on a site's pages
|
|
33
33
|
drop2run comments reply <thread> <text> Answer a thread as the owner [--resolve]
|
|
34
34
|
drop2run comments resolve|reopen <thread> Resolve a thread, or open it again
|
|
35
|
+
drop2run people [site] Who the site is shared with
|
|
36
|
+
drop2run invite <email>... [--role comment] Share it with people; each is emailed
|
|
37
|
+
drop2run uninvite <email> Take someone off the list
|
|
35
38
|
drop2run token list List your access tokens
|
|
36
39
|
drop2run whoami Check the token and whose it is
|
|
37
40
|
drop2run where Show which token source is in use
|
|
@@ -40,6 +43,18 @@ drop2run --version Print the version
|
|
|
40
43
|
|
|
41
44
|
`--json` on any command prints machine-readable output instead of text.
|
|
42
45
|
|
|
46
|
+
## Sharing for review
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
drop2run invite dana@example.com --role comment --site calm-cedar # emailed an invitation
|
|
50
|
+
drop2run set invite-only on --site calm-cedar # only the list gets in
|
|
51
|
+
drop2run set comments invited --site calm-cedar # the people invited to comment can
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`drop2run people` shows the list and `drop2run uninvite` takes someone off it. `set comments anyone` lets
|
|
55
|
+
anybody signed in comment instead, on a site you keep public or behind a password. Both are plan features,
|
|
56
|
+
refused with the reason where the plan lacks them.
|
|
57
|
+
|
|
43
58
|
## Comments
|
|
44
59
|
|
|
45
60
|
On a plan that includes comments, people viewing a site can pin comments to its pages. `drop2run comments`
|
package/dist/index.js
CHANGED
|
@@ -190,10 +190,7 @@ async function folderFromListing(credentials, siteId, subdomain) {
|
|
|
190
190
|
}
|
|
191
191
|
async function getSite(credentials, siteId) {
|
|
192
192
|
const [site, invitedOnly] = await Promise.all([
|
|
193
|
-
call(
|
|
194
|
-
credentials,
|
|
195
|
-
`sites/${encodeURIComponent(siteId)}`
|
|
196
|
-
),
|
|
193
|
+
call(credentials, `sites/${encodeURIComponent(siteId)}`),
|
|
197
194
|
readInvitedOnly(credentials, siteId)
|
|
198
195
|
]);
|
|
199
196
|
const folderId = site.folderId !== void 0 ? site.folderId : await folderFromListing(credentials, site.siteId, site.subdomain);
|
|
@@ -215,6 +212,8 @@ async function getSite(credentials, siteId) {
|
|
|
215
212
|
expiresAt: site.expiresAt,
|
|
216
213
|
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
217
214
|
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
215
|
+
comments: audienceOf(site.commentAudience),
|
|
216
|
+
commentsAvailable: site.feedbackAvailable === true,
|
|
218
217
|
folderId,
|
|
219
218
|
deploys: site.deploys.map((deploy2) => ({
|
|
220
219
|
deployId: deploy2.deployId,
|
|
@@ -239,9 +238,11 @@ async function updateSiteSettings(credentials, siteId, change) {
|
|
|
239
238
|
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
240
239
|
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
241
240
|
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
241
|
+
if (change.invitedOnly !== void 0) body.invitedOnly = change.invitedOnly;
|
|
242
|
+
if (change.comments !== void 0) body.commentAudience = API_AUDIENCE[change.comments];
|
|
242
243
|
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
243
244
|
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
244
|
-
const settings = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
245
|
+
const { commentAudience, ...settings } = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
245
246
|
method: "PATCH",
|
|
246
247
|
body: JSON.stringify(body)
|
|
247
248
|
});
|
|
@@ -250,9 +251,19 @@ async function updateSiteSettings(credentials, siteId, change) {
|
|
|
250
251
|
...settings,
|
|
251
252
|
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
252
253
|
invitedOnly,
|
|
254
|
+
comments: audienceOf(commentAudience),
|
|
253
255
|
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
254
256
|
};
|
|
255
257
|
}
|
|
258
|
+
const API_AUDIENCE = {
|
|
259
|
+
off: "Off",
|
|
260
|
+
invited: "Invited",
|
|
261
|
+
anyone: "Anyone"
|
|
262
|
+
};
|
|
263
|
+
function audienceOf(value) {
|
|
264
|
+
const lower = value?.toLowerCase();
|
|
265
|
+
return lower === "off" || lower === "invited" || lower === "anyone" ? lower : null;
|
|
266
|
+
}
|
|
256
267
|
async function setSitePaused(credentials, siteId, action) {
|
|
257
268
|
const state = await call(
|
|
258
269
|
credentials,
|
|
@@ -371,6 +382,34 @@ async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
|
371
382
|
{ method: "POST" }
|
|
372
383
|
);
|
|
373
384
|
}
|
|
385
|
+
const PEOPLE_PER_REQUEST = 50;
|
|
386
|
+
async function listPeople(credentials, siteId) {
|
|
387
|
+
const page = await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers?pageSize=${PEOPLE_PER_REQUEST}`);
|
|
388
|
+
return {
|
|
389
|
+
invitedOnly: page.invitedOnly,
|
|
390
|
+
total: page.total,
|
|
391
|
+
limit: page.limit,
|
|
392
|
+
people: page.viewers.map((row) => ({
|
|
393
|
+
email: row.email,
|
|
394
|
+
role: enumName(row.role),
|
|
395
|
+
owner: row.owner,
|
|
396
|
+
status: enumName(row.status)
|
|
397
|
+
}))
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async function invitePeople(credentials, siteId, emails, role) {
|
|
401
|
+
return await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers`, {
|
|
402
|
+
method: "POST",
|
|
403
|
+
body: JSON.stringify({ emails, role: role === "comment" ? "Comment" : "View", notify: true })
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function removePerson(credentials, siteId, email) {
|
|
407
|
+
await call(
|
|
408
|
+
credentials,
|
|
409
|
+
`sites/${encodeURIComponent(siteId)}/viewers?email=${encodeURIComponent(email)}`,
|
|
410
|
+
{ method: "DELETE" }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
374
413
|
const VERSIONS_LISTED = 10;
|
|
375
414
|
function servingModeOf$1(site) {
|
|
376
415
|
if (site.spaMode) return "spa";
|
|
@@ -399,6 +438,7 @@ function describeSite(site, folderPath) {
|
|
|
399
438
|
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
400
439
|
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
401
440
|
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
441
|
+
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
402
442
|
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
403
443
|
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
404
444
|
"",
|
|
@@ -671,7 +711,9 @@ async function prepareDeploy(siteId, files, options, signal) {
|
|
|
671
711
|
}));
|
|
672
712
|
const prepared = await request(
|
|
673
713
|
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
674
|
-
|
|
714
|
+
// `batches` asks for batch permits; an API that predates them ignores the field, and one that
|
|
715
|
+
// knows them answers with single permits until they are switched on.
|
|
716
|
+
{ files: manifest, batches: true },
|
|
675
717
|
options,
|
|
676
718
|
signal
|
|
677
719
|
);
|
|
@@ -760,6 +802,47 @@ async function problemToError(response) {
|
|
|
760
802
|
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
761
803
|
return new DeployError(code, message, problem);
|
|
762
804
|
}
|
|
805
|
+
function batchLayout(members) {
|
|
806
|
+
return members.map((member) => `${member.sha256} ${member.size}
|
|
807
|
+
`).join("");
|
|
808
|
+
}
|
|
809
|
+
function isBatch(target) {
|
|
810
|
+
return "files" in target;
|
|
811
|
+
}
|
|
812
|
+
function parseBatchResult(body) {
|
|
813
|
+
try {
|
|
814
|
+
const parsed = JSON.parse(body);
|
|
815
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
816
|
+
const { stored, present, failed } = parsed;
|
|
817
|
+
if (typeof stored !== "number" || typeof present !== "number" || !Array.isArray(failed))
|
|
818
|
+
return null;
|
|
819
|
+
return { stored, present, failed };
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function mapInOrder(items, limit, fn, signal) {
|
|
825
|
+
const results = new Array(items.length);
|
|
826
|
+
let next = 0;
|
|
827
|
+
let failure2 = null;
|
|
828
|
+
const lane = async () => {
|
|
829
|
+
for (; ; ) {
|
|
830
|
+
if (failure2 !== null) return;
|
|
831
|
+
const index = next++;
|
|
832
|
+
if (index >= items.length) return;
|
|
833
|
+
try {
|
|
834
|
+
results[index] = await fn(items[index], index);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
failure2 ??= { error };
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const lanes = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, lane);
|
|
842
|
+
await Promise.all(lanes);
|
|
843
|
+
if (failure2 !== null) throw failure2.error;
|
|
844
|
+
return results;
|
|
845
|
+
}
|
|
763
846
|
const FILE_TYPES = [
|
|
764
847
|
// Documents. The three extensions that make a folder a documents site.
|
|
765
848
|
{
|
|
@@ -874,13 +957,14 @@ const FILE_TYPES = [
|
|
|
874
957
|
reader: true,
|
|
875
958
|
editable: true
|
|
876
959
|
},
|
|
877
|
-
// Source the viewer highlights.
|
|
960
|
+
// Source the viewer highlights. Inside a documents site a link straight to one opens it in the
|
|
961
|
+
// reader; on an ordinary site, and with `?raw`, it hands over the file.
|
|
878
962
|
{
|
|
879
963
|
ext: ".css",
|
|
880
964
|
mime: "text/css; charset=utf-8",
|
|
881
965
|
kind: "code",
|
|
882
966
|
document: false,
|
|
883
|
-
reader:
|
|
967
|
+
reader: true,
|
|
884
968
|
editable: true
|
|
885
969
|
},
|
|
886
970
|
{
|
|
@@ -888,7 +972,7 @@ const FILE_TYPES = [
|
|
|
888
972
|
mime: "text/javascript; charset=utf-8",
|
|
889
973
|
kind: "code",
|
|
890
974
|
document: false,
|
|
891
|
-
reader:
|
|
975
|
+
reader: true,
|
|
892
976
|
editable: true
|
|
893
977
|
},
|
|
894
978
|
{
|
|
@@ -896,7 +980,7 @@ const FILE_TYPES = [
|
|
|
896
980
|
mime: null,
|
|
897
981
|
kind: "code",
|
|
898
982
|
document: false,
|
|
899
|
-
reader:
|
|
983
|
+
reader: true,
|
|
900
984
|
editable: false
|
|
901
985
|
},
|
|
902
986
|
{
|
|
@@ -904,7 +988,7 @@ const FILE_TYPES = [
|
|
|
904
988
|
mime: null,
|
|
905
989
|
kind: "code",
|
|
906
990
|
document: false,
|
|
907
|
-
reader:
|
|
991
|
+
reader: true,
|
|
908
992
|
editable: false
|
|
909
993
|
},
|
|
910
994
|
{
|
|
@@ -912,7 +996,7 @@ const FILE_TYPES = [
|
|
|
912
996
|
mime: null,
|
|
913
997
|
kind: "code",
|
|
914
998
|
document: false,
|
|
915
|
-
reader:
|
|
999
|
+
reader: true,
|
|
916
1000
|
editable: false
|
|
917
1001
|
},
|
|
918
1002
|
{
|
|
@@ -920,19 +1004,18 @@ const FILE_TYPES = [
|
|
|
920
1004
|
mime: null,
|
|
921
1005
|
kind: "code",
|
|
922
1006
|
document: false,
|
|
923
|
-
reader:
|
|
1007
|
+
reader: true,
|
|
924
1008
|
editable: false
|
|
925
1009
|
},
|
|
926
1010
|
// The languages an assistant writes most, beyond the web's own. Same row as `.ts`: shown as source
|
|
927
|
-
//
|
|
928
|
-
// link names it directly, and no media type — adding one changes what a browser does with a
|
|
1011
|
+
// in the reader, handed over as a file with `?raw` or on an ordinary site, and no media type — adding one changes what a browser does with a
|
|
929
1012
|
// published file, which I10 makes a decision of its own rather than a side effect of this list.
|
|
930
1013
|
{
|
|
931
1014
|
ext: ".py",
|
|
932
1015
|
mime: null,
|
|
933
1016
|
kind: "code",
|
|
934
1017
|
document: false,
|
|
935
|
-
reader:
|
|
1018
|
+
reader: true,
|
|
936
1019
|
editable: false
|
|
937
1020
|
},
|
|
938
1021
|
{
|
|
@@ -940,7 +1023,7 @@ const FILE_TYPES = [
|
|
|
940
1023
|
mime: null,
|
|
941
1024
|
kind: "code",
|
|
942
1025
|
document: false,
|
|
943
|
-
reader:
|
|
1026
|
+
reader: true,
|
|
944
1027
|
editable: false
|
|
945
1028
|
},
|
|
946
1029
|
{
|
|
@@ -948,7 +1031,7 @@ const FILE_TYPES = [
|
|
|
948
1031
|
mime: null,
|
|
949
1032
|
kind: "code",
|
|
950
1033
|
document: false,
|
|
951
|
-
reader:
|
|
1034
|
+
reader: true,
|
|
952
1035
|
editable: false
|
|
953
1036
|
},
|
|
954
1037
|
{
|
|
@@ -956,7 +1039,7 @@ const FILE_TYPES = [
|
|
|
956
1039
|
mime: null,
|
|
957
1040
|
kind: "code",
|
|
958
1041
|
document: false,
|
|
959
|
-
reader:
|
|
1042
|
+
reader: true,
|
|
960
1043
|
editable: false
|
|
961
1044
|
},
|
|
962
1045
|
{
|
|
@@ -964,7 +1047,7 @@ const FILE_TYPES = [
|
|
|
964
1047
|
mime: null,
|
|
965
1048
|
kind: "code",
|
|
966
1049
|
document: false,
|
|
967
|
-
reader:
|
|
1050
|
+
reader: true,
|
|
968
1051
|
editable: false
|
|
969
1052
|
},
|
|
970
1053
|
{
|
|
@@ -972,7 +1055,7 @@ const FILE_TYPES = [
|
|
|
972
1055
|
mime: null,
|
|
973
1056
|
kind: "code",
|
|
974
1057
|
document: false,
|
|
975
|
-
reader:
|
|
1058
|
+
reader: true,
|
|
976
1059
|
editable: false
|
|
977
1060
|
},
|
|
978
1061
|
{
|
|
@@ -980,7 +1063,7 @@ const FILE_TYPES = [
|
|
|
980
1063
|
mime: null,
|
|
981
1064
|
kind: "code",
|
|
982
1065
|
document: false,
|
|
983
|
-
reader:
|
|
1066
|
+
reader: true,
|
|
984
1067
|
editable: false
|
|
985
1068
|
},
|
|
986
1069
|
{
|
|
@@ -988,7 +1071,7 @@ const FILE_TYPES = [
|
|
|
988
1071
|
mime: null,
|
|
989
1072
|
kind: "code",
|
|
990
1073
|
document: false,
|
|
991
|
-
reader:
|
|
1074
|
+
reader: true,
|
|
992
1075
|
editable: false
|
|
993
1076
|
},
|
|
994
1077
|
{
|
|
@@ -996,7 +1079,7 @@ const FILE_TYPES = [
|
|
|
996
1079
|
mime: null,
|
|
997
1080
|
kind: "code",
|
|
998
1081
|
document: false,
|
|
999
|
-
reader:
|
|
1082
|
+
reader: true,
|
|
1000
1083
|
editable: false
|
|
1001
1084
|
},
|
|
1002
1085
|
{
|
|
@@ -1004,7 +1087,7 @@ const FILE_TYPES = [
|
|
|
1004
1087
|
mime: null,
|
|
1005
1088
|
kind: "code",
|
|
1006
1089
|
document: false,
|
|
1007
|
-
reader:
|
|
1090
|
+
reader: true,
|
|
1008
1091
|
editable: false
|
|
1009
1092
|
},
|
|
1010
1093
|
{
|
|
@@ -1012,7 +1095,7 @@ const FILE_TYPES = [
|
|
|
1012
1095
|
mime: null,
|
|
1013
1096
|
kind: "code",
|
|
1014
1097
|
document: false,
|
|
1015
|
-
reader:
|
|
1098
|
+
reader: true,
|
|
1016
1099
|
editable: false
|
|
1017
1100
|
},
|
|
1018
1101
|
{
|
|
@@ -1020,7 +1103,7 @@ const FILE_TYPES = [
|
|
|
1020
1103
|
mime: null,
|
|
1021
1104
|
kind: "code",
|
|
1022
1105
|
document: false,
|
|
1023
|
-
reader:
|
|
1106
|
+
reader: true,
|
|
1024
1107
|
editable: false
|
|
1025
1108
|
},
|
|
1026
1109
|
{
|
|
@@ -1028,7 +1111,7 @@ const FILE_TYPES = [
|
|
|
1028
1111
|
mime: null,
|
|
1029
1112
|
kind: "code",
|
|
1030
1113
|
document: false,
|
|
1031
|
-
reader:
|
|
1114
|
+
reader: true,
|
|
1032
1115
|
editable: false
|
|
1033
1116
|
},
|
|
1034
1117
|
{
|
|
@@ -1036,7 +1119,7 @@ const FILE_TYPES = [
|
|
|
1036
1119
|
mime: null,
|
|
1037
1120
|
kind: "code",
|
|
1038
1121
|
document: false,
|
|
1039
|
-
reader:
|
|
1122
|
+
reader: true,
|
|
1040
1123
|
editable: false
|
|
1041
1124
|
},
|
|
1042
1125
|
{
|
|
@@ -1044,7 +1127,7 @@ const FILE_TYPES = [
|
|
|
1044
1127
|
mime: "application/xml",
|
|
1045
1128
|
kind: "code",
|
|
1046
1129
|
document: false,
|
|
1047
|
-
reader:
|
|
1130
|
+
reader: true,
|
|
1048
1131
|
editable: true
|
|
1049
1132
|
},
|
|
1050
1133
|
// Pictures, shown with an img element.
|
|
@@ -1115,25 +1198,25 @@ const FILE_TYPES = [
|
|
|
1115
1198
|
reader: false,
|
|
1116
1199
|
editable: false
|
|
1117
1200
|
},
|
|
1118
|
-
// A customer's own pages.
|
|
1201
|
+
// A customer's own pages. Inside a documents site a navigation opens the reader, which frames the
|
|
1202
|
+
// page; `?raw` and every ordinary site serve the page itself.
|
|
1119
1203
|
{
|
|
1120
1204
|
ext: ".html",
|
|
1121
1205
|
mime: "text/html; charset=utf-8",
|
|
1122
1206
|
kind: "html",
|
|
1123
1207
|
document: false,
|
|
1124
|
-
reader:
|
|
1208
|
+
reader: true,
|
|
1125
1209
|
editable: true
|
|
1126
1210
|
},
|
|
1127
1211
|
{
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
// introduced as a refactor and a refactor that changes what a URL answers is not one.
|
|
1212
|
+
// An HTML page in every respect the Worker decides by extension: media type, the revalidated
|
|
1213
|
+
// cache policy invariant I7 requires, and the badge's `isHtml`. Missing from `TYPES` it used to
|
|
1214
|
+
// download, which inside the reader meant a frame that downloaded instead of showing a page.
|
|
1132
1215
|
ext: ".htm",
|
|
1133
|
-
mime:
|
|
1216
|
+
mime: "text/html; charset=utf-8",
|
|
1134
1217
|
kind: "html",
|
|
1135
1218
|
document: false,
|
|
1136
|
-
reader:
|
|
1219
|
+
reader: true,
|
|
1137
1220
|
editable: true
|
|
1138
1221
|
},
|
|
1139
1222
|
// Everything below renders as nothing — the viewer offers a download — but still needs a media
|
|
@@ -1243,13 +1326,46 @@ async function sha256Hex(bytes) {
|
|
|
1243
1326
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1244
1327
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1245
1328
|
}
|
|
1329
|
+
const HASH_CONCURRENCY = 4;
|
|
1330
|
+
const HASH_BYTES_IN_FLIGHT = 16 * 1024 * 1024;
|
|
1246
1331
|
async function hashAll(files, onProgress, signal) {
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1332
|
+
const budget = byteBudget(HASH_BYTES_IN_FLIGHT);
|
|
1333
|
+
let done = 0;
|
|
1334
|
+
return mapInOrder(
|
|
1335
|
+
files,
|
|
1336
|
+
HASH_CONCURRENCY,
|
|
1337
|
+
async (file) => {
|
|
1338
|
+
const size = sizeOf(file.bytes);
|
|
1339
|
+
await budget.acquire(size);
|
|
1340
|
+
try {
|
|
1341
|
+
signal?.throwIfAborted();
|
|
1342
|
+
const hashed = { ...file, sha256: await sha256Hex(await readForHash(file)) };
|
|
1343
|
+
done += 1;
|
|
1344
|
+
onProgress?.(done, files.length);
|
|
1345
|
+
return hashed;
|
|
1346
|
+
} finally {
|
|
1347
|
+
budget.release(size);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
function byteBudget(limit) {
|
|
1353
|
+
let held = 0;
|
|
1354
|
+
let waiting = [];
|
|
1355
|
+
return {
|
|
1356
|
+
async acquire(size) {
|
|
1357
|
+
while (held > 0 && held + size > limit) {
|
|
1358
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
1359
|
+
}
|
|
1360
|
+
held += size;
|
|
1361
|
+
},
|
|
1362
|
+
release(size) {
|
|
1363
|
+
held -= size;
|
|
1364
|
+
const woken = waiting;
|
|
1365
|
+
waiting = [];
|
|
1366
|
+
for (const wake of woken) wake();
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1253
1369
|
}
|
|
1254
1370
|
async function readForHash(file) {
|
|
1255
1371
|
try {
|
|
@@ -1278,10 +1394,13 @@ const IGNORED_DIRECTORIES = [
|
|
|
1278
1394
|
".codex",
|
|
1279
1395
|
".gemini"
|
|
1280
1396
|
];
|
|
1397
|
+
function isEnvironmentFile(name) {
|
|
1398
|
+
return name === ".env" || name === ".envrc" || name.startsWith(".env.");
|
|
1399
|
+
}
|
|
1281
1400
|
function shouldIgnore(path) {
|
|
1282
1401
|
const segments = path.split("/");
|
|
1283
1402
|
const name = segments.at(-1) ?? "";
|
|
1284
|
-
if (name === "" || IGNORED_EXACT.has(name)) return true;
|
|
1403
|
+
if (name === "" || IGNORED_EXACT.has(name) || isEnvironmentFile(name)) return true;
|
|
1285
1404
|
if (name.startsWith("._")) return true;
|
|
1286
1405
|
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
1287
1406
|
}
|
|
@@ -1416,9 +1535,7 @@ async function readSiteName(files) {
|
|
|
1416
1535
|
}
|
|
1417
1536
|
}
|
|
1418
1537
|
function decodeHead(bytes) {
|
|
1419
|
-
return new TextDecoder("utf-8", { fatal: false }).decode(
|
|
1420
|
-
bytes.subarray(0, HEAD_BYTES)
|
|
1421
|
-
);
|
|
1538
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, HEAD_BYTES));
|
|
1422
1539
|
}
|
|
1423
1540
|
function nameFromHtml(html) {
|
|
1424
1541
|
const document = parse(html);
|
|
@@ -1443,9 +1560,7 @@ function parse(html) {
|
|
|
1443
1560
|
}
|
|
1444
1561
|
}
|
|
1445
1562
|
function matchTag(html, tag) {
|
|
1446
|
-
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(
|
|
1447
|
-
html
|
|
1448
|
-
);
|
|
1563
|
+
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(html);
|
|
1449
1564
|
return match?.[1] === void 0 ? null : decodeEntities(match[1]);
|
|
1450
1565
|
}
|
|
1451
1566
|
function matchOgSiteName(html) {
|
|
@@ -1546,6 +1661,7 @@ function trimSuffix(title) {
|
|
|
1546
1661
|
return title;
|
|
1547
1662
|
}
|
|
1548
1663
|
const DEFAULT_CONCURRENCY = 8;
|
|
1664
|
+
const MAX_BATCH_CONCURRENCY = 64;
|
|
1549
1665
|
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1550
1666
|
const DEFAULT_RETRIES = 3;
|
|
1551
1667
|
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
@@ -1562,14 +1678,31 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
1562
1678
|
const put = deps.put ?? defaultTransport(deps.fetch);
|
|
1563
1679
|
const delay = deps.delay ?? defaultDelay;
|
|
1564
1680
|
const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1681
|
+
const batchConcurrency = Math.max(
|
|
1682
|
+
1,
|
|
1683
|
+
Math.min(MAX_BATCH_CONCURRENCY, Math.floor(deps.batchConcurrency ?? concurrency))
|
|
1684
|
+
);
|
|
1565
1685
|
const retries = deps.retries ?? DEFAULT_RETRIES;
|
|
1566
1686
|
const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
|
|
1567
1687
|
const now = deps.now ?? Date.now;
|
|
1568
1688
|
const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
|
|
1569
1689
|
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1690
|
+
const fileCount = targets.reduce(
|
|
1691
|
+
(sum, target) => sum + (isBatch(target) ? target.files.length : 1),
|
|
1692
|
+
0
|
|
1693
|
+
);
|
|
1570
1694
|
let nextIndex = 0;
|
|
1571
1695
|
let done = 0;
|
|
1572
1696
|
let failed = false;
|
|
1697
|
+
let firstFailure;
|
|
1698
|
+
const local = new AbortController();
|
|
1699
|
+
const inner = local.signal;
|
|
1700
|
+
const fail = (error) => {
|
|
1701
|
+
if (failed) return;
|
|
1702
|
+
failed = true;
|
|
1703
|
+
firstFailure = error;
|
|
1704
|
+
local.abort();
|
|
1705
|
+
};
|
|
1573
1706
|
let reserved = 0;
|
|
1574
1707
|
let waiting = [];
|
|
1575
1708
|
const acquire = async (size) => {
|
|
@@ -1584,17 +1717,32 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
1584
1717
|
waiting = [];
|
|
1585
1718
|
for (const wake of woken) wake();
|
|
1586
1719
|
};
|
|
1720
|
+
const running = { single: 0, batch: 0 };
|
|
1721
|
+
const limit = { single: concurrency, batch: batchConcurrency };
|
|
1722
|
+
let slotWaiting = [];
|
|
1723
|
+
const takeSlot = async (kind) => {
|
|
1724
|
+
while (running[kind] >= limit[kind]) {
|
|
1725
|
+
await new Promise((resolve2) => slotWaiting.push(resolve2));
|
|
1726
|
+
}
|
|
1727
|
+
running[kind] += 1;
|
|
1728
|
+
};
|
|
1729
|
+
const freeSlot = (kind) => {
|
|
1730
|
+
running[kind] -= 1;
|
|
1731
|
+
const woken = slotWaiting;
|
|
1732
|
+
slotWaiting = [];
|
|
1733
|
+
for (const wake of woken) wake();
|
|
1734
|
+
};
|
|
1587
1735
|
let landedBytes = 0;
|
|
1588
1736
|
const inFlight = /* @__PURE__ */ new Map();
|
|
1589
1737
|
let lastReport = Number.NEGATIVE_INFINITY;
|
|
1590
1738
|
const report = (path) => {
|
|
1591
|
-
if (onProgress === void 0) return;
|
|
1739
|
+
if (onProgress === void 0 || failed) return;
|
|
1592
1740
|
const at = now();
|
|
1593
1741
|
if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
|
|
1594
1742
|
lastReport = at;
|
|
1595
1743
|
let bytes = landedBytes;
|
|
1596
1744
|
for (const sent of inFlight.values()) bytes += sent;
|
|
1597
|
-
onProgress(done,
|
|
1745
|
+
onProgress(done, fileCount, bytes, path);
|
|
1598
1746
|
};
|
|
1599
1747
|
const worker = async () => {
|
|
1600
1748
|
for (; ; ) {
|
|
@@ -1602,25 +1750,49 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
1602
1750
|
const index = nextIndex++;
|
|
1603
1751
|
const target = targets[index];
|
|
1604
1752
|
if (target === void 0) return;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1753
|
+
inner.throwIfAborted();
|
|
1754
|
+
const refs = isBatch(target) ? target.files : [target];
|
|
1755
|
+
const members = [];
|
|
1756
|
+
for (const ref of refs) {
|
|
1757
|
+
const file = byPath.get(ref.path);
|
|
1758
|
+
if (file === void 0) {
|
|
1759
|
+
const missing = new DeployError(
|
|
1760
|
+
ClientErrorCode.UploadFailed,
|
|
1761
|
+
`The server asked for ${ref.path}, which is not in the manifest.`,
|
|
1762
|
+
{ path: ref.path }
|
|
1763
|
+
);
|
|
1764
|
+
fail(missing);
|
|
1765
|
+
throw missing;
|
|
1766
|
+
}
|
|
1767
|
+
members.push(file);
|
|
1613
1768
|
}
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1769
|
+
const layout = isBatch(target) ? new TextEncoder().encode(
|
|
1770
|
+
batchLayout(
|
|
1771
|
+
target.files.map((ref, i) => ({
|
|
1772
|
+
sha256: ref.sha256,
|
|
1773
|
+
size: sizeOf(members[i].bytes)
|
|
1774
|
+
}))
|
|
1775
|
+
)
|
|
1776
|
+
) : null;
|
|
1777
|
+
const size = members.reduce((sum, file) => sum + sizeOf(file.bytes), layout?.length ?? 0);
|
|
1778
|
+
const copied = isBatch(target) ? members.reduce(
|
|
1779
|
+
(sum, file) => sum + (file.bytes instanceof Blob ? 0 : sizeOf(file.bytes)),
|
|
1780
|
+
0
|
|
1781
|
+
) : 0;
|
|
1782
|
+
const reservation = size + copied;
|
|
1783
|
+
const first = members[0].path;
|
|
1784
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1785
|
+
const kind = isBatch(target) ? "batch" : "single";
|
|
1786
|
+
await takeSlot(kind);
|
|
1787
|
+
await acquire(reservation);
|
|
1616
1788
|
try {
|
|
1617
1789
|
if (failed) return;
|
|
1618
|
-
|
|
1619
|
-
|
|
1790
|
+
inner.throwIfAborted();
|
|
1791
|
+
const context = {
|
|
1620
1792
|
put,
|
|
1621
1793
|
delay,
|
|
1622
1794
|
retries,
|
|
1623
|
-
signal,
|
|
1795
|
+
signal: inner,
|
|
1624
1796
|
uploadUrl,
|
|
1625
1797
|
stallMs,
|
|
1626
1798
|
onSent: (sent) => {
|
|
@@ -1629,26 +1801,34 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
1629
1801
|
},
|
|
1630
1802
|
onRetry: (attempt, stalled) => {
|
|
1631
1803
|
inFlight.set(index, 0);
|
|
1632
|
-
onRetry?.(
|
|
1804
|
+
onRetry?.(label, attempt, retries + 1, stalled);
|
|
1633
1805
|
}
|
|
1634
|
-
}
|
|
1806
|
+
};
|
|
1807
|
+
if (isBatch(target) && layout !== null)
|
|
1808
|
+
await uploadBatchOne(target, members, layout, context);
|
|
1809
|
+
else await uploadOne(target, members[0], context);
|
|
1635
1810
|
} catch (error) {
|
|
1636
|
-
|
|
1811
|
+
fail(error);
|
|
1637
1812
|
throw error;
|
|
1638
1813
|
} finally {
|
|
1639
|
-
release(
|
|
1814
|
+
release(reservation);
|
|
1815
|
+
freeSlot(kind);
|
|
1640
1816
|
}
|
|
1641
1817
|
inFlight.delete(index);
|
|
1642
|
-
done +=
|
|
1818
|
+
done += members.length;
|
|
1643
1819
|
landedBytes += size;
|
|
1644
|
-
report(
|
|
1820
|
+
report(members.at(-1).path);
|
|
1645
1821
|
}
|
|
1646
1822
|
};
|
|
1647
|
-
const workers = Array.from(
|
|
1823
|
+
const workers = Array.from(
|
|
1824
|
+
{ length: Math.min(Math.max(concurrency, batchConcurrency), targets.length) },
|
|
1825
|
+
worker
|
|
1826
|
+
);
|
|
1648
1827
|
try {
|
|
1649
1828
|
await Promise.all(workers);
|
|
1650
1829
|
} catch (error) {
|
|
1651
|
-
throw asDeployError(error);
|
|
1830
|
+
throw asDeployError(firstFailure ?? error);
|
|
1831
|
+
} finally {
|
|
1652
1832
|
}
|
|
1653
1833
|
}
|
|
1654
1834
|
async function uploadOne(target, file, context) {
|
|
@@ -1661,7 +1841,7 @@ async function uploadOne(target, file, context) {
|
|
|
1661
1841
|
context.signal?.throwIfAborted();
|
|
1662
1842
|
}
|
|
1663
1843
|
try {
|
|
1664
|
-
const
|
|
1844
|
+
const answer = await context.put({
|
|
1665
1845
|
url: context.uploadUrl,
|
|
1666
1846
|
token: target.token,
|
|
1667
1847
|
body: untyped(file.bytes),
|
|
@@ -1669,6 +1849,7 @@ async function uploadOne(target, file, context) {
|
|
|
1669
1849
|
onSent: context.onSent,
|
|
1670
1850
|
stallMs: context.stallMs
|
|
1671
1851
|
});
|
|
1852
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1672
1853
|
if (status >= 200 && status < 300) return;
|
|
1673
1854
|
if (status === 401 || status === 403 || status === 429) {
|
|
1674
1855
|
throw new DeployError(
|
|
@@ -1697,6 +1878,85 @@ async function uploadOne(target, file, context) {
|
|
|
1697
1878
|
cause: describeError(lastError)
|
|
1698
1879
|
});
|
|
1699
1880
|
}
|
|
1881
|
+
async function uploadBatchOne(batch, members, layout, context) {
|
|
1882
|
+
const first = members[0].path;
|
|
1883
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1884
|
+
let lastError;
|
|
1885
|
+
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1886
|
+
context.signal?.throwIfAborted();
|
|
1887
|
+
if (attempt > 0) {
|
|
1888
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1889
|
+
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1890
|
+
context.signal?.throwIfAborted();
|
|
1891
|
+
}
|
|
1892
|
+
try {
|
|
1893
|
+
const body = new Blob([layout, ...members.map((file) => file.bytes)]);
|
|
1894
|
+
const answer = await context.put({
|
|
1895
|
+
url: context.uploadUrl,
|
|
1896
|
+
token: batch.token,
|
|
1897
|
+
body,
|
|
1898
|
+
signal: context.signal,
|
|
1899
|
+
onSent: context.onSent,
|
|
1900
|
+
stallMs: context.stallMs,
|
|
1901
|
+
wantsBody: true
|
|
1902
|
+
});
|
|
1903
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1904
|
+
if (status >= 200 && status < 300) {
|
|
1905
|
+
const result = typeof answer === "number" ? null : parseBatchResult(answer.body);
|
|
1906
|
+
if (result === null) {
|
|
1907
|
+
lastError = new Error(
|
|
1908
|
+
"The upload service answered with something that is not a batch result."
|
|
1909
|
+
);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
if (result.failed.length === 0) return;
|
|
1913
|
+
const changed = result.failed.find((failure2) => failure2.reason === "hash_mismatch");
|
|
1914
|
+
if (changed !== void 0) {
|
|
1915
|
+
const path = members[changed.i]?.path ?? first;
|
|
1916
|
+
throw new DeployError(
|
|
1917
|
+
ClientErrorCode.ReadFailed,
|
|
1918
|
+
`${path} changed after it was read, so it no longer matches what was checked. Drop the folder again.`,
|
|
1919
|
+
{ path }
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
lastError = new Error(
|
|
1923
|
+
`${result.failed.length} of ${members.length} files were not stored.`
|
|
1924
|
+
);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
if ([400, 401, 403, 411, 413, 429].includes(status)) {
|
|
1928
|
+
throw new DeployError(ClientErrorCode.UploadFailed, refusalMessage(status, label), {
|
|
1929
|
+
path: first,
|
|
1930
|
+
status
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (error instanceof DeployError) throw error;
|
|
1936
|
+
if (isAbort(error)) throw cancelled();
|
|
1937
|
+
lastError = error;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
const message = lastError instanceof StalledError ? `Upload of ${label} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${label} after ${context.retries + 1} attempts.`;
|
|
1941
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1942
|
+
path: first,
|
|
1943
|
+
cause: describeError(lastError)
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
function refusalMessage(status, label) {
|
|
1947
|
+
switch (status) {
|
|
1948
|
+
case 401:
|
|
1949
|
+
return `Upload of ${label} was refused because its upload window expired. Try deploying again.`;
|
|
1950
|
+
case 429:
|
|
1951
|
+
return `Upload of ${label} was refused: this deploy has used every upload it was given. Try deploying again.`;
|
|
1952
|
+
case 400:
|
|
1953
|
+
case 411:
|
|
1954
|
+
case 413:
|
|
1955
|
+
return `Upload of ${label} was refused because it no longer matches what was checked — a file may have changed since you dropped it. Drop the folder again.`;
|
|
1956
|
+
default:
|
|
1957
|
+
return `Upload of ${label} was refused by the upload service. Try deploying again.`;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1700
1960
|
function untyped(content) {
|
|
1701
1961
|
if (ArrayBuffer.isView(content) || content.type === "") return content;
|
|
1702
1962
|
return content.slice(0, content.size);
|
|
@@ -1707,7 +1967,8 @@ function defaultTransport(doFetch) {
|
|
|
1707
1967
|
return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
|
|
1708
1968
|
}
|
|
1709
1969
|
function fetchTransport(doFetch) {
|
|
1710
|
-
return async (
|
|
1970
|
+
return async (request2) => {
|
|
1971
|
+
const { url, token, body, signal } = request2;
|
|
1711
1972
|
const response = await doFetch(url, {
|
|
1712
1973
|
method: "PUT",
|
|
1713
1974
|
// The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
|
|
@@ -1722,11 +1983,11 @@ function fetchTransport(doFetch) {
|
|
|
1722
1983
|
headers: { Authorization: `Bearer ${token}` },
|
|
1723
1984
|
...signal ? { signal } : {}
|
|
1724
1985
|
});
|
|
1725
|
-
return response.status;
|
|
1986
|
+
return request2.wantsBody === true ? { status: response.status, body: await response.text() } : response.status;
|
|
1726
1987
|
};
|
|
1727
1988
|
}
|
|
1728
1989
|
function xhrTransport(Xhr) {
|
|
1729
|
-
return ({ url, token, body, signal, onSent, stallMs }) => new Promise((resolve2, reject) => {
|
|
1990
|
+
return ({ url, token, body, signal, onSent, stallMs, wantsBody }) => new Promise((resolve2, reject) => {
|
|
1730
1991
|
const xhr = new Xhr();
|
|
1731
1992
|
let settled = false;
|
|
1732
1993
|
let timer;
|
|
@@ -1760,7 +2021,11 @@ function xhrTransport(Xhr) {
|
|
|
1760
2021
|
onSent(event.loaded);
|
|
1761
2022
|
};
|
|
1762
2023
|
xhr.onprogress = arm;
|
|
1763
|
-
xhr.onload = () => settle(
|
|
2024
|
+
xhr.onload = () => settle(
|
|
2025
|
+
() => resolve2(
|
|
2026
|
+
wantsBody === true ? { status: xhr.status, body: xhr.responseText ?? "" } : xhr.status
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
1764
2029
|
xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
|
|
1765
2030
|
arm();
|
|
1766
2031
|
xhr.send(body);
|
|
@@ -1825,11 +2090,16 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1825
2090
|
return;
|
|
1826
2091
|
}
|
|
1827
2092
|
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2093
|
+
const batches = prepared.batches ?? [];
|
|
2094
|
+
const targets = [...prepared.upload, ...batches];
|
|
2095
|
+
const totalBytes = prepared.upload.reduce((sum, target) => sum + (sizes.get(target.path) ?? 0), 0) + batches.reduce((sum, batch) => {
|
|
2096
|
+
const members = batch.files.map((ref) => ({
|
|
2097
|
+
sha256: ref.sha256,
|
|
2098
|
+
size: sizes.get(ref.path) ?? 0
|
|
2099
|
+
}));
|
|
2100
|
+
return sum + batchLayout(members).length + members.reduce((n, member) => n + member.size, 0);
|
|
2101
|
+
}, 0);
|
|
2102
|
+
if (targets.length > 0) {
|
|
1833
2103
|
onProgress({
|
|
1834
2104
|
type: "uploading",
|
|
1835
2105
|
done: prepared.reused,
|
|
@@ -1840,7 +2110,7 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1840
2110
|
});
|
|
1841
2111
|
}
|
|
1842
2112
|
await uploadAll(
|
|
1843
|
-
|
|
2113
|
+
targets,
|
|
1844
2114
|
prepared.uploadUrl,
|
|
1845
2115
|
hashed,
|
|
1846
2116
|
(done, _total, bytes, path) => onProgress({
|
|
@@ -1853,7 +2123,9 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1853
2123
|
...path === void 0 ? {} : { path }
|
|
1854
2124
|
}),
|
|
1855
2125
|
signal,
|
|
1856
|
-
|
|
2126
|
+
// The server's number unless the caller set one: it is tuned on the server so a deploy of many
|
|
2127
|
+
// small files speeds up without a CLI or MCP release.
|
|
2128
|
+
typeof prepared.batchConcurrency === "number" && options.upload?.batchConcurrency === void 0 ? { ...options.upload, batchConcurrency: prepared.batchConcurrency } : options.upload,
|
|
1857
2129
|
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1858
2130
|
);
|
|
1859
2131
|
onProgress({ type: "completing", reused: prepared.reused });
|
|
@@ -2525,7 +2797,9 @@ const SETTINGS_USAGE = [
|
|
|
2525
2797
|
" password Read from stdin, or typed at a hidden prompt",
|
|
2526
2798
|
" forms on|off Whether its forms accept submissions",
|
|
2527
2799
|
" expires <ISO 8601 instant> When it comes down; add --then delete to delete rather than pause",
|
|
2528
|
-
" folder <path|id|root> Which folder it is filed in"
|
|
2800
|
+
" folder <path|id|root> Which folder it is filed in",
|
|
2801
|
+
" invite-only on|off Whether only the people on its list can open it",
|
|
2802
|
+
" comments off|invited|anyone Who may comment on its pages"
|
|
2529
2803
|
].join("\n");
|
|
2530
2804
|
function settingChange(setting, values, then) {
|
|
2531
2805
|
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
@@ -2573,6 +2847,14 @@ function settingChange(setting, values, then) {
|
|
|
2573
2847
|
}
|
|
2574
2848
|
case "folder":
|
|
2575
2849
|
return value ? { folder: value } : needs("Clients/Acme");
|
|
2850
|
+
case "invite-only":
|
|
2851
|
+
if (value === "on" || value === "off") return { change: { invitedOnly: value === "on" } };
|
|
2852
|
+
return { refusal: "`set invite-only` takes `on` or `off`." };
|
|
2853
|
+
case "comments":
|
|
2854
|
+
if (value === "off" || value === "invited" || value === "anyone") {
|
|
2855
|
+
return { change: { comments: value } };
|
|
2856
|
+
}
|
|
2857
|
+
return { refusal: "`set comments` takes `off`, `invited` or `anyone`." };
|
|
2576
2858
|
default:
|
|
2577
2859
|
return {
|
|
2578
2860
|
refusal: `${setting === void 0 ? "Name a setting." : `\`${setting}\` is not a setting.`} Settings:
|
|
@@ -2650,9 +2932,14 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2650
2932
|
` access ${accessOf(settings)}`,
|
|
2651
2933
|
` mode ${servingModeOf$1(settings)}`,
|
|
2652
2934
|
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2935
|
+
` comments ${settings.comments ?? "unknown"}`,
|
|
2653
2936
|
` takedown ${takedown}`,
|
|
2654
2937
|
...settings.replacedInviteOnly ? [
|
|
2655
|
-
"The password replaced invite-only: the people on its list can no longer get in without it. The list is kept
|
|
2938
|
+
"The password replaced invite-only: the people on its list can no longer get in without it. The list is kept; `drop2run set invite-only on` switches back."
|
|
2939
|
+
] : [],
|
|
2940
|
+
// The one combination that reads as on and does nothing, which the API keeps on purpose.
|
|
2941
|
+
...settings.comments === "invited" && settings.invitedOnly === false ? [
|
|
2942
|
+
"Comments are for invited people, but the site is not invite-only, so nobody can comment yet. Run `drop2run set invite-only on`, or `drop2run set comments anyone`."
|
|
2656
2943
|
] : [],
|
|
2657
2944
|
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2658
2945
|
].join("\n"),
|
|
@@ -2678,6 +2965,72 @@ async function tokens() {
|
|
|
2678
2965
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2679
2966
|
}
|
|
2680
2967
|
}
|
|
2968
|
+
async function people(site) {
|
|
2969
|
+
const found = credentialsOr();
|
|
2970
|
+
if ("result" in found) return found.result;
|
|
2971
|
+
const target = siteOr(site, "drop2run people <subdomain>");
|
|
2972
|
+
if ("result" in target) return target.result;
|
|
2973
|
+
try {
|
|
2974
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2975
|
+
const list2 = await listPeople(found.credentials, summary.siteId);
|
|
2976
|
+
const head = `${list2.total} of ${list2.limit} on ${summary.subdomain}'s list. ` + (list2.invitedOnly ? "Only they can open it." : "It is not invite-only, so anybody with the link can open it — `drop2run set invite-only on`.");
|
|
2977
|
+
const rows = list2.people.map(
|
|
2978
|
+
(person) => `${person.email} ${person.role}${person.owner ? ", owner" : ""} ${person.status}`
|
|
2979
|
+
);
|
|
2980
|
+
return { text: [head, ...rows].join("\n"), json: list2, code: 0 };
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
async function invite(emails, site, role) {
|
|
2986
|
+
const found = credentialsOr();
|
|
2987
|
+
if ("result" in found) return found.result;
|
|
2988
|
+
if (emails.length === 0)
|
|
2989
|
+
return failure("Name at least one address: `drop2run invite dana@example.com`.");
|
|
2990
|
+
if (role !== void 0 && role !== "view" && role !== "comment") {
|
|
2991
|
+
return failure("`--role` is `view` or `comment`.");
|
|
2992
|
+
}
|
|
2993
|
+
const target = siteOr(site, "drop2run invite <email> --site <subdomain>");
|
|
2994
|
+
if ("result" in target) return target.result;
|
|
2995
|
+
try {
|
|
2996
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2997
|
+
const invited = await invitePeople(
|
|
2998
|
+
found.credentials,
|
|
2999
|
+
summary.siteId,
|
|
3000
|
+
emails,
|
|
3001
|
+
role === "comment" ? "comment" : "view"
|
|
3002
|
+
);
|
|
3003
|
+
const list2 = await listPeople(found.credentials, summary.siteId);
|
|
3004
|
+
return {
|
|
3005
|
+
text: [
|
|
3006
|
+
invited.added.length === 0 ? `Nobody new on ${summary.subdomain}'s list.` : `Invited ${invited.added.join(", ")} to ${summary.subdomain} — each was emailed.`,
|
|
3007
|
+
...invited.alreadyListed.length === 0 ? [] : [`Already on the list, not emailed again: ${invited.alreadyListed.join(", ")}.`],
|
|
3008
|
+
...list2.invitedOnly ? [] : [
|
|
3009
|
+
"The site is not invite-only yet, so the list does not decide who opens it. `drop2run set invite-only on` does."
|
|
3010
|
+
]
|
|
3011
|
+
].join("\n"),
|
|
3012
|
+
json: invited,
|
|
3013
|
+
code: 0
|
|
3014
|
+
};
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
async function uninvite(email, site) {
|
|
3020
|
+
const found = credentialsOr();
|
|
3021
|
+
if ("result" in found) return found.result;
|
|
3022
|
+
if (email === void 0)
|
|
3023
|
+
return failure("Name the address: `drop2run uninvite dana@example.com`.");
|
|
3024
|
+
const target = siteOr(site, "drop2run uninvite <email> --site <subdomain>");
|
|
3025
|
+
if ("result" in target) return target.result;
|
|
3026
|
+
try {
|
|
3027
|
+
const summary = await findSite(found.credentials, target.named);
|
|
3028
|
+
await removePerson(found.credentials, summary.siteId, email);
|
|
3029
|
+
return { text: `Removed ${email} from ${summary.subdomain}'s list.`, json: { email }, code: 0 };
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
2681
3034
|
const COMMENTS_USAGE = [
|
|
2682
3035
|
" drop2run comments [site] [--status open|resolved|all] [--path /page]",
|
|
2683
3036
|
" drop2run comments reply <thread> <text> [--resolve] [--site X]",
|
|
@@ -2808,6 +3161,9 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
2808
3161
|
drop2run resume [site] Put a paused site back on the air
|
|
2809
3162
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
2810
3163
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
3164
|
+
drop2run people [site] Who the site is shared with
|
|
3165
|
+
drop2run invite <email>... [--role comment] Share it with people; each is emailed
|
|
3166
|
+
drop2run uninvite <email> Take someone off the list
|
|
2811
3167
|
drop2run comments [site] Read the comments left on a site's pages
|
|
2812
3168
|
drop2run comments reply <thread> <text> Answer a thread as the owner [--resolve]
|
|
2813
3169
|
drop2run comments resolve|reopen <thread> Resolve a thread, or open it again
|
|
@@ -2829,6 +3185,7 @@ Flags
|
|
|
2829
3185
|
--status X Which comment threads: open (the default), resolved or all
|
|
2830
3186
|
--path X Only the comments on one page, such as /pricing
|
|
2831
3187
|
--resolve Resolve the thread with the reply (\`comments reply\` only)
|
|
3188
|
+
--role X What invited people may do: view (the default) or comment
|
|
2832
3189
|
|
|
2833
3190
|
Comments
|
|
2834
3191
|
\`comments reply\`, \`resolve\` and \`reopen\` act on --site, else the site in
|
|
@@ -2847,6 +3204,11 @@ Settings
|
|
|
2847
3204
|
set forms on|off Whether its forms accept submissions
|
|
2848
3205
|
set expires <ISO 8601> Take it down then, e.g. 2026-12-31T09:00:00Z
|
|
2849
3206
|
set folder <path|id|root> Move it to a folder, or root for the top level
|
|
3207
|
+
set invite-only on|off Only the people on its list (\`invite\`) can open it;
|
|
3208
|
+
replaces a password
|
|
3209
|
+
set comments off|invited|anyone
|
|
3210
|
+
Who may comment: nobody, people invited to
|
|
3211
|
+
comment, or anybody signed in
|
|
2850
3212
|
|
|
2851
3213
|
\`set\` and \`unset\` act on --site, else the site in drop2run.json. A
|
|
2852
3214
|
setting your plan does not include is refused with the reason, and
|
|
@@ -2879,7 +3241,7 @@ function flagValue(args, flag) {
|
|
|
2879
3241
|
const value = args[at + 1];
|
|
2880
3242
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
2881
3243
|
}
|
|
2882
|
-
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then", "--status", "--path"];
|
|
3244
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then", "--status", "--path", "--role"];
|
|
2883
3245
|
function positionals(argv) {
|
|
2884
3246
|
const found = [];
|
|
2885
3247
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -2928,7 +3290,7 @@ async function run(commandLine) {
|
|
|
2928
3290
|
const positional = [...positionals(argv), ...literal];
|
|
2929
3291
|
const [command, ...rest] = positional;
|
|
2930
3292
|
if (argv.includes("--version")) {
|
|
2931
|
-
const { version } = await import("./package-
|
|
3293
|
+
const { version } = await import("./package-B79z0E4v.js").then(
|
|
2932
3294
|
(module) => module.default
|
|
2933
3295
|
);
|
|
2934
3296
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -2963,11 +3325,12 @@ async function run(commandLine) {
|
|
|
2963
3325
|
status: flagValue(argv, "--status"),
|
|
2964
3326
|
path: flagValue(argv, "--path"),
|
|
2965
3327
|
resolve: argv.includes("--resolve")
|
|
2966
|
-
}
|
|
3328
|
+
},
|
|
3329
|
+
flagValue(argv, "--role")
|
|
2967
3330
|
);
|
|
2968
3331
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
2969
3332
|
}
|
|
2970
|
-
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0, commentOptions = {}) {
|
|
3333
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0, commentOptions = {}, role = void 0) {
|
|
2971
3334
|
switch (command) {
|
|
2972
3335
|
case "login": {
|
|
2973
3336
|
const print = json ? () => {
|
|
@@ -3004,6 +3367,12 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
|
|
|
3004
3367
|
return await pauseOrResume(command, rest[0] ?? site);
|
|
3005
3368
|
case "rollback":
|
|
3006
3369
|
return await rollback(rest[0], site);
|
|
3370
|
+
case "people":
|
|
3371
|
+
return await people(rest[0] ?? site);
|
|
3372
|
+
case "invite":
|
|
3373
|
+
return await invite(rest, site, role);
|
|
3374
|
+
case "uninvite":
|
|
3375
|
+
return await uninvite(rest[0], site);
|
|
3007
3376
|
case "rm":
|
|
3008
3377
|
return await remove(rest[0] ?? site, confirmed);
|
|
3009
3378
|
case "comments":
|