drop2run 0.7.0 → 0.9.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 +25 -0
- package/dist/index.js +659 -17
- package/dist/{package-D8mWS-nC.js → package-LdzYr7IV.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,8 +22,16 @@ drop2run init|deploy --folder <path> File the new site in one of your fo
|
|
|
22
22
|
drop2run ls List your sites
|
|
23
23
|
drop2run folders List your folders, as --folder takes them
|
|
24
24
|
drop2run open [site] Open a site in a browser
|
|
25
|
+
drop2run info [site] Show a site's settings and versions
|
|
26
|
+
drop2run set <setting> <value> [--site X] Change one setting
|
|
27
|
+
drop2run unset name|password|expires Clear a name, a password or a takedown
|
|
28
|
+
drop2run pause [site] Take a site off the air, keeping its files
|
|
29
|
+
drop2run resume [site] Put a paused site back on the air
|
|
25
30
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
26
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
|
|
27
35
|
drop2run token list List your access tokens
|
|
28
36
|
drop2run whoami Check the token and whose it is
|
|
29
37
|
drop2run where Show which token source is in use
|
|
@@ -32,6 +40,23 @@ drop2run --version Print the version
|
|
|
32
40
|
|
|
33
41
|
`--json` on any command prints machine-readable output instead of text.
|
|
34
42
|
|
|
43
|
+
## Comments
|
|
44
|
+
|
|
45
|
+
On a plan that includes comments, people viewing a site can pin comments to its pages. `drop2run comments`
|
|
46
|
+
reads them — each thread's id, page, the words it points at, and every comment — with `--status
|
|
47
|
+
open|resolved|all` and `--path /page` to narrow it. The owner of the account can answer from here:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
drop2run comments calm-cedar # open threads
|
|
51
|
+
drop2run deploy dist # publish the fix first
|
|
52
|
+
drop2run comments reply 01JTHREAD... "Fixed — please check" --site calm-cedar
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The reply shows on the page under your name, as one typed there would, and the people in the thread are
|
|
56
|
+
emailed.
|
|
57
|
+
It leaves the thread open unless `--resolve` is given, so the person who raised it can check the fix and
|
|
58
|
+
resolve it themselves. `comments resolve` and `comments reopen` change a thread's state on its own.
|
|
59
|
+
|
|
35
60
|
## Which site a command acts on
|
|
36
61
|
|
|
37
62
|
`--subdomain` creates one under the name you give; otherwise `--site`, then `drop2run.json`, then a new
|
package/dist/index.js
CHANGED
|
@@ -118,7 +118,9 @@ async function call(credentials, path, init2 = {}) {
|
|
|
118
118
|
async function findSite(credentials, site) {
|
|
119
119
|
const found = await lookUpSite(credentials, site);
|
|
120
120
|
if (found === void 0) {
|
|
121
|
-
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
|
+
);
|
|
122
124
|
}
|
|
123
125
|
return found;
|
|
124
126
|
}
|
|
@@ -162,6 +164,108 @@ async function promoteDeploy(credentials, siteId, deployId) {
|
|
|
162
164
|
{ method: "POST" }
|
|
163
165
|
);
|
|
164
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
|
+
}
|
|
165
269
|
async function listTokens(credentials) {
|
|
166
270
|
const body = await call(credentials, "tokens");
|
|
167
271
|
return body.tokens;
|
|
@@ -236,6 +340,73 @@ async function resolveFolder(credentials, folder) {
|
|
|
236
340
|
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
237
341
|
);
|
|
238
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
|
+
async function listComments(credentials, siteId, query = {}) {
|
|
348
|
+
const search = new URLSearchParams();
|
|
349
|
+
if (query.status !== void 0) search.set("status", query.status);
|
|
350
|
+
if (query.path !== void 0) search.set("path", query.path);
|
|
351
|
+
if (query.since !== void 0) search.set("since", String(query.since));
|
|
352
|
+
if (query.page !== void 0) search.set("page", String(query.page));
|
|
353
|
+
const encoded = search.toString();
|
|
354
|
+
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
355
|
+
return await call(
|
|
356
|
+
credentials,
|
|
357
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
361
|
+
return await call(
|
|
362
|
+
credentials,
|
|
363
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
364
|
+
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
368
|
+
return await call(
|
|
369
|
+
credentials,
|
|
370
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
371
|
+
{ method: "POST" }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const VERSIONS_LISTED = 10;
|
|
375
|
+
function servingModeOf$1(site) {
|
|
376
|
+
if (site.spaMode) return "spa";
|
|
377
|
+
if (site.docsMode) return "docs";
|
|
378
|
+
return "static";
|
|
379
|
+
}
|
|
380
|
+
function accessOf(site) {
|
|
381
|
+
if (site.passwordProtected) return "password";
|
|
382
|
+
if (site.invitedOnly === true) return "invite-only";
|
|
383
|
+
if (site.invitedOnly === false) return "public";
|
|
384
|
+
return "no password (whether it is invite-only could not be read)";
|
|
385
|
+
}
|
|
386
|
+
function describeSite(site, folderPath) {
|
|
387
|
+
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
388
|
+
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
389
|
+
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
390
|
+
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
391
|
+
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
392
|
+
});
|
|
393
|
+
const more = site.deploys.length - versions.length;
|
|
394
|
+
return [
|
|
395
|
+
`${site.subdomain} — ${site.url}`,
|
|
396
|
+
` name ${site.name ?? "(none)"}`,
|
|
397
|
+
` status ${site.status}`,
|
|
398
|
+
` access ${accessOf(site)}`,
|
|
399
|
+
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
400
|
+
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
401
|
+
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
402
|
+
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
403
|
+
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
404
|
+
"",
|
|
405
|
+
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
406
|
+
...versions,
|
|
407
|
+
...more > 0 ? [` … and ${more} older`] : []
|
|
408
|
+
].join("\n");
|
|
409
|
+
}
|
|
239
410
|
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
240
411
|
function newAttempt() {
|
|
241
412
|
const verifier = randomBytes(32).toString("base64url");
|
|
@@ -1140,6 +1311,24 @@ function canPublish(paths) {
|
|
|
1140
1311
|
if (isImageFolder(paths)) return true;
|
|
1141
1312
|
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
1142
1313
|
}
|
|
1314
|
+
function servingModeOf(flags) {
|
|
1315
|
+
if (flags.docsMode) return "docs";
|
|
1316
|
+
if (flags.spaMode) return "spa";
|
|
1317
|
+
return "static";
|
|
1318
|
+
}
|
|
1319
|
+
function predictedModeOf(paths) {
|
|
1320
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
|
|
1321
|
+
if (paths.some(isDocumentPath)) return "docs";
|
|
1322
|
+
if (isImageFolder(paths)) return "docs";
|
|
1323
|
+
if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
|
|
1324
|
+
return "static";
|
|
1325
|
+
}
|
|
1326
|
+
function nestedIndexOf(paths) {
|
|
1327
|
+
if (predictedModeOf(paths) !== "docs") return null;
|
|
1328
|
+
const suffix = `/${REQUIRED_INDEX}`;
|
|
1329
|
+
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));
|
|
1330
|
+
return folders2[0] ?? null;
|
|
1331
|
+
}
|
|
1143
1332
|
function renameOfLoneHtmlPage(paths) {
|
|
1144
1333
|
if (paths.length !== 1) return null;
|
|
1145
1334
|
const [only] = paths;
|
|
@@ -1675,7 +1864,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1675
1864
|
signal,
|
|
1676
1865
|
await readSiteName(files)
|
|
1677
1866
|
);
|
|
1678
|
-
onProgress({
|
|
1867
|
+
onProgress({
|
|
1868
|
+
type: "done",
|
|
1869
|
+
url: completed.url,
|
|
1870
|
+
name: completed.name,
|
|
1871
|
+
mode: servingModeOf(completed),
|
|
1872
|
+
modeIsManual: completed.modeIsManual
|
|
1873
|
+
});
|
|
1679
1874
|
} catch (error) {
|
|
1680
1875
|
const failure2 = asDeployError(error);
|
|
1681
1876
|
onProgress({
|
|
@@ -1728,6 +1923,15 @@ function escapesRoot(root, candidate) {
|
|
|
1728
1923
|
const inside = relative(root, candidate);
|
|
1729
1924
|
return inside === "" || inside.startsWith("..");
|
|
1730
1925
|
}
|
|
1926
|
+
function describeMode(result) {
|
|
1927
|
+
if (result.mode === null) return null;
|
|
1928
|
+
if (result.nestedIndex !== null) {
|
|
1929
|
+
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.`;
|
|
1930
|
+
}
|
|
1931
|
+
if (result.mode === "docs") return "Served as documents, in the documents reader.";
|
|
1932
|
+
if (result.mode === "spa") return "Served as a single-page app.";
|
|
1933
|
+
return "Served as a static site.";
|
|
1934
|
+
}
|
|
1731
1935
|
async function resolveSite(credentials, site, newSite) {
|
|
1732
1936
|
const { subdomain, folder } = newSite;
|
|
1733
1937
|
if (site !== void 0 && subdomain !== void 0) {
|
|
@@ -1737,7 +1941,7 @@ async function resolveSite(credentials, site, newSite) {
|
|
|
1737
1941
|
}
|
|
1738
1942
|
if (site !== void 0 && folder !== void 0) {
|
|
1739
1943
|
throw new Error(
|
|
1740
|
-
"A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site
|
|
1944
|
+
"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`."
|
|
1741
1945
|
);
|
|
1742
1946
|
}
|
|
1743
1947
|
if (site === void 0) {
|
|
@@ -1757,6 +1961,8 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1757
1961
|
let files = 0;
|
|
1758
1962
|
let url = target.url;
|
|
1759
1963
|
let unchanged = false;
|
|
1964
|
+
let mode = null;
|
|
1965
|
+
let paths = [];
|
|
1760
1966
|
let failure2 = null;
|
|
1761
1967
|
let failureDetail;
|
|
1762
1968
|
const record = (event) => {
|
|
@@ -1765,14 +1971,22 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1765
1971
|
unchanged = true;
|
|
1766
1972
|
url = event.url || target.url;
|
|
1767
1973
|
}
|
|
1768
|
-
if (event.type === "done")
|
|
1974
|
+
if (event.type === "done") {
|
|
1975
|
+
url = event.url;
|
|
1976
|
+
mode = event.mode;
|
|
1977
|
+
}
|
|
1769
1978
|
if (event.type === "error") {
|
|
1770
1979
|
failure2 = event.message;
|
|
1771
1980
|
failureDetail = event.detail;
|
|
1772
1981
|
}
|
|
1773
1982
|
onProgress?.(event);
|
|
1774
1983
|
};
|
|
1775
|
-
|
|
1984
|
+
const watched = async (signal, onRead) => {
|
|
1985
|
+
const collected = await source(signal, onRead);
|
|
1986
|
+
paths = collected.map((file) => file.path);
|
|
1987
|
+
return collected;
|
|
1988
|
+
};
|
|
1989
|
+
await deploy(watched, target.siteId, record, void 0, {
|
|
1776
1990
|
baseUrl: credentials.apiBaseUrl,
|
|
1777
1991
|
token: credentials.token
|
|
1778
1992
|
}).catch((error) => {
|
|
@@ -1782,7 +1996,15 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
|
1782
1996
|
thrown.detail = failureDetail ?? error?.detail;
|
|
1783
1997
|
throw thrown;
|
|
1784
1998
|
});
|
|
1785
|
-
return {
|
|
1999
|
+
return {
|
|
2000
|
+
url,
|
|
2001
|
+
siteId: target.siteId,
|
|
2002
|
+
subdomain: target.subdomain,
|
|
2003
|
+
files,
|
|
2004
|
+
unchanged,
|
|
2005
|
+
mode,
|
|
2006
|
+
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
2007
|
+
};
|
|
1786
2008
|
}
|
|
1787
2009
|
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
1788
2010
|
const found = await stat(directory).catch(() => null);
|
|
@@ -1861,6 +2083,82 @@ function progressWriter(write, isTerminal, width = 80) {
|
|
|
1861
2083
|
};
|
|
1862
2084
|
return writer;
|
|
1863
2085
|
}
|
|
2086
|
+
async function readSecret(prompt, input = process.stdin, output = process.stderr) {
|
|
2087
|
+
if (input.isTTY !== true || input.setRawMode === void 0) {
|
|
2088
|
+
const parts = [];
|
|
2089
|
+
for await (const chunk of input)
|
|
2090
|
+
parts.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
2091
|
+
return parts.join("").replace(/\r?\n$/, "");
|
|
2092
|
+
}
|
|
2093
|
+
const setRawMode = input.setRawMode.bind(input);
|
|
2094
|
+
output.write(prompt);
|
|
2095
|
+
setRawMode(true);
|
|
2096
|
+
input.setEncoding("utf8");
|
|
2097
|
+
input.resume();
|
|
2098
|
+
return await new Promise((resolve2, reject) => {
|
|
2099
|
+
const line = new HiddenLine();
|
|
2100
|
+
const finish = () => {
|
|
2101
|
+
setRawMode(false);
|
|
2102
|
+
input.pause();
|
|
2103
|
+
input.off("data", onData);
|
|
2104
|
+
output.write("\n");
|
|
2105
|
+
};
|
|
2106
|
+
function onData(chunk) {
|
|
2107
|
+
const outcome = line.type(chunk);
|
|
2108
|
+
if (outcome === "more") return;
|
|
2109
|
+
finish();
|
|
2110
|
+
if (outcome === "cancelled") reject(new Error("Cancelled."));
|
|
2111
|
+
else resolve2(line.value);
|
|
2112
|
+
}
|
|
2113
|
+
input.on("data", onData);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
class HiddenLine {
|
|
2117
|
+
/** What has been typed, one entry per code point, so Backspace never splits a surrogate pair. */
|
|
2118
|
+
characters = [];
|
|
2119
|
+
/** Where an escape sequence has got to: none, just after ESC, or inside a `CSI`/`SS3` sequence. */
|
|
2120
|
+
escape = "none";
|
|
2121
|
+
/** The line so far. */
|
|
2122
|
+
get value() {
|
|
2123
|
+
return this.characters.join("");
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Takes the characters one chunk of input carries.
|
|
2127
|
+
*
|
|
2128
|
+
* @param chunk What arrived.
|
|
2129
|
+
* @returns `done` on Enter or Ctrl+D, `cancelled` on Ctrl+C, `more` otherwise.
|
|
2130
|
+
*/
|
|
2131
|
+
type(chunk) {
|
|
2132
|
+
for (const character of chunk) {
|
|
2133
|
+
const code = character.codePointAt(0) ?? 0;
|
|
2134
|
+
if (this.escape === "started") {
|
|
2135
|
+
this.escape = character === "[" || character === "O" ? "sequence" : "none";
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
if (this.escape === "sequence") {
|
|
2139
|
+
if (code >= 64 && code <= 126) this.escape = "none";
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
if (character === "\r" || character === "\n" || character === "") return "done";
|
|
2143
|
+
if (character === "") return "cancelled";
|
|
2144
|
+
if (character === "\x1B") {
|
|
2145
|
+
this.escape = "started";
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
if (character === "" || character === "\b") {
|
|
2149
|
+
this.characters.pop();
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
if (character === "") {
|
|
2153
|
+
this.characters.length = 0;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (code < 32) continue;
|
|
2157
|
+
this.characters.push(character);
|
|
2158
|
+
}
|
|
2159
|
+
return "more";
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1864
2162
|
function failure(text) {
|
|
1865
2163
|
return { text, json: { error: text }, code: 1 };
|
|
1866
2164
|
}
|
|
@@ -1972,9 +2270,9 @@ async function whoami() {
|
|
|
1972
2270
|
const response = await fetch(`${found.credentials.apiBaseUrl}/me`, {
|
|
1973
2271
|
headers: { Authorization: `Bearer ${found.credentials.token}` }
|
|
1974
2272
|
});
|
|
1975
|
-
if (!response.ok) {
|
|
2273
|
+
if (!response.ok || response.status === 204) {
|
|
1976
2274
|
return failure(
|
|
1977
|
-
response.status === 401 ? "This token is not valid any more. It may have been revoked or expired — create another at https://app.dropto.run/account/tokens." : `The API answered ${response.status}.`
|
|
2275
|
+
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}.`
|
|
1978
2276
|
);
|
|
1979
2277
|
}
|
|
1980
2278
|
const me = await response.json();
|
|
@@ -2040,7 +2338,7 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
|
|
|
2040
2338
|
const target = newSite.subdomain === void 0 ? site ?? project?.siteId : void 0;
|
|
2041
2339
|
if (newSite.folder !== void 0 && target !== void 0 && project !== null) {
|
|
2042
2340
|
return failure(
|
|
2043
|
-
`This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site
|
|
2341
|
+
`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.`
|
|
2044
2342
|
);
|
|
2045
2343
|
}
|
|
2046
2344
|
try {
|
|
@@ -2052,9 +2350,11 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
|
|
|
2052
2350
|
newSite
|
|
2053
2351
|
);
|
|
2054
2352
|
const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
|
|
2353
|
+
const mode = describeMode(result);
|
|
2055
2354
|
return {
|
|
2056
2355
|
text: `${what}
|
|
2057
|
-
${result.url}
|
|
2356
|
+
${result.url}${mode === null ? "" : `
|
|
2357
|
+
${mode}`}`,
|
|
2058
2358
|
json: result,
|
|
2059
2359
|
code: 0
|
|
2060
2360
|
};
|
|
@@ -2169,6 +2469,197 @@ async function open(site, launch = openBrowser) {
|
|
|
2169
2469
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2170
2470
|
}
|
|
2171
2471
|
}
|
|
2472
|
+
function siteOr(site, usage) {
|
|
2473
|
+
const named = site ?? readProject()?.siteId;
|
|
2474
|
+
return named === void 0 ? {
|
|
2475
|
+
result: failure(
|
|
2476
|
+
`Name the site: \`${usage}\`, or run this in a folder with a ${PROJECT_FILE}.`
|
|
2477
|
+
)
|
|
2478
|
+
} : { named };
|
|
2479
|
+
}
|
|
2480
|
+
async function info(site) {
|
|
2481
|
+
const found = credentialsOr();
|
|
2482
|
+
if ("result" in found) return found.result;
|
|
2483
|
+
const target = siteOr(site, "drop2run info <subdomain>");
|
|
2484
|
+
if ("result" in target) return target.result;
|
|
2485
|
+
try {
|
|
2486
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2487
|
+
const detail = await getSite(found.credentials, summary.siteId);
|
|
2488
|
+
const folderPath = detail.folderId === null ? void 0 : (await listFolders(found.credentials)).find(
|
|
2489
|
+
(folder) => folder.folderId === detail.folderId
|
|
2490
|
+
)?.path;
|
|
2491
|
+
return {
|
|
2492
|
+
text: describeSite(detail, folderPath),
|
|
2493
|
+
json: { ...detail, mode: servingModeOf$1(detail), folderPath: folderPath ?? null },
|
|
2494
|
+
code: 0
|
|
2495
|
+
};
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
async function pauseOrResume(action, site) {
|
|
2501
|
+
const found = credentialsOr();
|
|
2502
|
+
if ("result" in found) return found.result;
|
|
2503
|
+
const target = siteOr(site, `drop2run ${action} <subdomain>`);
|
|
2504
|
+
if ("result" in target) return target.result;
|
|
2505
|
+
try {
|
|
2506
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2507
|
+
const state = await setSitePaused(found.credentials, summary.siteId, action);
|
|
2508
|
+
return {
|
|
2509
|
+
text: action === "resume" ? `${state.subdomain} is back on the air.
|
|
2510
|
+
${summary.url}` : state.status === "paused" ? `Paused ${state.subdomain}. Its files and subdomain are kept; \`drop2run resume\` puts it back.` : (
|
|
2511
|
+
// A site Drop2Run suspended stays suspended: the pause is recorded for when that lifts,
|
|
2512
|
+
// and promising `resume` here would be promising something the API refuses.
|
|
2513
|
+
`${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.`
|
|
2514
|
+
),
|
|
2515
|
+
json: state,
|
|
2516
|
+
code: 0
|
|
2517
|
+
};
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const SETTINGS_USAGE = [
|
|
2523
|
+
" name <text> What the dashboard calls the site",
|
|
2524
|
+
" mode static|spa|docs How it serves files",
|
|
2525
|
+
" password Read from stdin, or typed at a hidden prompt",
|
|
2526
|
+
" forms on|off Whether its forms accept submissions",
|
|
2527
|
+
" 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"
|
|
2529
|
+
].join("\n");
|
|
2530
|
+
function settingChange(setting, values, then) {
|
|
2531
|
+
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
2532
|
+
const needs = (example) => ({
|
|
2533
|
+
refusal: `\`set ${setting}\` needs a value, for example \`drop2run set ${setting} ${example}\`.`
|
|
2534
|
+
});
|
|
2535
|
+
if (then !== void 0 && setting !== "expires") {
|
|
2536
|
+
return { refusal: "`--then` only applies to `set expires`." };
|
|
2537
|
+
}
|
|
2538
|
+
if (setting !== "name" && setting !== "password" && values.length > 1) {
|
|
2539
|
+
return {
|
|
2540
|
+
refusal: `\`set ${setting}\` takes one value, and "${values.slice(1).join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
switch (setting) {
|
|
2544
|
+
case "name":
|
|
2545
|
+
return value ? { change: { name: value } } : needs('"Launch notes"');
|
|
2546
|
+
case "mode":
|
|
2547
|
+
if (value === "static" || value === "spa" || value === "docs") {
|
|
2548
|
+
return { change: { mode: value } };
|
|
2549
|
+
}
|
|
2550
|
+
return { refusal: "`set mode` takes `static`, `spa` or `docs`." };
|
|
2551
|
+
case "forms":
|
|
2552
|
+
if (value === "on" || value === "off") return { change: { formsEnabled: value === "on" } };
|
|
2553
|
+
return { refusal: "`set forms` takes `on` or `off`." };
|
|
2554
|
+
case "password": {
|
|
2555
|
+
if (value !== void 0) {
|
|
2556
|
+
return {
|
|
2557
|
+
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`.'
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
return { password: true };
|
|
2561
|
+
}
|
|
2562
|
+
case "expires": {
|
|
2563
|
+
if (value === void 0) return needs("2026-12-31T09:00:00Z");
|
|
2564
|
+
if (then !== void 0 && then !== "pause" && then !== "delete") {
|
|
2565
|
+
return { refusal: "`--then` takes `pause` or `delete`." };
|
|
2566
|
+
}
|
|
2567
|
+
return {
|
|
2568
|
+
change: {
|
|
2569
|
+
expiresAt: value,
|
|
2570
|
+
...then === void 0 ? {} : { expiryAction: then }
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
case "folder":
|
|
2575
|
+
return value ? { folder: value } : needs("Clients/Acme");
|
|
2576
|
+
default:
|
|
2577
|
+
return {
|
|
2578
|
+
refusal: `${setting === void 0 ? "Name a setting." : `\`${setting}\` is not a setting.`} Settings:
|
|
2579
|
+
${SETTINGS_USAGE}`
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function set(site, setting, values, options, readPassword = () => readSecret("Password for visitors: ")) {
|
|
2584
|
+
const parsed = settingChange(setting, values, options.then);
|
|
2585
|
+
if ("refusal" in parsed) return failure(parsed.refusal);
|
|
2586
|
+
const found = credentialsOr();
|
|
2587
|
+
if ("result" in found) return found.result;
|
|
2588
|
+
const target = siteOr(site, `drop2run set ${setting} … --site <subdomain>`);
|
|
2589
|
+
if ("result" in target) return target.result;
|
|
2590
|
+
try {
|
|
2591
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2592
|
+
let change;
|
|
2593
|
+
if ("folder" in parsed) {
|
|
2594
|
+
change = { folderId: await resolveFolderTarget(found.credentials, parsed.folder) };
|
|
2595
|
+
} else if ("password" in parsed) {
|
|
2596
|
+
const password = await readPassword();
|
|
2597
|
+
if (password === "") {
|
|
2598
|
+
return failure("No password was given. To remove one, run `drop2run unset password`.");
|
|
2599
|
+
}
|
|
2600
|
+
change = { password };
|
|
2601
|
+
} else {
|
|
2602
|
+
change = parsed.change;
|
|
2603
|
+
}
|
|
2604
|
+
if (change.expiryAction === "delete" && !options.confirmed) {
|
|
2605
|
+
return failure(
|
|
2606
|
+
`This would delete ${summary.subdomain} and every version published to it when ${change.expiresAt} arrives, with no way back then.
|
|
2607
|
+
Add --yes if that is what you want, or leave out --then to pause it instead.`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
return await applied(found.credentials, summary.siteId, summary.subdomain, change);
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
async function unset(site, setting, extra = []) {
|
|
2616
|
+
if (extra.length > 0) {
|
|
2617
|
+
return failure(
|
|
2618
|
+
`\`unset ${setting}\` takes nothing after it, and "${extra.join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
const clear = /* @__PURE__ */ new Map([
|
|
2622
|
+
["name", { name: "" }],
|
|
2623
|
+
["password", { password: "" }],
|
|
2624
|
+
["expires", { expiresAt: "" }]
|
|
2625
|
+
]);
|
|
2626
|
+
const change = setting === void 0 ? void 0 : clear.get(setting);
|
|
2627
|
+
if (change === void 0) {
|
|
2628
|
+
return failure(
|
|
2629
|
+
"`unset` clears `name`, `password` or `expires`. To file the site at the top level, run `drop2run set folder root`."
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
const found = credentialsOr();
|
|
2633
|
+
if ("result" in found) return found.result;
|
|
2634
|
+
const target = siteOr(site, `drop2run unset ${setting} --site <subdomain>`);
|
|
2635
|
+
if ("result" in target) return target.result;
|
|
2636
|
+
try {
|
|
2637
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2638
|
+
return await applied(found.credentials, summary.siteId, summary.subdomain, change);
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
async function applied(credentials, siteId, subdomain, change) {
|
|
2644
|
+
const settings = await updateSiteSettings(credentials, siteId, change);
|
|
2645
|
+
const takedown = settings.expiresAt === null ? "none" : `${settings.expiresAt} (${settings.expiryAction})`;
|
|
2646
|
+
return {
|
|
2647
|
+
text: [
|
|
2648
|
+
`Updated ${subdomain}.`,
|
|
2649
|
+
` name ${settings.name ?? "(none)"}`,
|
|
2650
|
+
` access ${accessOf(settings)}`,
|
|
2651
|
+
` mode ${servingModeOf$1(settings)}`,
|
|
2652
|
+
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2653
|
+
` takedown ${takedown}`,
|
|
2654
|
+
...settings.replacedInviteOnly ? [
|
|
2655
|
+
"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."
|
|
2656
|
+
] : [],
|
|
2657
|
+
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2658
|
+
].join("\n"),
|
|
2659
|
+
json: { ...settings, mode: servingModeOf$1(settings) },
|
|
2660
|
+
code: 0
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2172
2663
|
async function tokens() {
|
|
2173
2664
|
const found = credentialsOr();
|
|
2174
2665
|
if ("result" in found) return found.result;
|
|
@@ -2187,6 +2678,96 @@ async function tokens() {
|
|
|
2187
2678
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2188
2679
|
}
|
|
2189
2680
|
}
|
|
2681
|
+
const COMMENTS_USAGE = [
|
|
2682
|
+
" drop2run comments [site] [--status open|resolved|all] [--path /page]",
|
|
2683
|
+
" drop2run comments reply <thread> <text> [--resolve] [--site X]",
|
|
2684
|
+
" drop2run comments resolve|reopen <thread> [--site X]"
|
|
2685
|
+
].join("\n");
|
|
2686
|
+
async function comments(subcommand, rest, site, options) {
|
|
2687
|
+
const found = credentialsOr();
|
|
2688
|
+
if ("result" in found) return found.result;
|
|
2689
|
+
const acting = subcommand === "reply" || subcommand === "resolve" || subcommand === "reopen";
|
|
2690
|
+
if (!acting) {
|
|
2691
|
+
const status = options.status ?? "open";
|
|
2692
|
+
if (status !== "open" && status !== "resolved" && status !== "all") {
|
|
2693
|
+
return failure(
|
|
2694
|
+
`\`--status\` is open, resolved or all, not "${status}".
|
|
2695
|
+
|
|
2696
|
+
${COMMENTS_USAGE}`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
const target2 = siteOr(subcommand ?? site, "drop2run comments <subdomain>");
|
|
2700
|
+
if ("result" in target2) return target2.result;
|
|
2701
|
+
try {
|
|
2702
|
+
const summary = await findSite(found.credentials, target2.named);
|
|
2703
|
+
const read = await listComments(found.credentials, summary.siteId, {
|
|
2704
|
+
status,
|
|
2705
|
+
path: options.path
|
|
2706
|
+
});
|
|
2707
|
+
return {
|
|
2708
|
+
text: describeComments(read.threads, status, summary.subdomain, read.more),
|
|
2709
|
+
json: read,
|
|
2710
|
+
code: 0
|
|
2711
|
+
};
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
const [thread, ...words] = rest;
|
|
2717
|
+
if (thread === void 0)
|
|
2718
|
+
return failure(`Name the thread, from \`drop2run comments\`.
|
|
2719
|
+
|
|
2720
|
+
${COMMENTS_USAGE}`);
|
|
2721
|
+
const body = words.join(" ").trim();
|
|
2722
|
+
if (subcommand === "reply" && body === "") {
|
|
2723
|
+
return failure(`Write the reply after the thread id.
|
|
2724
|
+
|
|
2725
|
+
${COMMENTS_USAGE}`);
|
|
2726
|
+
}
|
|
2727
|
+
const target = siteOr(site, `drop2run comments ${subcommand} <thread> --site <subdomain>`);
|
|
2728
|
+
if ("result" in target) return target.result;
|
|
2729
|
+
try {
|
|
2730
|
+
const summary = await findSite(found.credentials, target.named);
|
|
2731
|
+
const answered = subcommand === "reply" ? await replyToComment(
|
|
2732
|
+
found.credentials,
|
|
2733
|
+
summary.siteId,
|
|
2734
|
+
thread,
|
|
2735
|
+
body,
|
|
2736
|
+
options.resolve === true
|
|
2737
|
+
) : await setCommentResolved(
|
|
2738
|
+
found.credentials,
|
|
2739
|
+
summary.siteId,
|
|
2740
|
+
thread,
|
|
2741
|
+
subcommand === "resolve"
|
|
2742
|
+
);
|
|
2743
|
+
const done = subcommand === "reply" ? `Replied on ${answered.path}${answered.resolvedAt === null ? "" : " and resolved the thread"}.` : `${subcommand === "resolve" ? "Resolved" : "Reopened"} the thread on ${answered.path}.`;
|
|
2744
|
+
return { text: `${done}
|
|
2745
|
+
${answered.url}`, json: answered, code: 0 };
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
function describeComments(threads, status, subdomain, more) {
|
|
2751
|
+
const which = status === "all" ? "" : `${status} `;
|
|
2752
|
+
if (threads.length === 0) return `No ${which}comment threads on ${subdomain}.`;
|
|
2753
|
+
const heading = `${threads.length} ${which}comment ${threads.length === 1 ? "thread" : "threads"} on ${subdomain}` + (more ? " (more follow; `--json` shows the cursor)." : ".");
|
|
2754
|
+
const blocks = threads.map((thread) => {
|
|
2755
|
+
const where2 = thread.place.quote ?? (thread.place.snippet || null);
|
|
2756
|
+
const flags = [
|
|
2757
|
+
...thread.resolvedAt === null ? [] : ["resolved"],
|
|
2758
|
+
...thread.outdated ? ["earlier version"] : []
|
|
2759
|
+
];
|
|
2760
|
+
return [
|
|
2761
|
+
`${thread.id} ${thread.path}${where2 === null ? "" : ` "${where2}"`}` + (flags.length === 0 ? "" : ` (${flags.join(", ")})`),
|
|
2762
|
+
...thread.comments.map((comment) => {
|
|
2763
|
+
const who = comment.author.owner ? `${comment.author.name} (owner)` : comment.author.name;
|
|
2764
|
+
return ` ${who}: ${comment.body.replace(/\s*\n\s*/g, " ")}`;
|
|
2765
|
+
}),
|
|
2766
|
+
` ${thread.url}`
|
|
2767
|
+
].join("\n");
|
|
2768
|
+
});
|
|
2769
|
+
return [heading, ...blocks].join("\n\n");
|
|
2770
|
+
}
|
|
2190
2771
|
function day(iso) {
|
|
2191
2772
|
const parsed = new Date(iso);
|
|
2192
2773
|
return Number.isNaN(parsed.getTime()) ? iso : parsed.toISOString().slice(0, 10);
|
|
@@ -2220,8 +2801,16 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
2220
2801
|
drop2run ls List your sites
|
|
2221
2802
|
drop2run folders List your folders, as --folder takes them
|
|
2222
2803
|
drop2run open [site] Open a site in a browser
|
|
2804
|
+
drop2run info [site] Show a site's settings and versions
|
|
2805
|
+
drop2run set <setting> <value> [--site X] Change one setting (see Settings below)
|
|
2806
|
+
drop2run unset name|password|expires Clear a name, a password or a takedown
|
|
2807
|
+
drop2run pause [site] Take a site off the air, keeping its files
|
|
2808
|
+
drop2run resume [site] Put a paused site back on the air
|
|
2223
2809
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
2224
2810
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
2811
|
+
drop2run comments [site] Read the comments left on a site's pages
|
|
2812
|
+
drop2run comments reply <thread> <text> Answer a thread as the owner [--resolve]
|
|
2813
|
+
drop2run comments resolve|reopen <thread> Resolve a thread, or open it again
|
|
2225
2814
|
drop2run token list List your access tokens
|
|
2226
2815
|
drop2run whoami Check the token and whose it is
|
|
2227
2816
|
drop2run where Show which token source is in use
|
|
@@ -2235,6 +2824,33 @@ Flags
|
|
|
2235
2824
|
or a folder id (init and deploy only)
|
|
2236
2825
|
--device Sign in by approving a code on another machine
|
|
2237
2826
|
--yes Confirm a deletion, which cannot be undone
|
|
2827
|
+
--then X What \`set expires\` does when the date arrives: pause
|
|
2828
|
+
(the default, keeps the files) or delete (needs --yes)
|
|
2829
|
+
--status X Which comment threads: open (the default), resolved or all
|
|
2830
|
+
--path X Only the comments on one page, such as /pricing
|
|
2831
|
+
--resolve Resolve the thread with the reply (\`comments reply\` only)
|
|
2832
|
+
|
|
2833
|
+
Comments
|
|
2834
|
+
\`comments reply\`, \`resolve\` and \`reopen\` act on --site, else the site in
|
|
2835
|
+
drop2run.json, and only the account's owner can use them. Deploy the fix
|
|
2836
|
+
before replying that it is fixed. A reply leaves the thread open unless
|
|
2837
|
+
--resolve is given, so the person who raised it can check the fix and
|
|
2838
|
+
resolve it themselves.
|
|
2839
|
+
|
|
2840
|
+
Settings
|
|
2841
|
+
set name <text> What the dashboard calls the site; put a
|
|
2842
|
+
name with words starting "-" after --
|
|
2843
|
+
set mode static|spa|docs static answers 404 for a missing path, spa
|
|
2844
|
+
falls back to index.html, docs uses the viewer
|
|
2845
|
+
set password Typed at a hidden prompt, or piped in:
|
|
2846
|
+
printf %s "$PW" | drop2run set password
|
|
2847
|
+
set forms on|off Whether its forms accept submissions
|
|
2848
|
+
set expires <ISO 8601> Take it down then, e.g. 2026-12-31T09:00:00Z
|
|
2849
|
+
set folder <path|id|root> Move it to a folder, or root for the top level
|
|
2850
|
+
|
|
2851
|
+
\`set\` and \`unset\` act on --site, else the site in drop2run.json. A
|
|
2852
|
+
setting your plan does not include is refused with the reason, and
|
|
2853
|
+
\`info\` says which ones those are.
|
|
2238
2854
|
|
|
2239
2855
|
Where a site comes from
|
|
2240
2856
|
--subdomain creates one under the name you give. Otherwise: --site, then
|
|
@@ -2263,7 +2879,7 @@ function flagValue(args, flag) {
|
|
|
2263
2879
|
const value = args[at + 1];
|
|
2264
2880
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
2265
2881
|
}
|
|
2266
|
-
const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
|
|
2882
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then", "--status", "--path"];
|
|
2267
2883
|
function positionals(argv) {
|
|
2268
2884
|
const found = [];
|
|
2269
2885
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -2287,7 +2903,7 @@ const SUBDOMAIN_FLAG = {
|
|
|
2287
2903
|
const FOLDER_FLAG = {
|
|
2288
2904
|
flag: "--folder",
|
|
2289
2905
|
example: "Clients/Acme",
|
|
2290
|
-
effect: "`--folder` only files a new one — move an existing site
|
|
2906
|
+
effect: "`--folder` only files a new one — move an existing site with `drop2run set folder`",
|
|
2291
2907
|
elsewhere: (command) => command === "ls" || command === "list" ? " `drop2run folders` lists them." : ""
|
|
2292
2908
|
};
|
|
2293
2909
|
function newSiteFlagProblem(argv, command, site, value, described) {
|
|
@@ -2304,12 +2920,15 @@ function newSiteFlagProblem(argv, command, site, value, described) {
|
|
|
2304
2920
|
}
|
|
2305
2921
|
return void 0;
|
|
2306
2922
|
}
|
|
2307
|
-
async function run(
|
|
2923
|
+
async function run(commandLine) {
|
|
2924
|
+
const end = commandLine.indexOf("--");
|
|
2925
|
+
const argv = end === -1 ? commandLine : commandLine.slice(0, end);
|
|
2926
|
+
const literal = end === -1 ? [] : commandLine.slice(end + 1);
|
|
2308
2927
|
const json = argv.includes("--json");
|
|
2309
|
-
const positional = positionals(argv);
|
|
2928
|
+
const positional = [...positionals(argv), ...literal];
|
|
2310
2929
|
const [command, ...rest] = positional;
|
|
2311
2930
|
if (argv.includes("--version")) {
|
|
2312
|
-
const { version } = await import("./package-
|
|
2931
|
+
const { version } = await import("./package-LdzYr7IV.js").then(
|
|
2313
2932
|
(module) => module.default
|
|
2314
2933
|
);
|
|
2315
2934
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -2325,6 +2944,12 @@ async function run(argv) {
|
|
|
2325
2944
|
const failed = { text: refusal, json: { error: refusal }, code: 1 };
|
|
2326
2945
|
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
2327
2946
|
}
|
|
2947
|
+
const then = flagValue(argv, "--then");
|
|
2948
|
+
if (argv.includes("--then") && then === void 0) {
|
|
2949
|
+
const message = "`--then` needs a value: `--then pause` or `--then delete`.";
|
|
2950
|
+
const failed = { text: message, json: { error: message }, code: 1 };
|
|
2951
|
+
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
2952
|
+
}
|
|
2328
2953
|
const result = await dispatch(
|
|
2329
2954
|
command,
|
|
2330
2955
|
rest,
|
|
@@ -2332,11 +2957,17 @@ async function run(argv) {
|
|
|
2332
2957
|
json,
|
|
2333
2958
|
argv.includes("--device"),
|
|
2334
2959
|
argv.includes("--yes"),
|
|
2335
|
-
{ subdomain, folder }
|
|
2960
|
+
{ subdomain, folder },
|
|
2961
|
+
then,
|
|
2962
|
+
{
|
|
2963
|
+
status: flagValue(argv, "--status"),
|
|
2964
|
+
path: flagValue(argv, "--path"),
|
|
2965
|
+
resolve: argv.includes("--resolve")
|
|
2966
|
+
}
|
|
2336
2967
|
);
|
|
2337
2968
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
2338
2969
|
}
|
|
2339
|
-
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
|
|
2970
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0, commentOptions = {}) {
|
|
2340
2971
|
switch (command) {
|
|
2341
2972
|
case "login": {
|
|
2342
2973
|
const print = json ? () => {
|
|
@@ -2362,10 +2993,21 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
|
|
|
2362
2993
|
return await folders();
|
|
2363
2994
|
case "open":
|
|
2364
2995
|
return await open(rest[0] ?? site);
|
|
2996
|
+
case "info":
|
|
2997
|
+
return await info(rest[0] ?? site);
|
|
2998
|
+
case "set":
|
|
2999
|
+
return await set(site, rest[0], rest.slice(1), { then, confirmed });
|
|
3000
|
+
case "unset":
|
|
3001
|
+
return await unset(site, rest[0], rest.slice(1));
|
|
3002
|
+
case "pause":
|
|
3003
|
+
case "resume":
|
|
3004
|
+
return await pauseOrResume(command, rest[0] ?? site);
|
|
2365
3005
|
case "rollback":
|
|
2366
3006
|
return await rollback(rest[0], site);
|
|
2367
3007
|
case "rm":
|
|
2368
3008
|
return await remove(rest[0] ?? site, confirmed);
|
|
3009
|
+
case "comments":
|
|
3010
|
+
return await comments(rest[0], rest.slice(1), site, commentOptions);
|
|
2369
3011
|
case "token":
|
|
2370
3012
|
case "tokens":
|
|
2371
3013
|
return rest[0] === void 0 || rest[0] === "list" ? await tokens() : {
|