drop2run 0.5.0 → 0.6.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
@@ -18,7 +18,9 @@ drop2run logout Remove the stored token
18
18
  drop2run init [dir] [--site <subdomain>] Tie this folder to a site
19
19
  drop2run deploy [dir] [--site <subdomain>] Publish a folder
20
20
  drop2run init|deploy --subdomain <name> Create a new site under a name you pick
21
+ drop2run init|deploy --folder <path> File the new site in one of your folders
21
22
  drop2run ls List your sites
23
+ drop2run folders List your folders, as --folder takes them
22
24
  drop2run open [site] Open a site in a browser
23
25
  drop2run rollback <deployId> [--site X] Put an earlier version back live
24
26
  drop2run rm <site> --yes Delete a site and everything on it
@@ -52,6 +54,17 @@ which is not undoable.
52
54
  `--site` never creates anything: it finds a site you already have, and fails when nothing matches. That
53
55
  is what keeps a subdomain typed one letter wrong from quietly becoming a second site.
54
56
 
57
+ `--folder` files the site being created in one of your dashboard folders — by path, as `drop2run folders`
58
+ prints it, or by folder id:
59
+
60
+ ```bash
61
+ drop2run init dist --folder Clients/Acme
62
+ ```
63
+
64
+ Case does not matter. It never creates a folder and never moves an existing site, so it cannot be given
65
+ with `--site`, or to `deploy` in a project whose `drop2run.json` already names a site; a folder that does
66
+ not exist is refused with the list of those that do, before any site is made.
67
+
55
68
  `deploy` never creates a second site while a `drop2run.json` sits next to it — that would leave the real
56
69
  site untouched and you looking at a URL you did not expect.
57
70
 
package/dist/index.js CHANGED
@@ -5,70 +5,6 @@ import { spawn } from "node:child_process";
5
5
  import { randomBytes, createHash } from "node:crypto";
6
6
  import { createServer } from "node:http";
7
7
  import { realpath, readdir, stat, readFile } from "node:fs/promises";
8
- async function call(credentials, path, init2 = {}) {
9
- const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
10
- ...init2,
11
- headers: {
12
- ...init2.headers,
13
- Authorization: `Bearer ${credentials.token}`,
14
- ...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
15
- }
16
- });
17
- if (!response.ok) {
18
- const problem = await response.json().catch(() => null);
19
- throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
20
- }
21
- return await response.json();
22
- }
23
- async function findSite(credentials, site) {
24
- const wanted = site.trim().toLowerCase();
25
- const found = (await listSites(credentials)).find(
26
- (candidate) => candidate.siteId.toLowerCase() === wanted || candidate.subdomain.toLowerCase() === wanted
27
- );
28
- if (found === void 0) {
29
- throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
30
- }
31
- return found;
32
- }
33
- async function deleteSite(credentials, siteId) {
34
- const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
35
- method: "DELETE",
36
- headers: { Authorization: `Bearer ${credentials.token}` }
37
- });
38
- if (!response.ok) {
39
- const problem = await response.json().catch(() => null);
40
- throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
41
- }
42
- }
43
- async function promoteDeploy(credentials, siteId, deployId) {
44
- return await call(
45
- credentials,
46
- `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
47
- { method: "POST" }
48
- );
49
- }
50
- async function listTokens(credentials) {
51
- const body = await call(credentials, "tokens");
52
- return body.tokens;
53
- }
54
- async function listSites(credentials) {
55
- const body = await call(credentials, "sites");
56
- return body.sites;
57
- }
58
- async function createSite(credentials, subdomain) {
59
- const created = await call(
60
- credentials,
61
- "sites",
62
- {
63
- method: "POST",
64
- // An absent field rather than a null one, because the API reads null, empty and missing the
65
- // same way and a body carrying only what was asked for is the one that reads correctly in a
66
- // log.
67
- body: JSON.stringify(subdomain === void 0 ? {} : { subdomain })
68
- }
69
- );
70
- return { ...created, name: null };
71
- }
72
8
  const TOKEN_VARIABLE = "DROP2RUN_TOKEN";
73
9
  function configPath() {
74
10
  return join(homedir(), ".config", "drop2run", "config.json");
@@ -161,6 +97,142 @@ function missingCredentialsMessage(surface = "cli") {
161
97
  "The token is shown once when it is created and cannot be recovered afterwards."
162
98
  ].join("\n");
163
99
  }
100
+ async function call(credentials, path, init2 = {}) {
101
+ const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
102
+ ...init2,
103
+ headers: {
104
+ ...init2.headers,
105
+ Authorization: `Bearer ${credentials.token}`,
106
+ ...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
107
+ }
108
+ });
109
+ if (!response.ok) {
110
+ const problem = await response.json().catch(() => null);
111
+ throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
112
+ }
113
+ return await response.json();
114
+ }
115
+ async function findSite(credentials, site) {
116
+ const found = await lookUpSite(credentials, site);
117
+ if (found === void 0) {
118
+ throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
119
+ }
120
+ return found;
121
+ }
122
+ const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
123
+ async function lookUpSite(credentials, site) {
124
+ const wanted = site.trim().toLowerCase();
125
+ if (wanted === "") return void 0;
126
+ const { sites } = await call(
127
+ credentials,
128
+ `sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(wanted)}`
129
+ );
130
+ const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
131
+ if (bySubdomain !== void 0) return bySubdomain;
132
+ if (!SITE_ID_SHAPE.test(wanted)) return void 0;
133
+ const response = await fetch(
134
+ `${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
135
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
136
+ );
137
+ if (response.status === 404) return void 0;
138
+ if (!response.ok) {
139
+ const problem = await response.json().catch(() => null);
140
+ throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
141
+ }
142
+ const detail = await response.json();
143
+ return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
144
+ }
145
+ async function deleteSite(credentials, siteId) {
146
+ const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
147
+ method: "DELETE",
148
+ headers: { Authorization: `Bearer ${credentials.token}` }
149
+ });
150
+ if (!response.ok) {
151
+ const problem = await response.json().catch(() => null);
152
+ throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
153
+ }
154
+ }
155
+ async function promoteDeploy(credentials, siteId, deployId) {
156
+ return await call(
157
+ credentials,
158
+ `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
159
+ { method: "POST" }
160
+ );
161
+ }
162
+ async function listTokens(credentials) {
163
+ const body = await call(credentials, "tokens");
164
+ return body.tokens;
165
+ }
166
+ const SITES_PER_REQUEST = 100;
167
+ async function listSites(credentials) {
168
+ const body = await call(
169
+ credentials,
170
+ `sites?pageSize=${SITES_PER_REQUEST}`
171
+ );
172
+ return { sites: body.sites, total: body.total };
173
+ }
174
+ async function createSite(credentials, subdomain, folderId) {
175
+ const created = await call(
176
+ credentials,
177
+ "sites",
178
+ {
179
+ method: "POST",
180
+ // Absent fields rather than null ones, because the API reads null, empty and missing the same
181
+ // way and a body carrying only what was asked for is the one that reads correctly in a log.
182
+ body: JSON.stringify({
183
+ ...subdomain === void 0 ? {} : { subdomain },
184
+ ...folderId === void 0 ? {} : { folderId }
185
+ })
186
+ }
187
+ );
188
+ return { ...created, name: null };
189
+ }
190
+ async function listFolders(credentials) {
191
+ const body = await call(credentials, "site-folders");
192
+ const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
193
+ const pathOf = (folderId) => {
194
+ const names = [];
195
+ let cursor = byId.get(folderId);
196
+ while (cursor !== void 0 && names.length <= byId.size) {
197
+ names.unshift(cursor.name);
198
+ cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
199
+ }
200
+ return names.join("/");
201
+ };
202
+ return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
203
+ }
204
+ function comparablePath(path) {
205
+ return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
206
+ }
207
+ const FOLDERS_LISTED_ON_MISS = 20;
208
+ async function resolveFolder(credentials, folder) {
209
+ const wanted = comparablePath(folder);
210
+ if (wanted === "") {
211
+ throw new Error(
212
+ `"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
213
+ );
214
+ }
215
+ const folders2 = await listFolders(credentials);
216
+ const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
217
+ if (byId !== void 0) return byId.folderId;
218
+ const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
219
+ if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
220
+ if (matches.length > 1) {
221
+ throw new Error(
222
+ `More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
223
+ );
224
+ }
225
+ if (folders2.length === 0) {
226
+ throw new Error(
227
+ `No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
228
+ );
229
+ }
230
+ const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
231
+ const more = folders2.length - listed.length;
232
+ throw new Error(
233
+ `No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
234
+ );
235
+ }
164
236
  const TIMEOUT_MS = 5 * 60 * 1e3;
165
237
  function newAttempt() {
166
238
  const verifier = randomBytes(32).toString("base64url");
@@ -1334,20 +1406,23 @@ function escapesRoot(root, candidate) {
1334
1406
  const inside = relative(root, candidate);
1335
1407
  return inside === "" || inside.startsWith("..");
1336
1408
  }
1337
- async function resolveSite(credentials, site, subdomain) {
1338
- if (subdomain !== void 0) {
1339
- if (site !== void 0) {
1340
- throw new Error(
1341
- "Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
1342
- );
1343
- }
1344
- return await createSite(credentials, subdomain);
1409
+ async function resolveSite(credentials, site, newSite) {
1410
+ const { subdomain, folder } = newSite;
1411
+ if (site !== void 0 && subdomain !== void 0) {
1412
+ throw new Error(
1413
+ "Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
1414
+ );
1345
1415
  }
1346
- if (site === void 0) return await createSite(credentials);
1347
- const wanted = site.trim().toLowerCase();
1348
- const found = (await listSites(credentials)).find(
1349
- (candidate) => candidate.siteId.toLowerCase() === wanted || candidate.subdomain.toLowerCase() === wanted
1350
- );
1416
+ if (site !== void 0 && folder !== void 0) {
1417
+ throw new Error(
1418
+ "A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site from the dashboard."
1419
+ );
1420
+ }
1421
+ if (site === void 0) {
1422
+ const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
1423
+ return await createSite(credentials, subdomain, folderId);
1424
+ }
1425
+ const found = await lookUpSite(credentials, site);
1351
1426
  if (found === void 0) {
1352
1427
  throw new Error(
1353
1428
  `No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
@@ -1355,8 +1430,8 @@ async function resolveSite(credentials, site, subdomain) {
1355
1430
  }
1356
1431
  return found;
1357
1432
  }
1358
- async function publish(credentials, source, site, onProgress, subdomain) {
1359
- const target = await resolveSite(credentials, site, subdomain);
1433
+ async function publish(credentials, source, site, onProgress, newSite = {}) {
1434
+ const target = await resolveSite(credentials, site, newSite);
1360
1435
  let files = 0;
1361
1436
  let url = target.url;
1362
1437
  let unchanged = false;
@@ -1387,11 +1462,11 @@ async function publish(credentials, source, site, onProgress, subdomain) {
1387
1462
  });
1388
1463
  return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
1389
1464
  }
1390
- async function publishDirectory(credentials, directory, site, onProgress, subdomain) {
1465
+ async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
1391
1466
  const found = await stat(directory).catch(() => null);
1392
1467
  if (found === null) throw new Error(`There is no folder at ${directory}.`);
1393
1468
  if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
1394
- return await publish(credentials, directorySource(directory), site, onProgress, subdomain);
1469
+ return await publish(credentials, directorySource(directory), site, onProgress, newSite);
1395
1470
  }
1396
1471
  const silentProgress = Object.assign(() => {
1397
1472
  }, { done: () => {
@@ -1588,33 +1663,67 @@ API: ${found.credentials.apiBaseUrl}`,
1588
1663
  async function list() {
1589
1664
  const found = credentialsOr();
1590
1665
  if ("result" in found) return found.result;
1591
- const sites = await listSites(found.credentials);
1666
+ const { sites, total } = await listSites(found.credentials);
1592
1667
  if (sites.length === 0) {
1593
1668
  return {
1594
1669
  text: "No sites yet. `drop2run deploy` publishes one.",
1595
- json: { sites: [] },
1670
+ json: { sites: [], total: 0 },
1596
1671
  code: 0
1597
1672
  };
1598
1673
  }
1674
+ const lines = sites.map(
1675
+ (site) => `${site.subdomain} ${site.url}${site.name ? ` ${site.name}` : ""}`
1676
+ );
1677
+ if (total > sites.length) {
1678
+ lines.push(
1679
+ `… and ${(total - sites.length).toLocaleString()} more — this lists the newest ${sites.length.toLocaleString()} of ${total.toLocaleString()}.`
1680
+ );
1681
+ }
1599
1682
  return {
1600
- text: sites.map((site) => `${site.subdomain} ${site.url}${site.name ? ` ${site.name}` : ""}`).join("\n"),
1601
- json: { sites },
1683
+ text: lines.join("\n"),
1684
+ json: { sites, total },
1602
1685
  code: 0
1603
1686
  };
1604
1687
  }
1605
- async function deployCommand(directory, site, report = silentProgress, subdomain) {
1688
+ async function folders() {
1689
+ const found = credentialsOr();
1690
+ if ("result" in found) return found.result;
1691
+ try {
1692
+ const all = await listFolders(found.credentials);
1693
+ if (all.length === 0) {
1694
+ return {
1695
+ text: `No folders yet. Make one in the dashboard at ${dashboardUrlFor(found.credentials.apiBaseUrl)}, then file a new site in it with \`--folder\`.`,
1696
+ json: { folders: [] },
1697
+ code: 0
1698
+ };
1699
+ }
1700
+ return {
1701
+ text: all.map((folder) => `${folder.path} ${folder.folderId}`).join("\n"),
1702
+ json: { folders: all },
1703
+ code: 0
1704
+ };
1705
+ } catch (error) {
1706
+ return failure(error instanceof Error ? error.message : String(error));
1707
+ }
1708
+ }
1709
+ async function deployCommand(directory, site, report = silentProgress, newSite = {}) {
1606
1710
  const found = credentialsOr();
1607
1711
  if ("result" in found) return found.result;
1608
1712
  const project = readProject();
1609
1713
  const folder = directory ?? project?.dir ?? ".";
1610
- const target = subdomain === void 0 ? site ?? project?.siteId : void 0;
1714
+ const target = newSite.subdomain === void 0 ? site ?? project?.siteId : void 0;
1715
+ if (newSite.folder !== void 0 && target !== void 0 && project !== null) {
1716
+ return failure(
1717
+ `This folder's ${PROJECT_FILE} already publishes to ${project.subdomain || project.siteId}, and \`--folder\` only files a new site. Move that site from the dashboard, or add \`--subdomain\` to create a new one in the folder.`
1718
+ );
1719
+ }
1611
1720
  try {
1612
1721
  const result = await publishDirectory(
1613
1722
  found.credentials,
1614
1723
  resolve(folder),
1615
1724
  target,
1616
1725
  report,
1617
- subdomain
1726
+ newSite
1618
1727
  );
1619
1728
  const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
1620
1729
  return {
@@ -1629,7 +1738,7 @@ ${result.url}`,
1629
1738
  report.done();
1630
1739
  }
1631
1740
  }
1632
- async function init(directory, site, subdomain) {
1741
+ async function init(directory, site, newSite = {}) {
1633
1742
  const found = credentialsOr();
1634
1743
  if ("result" in found) return found.result;
1635
1744
  if (readProject() !== null) {
@@ -1638,7 +1747,11 @@ async function init(directory, site, subdomain) {
1638
1747
  );
1639
1748
  }
1640
1749
  try {
1641
- const target = site === void 0 ? await createSite(found.credentials, subdomain) : await findSite(found.credentials, site);
1750
+ const target = site === void 0 ? await createSite(
1751
+ found.credentials,
1752
+ newSite.subdomain,
1753
+ newSite.folder === void 0 ? void 0 : await resolveFolder(found.credentials, newSite.folder)
1754
+ ) : await findSite(found.credentials, site);
1642
1755
  const project = {
1643
1756
  siteId: target.siteId,
1644
1757
  subdomain: target.subdomain,
@@ -1777,7 +1890,9 @@ const HELP = `drop2run — publish a static site from the command line
1777
1890
  drop2run init [dir] [--site <subdomain>] Tie this folder to a site (writes drop2run.json)
1778
1891
  drop2run deploy [dir] [--site <subdomain>] Publish a folder (default: . or drop2run.json)
1779
1892
  drop2run init|deploy --subdomain <name> Create a new site under a name you pick
1893
+ drop2run init|deploy --folder <path> File the new site in one of your folders
1780
1894
  drop2run ls List your sites
1895
+ drop2run folders List your folders, as --folder takes them
1781
1896
  drop2run open [site] Open a site in a browser
1782
1897
  drop2run rollback <deployId> [--site X] Put an earlier version back live
1783
1898
  drop2run rm <site> --yes Delete a site and everything on it
@@ -1790,6 +1905,8 @@ Flags
1790
1905
  --json Print machine-readable output instead of text
1791
1906
  --site X Act on an existing site rather than the one in drop2run.json
1792
1907
  --subdomain X Create a new site under this name (init and deploy only)
1908
+ --folder X File the new site in this folder: a path such as Clients/Acme,
1909
+ or a folder id (init and deploy only)
1793
1910
  --device Sign in by approving a code on another machine
1794
1911
  --yes Confirm a deletion, which cannot be undone
1795
1912
 
@@ -1801,6 +1918,10 @@ Where a site comes from
1801
1918
  --site only ever finds a site you already have; it never creates one, so a
1802
1919
  subdomain typed wrong fails instead of quietly becoming a second site.
1803
1920
 
1921
+ --folder only applies to a site being created. It never creates a folder
1922
+ and never moves an existing site; make and rearrange folders in the
1923
+ dashboard.
1924
+
1804
1925
  Signing in
1805
1926
  \`login\` opens a browser and listens on 127.0.0.1, so it needs both on this
1806
1927
  machine. Over SSH, in a container or under WSL, use \`login --device\`: it
@@ -1816,7 +1937,7 @@ function flagValue(args, flag) {
1816
1937
  const value = args[at + 1];
1817
1938
  return value === void 0 || value.startsWith("-") ? void 0 : value;
1818
1939
  }
1819
- const VALUE_FLAGS = ["--site", "--subdomain"];
1940
+ const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
1820
1941
  function positionals(argv) {
1821
1942
  const found = [];
1822
1943
  for (let at = 0; at < argv.length; at++) {
@@ -1831,16 +1952,29 @@ function positionals(argv) {
1831
1952
  }
1832
1953
  return found;
1833
1954
  }
1834
- function subdomainFlagProblem(argv, command, site, subdomain) {
1835
- if (!argv.includes("--subdomain")) return void 0;
1836
- if (subdomain === void 0) {
1837
- return "`--subdomain` needs a name after it, for example `--subdomain my-docs`.";
1955
+ const SUBDOMAIN_FLAG = {
1956
+ flag: "--subdomain",
1957
+ example: "my-docs",
1958
+ effect: "`--subdomain` creates a new one",
1959
+ elsewhere: (command) => ` Use \`--site\` to tell \`${command}\` which of your sites to act on.`
1960
+ };
1961
+ const FOLDER_FLAG = {
1962
+ flag: "--folder",
1963
+ example: "Clients/Acme",
1964
+ effect: "`--folder` only files a new one — move an existing site from the dashboard",
1965
+ elsewhere: (command) => command === "ls" || command === "list" ? " `drop2run folders` lists them." : ""
1966
+ };
1967
+ function newSiteFlagProblem(argv, command, site, value, described) {
1968
+ const { flag, example, effect, elsewhere } = described;
1969
+ if (!argv.includes(flag)) return void 0;
1970
+ if (value === void 0) {
1971
+ return `\`${flag}\` needs a value after it, for example \`${flag} ${example}\`.`;
1838
1972
  }
1839
1973
  if (site !== void 0) {
1840
- return "`--site` and `--subdomain` cannot both be given: `--site` publishes to a site you already have, and `--subdomain` creates a new one. Drop whichever is not what you meant.";
1974
+ return `\`--site\` and \`${flag}\` cannot both be given: \`--site\` publishes to a site you already have, and ${effect}. Drop whichever is not what you meant.`;
1841
1975
  }
1842
1976
  if (command !== "init" && command !== "deploy") {
1843
- return `\`--subdomain\` only applies to \`init\` and \`deploy\`, which can create a site. Use \`--site\` to tell \`${command}\` which of your sites to act on.`;
1977
+ return `\`${flag}\` only applies to \`init\` and \`deploy\`, which can create a site.` + elsewhere(command);
1844
1978
  }
1845
1979
  return void 0;
1846
1980
  }
@@ -1849,7 +1983,7 @@ async function run(argv) {
1849
1983
  const positional = positionals(argv);
1850
1984
  const [command, ...rest] = positional;
1851
1985
  if (argv.includes("--version")) {
1852
- const { version } = await import("./package-DXmy47q4.js").then(
1986
+ const { version } = await import("./package-bzaCMW_E.js").then(
1853
1987
  (module) => module.default
1854
1988
  );
1855
1989
  return { text: version, json: { version }, code: 0 };
@@ -1859,7 +1993,8 @@ async function run(argv) {
1859
1993
  if (command === void 0) return { text: HELP, json: { help: HELP }, code: 1 };
1860
1994
  const site = flagValue(argv, "--site");
1861
1995
  const subdomain = flagValue(argv, "--subdomain");
1862
- const refusal = subdomainFlagProblem(argv, command, site, subdomain);
1996
+ const folder = flagValue(argv, "--folder");
1997
+ const refusal = newSiteFlagProblem(argv, command, site, subdomain, SUBDOMAIN_FLAG) ?? newSiteFlagProblem(argv, command, site, folder, FOLDER_FLAG);
1863
1998
  if (refusal !== void 0) {
1864
1999
  const failed = { text: refusal, json: { error: refusal }, code: 1 };
1865
2000
  return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
@@ -1871,11 +2006,11 @@ async function run(argv) {
1871
2006
  json,
1872
2007
  argv.includes("--device"),
1873
2008
  argv.includes("--yes"),
1874
- subdomain
2009
+ { subdomain, folder }
1875
2010
  );
1876
2011
  return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
1877
2012
  }
1878
- async function dispatch(command, rest, site, json, device, confirmed, subdomain) {
2013
+ async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
1879
2014
  switch (command) {
1880
2015
  case "login": {
1881
2016
  const print = json ? () => {
@@ -1885,18 +2020,20 @@ async function dispatch(command, rest, site, json, device, confirmed, subdomain)
1885
2020
  case "logout":
1886
2021
  return logout();
1887
2022
  case "init":
1888
- return await init(rest[0] ?? ".", site, subdomain);
2023
+ return await init(rest[0] ?? ".", site, newSite);
1889
2024
  case "deploy": {
1890
2025
  const report = json ? silentProgress : progressWriter(
1891
2026
  (text) => process.stderr.write(text),
1892
2027
  process.stderr.isTTY === true,
1893
2028
  process.stderr.columns ?? 80
1894
2029
  );
1895
- return await deployCommand(rest[0], site, report, subdomain);
2030
+ return await deployCommand(rest[0], site, report, newSite);
1896
2031
  }
1897
2032
  case "ls":
1898
2033
  case "list":
1899
2034
  return await list();
2035
+ case "folders":
2036
+ return await folders();
1900
2037
  case "open":
1901
2038
  return await open(rest[0] ?? site);
1902
2039
  case "rollback":
@@ -1,5 +1,5 @@
1
1
  const name = "drop2run";
2
- const version = "0.5.0";
2
+ const version = "0.6.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.5.0",
3
+ "version": "0.6.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",