drop2run 0.2.0 → 0.3.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 +3 -1
- package/dist/index.js +261 -166
- package/dist/{package-C6bEGZf_.js → package-B5PgBI_i.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,9 @@ DROP2RUN_TOKEN=d2r_...
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
The environment wins over `~/.config/drop2run/config.json`, and that is the same file and precedence
|
|
75
|
-
`@drop2run/mcp` uses, so signing in once covers both.
|
|
75
|
+
`@drop2run/mcp` uses, so signing in once covers both — in either direction. That server runs the same
|
|
76
|
+
two flows from its own `login` and `login_code` tools, so a sign-in done in a chat leaves this command
|
|
77
|
+
line signed in too.
|
|
76
78
|
|
|
77
79
|
`drop2run where` says which source is in force without printing the token, so "why is it using the wrong
|
|
78
80
|
account" is answerable in an issue report or a CI log.
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { join, dirname, relative, sep, resolve } from "node:path";
|
|
2
2
|
import { readFileSync, writeFileSync, chmodSync, renameSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
-
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
5
4
|
import { spawn } from "node:child_process";
|
|
6
5
|
import { randomBytes, createHash } from "node:crypto";
|
|
7
6
|
import { createServer } from "node:http";
|
|
7
|
+
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
8
8
|
async function call(credentials, path, init2 = {}) {
|
|
9
9
|
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
10
10
|
...init2,
|
|
@@ -125,20 +125,188 @@ function clearToken(path = configPath()) {
|
|
|
125
125
|
renameSync(temporary, path);
|
|
126
126
|
return true;
|
|
127
127
|
}
|
|
128
|
-
function missingCredentialsMessage() {
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
"",
|
|
132
|
-
"Run `drop2run login` to sign in through a browser, which stores one for you.",
|
|
128
|
+
function missingCredentialsMessage(surface = "cli") {
|
|
129
|
+
const signIn = surface === "mcp" ? [
|
|
130
|
+
"Call the `login` tool to sign in through a browser, which stores a token.",
|
|
133
131
|
"",
|
|
132
|
+
"Where no browser can be opened on this machine — a container, a remote host — call",
|
|
133
|
+
"`login_code` instead and pass on the short code it gives you."
|
|
134
|
+
] : ["Run `drop2run login` to sign in through a browser, which stores one for you."];
|
|
135
|
+
const byHand = surface === "mcp" ? [
|
|
136
|
+
"To set one without signing in here, create a token at",
|
|
137
|
+
"https://dropto.run/account/tokens and either:",
|
|
138
|
+
` - put it in ${configPath()} as {"token": "d2r_..."}, which this`,
|
|
139
|
+
" server reads on every call and needs no restart, or",
|
|
140
|
+
` - set ${TOKEN_VARIABLE} in the environment and restart this server, since it reads`,
|
|
141
|
+
" the environment it was started in."
|
|
142
|
+
] : [
|
|
134
143
|
"For CI, or where no browser can be opened, create a token at",
|
|
135
144
|
"https://dropto.run/account/tokens and either:",
|
|
136
145
|
` - set ${TOKEN_VARIABLE} in the environment, or`,
|
|
137
|
-
` - put it in ${configPath()} as {"token": "d2r_..."}
|
|
146
|
+
` - put it in ${configPath()} as {"token": "d2r_..."}`
|
|
147
|
+
];
|
|
148
|
+
return [
|
|
149
|
+
"No Drop2Run access token.",
|
|
150
|
+
"",
|
|
151
|
+
...signIn,
|
|
152
|
+
"",
|
|
153
|
+
...byHand,
|
|
138
154
|
"",
|
|
139
155
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
140
156
|
].join("\n");
|
|
141
157
|
}
|
|
158
|
+
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
159
|
+
function newAttempt() {
|
|
160
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
161
|
+
return {
|
|
162
|
+
verifier,
|
|
163
|
+
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
164
|
+
state: randomBytes(16).toString("base64url")
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function clientName(surface) {
|
|
168
|
+
const clean2 = (value) => value.trim().replace(/[^A-Za-z0-9._-]/g, "-");
|
|
169
|
+
const label = "";
|
|
170
|
+
const raw = clean2(hostname()).slice(0, 64 - label.length);
|
|
171
|
+
return `${raw === "" ? "drop2run" : raw}${label}`;
|
|
172
|
+
}
|
|
173
|
+
function openBrowser(url) {
|
|
174
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
175
|
+
try {
|
|
176
|
+
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
177
|
+
child.unref();
|
|
178
|
+
child.on("error", () => {
|
|
179
|
+
});
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function listen(state) {
|
|
186
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
187
|
+
let settle = null;
|
|
188
|
+
let fail = null;
|
|
189
|
+
const received = new Promise((resolveCode, rejectCode) => {
|
|
190
|
+
settle = resolveCode;
|
|
191
|
+
fail = rejectCode;
|
|
192
|
+
});
|
|
193
|
+
const server = createServer((request2, response) => {
|
|
194
|
+
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
195
|
+
if (url.pathname !== "/cb") {
|
|
196
|
+
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
197
|
+
response.end("Not found.\n");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const code = url.searchParams.get("code") ?? "";
|
|
201
|
+
const returned = url.searchParams.get("state") ?? "";
|
|
202
|
+
if (returned !== state || code === "") {
|
|
203
|
+
respond(
|
|
204
|
+
response,
|
|
205
|
+
400,
|
|
206
|
+
"Sign-in could not be completed",
|
|
207
|
+
"This did not come from the sign-in that was started here. Nothing has been changed."
|
|
208
|
+
);
|
|
209
|
+
fail?.(new Error("The browser returned a response that did not match this sign-in."));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
respond(response, 200, "Signed in", "You can close this tab now.");
|
|
213
|
+
settle?.({ code });
|
|
214
|
+
});
|
|
215
|
+
server.on("error", (error) => {
|
|
216
|
+
rejectListener(error);
|
|
217
|
+
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
218
|
+
});
|
|
219
|
+
server.listen(0, "127.0.0.1", () => {
|
|
220
|
+
const address = server.address();
|
|
221
|
+
if (address === null) {
|
|
222
|
+
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
resolveListener({
|
|
226
|
+
port: address.port,
|
|
227
|
+
received,
|
|
228
|
+
close: () => server.close()
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function respond(response, status, title, detail) {
|
|
234
|
+
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
235
|
+
response.end(
|
|
236
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
237
|
+
`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
241
|
+
let timer;
|
|
242
|
+
try {
|
|
243
|
+
return await Promise.race([
|
|
244
|
+
received,
|
|
245
|
+
new Promise((_, reject) => {
|
|
246
|
+
timer = setTimeout(
|
|
247
|
+
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
248
|
+
timeoutMs
|
|
249
|
+
);
|
|
250
|
+
})
|
|
251
|
+
]);
|
|
252
|
+
} finally {
|
|
253
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function consentUrl(dashboardUrl, attempt, port, name) {
|
|
257
|
+
const url = new URL("/cli-auth", dashboardUrl);
|
|
258
|
+
url.searchParams.set("challenge", attempt.challenge);
|
|
259
|
+
url.searchParams.set("state", attempt.state);
|
|
260
|
+
url.searchParams.set("port", String(port));
|
|
261
|
+
url.searchParams.set("name", name);
|
|
262
|
+
return url.toString();
|
|
263
|
+
}
|
|
264
|
+
async function startDevice(apiBaseUrl, clientName2) {
|
|
265
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/device`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
body: JSON.stringify({ clientName: clientName2 })
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
271
|
+
return await response.json();
|
|
272
|
+
}
|
|
273
|
+
async function pollDevice(apiBaseUrl, deviceCode) {
|
|
274
|
+
let response;
|
|
275
|
+
try {
|
|
276
|
+
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { "Content-Type": "application/json" },
|
|
279
|
+
body: JSON.stringify({ deviceCode })
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
return { state: "pending" };
|
|
283
|
+
}
|
|
284
|
+
if (response.ok) {
|
|
285
|
+
const body = await response.json();
|
|
286
|
+
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
287
|
+
}
|
|
288
|
+
const problem = await response.json().catch(() => null);
|
|
289
|
+
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
290
|
+
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
291
|
+
return {
|
|
292
|
+
state: "dead",
|
|
293
|
+
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function problemDetail(response) {
|
|
297
|
+
const problem = await response.json().catch(() => null);
|
|
298
|
+
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
299
|
+
}
|
|
300
|
+
async function exchange(apiBaseUrl, code, verifier) {
|
|
301
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: { "Content-Type": "application/json" },
|
|
304
|
+
body: JSON.stringify({ code, verifier })
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
307
|
+
const body = await response.json();
|
|
308
|
+
return { token: body.token, name: body.name, email: body.email };
|
|
309
|
+
}
|
|
142
310
|
const PROJECT_FILE = "drop2run.json";
|
|
143
311
|
function projectPath(directory = process.cwd()) {
|
|
144
312
|
return join(directory, PROJECT_FILE);
|
|
@@ -349,7 +517,7 @@ function renameOfLoneHtmlPage(paths) {
|
|
|
349
517
|
const lower = only.toLowerCase();
|
|
350
518
|
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
351
519
|
}
|
|
352
|
-
function formatBytes(bytes) {
|
|
520
|
+
function formatBytes$1(bytes) {
|
|
353
521
|
if (bytes < 1024) return `${bytes} B`;
|
|
354
522
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
355
523
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -380,7 +548,7 @@ function checkLimits(files, limits) {
|
|
|
380
548
|
if (oversized) {
|
|
381
549
|
throw new DeployError(
|
|
382
550
|
ClientErrorCode.FileTooLarge,
|
|
383
|
-
`${oversized.path} is ${formatBytes(oversized.bytes.length)}, and this plan allows ${formatBytes(limits.maxFileBytes)} per file.`,
|
|
551
|
+
`${oversized.path} is ${formatBytes$1(oversized.bytes.length)}, and this plan allows ${formatBytes$1(limits.maxFileBytes)} per file.`,
|
|
384
552
|
{
|
|
385
553
|
path: oversized.path,
|
|
386
554
|
limit: limits.maxFileBytes,
|
|
@@ -392,7 +560,7 @@ function checkLimits(files, limits) {
|
|
|
392
560
|
if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
|
|
393
561
|
throw new DeployError(
|
|
394
562
|
ClientErrorCode.TooLarge,
|
|
395
|
-
`That drop is ${formatBytes(total)}, and this plan allows ${formatBytes(limits.maxSiteBytes)}.`,
|
|
563
|
+
`That drop is ${formatBytes$1(total)}, and this plan allows ${formatBytes$1(limits.maxSiteBytes)}.`,
|
|
396
564
|
{ limit: limits.maxSiteBytes, actual: total }
|
|
397
565
|
);
|
|
398
566
|
}
|
|
@@ -530,7 +698,7 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
530
698
|
});
|
|
531
699
|
done += 1;
|
|
532
700
|
uploadedBytes += file.bytes.length;
|
|
533
|
-
onProgress?.(done, targets.length, uploadedBytes);
|
|
701
|
+
onProgress?.(done, targets.length, uploadedBytes, file.path);
|
|
534
702
|
}
|
|
535
703
|
};
|
|
536
704
|
const workers = Array.from(
|
|
@@ -625,12 +793,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
625
793
|
prepared.upload,
|
|
626
794
|
prepared.uploadUrl,
|
|
627
795
|
hashed,
|
|
628
|
-
(done, _total, bytes) => onProgress({
|
|
796
|
+
(done, _total, bytes, path) => onProgress({
|
|
629
797
|
type: "uploading",
|
|
630
798
|
done: prepared.reused + done,
|
|
631
799
|
total: prepared.total,
|
|
632
800
|
bytes,
|
|
633
|
-
reused: prepared.reused
|
|
801
|
+
reused: prepared.reused,
|
|
802
|
+
path
|
|
634
803
|
}),
|
|
635
804
|
signal,
|
|
636
805
|
options.upload
|
|
@@ -704,12 +873,12 @@ async function resolveSite(credentials, site) {
|
|
|
704
873
|
);
|
|
705
874
|
if (found === void 0) {
|
|
706
875
|
throw new Error(
|
|
707
|
-
`No site of yours is called "${site}". Leave the site out to publish to a new one, or
|
|
876
|
+
`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.`
|
|
708
877
|
);
|
|
709
878
|
}
|
|
710
879
|
return found;
|
|
711
880
|
}
|
|
712
|
-
async function publish(credentials, source, site) {
|
|
881
|
+
async function publish(credentials, source, site, onProgress) {
|
|
713
882
|
const target = await resolveSite(credentials, site);
|
|
714
883
|
let files = 0;
|
|
715
884
|
let url = target.url;
|
|
@@ -727,6 +896,7 @@ async function publish(credentials, source, site) {
|
|
|
727
896
|
failure2 = event.message;
|
|
728
897
|
failureDetail = event.detail;
|
|
729
898
|
}
|
|
899
|
+
onProgress?.(event);
|
|
730
900
|
};
|
|
731
901
|
await deploy(source, target.siteId, record, void 0, {
|
|
732
902
|
baseUrl: credentials.apiBaseUrl,
|
|
@@ -740,158 +910,75 @@ async function publish(credentials, source, site) {
|
|
|
740
910
|
});
|
|
741
911
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
742
912
|
}
|
|
743
|
-
function publishDirectory(credentials, directory, site) {
|
|
744
|
-
return publish(credentials, directorySource(directory), site);
|
|
745
|
-
}
|
|
746
|
-
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
747
|
-
function newAttempt() {
|
|
748
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
749
|
-
return {
|
|
750
|
-
verifier,
|
|
751
|
-
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
752
|
-
state: randomBytes(16).toString("base64url")
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
function clientName() {
|
|
756
|
-
const raw = hostname().trim().replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 64);
|
|
757
|
-
return raw === "" ? "drop2run-cli" : raw;
|
|
913
|
+
function publishDirectory(credentials, directory, site, onProgress) {
|
|
914
|
+
return publish(credentials, directorySource(directory), site, onProgress);
|
|
758
915
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
916
|
+
const silentProgress = Object.assign(() => {
|
|
917
|
+
}, { done: () => {
|
|
918
|
+
} });
|
|
919
|
+
const STEP = 1024;
|
|
920
|
+
const UNITS = ["B", "KB", "MB", "GB"];
|
|
921
|
+
function formatBytes(bytes) {
|
|
922
|
+
let value = bytes;
|
|
923
|
+
let unit = 0;
|
|
924
|
+
while (value >= STEP && unit < UNITS.length - 1) {
|
|
925
|
+
value /= STEP;
|
|
926
|
+
unit += 1;
|
|
769
927
|
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
settle?.({ code });
|
|
800
|
-
});
|
|
801
|
-
server.on("error", (error) => {
|
|
802
|
-
rejectListener(error);
|
|
803
|
-
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
804
|
-
});
|
|
805
|
-
server.listen(0, "127.0.0.1", () => {
|
|
806
|
-
const address = server.address();
|
|
807
|
-
if (address === null) {
|
|
808
|
-
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
resolveListener({
|
|
812
|
-
port: address.port,
|
|
813
|
-
received,
|
|
814
|
-
close: () => server.close()
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
function respond(response, status, title, detail) {
|
|
820
|
-
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
821
|
-
response.end(
|
|
822
|
-
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
823
|
-
`
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
827
|
-
let timer;
|
|
828
|
-
try {
|
|
829
|
-
return await Promise.race([
|
|
830
|
-
received,
|
|
831
|
-
new Promise((_, reject) => {
|
|
832
|
-
timer = setTimeout(
|
|
833
|
-
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
834
|
-
timeoutMs
|
|
835
|
-
);
|
|
836
|
-
})
|
|
837
|
-
]);
|
|
838
|
-
} finally {
|
|
839
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
928
|
+
return `${unit === 0 ? value : value.toFixed(1)} ${UNITS[unit]}`;
|
|
929
|
+
}
|
|
930
|
+
function shortenPath(path, width) {
|
|
931
|
+
if (path.length <= width) return path;
|
|
932
|
+
return `…${path.slice(path.length - width + 1)}`;
|
|
933
|
+
}
|
|
934
|
+
function describe(event, width = 80) {
|
|
935
|
+
switch (event.type) {
|
|
936
|
+
case "collecting":
|
|
937
|
+
return `Collected ${event.files.toLocaleString()} ${event.files === 1 ? "file" : "files"}`;
|
|
938
|
+
case "hashing":
|
|
939
|
+
return `Hashing ${event.done.toLocaleString()}/${event.total.toLocaleString()}`;
|
|
940
|
+
case "preparing":
|
|
941
|
+
return "Asking the server which files it already has";
|
|
942
|
+
case "uploading": {
|
|
943
|
+
const counted = `Uploading ${event.done.toLocaleString()}/${event.total.toLocaleString()}`;
|
|
944
|
+
const reused = event.reused === 0 ? "" : ` (${event.reused.toLocaleString()} already there)`;
|
|
945
|
+
const head = `${counted}${reused} · ${formatBytes(event.bytes)}`;
|
|
946
|
+
const room = width - head.length - 3;
|
|
947
|
+
return event.path === void 0 || room < 12 ? head : `${head} · ${shortenPath(event.path, room)}`;
|
|
948
|
+
}
|
|
949
|
+
case "completing":
|
|
950
|
+
return "Publishing";
|
|
951
|
+
// Both end the deploy, and the command prints the URL from the result it returns. Saying it here
|
|
952
|
+
// as well would print it twice, once on stderr and once on stdout.
|
|
953
|
+
case "done":
|
|
954
|
+
case "unchanged":
|
|
955
|
+
case "error":
|
|
956
|
+
return null;
|
|
840
957
|
}
|
|
841
958
|
}
|
|
842
|
-
function
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
959
|
+
function progressWriter(write, isTerminal, width = 80) {
|
|
960
|
+
let painted = 0;
|
|
961
|
+
let lastStage = null;
|
|
962
|
+
const writer = ((event) => {
|
|
963
|
+
const line = describe(event, width - 1);
|
|
964
|
+
if (line === null) return;
|
|
965
|
+
if (!isTerminal) {
|
|
966
|
+
if (event.type === lastStage) return;
|
|
967
|
+
lastStage = event.type;
|
|
968
|
+
write(`${line}
|
|
969
|
+
`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
lastStage = event.type;
|
|
973
|
+
write(`\r${line}${" ".repeat(Math.max(0, painted - line.length))}`);
|
|
974
|
+
painted = line.length;
|
|
855
975
|
});
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
let response;
|
|
861
|
-
try {
|
|
862
|
-
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
863
|
-
method: "POST",
|
|
864
|
-
headers: { "Content-Type": "application/json" },
|
|
865
|
-
body: JSON.stringify({ deviceCode })
|
|
866
|
-
});
|
|
867
|
-
} catch {
|
|
868
|
-
return { state: "pending" };
|
|
869
|
-
}
|
|
870
|
-
if (response.ok) {
|
|
871
|
-
const body = await response.json();
|
|
872
|
-
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
873
|
-
}
|
|
874
|
-
const problem = await response.json().catch(() => null);
|
|
875
|
-
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
876
|
-
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
877
|
-
return {
|
|
878
|
-
state: "dead",
|
|
879
|
-
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
976
|
+
writer.done = () => {
|
|
977
|
+
if (!isTerminal || painted === 0) return;
|
|
978
|
+
write(`\r${" ".repeat(painted)}\r`);
|
|
979
|
+
painted = 0;
|
|
880
980
|
};
|
|
881
|
-
|
|
882
|
-
async function problemDetail(response) {
|
|
883
|
-
const problem = await response.json().catch(() => null);
|
|
884
|
-
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
885
|
-
}
|
|
886
|
-
async function exchange(apiBaseUrl, code, verifier) {
|
|
887
|
-
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
888
|
-
method: "POST",
|
|
889
|
-
headers: { "Content-Type": "application/json" },
|
|
890
|
-
body: JSON.stringify({ code, verifier })
|
|
891
|
-
});
|
|
892
|
-
if (!response.ok) throw new Error(await problemDetail(response));
|
|
893
|
-
const body = await response.json();
|
|
894
|
-
return { token: body.token, name: body.name, email: body.email };
|
|
981
|
+
return writer;
|
|
895
982
|
}
|
|
896
983
|
function failure(text) {
|
|
897
984
|
return { text, json: { error: text }, code: 1 };
|
|
@@ -1035,14 +1122,14 @@ async function list() {
|
|
|
1035
1122
|
code: 0
|
|
1036
1123
|
};
|
|
1037
1124
|
}
|
|
1038
|
-
async function deployCommand(directory, site) {
|
|
1125
|
+
async function deployCommand(directory, site, report = silentProgress) {
|
|
1039
1126
|
const found = credentialsOr();
|
|
1040
1127
|
if ("result" in found) return found.result;
|
|
1041
1128
|
const project = readProject();
|
|
1042
1129
|
const target = site ?? project?.siteId;
|
|
1043
1130
|
const folder = directory ?? project?.dir ?? ".";
|
|
1044
1131
|
try {
|
|
1045
|
-
const result = await publishDirectory(found.credentials, resolve(folder), target);
|
|
1132
|
+
const result = await publishDirectory(found.credentials, resolve(folder), target, report);
|
|
1046
1133
|
const what = result.unchanged ? "Already up to date — nothing needed publishing." : `Published ${result.files.toLocaleString()} ${result.files === 1 ? "file" : "files"}.`;
|
|
1047
1134
|
return {
|
|
1048
1135
|
text: `${what}
|
|
@@ -1052,6 +1139,8 @@ ${result.url}`,
|
|
|
1052
1139
|
};
|
|
1053
1140
|
} catch (error) {
|
|
1054
1141
|
return failureFrom(error);
|
|
1142
|
+
} finally {
|
|
1143
|
+
report.done();
|
|
1055
1144
|
}
|
|
1056
1145
|
}
|
|
1057
1146
|
async function init(directory, site) {
|
|
@@ -1240,7 +1329,7 @@ async function run(argv) {
|
|
|
1240
1329
|
const positional = argv.filter((argument) => !argument.startsWith("-"));
|
|
1241
1330
|
const [command, ...rest] = positional;
|
|
1242
1331
|
if (argv.includes("--version")) {
|
|
1243
|
-
const { version } = await import("./package-
|
|
1332
|
+
const { version } = await import("./package-B5PgBI_i.js").then(
|
|
1244
1333
|
(module) => module.default
|
|
1245
1334
|
);
|
|
1246
1335
|
return { text: version, json: { version }, code: 0 };
|
|
@@ -1270,8 +1359,14 @@ async function dispatch(command, rest, site, json, device, confirmed) {
|
|
|
1270
1359
|
return logout();
|
|
1271
1360
|
case "init":
|
|
1272
1361
|
return await init(rest[0] ?? ".", site);
|
|
1273
|
-
case "deploy":
|
|
1274
|
-
|
|
1362
|
+
case "deploy": {
|
|
1363
|
+
const report = json ? silentProgress : progressWriter(
|
|
1364
|
+
(text) => process.stderr.write(text),
|
|
1365
|
+
process.stderr.isTTY === true,
|
|
1366
|
+
process.stderr.columns ?? 80
|
|
1367
|
+
);
|
|
1368
|
+
return await deployCommand(rest[0], site, report);
|
|
1369
|
+
}
|
|
1275
1370
|
case "ls":
|
|
1276
1371
|
case "list":
|
|
1277
1372
|
return await list();
|