drop2run 0.4.1 → 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 +15 -1
- package/dist/index.js +747 -149
- package/dist/{package-sLisZctX.js → package-bzaCMW_E.js} +3 -3
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# drop2run
|
|
2
2
|
|
|
3
|
-
Publish a
|
|
3
|
+
Publish a page, a folder of documents or a built site to [Drop2Run](https://dropto.run) from the
|
|
4
|
+
command line — by hand, from CI, or from a coding agent.
|
|
4
5
|
|
|
5
6
|
```bash
|
|
6
7
|
npm i -g drop2run
|
|
@@ -17,7 +18,9 @@ drop2run logout Remove the stored token
|
|
|
17
18
|
drop2run init [dir] [--site <subdomain>] Tie this folder to a site
|
|
18
19
|
drop2run deploy [dir] [--site <subdomain>] Publish a folder
|
|
19
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
|
|
20
22
|
drop2run ls List your sites
|
|
23
|
+
drop2run folders List your folders, as --folder takes them
|
|
21
24
|
drop2run open [site] Open a site in a browser
|
|
22
25
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
23
26
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
@@ -51,6 +54,17 @@ which is not undoable.
|
|
|
51
54
|
`--site` never creates anything: it finds a site you already have, and fails when nothing matches. That
|
|
52
55
|
is what keeps a subdomain typed one letter wrong from quietly becoming a second site.
|
|
53
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
|
+
|
|
54
68
|
`deploy` never creates a second site while a `drop2run.json` sits next to it — that would leave the real
|
|
55
69
|
site untouched and you looking at a URL you did not expect.
|
|
56
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");
|
|
@@ -439,6 +511,485 @@ async function problemToError(response) {
|
|
|
439
511
|
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
440
512
|
return new DeployError(code, message, problem);
|
|
441
513
|
}
|
|
514
|
+
const FILE_TYPES = [
|
|
515
|
+
// Documents. The three extensions that make a folder a documents site.
|
|
516
|
+
{
|
|
517
|
+
ext: ".md",
|
|
518
|
+
mime: "text/markdown; charset=utf-8",
|
|
519
|
+
kind: "markdown",
|
|
520
|
+
document: true,
|
|
521
|
+
reader: true,
|
|
522
|
+
editable: true
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
ext: ".markdown",
|
|
526
|
+
mime: "text/markdown; charset=utf-8",
|
|
527
|
+
kind: "markdown",
|
|
528
|
+
document: true,
|
|
529
|
+
reader: true,
|
|
530
|
+
editable: true
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
ext: ".pdf",
|
|
534
|
+
mime: "application/pdf",
|
|
535
|
+
kind: "pdf",
|
|
536
|
+
document: true,
|
|
537
|
+
reader: true,
|
|
538
|
+
editable: false
|
|
539
|
+
},
|
|
540
|
+
// Office documents and ebooks. Each has its own kind because each needs its own renderer, loaded
|
|
541
|
+
// only when one is opened — `.docx` through mammoth, `.xlsx` through read-excel-file, `.epub`
|
|
542
|
+
// through foliate-js.
|
|
543
|
+
{
|
|
544
|
+
ext: ".docx",
|
|
545
|
+
mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
546
|
+
kind: "docx",
|
|
547
|
+
document: true,
|
|
548
|
+
reader: true,
|
|
549
|
+
editable: false
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
ext: ".xlsx",
|
|
553
|
+
mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
554
|
+
kind: "xlsx",
|
|
555
|
+
document: true,
|
|
556
|
+
reader: true,
|
|
557
|
+
editable: false
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
ext: ".epub",
|
|
561
|
+
mime: "application/epub+zip",
|
|
562
|
+
kind: "epub",
|
|
563
|
+
document: true,
|
|
564
|
+
reader: true,
|
|
565
|
+
editable: false
|
|
566
|
+
},
|
|
567
|
+
// Prose the reader shows preformatted.
|
|
568
|
+
{
|
|
569
|
+
ext: ".txt",
|
|
570
|
+
mime: "text/plain; charset=utf-8",
|
|
571
|
+
kind: "text",
|
|
572
|
+
document: false,
|
|
573
|
+
reader: true,
|
|
574
|
+
editable: true
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
ext: ".log",
|
|
578
|
+
mime: "text/plain; charset=utf-8",
|
|
579
|
+
kind: "text",
|
|
580
|
+
document: false,
|
|
581
|
+
reader: true,
|
|
582
|
+
editable: false
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
ext: ".csv",
|
|
586
|
+
mime: "text/csv; charset=utf-8",
|
|
587
|
+
kind: "text",
|
|
588
|
+
document: false,
|
|
589
|
+
reader: true,
|
|
590
|
+
editable: true
|
|
591
|
+
},
|
|
592
|
+
// Structured text. Read through the reader inside a folder of notes, which is why these carry
|
|
593
|
+
// `reader: true` while the source formats below do not.
|
|
594
|
+
{
|
|
595
|
+
ext: ".json",
|
|
596
|
+
mime: "application/json",
|
|
597
|
+
kind: "code",
|
|
598
|
+
document: false,
|
|
599
|
+
reader: true,
|
|
600
|
+
editable: true
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
ext: ".yaml",
|
|
604
|
+
mime: "text/yaml; charset=utf-8",
|
|
605
|
+
kind: "code",
|
|
606
|
+
document: false,
|
|
607
|
+
reader: true,
|
|
608
|
+
editable: true
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
ext: ".yml",
|
|
612
|
+
mime: "text/yaml; charset=utf-8",
|
|
613
|
+
kind: "code",
|
|
614
|
+
document: false,
|
|
615
|
+
reader: true,
|
|
616
|
+
editable: true
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
// No registered media type, so it takes text/plain rather than an invented one. The point is
|
|
620
|
+
// only that a browser shows it instead of downloading it.
|
|
621
|
+
ext: ".toml",
|
|
622
|
+
mime: "text/plain; charset=utf-8",
|
|
623
|
+
kind: "code",
|
|
624
|
+
document: false,
|
|
625
|
+
reader: true,
|
|
626
|
+
editable: true
|
|
627
|
+
},
|
|
628
|
+
// Source the viewer highlights. A link straight to one of these hands over the file.
|
|
629
|
+
{
|
|
630
|
+
ext: ".css",
|
|
631
|
+
mime: "text/css; charset=utf-8",
|
|
632
|
+
kind: "code",
|
|
633
|
+
document: false,
|
|
634
|
+
reader: false,
|
|
635
|
+
editable: true
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
ext: ".js",
|
|
639
|
+
mime: "text/javascript; charset=utf-8",
|
|
640
|
+
kind: "code",
|
|
641
|
+
document: false,
|
|
642
|
+
reader: false,
|
|
643
|
+
editable: true
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
ext: ".ts",
|
|
647
|
+
mime: null,
|
|
648
|
+
kind: "code",
|
|
649
|
+
document: false,
|
|
650
|
+
reader: false,
|
|
651
|
+
editable: false
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
ext: ".tsx",
|
|
655
|
+
mime: null,
|
|
656
|
+
kind: "code",
|
|
657
|
+
document: false,
|
|
658
|
+
reader: false,
|
|
659
|
+
editable: false
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
ext: ".jsx",
|
|
663
|
+
mime: null,
|
|
664
|
+
kind: "code",
|
|
665
|
+
document: false,
|
|
666
|
+
reader: false,
|
|
667
|
+
editable: false
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
ext: ".sh",
|
|
671
|
+
mime: null,
|
|
672
|
+
kind: "code",
|
|
673
|
+
document: false,
|
|
674
|
+
reader: false,
|
|
675
|
+
editable: false
|
|
676
|
+
},
|
|
677
|
+
// The languages an assistant writes most, beyond the web's own. Same row as `.ts`: shown as source
|
|
678
|
+
// when somebody opens one in the reader or publishes it on its own, handed over as a file when a
|
|
679
|
+
// link names it directly, and no media type — adding one changes what a browser does with a
|
|
680
|
+
// published file, which I10 makes a decision of its own rather than a side effect of this list.
|
|
681
|
+
{
|
|
682
|
+
ext: ".py",
|
|
683
|
+
mime: null,
|
|
684
|
+
kind: "code",
|
|
685
|
+
document: false,
|
|
686
|
+
reader: false,
|
|
687
|
+
editable: false
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
ext: ".sql",
|
|
691
|
+
mime: null,
|
|
692
|
+
kind: "code",
|
|
693
|
+
document: false,
|
|
694
|
+
reader: false,
|
|
695
|
+
editable: false
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
ext: ".go",
|
|
699
|
+
mime: null,
|
|
700
|
+
kind: "code",
|
|
701
|
+
document: false,
|
|
702
|
+
reader: false,
|
|
703
|
+
editable: false
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
ext: ".rs",
|
|
707
|
+
mime: null,
|
|
708
|
+
kind: "code",
|
|
709
|
+
document: false,
|
|
710
|
+
reader: false,
|
|
711
|
+
editable: false
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
ext: ".java",
|
|
715
|
+
mime: null,
|
|
716
|
+
kind: "code",
|
|
717
|
+
document: false,
|
|
718
|
+
reader: false,
|
|
719
|
+
editable: false
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
ext: ".kt",
|
|
723
|
+
mime: null,
|
|
724
|
+
kind: "code",
|
|
725
|
+
document: false,
|
|
726
|
+
reader: false,
|
|
727
|
+
editable: false
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
ext: ".swift",
|
|
731
|
+
mime: null,
|
|
732
|
+
kind: "code",
|
|
733
|
+
document: false,
|
|
734
|
+
reader: false,
|
|
735
|
+
editable: false
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
ext: ".rb",
|
|
739
|
+
mime: null,
|
|
740
|
+
kind: "code",
|
|
741
|
+
document: false,
|
|
742
|
+
reader: false,
|
|
743
|
+
editable: false
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
ext: ".php",
|
|
747
|
+
mime: null,
|
|
748
|
+
kind: "code",
|
|
749
|
+
document: false,
|
|
750
|
+
reader: false,
|
|
751
|
+
editable: false
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
ext: ".c",
|
|
755
|
+
mime: null,
|
|
756
|
+
kind: "code",
|
|
757
|
+
document: false,
|
|
758
|
+
reader: false,
|
|
759
|
+
editable: false
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
ext: ".h",
|
|
763
|
+
mime: null,
|
|
764
|
+
kind: "code",
|
|
765
|
+
document: false,
|
|
766
|
+
reader: false,
|
|
767
|
+
editable: false
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
ext: ".cpp",
|
|
771
|
+
mime: null,
|
|
772
|
+
kind: "code",
|
|
773
|
+
document: false,
|
|
774
|
+
reader: false,
|
|
775
|
+
editable: false
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
ext: ".cs",
|
|
779
|
+
mime: null,
|
|
780
|
+
kind: "code",
|
|
781
|
+
document: false,
|
|
782
|
+
reader: false,
|
|
783
|
+
editable: false
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
ext: ".r",
|
|
787
|
+
mime: null,
|
|
788
|
+
kind: "code",
|
|
789
|
+
document: false,
|
|
790
|
+
reader: false,
|
|
791
|
+
editable: false
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
ext: ".xml",
|
|
795
|
+
mime: "application/xml",
|
|
796
|
+
kind: "code",
|
|
797
|
+
document: false,
|
|
798
|
+
reader: false,
|
|
799
|
+
editable: true
|
|
800
|
+
},
|
|
801
|
+
// Pictures, shown with an img element.
|
|
802
|
+
{
|
|
803
|
+
ext: ".png",
|
|
804
|
+
mime: "image/png",
|
|
805
|
+
kind: "image",
|
|
806
|
+
document: false,
|
|
807
|
+
reader: false,
|
|
808
|
+
editable: false
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
ext: ".jpg",
|
|
812
|
+
mime: "image/jpeg",
|
|
813
|
+
kind: "image",
|
|
814
|
+
document: false,
|
|
815
|
+
reader: false,
|
|
816
|
+
editable: false
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
ext: ".jpeg",
|
|
820
|
+
mime: "image/jpeg",
|
|
821
|
+
kind: "image",
|
|
822
|
+
document: false,
|
|
823
|
+
reader: false,
|
|
824
|
+
editable: false
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
ext: ".gif",
|
|
828
|
+
mime: "image/gif",
|
|
829
|
+
kind: "image",
|
|
830
|
+
document: false,
|
|
831
|
+
reader: false,
|
|
832
|
+
editable: false
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
ext: ".webp",
|
|
836
|
+
mime: "image/webp",
|
|
837
|
+
kind: "image",
|
|
838
|
+
document: false,
|
|
839
|
+
reader: false,
|
|
840
|
+
editable: false
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
ext: ".avif",
|
|
844
|
+
mime: "image/avif",
|
|
845
|
+
kind: "image",
|
|
846
|
+
document: false,
|
|
847
|
+
reader: false,
|
|
848
|
+
editable: false
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
// Text, hand-edited in practice, and script-carrying. Editable anyway: the Worker derives the
|
|
852
|
+
// type at serve time (I10) and already serves whatever was uploaded, so withholding the edit
|
|
853
|
+
// would withhold nothing else.
|
|
854
|
+
ext: ".svg",
|
|
855
|
+
mime: "image/svg+xml",
|
|
856
|
+
kind: "image",
|
|
857
|
+
document: false,
|
|
858
|
+
reader: false,
|
|
859
|
+
editable: true
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
ext: ".ico",
|
|
863
|
+
mime: "image/x-icon",
|
|
864
|
+
kind: "image",
|
|
865
|
+
document: false,
|
|
866
|
+
reader: false,
|
|
867
|
+
editable: false
|
|
868
|
+
},
|
|
869
|
+
// A customer's own pages. Never rendered inside the reader.
|
|
870
|
+
{
|
|
871
|
+
ext: ".html",
|
|
872
|
+
mime: "text/html; charset=utf-8",
|
|
873
|
+
kind: "html",
|
|
874
|
+
document: false,
|
|
875
|
+
reader: false,
|
|
876
|
+
editable: true
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
// ⚠️ No media type, and that is the state of the code rather than a decision: `TYPES` in
|
|
880
|
+
// `headers.ts` has never held `htm`, so a published `.htm` is served as octet-stream and
|
|
881
|
+
// downloads instead of rendering. Recorded here rather than fixed, because this table was
|
|
882
|
+
// introduced as a refactor and a refactor that changes what a URL answers is not one.
|
|
883
|
+
ext: ".htm",
|
|
884
|
+
mime: null,
|
|
885
|
+
kind: "html",
|
|
886
|
+
document: false,
|
|
887
|
+
reader: false,
|
|
888
|
+
editable: true
|
|
889
|
+
},
|
|
890
|
+
// Everything below renders as nothing — the viewer offers a download — but still needs a media
|
|
891
|
+
// type, because these are the files a static site is built out of.
|
|
892
|
+
{
|
|
893
|
+
ext: ".mjs",
|
|
894
|
+
mime: "text/javascript; charset=utf-8",
|
|
895
|
+
kind: "binary",
|
|
896
|
+
document: false,
|
|
897
|
+
reader: false,
|
|
898
|
+
editable: true
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
ext: ".cjs",
|
|
902
|
+
mime: null,
|
|
903
|
+
kind: "binary",
|
|
904
|
+
document: false,
|
|
905
|
+
reader: false,
|
|
906
|
+
editable: true
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
ext: ".map",
|
|
910
|
+
mime: "application/json",
|
|
911
|
+
kind: "binary",
|
|
912
|
+
document: false,
|
|
913
|
+
reader: false,
|
|
914
|
+
editable: false
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
ext: ".wasm",
|
|
918
|
+
mime: "application/wasm",
|
|
919
|
+
kind: "binary",
|
|
920
|
+
document: false,
|
|
921
|
+
reader: false,
|
|
922
|
+
editable: false
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
ext: ".woff2",
|
|
926
|
+
mime: "font/woff2",
|
|
927
|
+
kind: "binary",
|
|
928
|
+
document: false,
|
|
929
|
+
reader: false,
|
|
930
|
+
editable: false
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
ext: ".woff",
|
|
934
|
+
mime: "font/woff",
|
|
935
|
+
kind: "binary",
|
|
936
|
+
document: false,
|
|
937
|
+
reader: false,
|
|
938
|
+
editable: false
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
ext: ".ttf",
|
|
942
|
+
mime: "font/ttf",
|
|
943
|
+
kind: "binary",
|
|
944
|
+
document: false,
|
|
945
|
+
reader: false,
|
|
946
|
+
editable: false
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
ext: ".mp4",
|
|
950
|
+
mime: "video/mp4",
|
|
951
|
+
kind: "binary",
|
|
952
|
+
document: false,
|
|
953
|
+
reader: false,
|
|
954
|
+
editable: false
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
ext: ".webm",
|
|
958
|
+
mime: "video/webm",
|
|
959
|
+
kind: "binary",
|
|
960
|
+
document: false,
|
|
961
|
+
reader: false,
|
|
962
|
+
editable: false
|
|
963
|
+
}
|
|
964
|
+
];
|
|
965
|
+
function extensionsWhere(include) {
|
|
966
|
+
return FILE_TYPES.filter(include).map((type) => type.ext);
|
|
967
|
+
}
|
|
968
|
+
const DOCUMENT_EXTENSIONS$1 = extensionsWhere((type) => type.document);
|
|
969
|
+
const VIEWABLE_EXTENSIONS$1 = extensionsWhere(
|
|
970
|
+
(type) => type.kind !== "binary"
|
|
971
|
+
);
|
|
972
|
+
extensionsWhere((type) => type.reader);
|
|
973
|
+
extensionsWhere((type) => type.editable);
|
|
974
|
+
const WEB_ASSET_EXTENSIONS = [
|
|
975
|
+
".html",
|
|
976
|
+
".htm",
|
|
977
|
+
".js",
|
|
978
|
+
".mjs",
|
|
979
|
+
".cjs",
|
|
980
|
+
".css"
|
|
981
|
+
];
|
|
982
|
+
const GALLERY_EXTENSIONS = extensionsWhere(
|
|
983
|
+
(type) => type.kind === "image"
|
|
984
|
+
);
|
|
985
|
+
Object.fromEntries(
|
|
986
|
+
FILE_TYPES.filter((type) => type.mime !== null).map((type) => [type.ext, type.mime])
|
|
987
|
+
);
|
|
988
|
+
function extensionOf(path) {
|
|
989
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
990
|
+
const dot = name.lastIndexOf(".");
|
|
991
|
+
return dot <= 0 ? "" : name.slice(dot).toLowerCase();
|
|
992
|
+
}
|
|
442
993
|
async function sha256Hex(bytes) {
|
|
443
994
|
const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
|
|
444
995
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -470,52 +1021,29 @@ function shouldIgnore(path) {
|
|
|
470
1021
|
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
471
1022
|
}
|
|
472
1023
|
const REQUIRED_INDEX = "index.html";
|
|
473
|
-
const DOCUMENT_EXTENSIONS =
|
|
1024
|
+
const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
|
|
474
1025
|
function isDocumentPath(path) {
|
|
475
1026
|
const lower = path.toLowerCase();
|
|
476
1027
|
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
477
1028
|
}
|
|
478
|
-
const VIEWABLE_EXTENSIONS =
|
|
479
|
-
// Documents — these also count at any depth, via DOCUMENT_EXTENSIONS.
|
|
480
|
-
".md",
|
|
481
|
-
".markdown",
|
|
482
|
-
".pdf",
|
|
483
|
-
// Prose the reader shows preformatted.
|
|
484
|
-
".txt",
|
|
485
|
-
".log",
|
|
486
|
-
".csv",
|
|
487
|
-
// Source the reader shows with highlighting.
|
|
488
|
-
".json",
|
|
489
|
-
".yaml",
|
|
490
|
-
".yml",
|
|
491
|
-
".toml",
|
|
492
|
-
".css",
|
|
493
|
-
".js",
|
|
494
|
-
".ts",
|
|
495
|
-
".tsx",
|
|
496
|
-
".jsx",
|
|
497
|
-
".sh",
|
|
498
|
-
".xml",
|
|
499
|
-
// Pictures the reader shows with an img element.
|
|
500
|
-
".png",
|
|
501
|
-
".jpg",
|
|
502
|
-
".jpeg",
|
|
503
|
-
".gif",
|
|
504
|
-
".webp",
|
|
505
|
-
".avif",
|
|
506
|
-
".svg",
|
|
507
|
-
".ico",
|
|
508
|
-
// See the note above: a floor for a caller that did not rename it.
|
|
509
|
-
".html",
|
|
510
|
-
".htm"
|
|
511
|
-
];
|
|
1029
|
+
const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
|
|
512
1030
|
function isViewablePath(path) {
|
|
513
1031
|
const lower = path.toLowerCase();
|
|
514
1032
|
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
515
1033
|
}
|
|
1034
|
+
function isImageFolder(paths) {
|
|
1035
|
+
let pictures = false;
|
|
1036
|
+
for (const path of paths) {
|
|
1037
|
+
const ext = extensionOf(path);
|
|
1038
|
+
if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
|
|
1039
|
+
if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
|
|
1040
|
+
}
|
|
1041
|
+
return pictures;
|
|
1042
|
+
}
|
|
516
1043
|
function canPublish(paths) {
|
|
517
1044
|
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
518
1045
|
if (paths.some(isDocumentPath)) return true;
|
|
1046
|
+
if (isImageFolder(paths)) return true;
|
|
519
1047
|
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
520
1048
|
}
|
|
521
1049
|
function renameOfLoneHtmlPage(paths) {
|
|
@@ -544,37 +1072,42 @@ function checkLimits(files, limits) {
|
|
|
544
1072
|
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
545
1073
|
);
|
|
546
1074
|
}
|
|
547
|
-
|
|
1075
|
+
const refusal = sizeRefusal(
|
|
1076
|
+
files.map((file) => ({ path: file.path, size: file.bytes.length })),
|
|
1077
|
+
limits
|
|
1078
|
+
);
|
|
1079
|
+
if (refusal) throw refusal;
|
|
1080
|
+
}
|
|
1081
|
+
function sizeRefusal(files, limits) {
|
|
1082
|
+
if (!limits) return null;
|
|
548
1083
|
if (files.length > limits.maxFiles) {
|
|
549
|
-
|
|
1084
|
+
return new DeployError(
|
|
550
1085
|
ClientErrorCode.TooManyFiles,
|
|
551
1086
|
`That is ${files.length} files, and this plan allows ${limits.maxFiles}.`,
|
|
552
1087
|
{ limit: limits.maxFiles, actual: files.length }
|
|
553
1088
|
);
|
|
554
1089
|
}
|
|
555
|
-
const oversized = files.find((file) => file.
|
|
1090
|
+
const oversized = files.find((file) => file.size > limits.maxFileBytes);
|
|
556
1091
|
if (oversized) {
|
|
557
|
-
|
|
1092
|
+
return new DeployError(
|
|
558
1093
|
ClientErrorCode.FileTooLarge,
|
|
559
|
-
`${oversized.path} is ${formatBytes$1(oversized.
|
|
1094
|
+
`${oversized.path} is ${formatBytes$1(oversized.size)}, and this plan allows ${formatBytes$1(limits.maxFileBytes)} per file.`,
|
|
560
1095
|
{
|
|
561
1096
|
path: oversized.path,
|
|
562
1097
|
limit: limits.maxFileBytes,
|
|
563
|
-
actual: oversized.
|
|
1098
|
+
actual: oversized.size
|
|
564
1099
|
}
|
|
565
1100
|
);
|
|
566
1101
|
}
|
|
567
|
-
const total =
|
|
1102
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
568
1103
|
if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
|
|
569
|
-
|
|
1104
|
+
return new DeployError(
|
|
570
1105
|
ClientErrorCode.TooLarge,
|
|
571
1106
|
`That drop is ${formatBytes$1(total)}, and this plan allows ${formatBytes$1(limits.maxSiteBytes)}.`,
|
|
572
1107
|
{ limit: limits.maxSiteBytes, actual: total }
|
|
573
1108
|
);
|
|
574
1109
|
}
|
|
575
|
-
|
|
576
|
-
function totalBytes(files) {
|
|
577
|
-
return files.reduce((sum, file) => sum + file.bytes.length, 0);
|
|
1110
|
+
return null;
|
|
578
1111
|
}
|
|
579
1112
|
const MAX_NAME_LENGTH = 60;
|
|
580
1113
|
const HEAD_BYTES = 64 * 1024;
|
|
@@ -873,20 +1406,23 @@ function escapesRoot(root, candidate) {
|
|
|
873
1406
|
const inside = relative(root, candidate);
|
|
874
1407
|
return inside === "" || inside.startsWith("..");
|
|
875
1408
|
}
|
|
876
|
-
async function resolveSite(credentials, site,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
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
|
+
);
|
|
884
1415
|
}
|
|
885
|
-
if (site
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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);
|
|
890
1426
|
if (found === void 0) {
|
|
891
1427
|
throw new Error(
|
|
892
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.`
|
|
@@ -894,8 +1430,8 @@ async function resolveSite(credentials, site, subdomain) {
|
|
|
894
1430
|
}
|
|
895
1431
|
return found;
|
|
896
1432
|
}
|
|
897
|
-
async function publish(credentials, source, site, onProgress,
|
|
898
|
-
const target = await resolveSite(credentials, site,
|
|
1433
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
1434
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
899
1435
|
let files = 0;
|
|
900
1436
|
let url = target.url;
|
|
901
1437
|
let unchanged = false;
|
|
@@ -926,11 +1462,11 @@ async function publish(credentials, source, site, onProgress, subdomain) {
|
|
|
926
1462
|
});
|
|
927
1463
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
928
1464
|
}
|
|
929
|
-
async function publishDirectory(credentials, directory, site, onProgress,
|
|
1465
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
930
1466
|
const found = await stat(directory).catch(() => null);
|
|
931
1467
|
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
932
1468
|
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
933
|
-
return await publish(credentials, directorySource(directory), site, onProgress,
|
|
1469
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
934
1470
|
}
|
|
935
1471
|
const silentProgress = Object.assign(() => {
|
|
936
1472
|
}, { done: () => {
|
|
@@ -1127,33 +1663,67 @@ API: ${found.credentials.apiBaseUrl}`,
|
|
|
1127
1663
|
async function list() {
|
|
1128
1664
|
const found = credentialsOr();
|
|
1129
1665
|
if ("result" in found) return found.result;
|
|
1130
|
-
const sites = await listSites(found.credentials);
|
|
1666
|
+
const { sites, total } = await listSites(found.credentials);
|
|
1131
1667
|
if (sites.length === 0) {
|
|
1132
1668
|
return {
|
|
1133
1669
|
text: "No sites yet. `drop2run deploy` publishes one.",
|
|
1134
|
-
json: { sites: [] },
|
|
1670
|
+
json: { sites: [], total: 0 },
|
|
1135
1671
|
code: 0
|
|
1136
1672
|
};
|
|
1137
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
|
+
}
|
|
1138
1682
|
return {
|
|
1139
|
-
text:
|
|
1140
|
-
json: { sites },
|
|
1683
|
+
text: lines.join("\n"),
|
|
1684
|
+
json: { sites, total },
|
|
1141
1685
|
code: 0
|
|
1142
1686
|
};
|
|
1143
1687
|
}
|
|
1144
|
-
async function
|
|
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 = {}) {
|
|
1145
1710
|
const found = credentialsOr();
|
|
1146
1711
|
if ("result" in found) return found.result;
|
|
1147
1712
|
const project = readProject();
|
|
1148
1713
|
const folder = directory ?? project?.dir ?? ".";
|
|
1149
|
-
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
|
+
}
|
|
1150
1720
|
try {
|
|
1151
1721
|
const result = await publishDirectory(
|
|
1152
1722
|
found.credentials,
|
|
1153
1723
|
resolve(folder),
|
|
1154
1724
|
target,
|
|
1155
1725
|
report,
|
|
1156
|
-
|
|
1726
|
+
newSite
|
|
1157
1727
|
);
|
|
1158
1728
|
const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
|
|
1159
1729
|
return {
|
|
@@ -1168,7 +1738,7 @@ ${result.url}`,
|
|
|
1168
1738
|
report.done();
|
|
1169
1739
|
}
|
|
1170
1740
|
}
|
|
1171
|
-
async function init(directory, site,
|
|
1741
|
+
async function init(directory, site, newSite = {}) {
|
|
1172
1742
|
const found = credentialsOr();
|
|
1173
1743
|
if ("result" in found) return found.result;
|
|
1174
1744
|
if (readProject() !== null) {
|
|
@@ -1177,7 +1747,11 @@ async function init(directory, site, subdomain) {
|
|
|
1177
1747
|
);
|
|
1178
1748
|
}
|
|
1179
1749
|
try {
|
|
1180
|
-
const target = site === void 0 ? await createSite(
|
|
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);
|
|
1181
1755
|
const project = {
|
|
1182
1756
|
siteId: target.siteId,
|
|
1183
1757
|
subdomain: target.subdomain,
|
|
@@ -1316,7 +1890,9 @@ const HELP = `drop2run — publish a static site from the command line
|
|
|
1316
1890
|
drop2run init [dir] [--site <subdomain>] Tie this folder to a site (writes drop2run.json)
|
|
1317
1891
|
drop2run deploy [dir] [--site <subdomain>] Publish a folder (default: . or drop2run.json)
|
|
1318
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
|
|
1319
1894
|
drop2run ls List your sites
|
|
1895
|
+
drop2run folders List your folders, as --folder takes them
|
|
1320
1896
|
drop2run open [site] Open a site in a browser
|
|
1321
1897
|
drop2run rollback <deployId> [--site X] Put an earlier version back live
|
|
1322
1898
|
drop2run rm <site> --yes Delete a site and everything on it
|
|
@@ -1329,6 +1905,8 @@ Flags
|
|
|
1329
1905
|
--json Print machine-readable output instead of text
|
|
1330
1906
|
--site X Act on an existing site rather than the one in drop2run.json
|
|
1331
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)
|
|
1332
1910
|
--device Sign in by approving a code on another machine
|
|
1333
1911
|
--yes Confirm a deletion, which cannot be undone
|
|
1334
1912
|
|
|
@@ -1340,6 +1918,10 @@ Where a site comes from
|
|
|
1340
1918
|
--site only ever finds a site you already have; it never creates one, so a
|
|
1341
1919
|
subdomain typed wrong fails instead of quietly becoming a second site.
|
|
1342
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
|
+
|
|
1343
1925
|
Signing in
|
|
1344
1926
|
\`login\` opens a browser and listens on 127.0.0.1, so it needs both on this
|
|
1345
1927
|
machine. Over SSH, in a container or under WSL, use \`login --device\`: it
|
|
@@ -1355,7 +1937,7 @@ function flagValue(args, flag) {
|
|
|
1355
1937
|
const value = args[at + 1];
|
|
1356
1938
|
return value === void 0 || value.startsWith("-") ? void 0 : value;
|
|
1357
1939
|
}
|
|
1358
|
-
const VALUE_FLAGS = ["--site", "--subdomain"];
|
|
1940
|
+
const VALUE_FLAGS = ["--site", "--subdomain", "--folder"];
|
|
1359
1941
|
function positionals(argv) {
|
|
1360
1942
|
const found = [];
|
|
1361
1943
|
for (let at = 0; at < argv.length; at++) {
|
|
@@ -1370,16 +1952,29 @@ function positionals(argv) {
|
|
|
1370
1952
|
}
|
|
1371
1953
|
return found;
|
|
1372
1954
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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}\`.`;
|
|
1377
1972
|
}
|
|
1378
1973
|
if (site !== void 0) {
|
|
1379
|
-
return
|
|
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.`;
|
|
1380
1975
|
}
|
|
1381
1976
|
if (command !== "init" && command !== "deploy") {
|
|
1382
|
-
return
|
|
1977
|
+
return `\`${flag}\` only applies to \`init\` and \`deploy\`, which can create a site.` + elsewhere(command);
|
|
1383
1978
|
}
|
|
1384
1979
|
return void 0;
|
|
1385
1980
|
}
|
|
@@ -1388,7 +1983,7 @@ async function run(argv) {
|
|
|
1388
1983
|
const positional = positionals(argv);
|
|
1389
1984
|
const [command, ...rest] = positional;
|
|
1390
1985
|
if (argv.includes("--version")) {
|
|
1391
|
-
const { version } = await import("./package-
|
|
1986
|
+
const { version } = await import("./package-bzaCMW_E.js").then(
|
|
1392
1987
|
(module) => module.default
|
|
1393
1988
|
);
|
|
1394
1989
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -1398,7 +1993,8 @@ async function run(argv) {
|
|
|
1398
1993
|
if (command === void 0) return { text: HELP, json: { help: HELP }, code: 1 };
|
|
1399
1994
|
const site = flagValue(argv, "--site");
|
|
1400
1995
|
const subdomain = flagValue(argv, "--subdomain");
|
|
1401
|
-
const
|
|
1996
|
+
const folder = flagValue(argv, "--folder");
|
|
1997
|
+
const refusal = newSiteFlagProblem(argv, command, site, subdomain, SUBDOMAIN_FLAG) ?? newSiteFlagProblem(argv, command, site, folder, FOLDER_FLAG);
|
|
1402
1998
|
if (refusal !== void 0) {
|
|
1403
1999
|
const failed = { text: refusal, json: { error: refusal }, code: 1 };
|
|
1404
2000
|
return json ? { ...failed, text: JSON.stringify(failed.json, null, 2) } : failed;
|
|
@@ -1410,11 +2006,11 @@ async function run(argv) {
|
|
|
1410
2006
|
json,
|
|
1411
2007
|
argv.includes("--device"),
|
|
1412
2008
|
argv.includes("--yes"),
|
|
1413
|
-
subdomain
|
|
2009
|
+
{ subdomain, folder }
|
|
1414
2010
|
);
|
|
1415
2011
|
return json ? { ...result, text: JSON.stringify(result.json, null, 2) } : result;
|
|
1416
2012
|
}
|
|
1417
|
-
async function dispatch(command, rest, site, json, device, confirmed,
|
|
2013
|
+
async function dispatch(command, rest, site, json, device, confirmed, newSite = {}) {
|
|
1418
2014
|
switch (command) {
|
|
1419
2015
|
case "login": {
|
|
1420
2016
|
const print = json ? () => {
|
|
@@ -1424,18 +2020,20 @@ async function dispatch(command, rest, site, json, device, confirmed, subdomain)
|
|
|
1424
2020
|
case "logout":
|
|
1425
2021
|
return logout();
|
|
1426
2022
|
case "init":
|
|
1427
|
-
return await init(rest[0] ?? ".", site,
|
|
2023
|
+
return await init(rest[0] ?? ".", site, newSite);
|
|
1428
2024
|
case "deploy": {
|
|
1429
2025
|
const report = json ? silentProgress : progressWriter(
|
|
1430
2026
|
(text) => process.stderr.write(text),
|
|
1431
2027
|
process.stderr.isTTY === true,
|
|
1432
2028
|
process.stderr.columns ?? 80
|
|
1433
2029
|
);
|
|
1434
|
-
return await deployCommand(rest[0], site, report,
|
|
2030
|
+
return await deployCommand(rest[0], site, report, newSite);
|
|
1435
2031
|
}
|
|
1436
2032
|
case "ls":
|
|
1437
2033
|
case "list":
|
|
1438
2034
|
return await list();
|
|
2035
|
+
case "folders":
|
|
2036
|
+
return await folders();
|
|
1439
2037
|
case "open":
|
|
1440
2038
|
return await open(rest[0] ?? site);
|
|
1441
2039
|
case "rollback":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const name = "drop2run";
|
|
2
|
-
const version = "0.
|
|
3
|
-
const description = "Publish a
|
|
2
|
+
const version = "0.6.0";
|
|
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";
|
|
6
6
|
const bin = { "drop2run": "bin/drop2run.mjs" };
|
|
@@ -10,7 +10,7 @@ const publishConfig = { "access": "public" };
|
|
|
10
10
|
const homepage = "https://dropto.run";
|
|
11
11
|
const repository = { "type": "git", "url": "git+https://github.com/Drop-to-run/drop2run-cli.git" };
|
|
12
12
|
const bugs = { "url": "https://github.com/Drop-to-run/drop2run-cli/issues" };
|
|
13
|
-
const keywords = ["static-site", "deploy", "hosting", "cli", "ai", "claude", "agent", "llm"];
|
|
13
|
+
const keywords = ["static-site", "markdown", "pdf", "deploy", "hosting", "cli", "ai", "claude", "agent", "llm"];
|
|
14
14
|
const scripts = { "prepublishOnly": "npm run build && npm test", "build": "vite build", "typecheck": "tsc --noEmit", "test": "vitest run", "lint": "biome check .", "format": "biome check --write ." };
|
|
15
15
|
const devDependencies = { "@biomejs/biome": "^2.5.11", "@types/node": "^26.4.0", "typescript": "^5.9.3", "vite": "^7.2.1", "vitest": "^4.1.11" };
|
|
16
16
|
const _package = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drop2run",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Publish a
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"static-site",
|
|
30
|
+
"markdown",
|
|
31
|
+
"pdf",
|
|
30
32
|
"deploy",
|
|
31
33
|
"hosting",
|
|
32
34
|
"cli",
|