drop2run 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,11 @@ 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
27
32
  drop2run token list List your access tokens
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(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
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,46 @@ 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
+ const VERSIONS_LISTED = 10;
348
+ function servingModeOf$1(site) {
349
+ if (site.spaMode) return "spa";
350
+ if (site.docsMode) return "docs";
351
+ return "static";
352
+ }
353
+ function accessOf(site) {
354
+ if (site.passwordProtected) return "password";
355
+ if (site.invitedOnly === true) return "invite-only";
356
+ if (site.invitedOnly === false) return "public";
357
+ return "no password (whether it is invite-only could not be read)";
358
+ }
359
+ function describeSite(site, folderPath) {
360
+ const unavailable = (available) => available ? "" : " (not on this plan)";
361
+ const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
362
+ const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
363
+ const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
364
+ return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
365
+ });
366
+ const more = site.deploys.length - versions.length;
367
+ return [
368
+ `${site.subdomain} — ${site.url}`,
369
+ ` name ${site.name ?? "(none)"}`,
370
+ ` status ${site.status}`,
371
+ ` access ${accessOf(site)}`,
372
+ ` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
373
+ ` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
374
+ ` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
375
+ ` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
376
+ ` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
377
+ "",
378
+ site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
379
+ ...versions,
380
+ ...more > 0 ? [` … and ${more} older`] : []
381
+ ].join("\n");
382
+ }
239
383
  const TIMEOUT_MS = 5 * 60 * 1e3;
240
384
  function newAttempt() {
241
385
  const verifier = randomBytes(32).toString("base64url");
@@ -1140,6 +1284,24 @@ function canPublish(paths) {
1140
1284
  if (isImageFolder(paths)) return true;
1141
1285
  return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
1142
1286
  }
1287
+ function servingModeOf(flags) {
1288
+ if (flags.docsMode) return "docs";
1289
+ if (flags.spaMode) return "spa";
1290
+ return "static";
1291
+ }
1292
+ function predictedModeOf(paths) {
1293
+ if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
1294
+ if (paths.some(isDocumentPath)) return "docs";
1295
+ if (isImageFolder(paths)) return "docs";
1296
+ if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
1297
+ return "static";
1298
+ }
1299
+ function nestedIndexOf(paths) {
1300
+ if (predictedModeOf(paths) !== "docs") return null;
1301
+ const suffix = `/${REQUIRED_INDEX}`;
1302
+ const folders2 = paths.filter((path) => path.endsWith(suffix)).map((path) => path.slice(0, -suffix.length)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
1303
+ return folders2[0] ?? null;
1304
+ }
1143
1305
  function renameOfLoneHtmlPage(paths) {
1144
1306
  if (paths.length !== 1) return null;
1145
1307
  const [only] = paths;
@@ -1675,7 +1837,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
1675
1837
  signal,
1676
1838
  await readSiteName(files)
1677
1839
  );
1678
- onProgress({ type: "done", url: completed.url, name: completed.name });
1840
+ onProgress({
1841
+ type: "done",
1842
+ url: completed.url,
1843
+ name: completed.name,
1844
+ mode: servingModeOf(completed),
1845
+ modeIsManual: completed.modeIsManual
1846
+ });
1679
1847
  } catch (error) {
1680
1848
  const failure2 = asDeployError(error);
1681
1849
  onProgress({
@@ -1728,6 +1896,15 @@ function escapesRoot(root, candidate) {
1728
1896
  const inside = relative(root, candidate);
1729
1897
  return inside === "" || inside.startsWith("..");
1730
1898
  }
1899
+ function describeMode(result) {
1900
+ if (result.mode === null) return null;
1901
+ if (result.nestedIndex !== null) {
1902
+ return `Served as documents: there is no index.html at the top, but ${result.nestedIndex}/index.html is there. To publish the website, publish the ${result.nestedIndex} folder instead.`;
1903
+ }
1904
+ if (result.mode === "docs") return "Served as documents, in the documents reader.";
1905
+ if (result.mode === "spa") return "Served as a single-page app.";
1906
+ return "Served as a static site.";
1907
+ }
1731
1908
  async function resolveSite(credentials, site, newSite) {
1732
1909
  const { subdomain, folder } = newSite;
1733
1910
  if (site !== void 0 && subdomain !== void 0) {
@@ -1737,7 +1914,7 @@ async function resolveSite(credentials, site, newSite) {
1737
1914
  }
1738
1915
  if (site !== void 0 && folder !== void 0) {
1739
1916
  throw new Error(
1740
- "A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site from the dashboard."
1917
+ "A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site with `drop2run set folder` or the `update_site` tool's `folder`."
1741
1918
  );
1742
1919
  }
1743
1920
  if (site === void 0) {
@@ -1757,6 +1934,8 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
1757
1934
  let files = 0;
1758
1935
  let url = target.url;
1759
1936
  let unchanged = false;
1937
+ let mode = null;
1938
+ let paths = [];
1760
1939
  let failure2 = null;
1761
1940
  let failureDetail;
1762
1941
  const record = (event) => {
@@ -1765,14 +1944,22 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
1765
1944
  unchanged = true;
1766
1945
  url = event.url || target.url;
1767
1946
  }
1768
- if (event.type === "done") url = event.url;
1947
+ if (event.type === "done") {
1948
+ url = event.url;
1949
+ mode = event.mode;
1950
+ }
1769
1951
  if (event.type === "error") {
1770
1952
  failure2 = event.message;
1771
1953
  failureDetail = event.detail;
1772
1954
  }
1773
1955
  onProgress?.(event);
1774
1956
  };
1775
- await deploy(source, target.siteId, record, void 0, {
1957
+ const watched = async (signal, onRead) => {
1958
+ const collected = await source(signal, onRead);
1959
+ paths = collected.map((file) => file.path);
1960
+ return collected;
1961
+ };
1962
+ await deploy(watched, target.siteId, record, void 0, {
1776
1963
  baseUrl: credentials.apiBaseUrl,
1777
1964
  token: credentials.token
1778
1965
  }).catch((error) => {
@@ -1782,7 +1969,15 @@ async function publish(credentials, source, site, onProgress, newSite = {}) {
1782
1969
  thrown.detail = failureDetail ?? error?.detail;
1783
1970
  throw thrown;
1784
1971
  });
1785
- return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
1972
+ return {
1973
+ url,
1974
+ siteId: target.siteId,
1975
+ subdomain: target.subdomain,
1976
+ files,
1977
+ unchanged,
1978
+ mode,
1979
+ nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
1980
+ };
1786
1981
  }
1787
1982
  async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
1788
1983
  const found = await stat(directory).catch(() => null);
@@ -1861,6 +2056,82 @@ function progressWriter(write, isTerminal, width = 80) {
1861
2056
  };
1862
2057
  return writer;
1863
2058
  }
2059
+ async function readSecret(prompt, input = process.stdin, output = process.stderr) {
2060
+ if (input.isTTY !== true || input.setRawMode === void 0) {
2061
+ const parts = [];
2062
+ for await (const chunk of input)
2063
+ parts.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
2064
+ return parts.join("").replace(/\r?\n$/, "");
2065
+ }
2066
+ const setRawMode = input.setRawMode.bind(input);
2067
+ output.write(prompt);
2068
+ setRawMode(true);
2069
+ input.setEncoding("utf8");
2070
+ input.resume();
2071
+ return await new Promise((resolve2, reject) => {
2072
+ const line = new HiddenLine();
2073
+ const finish = () => {
2074
+ setRawMode(false);
2075
+ input.pause();
2076
+ input.off("data", onData);
2077
+ output.write("\n");
2078
+ };
2079
+ function onData(chunk) {
2080
+ const outcome = line.type(chunk);
2081
+ if (outcome === "more") return;
2082
+ finish();
2083
+ if (outcome === "cancelled") reject(new Error("Cancelled."));
2084
+ else resolve2(line.value);
2085
+ }
2086
+ input.on("data", onData);
2087
+ });
2088
+ }
2089
+ class HiddenLine {
2090
+ /** What has been typed, one entry per code point, so Backspace never splits a surrogate pair. */
2091
+ characters = [];
2092
+ /** Where an escape sequence has got to: none, just after ESC, or inside a `CSI`/`SS3` sequence. */
2093
+ escape = "none";
2094
+ /** The line so far. */
2095
+ get value() {
2096
+ return this.characters.join("");
2097
+ }
2098
+ /**
2099
+ * Takes the characters one chunk of input carries.
2100
+ *
2101
+ * @param chunk What arrived.
2102
+ * @returns `done` on Enter or Ctrl+D, `cancelled` on Ctrl+C, `more` otherwise.
2103
+ */
2104
+ type(chunk) {
2105
+ for (const character of chunk) {
2106
+ const code = character.codePointAt(0) ?? 0;
2107
+ if (this.escape === "started") {
2108
+ this.escape = character === "[" || character === "O" ? "sequence" : "none";
2109
+ continue;
2110
+ }
2111
+ if (this.escape === "sequence") {
2112
+ if (code >= 64 && code <= 126) this.escape = "none";
2113
+ continue;
2114
+ }
2115
+ if (character === "\r" || character === "\n" || character === "") return "done";
2116
+ if (character === "") return "cancelled";
2117
+ if (character === "\x1B") {
2118
+ this.escape = "started";
2119
+ continue;
2120
+ }
2121
+ if (character === "" || character === "\b") {
2122
+ this.characters.pop();
2123
+ continue;
2124
+ }
2125
+ if (character === "") {
2126
+ this.characters.length = 0;
2127
+ continue;
2128
+ }
2129
+ if (code < 32) continue;
2130
+ this.characters.push(character);
2131
+ }
2132
+ return "more";
2133
+ }
2134
+ }
1864
2135
  function failure(text) {
1865
2136
  return { text, json: { error: text }, code: 1 };
1866
2137
  }
@@ -1972,9 +2243,9 @@ async function whoami() {
1972
2243
  const response = await fetch(`${found.credentials.apiBaseUrl}/me`, {
1973
2244
  headers: { Authorization: `Bearer ${found.credentials.token}` }
1974
2245
  });
1975
- if (!response.ok) {
2246
+ if (!response.ok || response.status === 204) {
1976
2247
  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}.`
2248
+ response.status === 401 || response.status === 204 ? "This token is not valid any more. It may have been revoked or expired — create another at https://app.dropto.run/account/tokens." : `The API answered ${response.status}.`
1978
2249
  );
1979
2250
  }
1980
2251
  const me = await response.json();
@@ -2040,7 +2311,7 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
2040
2311
  const target = newSite.subdomain === void 0 ? site ?? project?.siteId : void 0;
2041
2312
  if (newSite.folder !== void 0 && target !== void 0 && project !== null) {
2042
2313
  return failure(
2043
- `This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site from the dashboard, or add \`--subdomain\` to create a new one in the folder.`
2314
+ `This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site with \`drop2run set folder <path>\`, or add \`--subdomain\` to create a new one in the folder.`
2044
2315
  );
2045
2316
  }
2046
2317
  try {
@@ -2052,9 +2323,11 @@ async function deployCommand(directory, site, report = silentProgress, newSite =
2052
2323
  newSite
2053
2324
  );
2054
2325
  const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
2326
+ const mode = describeMode(result);
2055
2327
  return {
2056
2328
  text: `${what}
2057
- ${result.url}`,
2329
+ ${result.url}${mode === null ? "" : `
2330
+ ${mode}`}`,
2058
2331
  json: result,
2059
2332
  code: 0
2060
2333
  };
@@ -2169,6 +2442,197 @@ async function open(site, launch = openBrowser) {
2169
2442
  return failure(error instanceof Error ? error.message : String(error));
2170
2443
  }
2171
2444
  }
2445
+ function siteOr(site, usage) {
2446
+ const named = site ?? readProject()?.siteId;
2447
+ return named === void 0 ? {
2448
+ result: failure(
2449
+ `Name the site: \`${usage}\`, or run this in a folder with a ${PROJECT_FILE}.`
2450
+ )
2451
+ } : { named };
2452
+ }
2453
+ async function info(site) {
2454
+ const found = credentialsOr();
2455
+ if ("result" in found) return found.result;
2456
+ const target = siteOr(site, "drop2run info <subdomain>");
2457
+ if ("result" in target) return target.result;
2458
+ try {
2459
+ const summary = await findSite(found.credentials, target.named);
2460
+ const detail = await getSite(found.credentials, summary.siteId);
2461
+ const folderPath = detail.folderId === null ? void 0 : (await listFolders(found.credentials)).find(
2462
+ (folder) => folder.folderId === detail.folderId
2463
+ )?.path;
2464
+ return {
2465
+ text: describeSite(detail, folderPath),
2466
+ json: { ...detail, mode: servingModeOf$1(detail), folderPath: folderPath ?? null },
2467
+ code: 0
2468
+ };
2469
+ } catch (error) {
2470
+ return failure(error instanceof Error ? error.message : String(error));
2471
+ }
2472
+ }
2473
+ async function pauseOrResume(action, site) {
2474
+ const found = credentialsOr();
2475
+ if ("result" in found) return found.result;
2476
+ const target = siteOr(site, `drop2run ${action} <subdomain>`);
2477
+ if ("result" in target) return target.result;
2478
+ try {
2479
+ const summary = await findSite(found.credentials, target.named);
2480
+ const state = await setSitePaused(found.credentials, summary.siteId, action);
2481
+ return {
2482
+ text: action === "resume" ? `${state.subdomain} is back on the air.
2483
+ ${summary.url}` : state.status === "paused" ? `Paused ${state.subdomain}. Its files and subdomain are kept; \`drop2run resume\` puts it back.` : (
2484
+ // A site Drop2Run suspended stays suspended: the pause is recorded for when that lifts,
2485
+ // and promising `resume` here would be promising something the API refuses.
2486
+ `${state.subdomain} is ${state.status}, not paused by you, so \`drop2run resume\` cannot bring it back. The pause is recorded and applies if that is lifted.`
2487
+ ),
2488
+ json: state,
2489
+ code: 0
2490
+ };
2491
+ } catch (error) {
2492
+ return failure(error instanceof Error ? error.message : String(error));
2493
+ }
2494
+ }
2495
+ const SETTINGS_USAGE = [
2496
+ " name <text> What the dashboard calls the site",
2497
+ " mode static|spa|docs How it serves files",
2498
+ " password Read from stdin, or typed at a hidden prompt",
2499
+ " forms on|off Whether its forms accept submissions",
2500
+ " expires <ISO 8601 instant> When it comes down; add --then delete to delete rather than pause",
2501
+ " folder <path|id|root> Which folder it is filed in"
2502
+ ].join("\n");
2503
+ function settingChange(setting, values, then) {
2504
+ const value = setting === "name" ? values.join(" ").trim() : values[0];
2505
+ const needs = (example) => ({
2506
+ refusal: `\`set ${setting}\` needs a value, for example \`drop2run set ${setting} ${example}\`.`
2507
+ });
2508
+ if (then !== void 0 && setting !== "expires") {
2509
+ return { refusal: "`--then` only applies to `set expires`." };
2510
+ }
2511
+ if (setting !== "name" && setting !== "password" && values.length > 1) {
2512
+ return {
2513
+ refusal: `\`set ${setting}\` takes one value, and "${values.slice(1).join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
2514
+ };
2515
+ }
2516
+ switch (setting) {
2517
+ case "name":
2518
+ return value ? { change: { name: value } } : needs('"Launch notes"');
2519
+ case "mode":
2520
+ if (value === "static" || value === "spa" || value === "docs") {
2521
+ return { change: { mode: value } };
2522
+ }
2523
+ return { refusal: "`set mode` takes `static`, `spa` or `docs`." };
2524
+ case "forms":
2525
+ if (value === "on" || value === "off") return { change: { formsEnabled: value === "on" } };
2526
+ return { refusal: "`set forms` takes `on` or `off`." };
2527
+ case "password": {
2528
+ if (value !== void 0) {
2529
+ return {
2530
+ refusal: '`set password` does not take the password as an argument, where it would stay in your shell history. Run `drop2run set password` and type it, or pipe it in: `printf %s "$SITE_PASSWORD" | drop2run set password`.'
2531
+ };
2532
+ }
2533
+ return { password: true };
2534
+ }
2535
+ case "expires": {
2536
+ if (value === void 0) return needs("2026-12-31T09:00:00Z");
2537
+ if (then !== void 0 && then !== "pause" && then !== "delete") {
2538
+ return { refusal: "`--then` takes `pause` or `delete`." };
2539
+ }
2540
+ return {
2541
+ change: {
2542
+ expiresAt: value,
2543
+ ...then === void 0 ? {} : { expiryAction: then }
2544
+ }
2545
+ };
2546
+ }
2547
+ case "folder":
2548
+ return value ? { folder: value } : needs("Clients/Acme");
2549
+ default:
2550
+ return {
2551
+ refusal: `${setting === void 0 ? "Name a setting." : `\`${setting}\` is not a setting.`} Settings:
2552
+ ${SETTINGS_USAGE}`
2553
+ };
2554
+ }
2555
+ }
2556
+ async function set(site, setting, values, options, readPassword = () => readSecret("Password for visitors: ")) {
2557
+ const parsed = settingChange(setting, values, options.then);
2558
+ if ("refusal" in parsed) return failure(parsed.refusal);
2559
+ const found = credentialsOr();
2560
+ if ("result" in found) return found.result;
2561
+ const target = siteOr(site, `drop2run set ${setting} … --site <subdomain>`);
2562
+ if ("result" in target) return target.result;
2563
+ try {
2564
+ const summary = await findSite(found.credentials, target.named);
2565
+ let change;
2566
+ if ("folder" in parsed) {
2567
+ change = { folderId: await resolveFolderTarget(found.credentials, parsed.folder) };
2568
+ } else if ("password" in parsed) {
2569
+ const password = await readPassword();
2570
+ if (password === "") {
2571
+ return failure("No password was given. To remove one, run `drop2run unset password`.");
2572
+ }
2573
+ change = { password };
2574
+ } else {
2575
+ change = parsed.change;
2576
+ }
2577
+ if (change.expiryAction === "delete" && !options.confirmed) {
2578
+ return failure(
2579
+ `This would delete ${summary.subdomain} and every version published to it when ${change.expiresAt} arrives, with no way back then.
2580
+ Add --yes if that is what you want, or leave out --then to pause it instead.`
2581
+ );
2582
+ }
2583
+ return await applied(found.credentials, summary.siteId, summary.subdomain, change);
2584
+ } catch (error) {
2585
+ return failure(error instanceof Error ? error.message : String(error));
2586
+ }
2587
+ }
2588
+ async function unset(site, setting, extra = []) {
2589
+ if (extra.length > 0) {
2590
+ return failure(
2591
+ `\`unset ${setting}\` takes nothing after it, and "${extra.join(" ")}" is extra. Name the site with \`--site <subdomain>\`.`
2592
+ );
2593
+ }
2594
+ const clear = /* @__PURE__ */ new Map([
2595
+ ["name", { name: "" }],
2596
+ ["password", { password: "" }],
2597
+ ["expires", { expiresAt: "" }]
2598
+ ]);
2599
+ const change = setting === void 0 ? void 0 : clear.get(setting);
2600
+ if (change === void 0) {
2601
+ return failure(
2602
+ "`unset` clears `name`, `password` or `expires`. To file the site at the top level, run `drop2run set folder root`."
2603
+ );
2604
+ }
2605
+ const found = credentialsOr();
2606
+ if ("result" in found) return found.result;
2607
+ const target = siteOr(site, `drop2run unset ${setting} --site <subdomain>`);
2608
+ if ("result" in target) return target.result;
2609
+ try {
2610
+ const summary = await findSite(found.credentials, target.named);
2611
+ return await applied(found.credentials, summary.siteId, summary.subdomain, change);
2612
+ } catch (error) {
2613
+ return failure(error instanceof Error ? error.message : String(error));
2614
+ }
2615
+ }
2616
+ async function applied(credentials, siteId, subdomain, change) {
2617
+ const settings = await updateSiteSettings(credentials, siteId, change);
2618
+ const takedown = settings.expiresAt === null ? "none" : `${settings.expiresAt} (${settings.expiryAction})`;
2619
+ return {
2620
+ text: [
2621
+ `Updated ${subdomain}.`,
2622
+ ` name ${settings.name ?? "(none)"}`,
2623
+ ` access ${accessOf(settings)}`,
2624
+ ` mode ${servingModeOf$1(settings)}`,
2625
+ ` forms ${settings.formsEnabled ? "on" : "off"}`,
2626
+ ` takedown ${takedown}`,
2627
+ ...settings.replacedInviteOnly ? [
2628
+ "The password replaced invite-only: the people on its list can no longer get in without it. The list is kept for if you switch back in the dashboard."
2629
+ ] : [],
2630
+ settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
2631
+ ].join("\n"),
2632
+ json: { ...settings, mode: servingModeOf$1(settings) },
2633
+ code: 0
2634
+ };
2635
+ }
2172
2636
  async function tokens() {
2173
2637
  const found = credentialsOr();
2174
2638
  if ("result" in found) return found.result;
@@ -2220,6 +2684,11 @@ const HELP = `drop2run — publish a static site from the command line
2220
2684
  drop2run ls List your sites
2221
2685
  drop2run folders List your folders, as --folder takes them
2222
2686
  drop2run open [site] Open a site in a browser
2687
+ drop2run info [site] Show a site's settings and versions
2688
+ drop2run set <setting> <value> [--site X] Change one setting (see Settings below)
2689
+ drop2run unset name|password|expires Clear a name, a password or a takedown
2690
+ drop2run pause [site] Take a site off the air, keeping its files
2691
+ drop2run resume [site] Put a paused site back on the air
2223
2692
  drop2run rollback <deployId> [--site X] Put an earlier version back live
2224
2693
  drop2run rm <site> --yes Delete a site and everything on it
2225
2694
  drop2run token list List your access tokens
@@ -2235,6 +2704,23 @@ Flags
2235
2704
  or a folder id (init and deploy only)
2236
2705
  --device Sign in by approving a code on another machine
2237
2706
  --yes Confirm a deletion, which cannot be undone
2707
+ --then X What \`set expires\` does when the date arrives: pause
2708
+ (the default, keeps the files) or delete (needs --yes)
2709
+
2710
+ Settings
2711
+ set name <text> What the dashboard calls the site; put a
2712
+ name with words starting "-" after --
2713
+ set mode static|spa|docs static answers 404 for a missing path, spa
2714
+ falls back to index.html, docs uses the viewer
2715
+ set password Typed at a hidden prompt, or piped in:
2716
+ printf %s "$PW" | drop2run set password
2717
+ set forms on|off Whether its forms accept submissions
2718
+ set expires <ISO 8601> Take it down then, e.g. 2026-12-31T09:00:00Z
2719
+ set folder <path|id|root> Move it to a folder, or root for the top level
2720
+
2721
+ \`set\` and \`unset\` act on --site, else the site in drop2run.json. A
2722
+ setting your plan does not include is refused with the reason, and
2723
+ \`info\` says which ones those are.
2238
2724
 
2239
2725
  Where a site comes from
2240
2726
  --subdomain creates one under the name you give. Otherwise: --site, then
@@ -2263,7 +2749,7 @@ function flagValue(args, flag) {
2263
2749
  const value = args[at + 1];
2264
2750
  return value === void 0 || value.startsWith("-") ? void 0 : value;
2265
2751
  }
2266
- const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
2752
+ const VALUE_FLAGS = ["--site", "--subdomain", "--folder", "--then"];
2267
2753
  function positionals(argv) {
2268
2754
  const found = [];
2269
2755
  for (let at = 0; at < argv.length; at++) {
@@ -2287,7 +2773,7 @@ const SUBDOMAIN_FLAG = {
2287
2773
  const FOLDER_FLAG = {
2288
2774
  flag: "--folder",
2289
2775
  example: "Clients/Acme",
2290
- effect: "`--folder` only files a new one — move an existing site from the dashboard",
2776
+ effect: "`--folder` only files a new one — move an existing site with `drop2run set folder`",
2291
2777
  elsewhere: (command) => command === "ls" || command === "list" ? " `drop2run folders` lists them." : ""
2292
2778
  };
2293
2779
  function newSiteFlagProblem(argv, command, site, value, described) {
@@ -2304,12 +2790,15 @@ function newSiteFlagProblem(argv, command, site, value, described) {
2304
2790
  }
2305
2791
  return void 0;
2306
2792
  }
2307
- async function run(argv) {
2793
+ async function run(commandLine) {
2794
+ const end = commandLine.indexOf("--");
2795
+ const argv = end === -1 ? commandLine : commandLine.slice(0, end);
2796
+ const literal = end === -1 ? [] : commandLine.slice(end + 1);
2308
2797
  const json = argv.includes("--json");
2309
- const positional = positionals(argv);
2798
+ const positional = [...positionals(argv), ...literal];
2310
2799
  const [command, ...rest] = positional;
2311
2800
  if (argv.includes("--version")) {
2312
- const { version } = await import("./package-D8mWS-nC.js").then(
2801
+ const { version } = await import("./package-BycuI1VJ.js").then(
2313
2802
  (module) => module.default
2314
2803
  );
2315
2804
  return { text: version, json: { version }, code: 0 };
@@ -2325,6 +2814,12 @@ async function run(argv) {
2325
2814
  const failed = { text: refusal, json: { error: refusal }, code: 1 };
2326
2815
  return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
2327
2816
  }
2817
+ const then = flagValue(argv, "--then");
2818
+ if (argv.includes("--then") && then === void 0) {
2819
+ const message = "`--then` needs a value: `--then pause` or `--then delete`.";
2820
+ const failed = { text: message, json: { error: message }, code: 1 };
2821
+ return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
2822
+ }
2328
2823
  const result = await dispatch(
2329
2824
  command,
2330
2825
  rest,
@@ -2332,11 +2827,12 @@ async function run(argv) {
2332
2827
  json,
2333
2828
  argv.includes("--device"),
2334
2829
  argv.includes("--yes"),
2335
- { subdomain, folder }
2830
+ { subdomain, folder },
2831
+ then
2336
2832
  );
2337
2833
  return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
2338
2834
  }
2339
- async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
2835
+ async function dispatch(command, rest, site, json, device, confirmed, newSite = {}, then = void 0) {
2340
2836
  switch (command) {
2341
2837
  case "login": {
2342
2838
  const print = json ? () => {
@@ -2362,6 +2858,15 @@ async function dispatch(command, rest, site, json, device, confirmed, newSite =
2362
2858
  return await folders();
2363
2859
  case "open":
2364
2860
  return await open(rest[0] ?? site);
2861
+ case "info":
2862
+ return await info(rest[0] ?? site);
2863
+ case "set":
2864
+ return await set(site, rest[0], rest.slice(1), { then, confirmed });
2865
+ case "unset":
2866
+ return await unset(site, rest[0], rest.slice(1));
2867
+ case "pause":
2868
+ case "resume":
2869
+ return await pauseOrResume(command, rest[0] ?? site);
2365
2870
  case "rollback":
2366
2871
  return await rollback(rest[0], site);
2367
2872
  case "rm":
@@ -1,5 +1,5 @@
1
1
  const name = "drop2run";
2
- const version = "0.7.0";
2
+ const version = "0.8.0";
3
3
  const description = "Publish a page, a folder of documents or a built site to Drop2Run from the command line.";
4
4
  const license = "MIT";
5
5
  const type = "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drop2run",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Publish a page, a folder of documents or a built site to Drop2Run from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",