drop2run 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/index.js +317 -43
- package/dist/{package-BycuI1VJ.js → package-CGI7Yt81.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ drop2run pause [site] Take a site off the air, keeping it
|
|
|
29
29
|
drop2run resume [site] Put a paused site back on the air
|
|
30
30
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
31
31
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
32
|
+
drop2run comments [site] Read the comments left on a site's pages
|
|
33
|
+
drop2run comments reply <thread> <text> Answer a thread as the owner [--resolve]
|
|
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
|
|
32
38
|
drop2run token list List your access tokens
|
|
33
39
|
drop2run whoami Check the token and whose it is
|
|
34
40
|
drop2run where Show which token source is in use
|
|
@@ -37,6 +43,35 @@ drop2run --version Print the version
|
|
|
37
43
|
|
|
38
44
|
`--json` on any command prints machine-readable output instead of text.
|
|
39
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
|
+
|
|
58
|
+
## Comments
|
|
59
|
+
|
|
60
|
+
On a plan that includes comments, people viewing a site can pin comments to its pages. `drop2run comments`
|
|
61
|
+
reads them — each thread's id, page, the words it points at, and every comment — with `--status
|
|
62
|
+
open|resolved|all` and `--path /page` to narrow it. The owner of the account can answer from here:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
drop2run comments calm-cedar # open threads
|
|
66
|
+
drop2run deploy dist # publish the fix first
|
|
67
|
+
drop2run comments reply 01JTHREAD... "Fixed — please check" --site calm-cedar
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The reply shows on the page under your name, as one typed there would, and the people in the thread are
|
|
71
|
+
emailed.
|
|
72
|
+
It leaves the thread open unless `--resolve` is given, so the person who raised it can check the fix and
|
|
73
|
+
resolve it themselves. `comments resolve` and `comments reopen` change a thread's state on its own.
|
|
74
|
+
|
|
40
75
|
## Which site a command acts on
|
|
41
76
|
|
|
42
77
|
`--subdomain` creates one under the name you give; otherwise `--site`, then `drop2run.json`, then a new
|
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,
|
|
@@ -344,6 +355,61 @@ const ROOT_FOLDER = "root";
|
|
|
344
355
|
async function resolveFolderTarget(credentials, folder) {
|
|
345
356
|
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
346
357
|
}
|
|
358
|
+
async function listComments(credentials, siteId, query = {}) {
|
|
359
|
+
const search = new URLSearchParams();
|
|
360
|
+
if (query.status !== void 0) search.set("status", query.status);
|
|
361
|
+
if (query.path !== void 0) search.set("path", query.path);
|
|
362
|
+
if (query.since !== void 0) search.set("since", String(query.since));
|
|
363
|
+
if (query.page !== void 0) search.set("page", String(query.page));
|
|
364
|
+
const encoded = search.toString();
|
|
365
|
+
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
366
|
+
return await call(
|
|
367
|
+
credentials,
|
|
368
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
372
|
+
return await call(
|
|
373
|
+
credentials,
|
|
374
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
375
|
+
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
379
|
+
return await call(
|
|
380
|
+
credentials,
|
|
381
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
382
|
+
{ method: "POST" }
|
|
383
|
+
);
|
|
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
|
+
}
|
|
347
413
|
const VERSIONS_LISTED = 10;
|
|
348
414
|
function servingModeOf$1(site) {
|
|
349
415
|
if (site.spaMode) return "spa";
|
|
@@ -372,6 +438,7 @@ function describeSite(site, folderPath) {
|
|
|
372
438
|
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
373
439
|
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
374
440
|
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
441
|
+
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
375
442
|
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
376
443
|
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
377
444
|
"",
|
|
@@ -847,13 +914,14 @@ const FILE_TYPES = [
|
|
|
847
914
|
reader: true,
|
|
848
915
|
editable: true
|
|
849
916
|
},
|
|
850
|
-
// Source the viewer highlights.
|
|
917
|
+
// Source the viewer highlights. Inside a documents site a link straight to one opens it in the
|
|
918
|
+
// reader; on an ordinary site, and with `?raw`, it hands over the file.
|
|
851
919
|
{
|
|
852
920
|
ext: ".css",
|
|
853
921
|
mime: "text/css; charset=utf-8",
|
|
854
922
|
kind: "code",
|
|
855
923
|
document: false,
|
|
856
|
-
reader:
|
|
924
|
+
reader: true,
|
|
857
925
|
editable: true
|
|
858
926
|
},
|
|
859
927
|
{
|
|
@@ -861,7 +929,7 @@ const FILE_TYPES = [
|
|
|
861
929
|
mime: "text/javascript; charset=utf-8",
|
|
862
930
|
kind: "code",
|
|
863
931
|
document: false,
|
|
864
|
-
reader:
|
|
932
|
+
reader: true,
|
|
865
933
|
editable: true
|
|
866
934
|
},
|
|
867
935
|
{
|
|
@@ -869,7 +937,7 @@ const FILE_TYPES = [
|
|
|
869
937
|
mime: null,
|
|
870
938
|
kind: "code",
|
|
871
939
|
document: false,
|
|
872
|
-
reader:
|
|
940
|
+
reader: true,
|
|
873
941
|
editable: false
|
|
874
942
|
},
|
|
875
943
|
{
|
|
@@ -877,7 +945,7 @@ const FILE_TYPES = [
|
|
|
877
945
|
mime: null,
|
|
878
946
|
kind: "code",
|
|
879
947
|
document: false,
|
|
880
|
-
reader:
|
|
948
|
+
reader: true,
|
|
881
949
|
editable: false
|
|
882
950
|
},
|
|
883
951
|
{
|
|
@@ -885,7 +953,7 @@ const FILE_TYPES = [
|
|
|
885
953
|
mime: null,
|
|
886
954
|
kind: "code",
|
|
887
955
|
document: false,
|
|
888
|
-
reader:
|
|
956
|
+
reader: true,
|
|
889
957
|
editable: false
|
|
890
958
|
},
|
|
891
959
|
{
|
|
@@ -893,19 +961,18 @@ const FILE_TYPES = [
|
|
|
893
961
|
mime: null,
|
|
894
962
|
kind: "code",
|
|
895
963
|
document: false,
|
|
896
|
-
reader:
|
|
964
|
+
reader: true,
|
|
897
965
|
editable: false
|
|
898
966
|
},
|
|
899
967
|
// The languages an assistant writes most, beyond the web's own. Same row as `.ts`: shown as source
|
|
900
|
-
//
|
|
901
|
-
// link names it directly, and no media type — adding one changes what a browser does with a
|
|
968
|
+
// 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
|
|
902
969
|
// published file, which I10 makes a decision of its own rather than a side effect of this list.
|
|
903
970
|
{
|
|
904
971
|
ext: ".py",
|
|
905
972
|
mime: null,
|
|
906
973
|
kind: "code",
|
|
907
974
|
document: false,
|
|
908
|
-
reader:
|
|
975
|
+
reader: true,
|
|
909
976
|
editable: false
|
|
910
977
|
},
|
|
911
978
|
{
|
|
@@ -913,7 +980,7 @@ const FILE_TYPES = [
|
|
|
913
980
|
mime: null,
|
|
914
981
|
kind: "code",
|
|
915
982
|
document: false,
|
|
916
|
-
reader:
|
|
983
|
+
reader: true,
|
|
917
984
|
editable: false
|
|
918
985
|
},
|
|
919
986
|
{
|
|
@@ -921,7 +988,7 @@ const FILE_TYPES = [
|
|
|
921
988
|
mime: null,
|
|
922
989
|
kind: "code",
|
|
923
990
|
document: false,
|
|
924
|
-
reader:
|
|
991
|
+
reader: true,
|
|
925
992
|
editable: false
|
|
926
993
|
},
|
|
927
994
|
{
|
|
@@ -929,7 +996,7 @@ const FILE_TYPES = [
|
|
|
929
996
|
mime: null,
|
|
930
997
|
kind: "code",
|
|
931
998
|
document: false,
|
|
932
|
-
reader:
|
|
999
|
+
reader: true,
|
|
933
1000
|
editable: false
|
|
934
1001
|
},
|
|
935
1002
|
{
|
|
@@ -937,7 +1004,7 @@ const FILE_TYPES = [
|
|
|
937
1004
|
mime: null,
|
|
938
1005
|
kind: "code",
|
|
939
1006
|
document: false,
|
|
940
|
-
reader:
|
|
1007
|
+
reader: true,
|
|
941
1008
|
editable: false
|
|
942
1009
|
},
|
|
943
1010
|
{
|
|
@@ -945,7 +1012,7 @@ const FILE_TYPES = [
|
|
|
945
1012
|
mime: null,
|
|
946
1013
|
kind: "code",
|
|
947
1014
|
document: false,
|
|
948
|
-
reader:
|
|
1015
|
+
reader: true,
|
|
949
1016
|
editable: false
|
|
950
1017
|
},
|
|
951
1018
|
{
|
|
@@ -953,7 +1020,7 @@ const FILE_TYPES = [
|
|
|
953
1020
|
mime: null,
|
|
954
1021
|
kind: "code",
|
|
955
1022
|
document: false,
|
|
956
|
-
reader:
|
|
1023
|
+
reader: true,
|
|
957
1024
|
editable: false
|
|
958
1025
|
},
|
|
959
1026
|
{
|
|
@@ -961,7 +1028,7 @@ const FILE_TYPES = [
|
|
|
961
1028
|
mime: null,
|
|
962
1029
|
kind: "code",
|
|
963
1030
|
document: false,
|
|
964
|
-
reader:
|
|
1031
|
+
reader: true,
|
|
965
1032
|
editable: false
|
|
966
1033
|
},
|
|
967
1034
|
{
|
|
@@ -969,7 +1036,7 @@ const FILE_TYPES = [
|
|
|
969
1036
|
mime: null,
|
|
970
1037
|
kind: "code",
|
|
971
1038
|
document: false,
|
|
972
|
-
reader:
|
|
1039
|
+
reader: true,
|
|
973
1040
|
editable: false
|
|
974
1041
|
},
|
|
975
1042
|
{
|
|
@@ -977,7 +1044,7 @@ const FILE_TYPES = [
|
|
|
977
1044
|
mime: null,
|
|
978
1045
|
kind: "code",
|
|
979
1046
|
document: false,
|
|
980
|
-
reader:
|
|
1047
|
+
reader: true,
|
|
981
1048
|
editable: false
|
|
982
1049
|
},
|
|
983
1050
|
{
|
|
@@ -985,7 +1052,7 @@ const FILE_TYPES = [
|
|
|
985
1052
|
mime: null,
|
|
986
1053
|
kind: "code",
|
|
987
1054
|
document: false,
|
|
988
|
-
reader:
|
|
1055
|
+
reader: true,
|
|
989
1056
|
editable: false
|
|
990
1057
|
},
|
|
991
1058
|
{
|
|
@@ -993,7 +1060,7 @@ const FILE_TYPES = [
|
|
|
993
1060
|
mime: null,
|
|
994
1061
|
kind: "code",
|
|
995
1062
|
document: false,
|
|
996
|
-
reader:
|
|
1063
|
+
reader: true,
|
|
997
1064
|
editable: false
|
|
998
1065
|
},
|
|
999
1066
|
{
|
|
@@ -1001,7 +1068,7 @@ const FILE_TYPES = [
|
|
|
1001
1068
|
mime: null,
|
|
1002
1069
|
kind: "code",
|
|
1003
1070
|
document: false,
|
|
1004
|
-
reader:
|
|
1071
|
+
reader: true,
|
|
1005
1072
|
editable: false
|
|
1006
1073
|
},
|
|
1007
1074
|
{
|
|
@@ -1009,7 +1076,7 @@ const FILE_TYPES = [
|
|
|
1009
1076
|
mime: null,
|
|
1010
1077
|
kind: "code",
|
|
1011
1078
|
document: false,
|
|
1012
|
-
reader:
|
|
1079
|
+
reader: true,
|
|
1013
1080
|
editable: false
|
|
1014
1081
|
},
|
|
1015
1082
|
{
|
|
@@ -1017,7 +1084,7 @@ const FILE_TYPES = [
|
|
|
1017
1084
|
mime: "application/xml",
|
|
1018
1085
|
kind: "code",
|
|
1019
1086
|
document: false,
|
|
1020
|
-
reader:
|
|
1087
|
+
reader: true,
|
|
1021
1088
|
editable: true
|
|
1022
1089
|
},
|
|
1023
1090
|
// Pictures, shown with an img element.
|
|
@@ -1088,25 +1155,25 @@ const FILE_TYPES = [
|
|
|
1088
1155
|
reader: false,
|
|
1089
1156
|
editable: false
|
|
1090
1157
|
},
|
|
1091
|
-
// A customer's own pages.
|
|
1158
|
+
// A customer's own pages. Inside a documents site a navigation opens the reader, which frames the
|
|
1159
|
+
// page; `?raw` and every ordinary site serve the page itself.
|
|
1092
1160
|
{
|
|
1093
1161
|
ext: ".html",
|
|
1094
1162
|
mime: "text/html; charset=utf-8",
|
|
1095
1163
|
kind: "html",
|
|
1096
1164
|
document: false,
|
|
1097
|
-
reader:
|
|
1165
|
+
reader: true,
|
|
1098
1166
|
editable: true
|
|
1099
1167
|
},
|
|
1100
1168
|
{
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
// introduced as a refactor and a refactor that changes what a URL answers is not one.
|
|
1169
|
+
// An HTML page in every respect the Worker decides by extension: media type, the revalidated
|
|
1170
|
+
// cache policy invariant I7 requires, and the badge's `isHtml`. Missing from `TYPES` it used to
|
|
1171
|
+
// download, which inside the reader meant a frame that downloaded instead of showing a page.
|
|
1105
1172
|
ext: ".htm",
|
|
1106
|
-
mime:
|
|
1173
|
+
mime: "text/html; charset=utf-8",
|
|
1107
1174
|
kind: "html",
|
|
1108
1175
|
document: false,
|
|
1109
|
-
reader:
|
|
1176
|
+
reader: true,
|
|
1110
1177
|
editable: true
|
|
1111
1178
|
},
|
|
1112
1179
|
// Everything below renders as nothing — the viewer offers a download — but still needs a media
|
|
@@ -2498,7 +2565,9 @@ const SETTINGS_USAGE = [
|
|
|
2498
2565
|
" password Read from stdin, or typed at a hidden prompt",
|
|
2499
2566
|
" forms on|off Whether its forms accept submissions",
|
|
2500
2567
|
" 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"
|
|
2568
|
+
" folder <path|id|root> Which folder it is filed in",
|
|
2569
|
+
" invite-only on|off Whether only the people on its list can open it",
|
|
2570
|
+
" comments off|invited|anyone Who may comment on its pages"
|
|
2502
2571
|
].join("\n");
|
|
2503
2572
|
function settingChange(setting, values, then) {
|
|
2504
2573
|
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
@@ -2546,6 +2615,14 @@ function settingChange(setting, values, then) {
|
|
|
2546
2615
|
}
|
|
2547
2616
|
case "folder":
|
|
2548
2617
|
return value ? { folder: value } : needs("Clients/Acme");
|
|
2618
|
+
case "invite-only":
|
|
2619
|
+
if (value === "on" || value === "off") return { change: { invitedOnly: value === "on" } };
|
|
2620
|
+
return { refusal: "`set invite-only` takes `on` or `off`." };
|
|
2621
|
+
case "comments":
|
|
2622
|
+
if (value === "off" || value === "invited" || value === "anyone") {
|
|
2623
|
+
return { change: { comments: value } };
|
|
2624
|
+
}
|
|
2625
|
+
return { refusal: "`set comments` takes `off`, `invited` or `anyone`." };
|
|
2549
2626
|
default:
|
|
2550
2627
|
return {
|
|
2551
2628
|
refusal: `${setting === void 0 ? "Name a setting." : `\`${setting}\` is not a setting.`} Settings:
|
|
@@ -2623,9 +2700,14 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2623
2700
|
` access ${accessOf(settings)}`,
|
|
2624
2701
|
` mode ${servingModeOf$1(settings)}`,
|
|
2625
2702
|
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2703
|
+
` comments ${settings.comments ?? "unknown"}`,
|
|
2626
2704
|
` takedown ${takedown}`,
|
|
2627
2705
|
...settings.replacedInviteOnly ? [
|
|
2628
|
-
"The password replaced invite-only: the people on its list can no longer get in without it. The list is kept
|
|
2706
|
+
"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."
|
|
2707
|
+
] : [],
|
|
2708
|
+
// The one combination that reads as on and does nothing, which the API keeps on purpose.
|
|
2709
|
+
...settings.comments === "invited" && settings.invitedOnly === false ? [
|
|
2710
|
+
"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`."
|
|
2629
2711
|
] : [],
|
|
2630
2712
|
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2631
2713
|
].join("\n"),
|
|
@@ -2651,6 +2733,162 @@ async function tokens() {
|
|
|
2651
2733
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2652
2734
|
}
|
|
2653
2735
|
}
|
|
2736
|
+
async function people(site) {
|
|
2737
|
+
const found = credentialsOr();
|
|
2738
|
+
if ("result" in found) return found.result;
|
|
2739
|
+
const target = siteOr(site, "drop2run people <subdomain>");
|
|
2740
|
+
if ("result" in target) return target.result;
|
|
2741
|
+
try {
|
|
2742
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2743
|
+
const list2 = await listPeople(found.credentials, summary.siteId);
|
|
2744
|
+
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`.");
|
|
2745
|
+
const rows = list2.people.map(
|
|
2746
|
+
(person) => `${person.email} ${person.role}${person.owner ? ", owner" : ""} ${person.status}`
|
|
2747
|
+
);
|
|
2748
|
+
return { text: [head, ...rows].join("\n"), json: list2, code: 0 };
|
|
2749
|
+
} catch (error) {
|
|
2750
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
async function invite(emails, site, role) {
|
|
2754
|
+
const found = credentialsOr();
|
|
2755
|
+
if ("result" in found) return found.result;
|
|
2756
|
+
if (emails.length === 0)
|
|
2757
|
+
return failure("Name at least one address: `drop2run invite dana@example.com`.");
|
|
2758
|
+
if (role !== void 0 && role !== "view" && role !== "comment") {
|
|
2759
|
+
return failure("`--role` is `view` or `comment`.");
|
|
2760
|
+
}
|
|
2761
|
+
const target = siteOr(site, "drop2run invite <email> --site <subdomain>");
|
|
2762
|
+
if ("result" in target) return target.result;
|
|
2763
|
+
try {
|
|
2764
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2765
|
+
const invited = await invitePeople(
|
|
2766
|
+
found.credentials,
|
|
2767
|
+
summary.siteId,
|
|
2768
|
+
emails,
|
|
2769
|
+
role === "comment" ? "comment" : "view"
|
|
2770
|
+
);
|
|
2771
|
+
const list2 = await listPeople(found.credentials, summary.siteId);
|
|
2772
|
+
return {
|
|
2773
|
+
text: [
|
|
2774
|
+
invited.added.length === 0 ? `Nobody new on ${summary.subdomain}'s list.` : `Invited ${invited.added.join(", ")} to ${summary.subdomain} — each was emailed.`,
|
|
2775
|
+
...invited.alreadyListed.length === 0 ? [] : [`Already on the list, not emailed again: ${invited.alreadyListed.join(", ")}.`],
|
|
2776
|
+
...list2.invitedOnly ? [] : [
|
|
2777
|
+
"The site is not invite-only yet, so the list does not decide who opens it. `drop2run set invite-only on` does."
|
|
2778
|
+
]
|
|
2779
|
+
].join("\n"),
|
|
2780
|
+
json: invited,
|
|
2781
|
+
code: 0
|
|
2782
|
+
};
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
async function uninvite(email, site) {
|
|
2788
|
+
const found = credentialsOr();
|
|
2789
|
+
if ("result" in found) return found.result;
|
|
2790
|
+
if (email === void 0)
|
|
2791
|
+
return failure("Name the address: `drop2run uninvite dana@example.com`.");
|
|
2792
|
+
const target = siteOr(site, "drop2run uninvite <email> --site <subdomain>");
|
|
2793
|
+
if ("result" in target) return target.result;
|
|
2794
|
+
try {
|
|
2795
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2796
|
+
await removePerson(found.credentials, summary.siteId, email);
|
|
2797
|
+
return { text: `Removed ${email} from ${summary.subdomain}'s list.`, json: { email }, code: 0 };
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
const COMMENTS_USAGE = [
|
|
2803
|
+
" drop2run comments [site] [--status open|resolved|all] [--path /page]",
|
|
2804
|
+
" drop2run comments reply <thread> <text> [--resolve] [--site X]",
|
|
2805
|
+
" drop2run comments resolve|reopen <thread> [--site X]"
|
|
2806
|
+
].join("\n");
|
|
2807
|
+
async function comments(subcommand, rest, site, options) {
|
|
2808
|
+
const found = credentialsOr();
|
|
2809
|
+
if ("result" in found) return found.result;
|
|
2810
|
+
const acting = subcommand === "reply" || subcommand === "resolve" || subcommand === "reopen";
|
|
2811
|
+
if (!acting) {
|
|
2812
|
+
const status = options.status ?? "open";
|
|
2813
|
+
if (status !== "open" && status !== "resolved" && status !== "all") {
|
|
2814
|
+
return failure(
|
|
2815
|
+
`\`--status\` is open, resolved or all, not "${status}".
|
|
2816
|
+
|
|
2817
|
+
${COMMENTS_USAGE}`
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
const target2 = siteOr(subcommand ?? site, "drop2run comments <subdomain>");
|
|
2821
|
+
if ("result" in target2) return target2.result;
|
|
2822
|
+
try {
|
|
2823
|
+
const summary = await findSite(found.credentials, target2.named);
|
|
2824
|
+
const read = await listComments(found.credentials, summary.siteId, {
|
|
2825
|
+
status,
|
|
2826
|
+
path: options.path
|
|
2827
|
+
});
|
|
2828
|
+
return {
|
|
2829
|
+
text: describeComments(read.threads, status, summary.subdomain, read.more),
|
|
2830
|
+
json: read,
|
|
2831
|
+
code: 0
|
|
2832
|
+
};
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
const [thread, ...words] = rest;
|
|
2838
|
+
if (thread === void 0)
|
|
2839
|
+
return failure(`Name the thread, from \`drop2run comments\`.
|
|
2840
|
+
|
|
2841
|
+
${COMMENTS_USAGE}`);
|
|
2842
|
+
const body = words.join(" ").trim();
|
|
2843
|
+
if (subcommand === "reply" && body === "") {
|
|
2844
|
+
return failure(`Write the reply after the thread id.
|
|
2845
|
+
|
|
2846
|
+
${COMMENTS_USAGE}`);
|
|
2847
|
+
}
|
|
2848
|
+
const target = siteOr(site, `drop2run comments ${subcommand} <thread> --site <subdomain>`);
|
|
2849
|
+
if ("result" in target) return target.result;
|
|
2850
|
+
try {
|
|
2851
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2852
|
+
const answered = subcommand === "reply" ? await replyToComment(
|
|
2853
|
+
found.credentials,
|
|
2854
|
+
summary.siteId,
|
|
2855
|
+
thread,
|
|
2856
|
+
body,
|
|
2857
|
+
options.resolve === true
|
|
2858
|
+
) : await setCommentResolved(
|
|
2859
|
+
found.credentials,
|
|
2860
|
+
summary.siteId,
|
|
2861
|
+
thread,
|
|
2862
|
+
subcommand === "resolve"
|
|
2863
|
+
);
|
|
2864
|
+
const done = subcommand === "reply" ? `Replied on ${answered.path}${answered.resolvedAt === null ? "" : " and resolved the thread"}.` : `${subcommand === "resolve" ? "Resolved" : "Reopened"} the thread on ${answered.path}.`;
|
|
2865
|
+
return { text: `${done}
|
|
2866
|
+
${answered.url}`, json: answered, code: 0 };
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
function describeComments(threads, status, subdomain, more) {
|
|
2872
|
+
const which = status === "all" ? "" : `${status} `;
|
|
2873
|
+
if (threads.length === 0) return `No ${which}comment threads on ${subdomain}.`;
|
|
2874
|
+
const heading = `${threads.length} ${which}comment ${threads.length === 1 ? "thread" : "threads"} on ${subdomain}` + (more ? " (more follow; `--json` shows the cursor)." : ".");
|
|
2875
|
+
const blocks = threads.map((thread) => {
|
|
2876
|
+
const where2 = thread.place.quote ?? (thread.place.snippet || null);
|
|
2877
|
+
const flags = [
|
|
2878
|
+
...thread.resolvedAt === null ? [] : ["resolved"],
|
|
2879
|
+
...thread.outdated ? ["earlier version"] : []
|
|
2880
|
+
];
|
|
2881
|
+
return [
|
|
2882
|
+
`${thread.id} ${thread.path}${where2 === null ? "" : ` "${where2}"`}` + (flags.length === 0 ? "" : ` (${flags.join(", ")})`),
|
|
2883
|
+
...thread.comments.map((comment) => {
|
|
2884
|
+
const who = comment.author.owner ? `${comment.author.name} (owner)` : comment.author.name;
|
|
2885
|
+
return ` ${who}: ${comment.body.replace(/\s*\n\s*/g, " ")}`;
|
|
2886
|
+
}),
|
|
2887
|
+
` ${thread.url}`
|
|
2888
|
+
].join("\n");
|
|
2889
|
+
});
|
|
2890
|
+
return [heading, ...blocks].join("\n\n");
|
|
2891
|
+
}
|
|
2654
2892
|
function day(iso) {
|
|
2655
2893
|
const parsed = new Date(iso);
|
|
2656
2894
|
return Number.isNaN(parsed.getTime()) ? iso : parsed.toISOString().slice(0, 10);
|
|
@@ -2691,6 +2929,12 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
2691
2929
|
drop2run resume [site] Put a paused site back on the air
|
|
2692
2930
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
2693
2931
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
2932
|
+
drop2run people [site] Who the site is shared with
|
|
2933
|
+
drop2run invite <email>... [--role comment] Share it with people; each is emailed
|
|
2934
|
+
drop2run uninvite <email> Take someone off the list
|
|
2935
|
+
drop2run comments [site] Read the comments left on a site's pages
|
|
2936
|
+
drop2run comments reply <thread> <text> Answer a thread as the owner [--resolve]
|
|
2937
|
+
drop2run comments resolve|reopen <thread> Resolve a thread, or open it again
|
|
2694
2938
|
drop2run token list List your access tokens
|
|
2695
2939
|
drop2run whoami Check the token and whose it is
|
|
2696
2940
|
drop2run where Show which token source is in use
|
|
@@ -2706,6 +2950,17 @@ Flags
|
|
|
2706
2950
|
--yes Confirm a deletion, which cannot be undone
|
|
2707
2951
|
--then X What \`set expires\` does when the date arrives: pause
|
|
2708
2952
|
(the default, keeps the files) or delete (needs --yes)
|
|
2953
|
+
--status X Which comment threads: open (the default), resolved or all
|
|
2954
|
+
--path X Only the comments on one page, such as /pricing
|
|
2955
|
+
--resolve Resolve the thread with the reply (\`comments reply\` only)
|
|
2956
|
+
--role X What invited people may do: view (the default) or comment
|
|
2957
|
+
|
|
2958
|
+
Comments
|
|
2959
|
+
\`comments reply\`, \`resolve\` and \`reopen\` act on --site, else the site in
|
|
2960
|
+
drop2run.json, and only the account's owner can use them. Deploy the fix
|
|
2961
|
+
before replying that it is fixed. A reply leaves the thread open unless
|
|
2962
|
+
--resolve is given, so the person who raised it can check the fix and
|
|
2963
|
+
resolve it themselves.
|
|
2709
2964
|
|
|
2710
2965
|
Settings
|
|
2711
2966
|
set name <text> What the dashboard calls the site; put a
|
|
@@ -2717,6 +2972,11 @@ Settings
|
|
|
2717
2972
|
set forms on|off Whether its forms accept submissions
|
|
2718
2973
|
set expires <ISO 8601> Take it down then, e.g. 2026-12-31T09:00:00Z
|
|
2719
2974
|
set folder <path|id|root> Move it to a folder, or root for the top level
|
|
2975
|
+
set invite-only on|off Only the people on its list (\`invite\`) can open it;
|
|
2976
|
+
replaces a password
|
|
2977
|
+
set comments off|invited|anyone
|
|
2978
|
+
Who may comment: nobody, people invited to
|
|
2979
|
+
comment, or anybody signed in
|
|
2720
2980
|
|
|
2721
2981
|
\`set\` and \`unset\` act on --site, else the site in drop2run.json. A
|
|
2722
2982
|
setting your plan does not include is refused with the reason, and
|
|
@@ -2749,7 +3009,7 @@ function flagValue(args, flag) {
|
|
|
2749
3009
|
const value = args[at + 1];
|
|
2750
3010
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
2751
3011
|
}
|
|
2752
|
-
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then"];
|
|
3012
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then", "--status", "--path", "--role"];
|
|
2753
3013
|
function positionals(argv) {
|
|
2754
3014
|
const found = [];
|
|
2755
3015
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -2798,7 +3058,7 @@ async function run(commandLine) {
|
|
|
2798
3058
|
const positional = [...positionals(argv), ...literal];
|
|
2799
3059
|
const [command, ...rest] = positional;
|
|
2800
3060
|
if (argv.includes("--version")) {
|
|
2801
|
-
const { version } = await import("./package-
|
|
3061
|
+
const { version } = await import("./package-CGI7Yt81.js").then(
|
|
2802
3062
|
(module) => module.default
|
|
2803
3063
|
);
|
|
2804
3064
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -2828,11 +3088,17 @@ async function run(commandLine) {
|
|
|
2828
3088
|
argv.includes("--device"),
|
|
2829
3089
|
argv.includes("--yes"),
|
|
2830
3090
|
{ subdomain, folder },
|
|
2831
|
-
then
|
|
3091
|
+
then,
|
|
3092
|
+
{
|
|
3093
|
+
status: flagValue(argv, "--status"),
|
|
3094
|
+
path: flagValue(argv, "--path"),
|
|
3095
|
+
resolve: argv.includes("--resolve")
|
|
3096
|
+
},
|
|
3097
|
+
flagValue(argv, "--role")
|
|
2832
3098
|
);
|
|
2833
3099
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
2834
3100
|
}
|
|
2835
|
-
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0) {
|
|
3101
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0, commentOptions = {}, role = void 0) {
|
|
2836
3102
|
switch (command) {
|
|
2837
3103
|
case "login": {
|
|
2838
3104
|
const print = json ? () => {
|
|
@@ -2869,8 +3135,16 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
|
|
|
2869
3135
|
return await pauseOrResume(command, rest[0] ?? site);
|
|
2870
3136
|
case "rollback":
|
|
2871
3137
|
return await rollback(rest[0], site);
|
|
3138
|
+
case "people":
|
|
3139
|
+
return await people(rest[0] ?? site);
|
|
3140
|
+
case "invite":
|
|
3141
|
+
return await invite(rest, site, role);
|
|
3142
|
+
case "uninvite":
|
|
3143
|
+
return await uninvite(rest[0], site);
|
|
2872
3144
|
case "rm":
|
|
2873
3145
|
return await remove(rest[0] ?? site, confirmed);
|
|
3146
|
+
case "comments":
|
|
3147
|
+
return await comments(rest[0], rest.slice(1), site, commentOptions);
|
|
2874
3148
|
case "token":
|
|
2875
3149
|
case "tokens":
|
|
2876
3150
|
return rest[0] === void 0 || rest[0] === "list" ? await tokens() : {
|