landfall 0.5.3 → 0.5.4
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 +7 -3
- package/dist/cli.mjs +96 -89
- package/dist/index.html +109 -15
- package/dist/server-Cj2NicZ7.mjs +9 -0
- package/package.json +1 -1
- package/dist/server-BxTFpFmw.mjs +0 -9
package/dist/index.html
CHANGED
|
@@ -73,13 +73,32 @@ nav.servers { border-right: 1px solid var(--line); background: var(--bg); overfl
|
|
|
73
73
|
#navlist { padding: 0 8px 16px; }
|
|
74
74
|
nav h3 { margin: 14px 10px 4px; font-size: var(--fs-xs); color: var(--faint); font-weight: 500; display: flex; justify-content: space-between; }
|
|
75
75
|
nav h3 span { font-weight: 500; }
|
|
76
|
-
.srv { display:
|
|
76
|
+
.srv { display: grid; grid-template-columns: 8px minmax(0, 1fr); column-gap: 10px; align-items: center; width: 100%; text-align: left; border: 0; background: none; box-shadow: none; padding: 7px 10px; border-radius: var(--r-sm); }
|
|
77
|
+
.srv > .dot { margin: 0; }
|
|
78
|
+
.srv .n, .srv .t { grid-column: 2; }
|
|
79
|
+
.srv.off:not(.on) .n { color: var(--dim); }
|
|
80
|
+
.srv .t .refused { color: var(--warn); }
|
|
77
81
|
.srv:hover { background: var(--hover); }
|
|
78
82
|
.srv.on { background: var(--panel); box-shadow: 0 0 0 1px var(--line), var(--shadow); }
|
|
79
83
|
.srv.on .n { color: var(--text); }
|
|
80
84
|
.srv.on .t { color: var(--dim); }
|
|
81
85
|
.srv .n { font-weight: 550; display: flex; align-items: center; gap: 6px; }
|
|
82
|
-
.
|
|
86
|
+
.tile .n .dot { margin-right: 0; flex: none; }
|
|
87
|
+
nav h3 .grp { all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; }
|
|
88
|
+
nav h3 .grp:hover { color: var(--dim); }
|
|
89
|
+
nav h3 .grp:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
|
90
|
+
nav h3 .caret { width: 0; height: 0; border-left: 5px solid currentColor; border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: transform .15s; }
|
|
91
|
+
nav h3 .grp[aria-expanded="true"] .caret { transform: rotate(90deg); }
|
|
92
|
+
.nav-foot { position: sticky; bottom: 0; margin-top: auto; display: flex; align-items: center; gap: 8px; padding: 10px 12px 12px 18px; border-top: 1px solid var(--line); background: var(--bg); font-size: var(--fs-sm); }
|
|
93
|
+
.nav-foot .what { flex: 1; min-width: 0; color: var(--dim); }
|
|
94
|
+
.nav-foot .counts { display: flex; flex-wrap: wrap; gap: 4px 10px; color: var(--text); }
|
|
95
|
+
.nav-foot .counts .dot { margin-right: 5px; }
|
|
96
|
+
.nav-foot .when { color: var(--faint); font-size: var(--fs-xs); margin-top: 2px; }
|
|
97
|
+
.nav-foot .recheck { flex: none; display: inline-flex; align-items: center; gap: 6px; }
|
|
98
|
+
.nav-foot .recheck .spin { width: 10px; height: 10px; }
|
|
99
|
+
.srv .t { color: var(--faint); font-size: var(--fs-sm); display: flex; min-width: 0; white-space: nowrap; }
|
|
100
|
+
.srv .t .h { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
101
|
+
.srv .t .x { flex: none; white-space: pre; }
|
|
83
102
|
.nav-none { color: var(--faint); font-size: var(--fs-sm); padding: 6px 10px; }
|
|
84
103
|
.srv .t .live { color: var(--ok); }
|
|
85
104
|
/* The open server's sections, under it in the list. */
|
|
@@ -294,6 +313,7 @@ body.dragging::after {
|
|
|
294
313
|
<nav class="servers" id="nav">
|
|
295
314
|
<div class="nav-top"><input id="filter" type="search" placeholder="Filter servers" autocomplete="off" spellcheck="false" aria-label="Filter servers"><button type="button" id="add-server" title="Register an ssh alias or set up a fresh machine">+</button></div>
|
|
296
315
|
<div id="navlist"></div>
|
|
316
|
+
<div class="nav-foot" id="navfoot" hidden></div>
|
|
297
317
|
</nav>
|
|
298
318
|
<div class="server-pick" id="pick"></div>
|
|
299
319
|
<main id="main"></main>
|
|
@@ -332,6 +352,9 @@ const S = {
|
|
|
332
352
|
dns: {}, // name -> {data: {ips, domains: [{domain, ips, ok, https}]}}
|
|
333
353
|
open: new Set(), // expanded site rows
|
|
334
354
|
filter: "", // server list filter
|
|
355
|
+
reach: {}, // name -> {state: ok|login|down, detail, ms} from /api/reach
|
|
356
|
+
reaching: false,
|
|
357
|
+
reachedAt: null, // when the last check finished
|
|
335
358
|
jobs: new Map(), // id -> {info, lines, after, actions, es}
|
|
336
359
|
job: null, // shown job id
|
|
337
360
|
};
|
|
@@ -512,6 +535,30 @@ async function refreshState() {
|
|
|
512
535
|
S.state = await http("/api/state");
|
|
513
536
|
render();
|
|
514
537
|
}
|
|
538
|
+
/** Which servers take landfall's ssh login; the server caches each answer for 30s unless fresh. */
|
|
539
|
+
async function loadReach(fresh = false) {
|
|
540
|
+
if (S.reaching) return;
|
|
541
|
+
S.reaching = true;
|
|
542
|
+
renderNav();
|
|
543
|
+
try {
|
|
544
|
+
S.reach = await http(`/api/reach${fresh ? "?fresh=1" : ""}`);
|
|
545
|
+
S.reachedAt = new Date();
|
|
546
|
+
} catch (e) {
|
|
547
|
+
toast(`Checking servers failed: ${e.message}`);
|
|
548
|
+
} finally {
|
|
549
|
+
S.reaching = false;
|
|
550
|
+
// The home tiles show the dots too; an open server's view stays as it is.
|
|
551
|
+
S.sel ? renderNav() : render();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const REACH_DOT = { ok: ["up", "reachable"], login: ["warn", "login refused"], down: ["down", "unreachable"] };
|
|
555
|
+
function reachDot(n) {
|
|
556
|
+
const r = S.reach[n];
|
|
557
|
+
if (!r) return `<span class="dot" role="img" aria-label="not checked yet" title="${S.reaching ? "Checking…" : "Not checked yet"}"></span>`;
|
|
558
|
+
const [cls, word] = REACH_DOT[r.state];
|
|
559
|
+
const tip = r.state === "ok" ? `Key login works (${r.ms} ms)` : r.state === "login" ? `sshd answers but refuses the login: ${r.detail}` : `Unreachable: ${r.detail}`;
|
|
560
|
+
return `<span class="dot ${cls}" role="img" aria-label="${word}" title="${esc(tip)}"></span>`;
|
|
561
|
+
}
|
|
515
562
|
|
|
516
563
|
// ---- jobs ----------------------------------------------------------------
|
|
517
564
|
|
|
@@ -1322,31 +1369,75 @@ function makeDefault(n) {
|
|
|
1322
1369
|
|
|
1323
1370
|
function renderNav() {
|
|
1324
1371
|
const st = S.state;
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1372
|
+
// Reachable first, then refused, then down; unchecked last. sort() is stable, so ties keep config order.
|
|
1373
|
+
const list = st.servers.slice().sort((a, b) => reachRank(a.name) - reachRank(b.name));
|
|
1374
|
+
// Down servers sit in their own group, closed until opened, so the rest stay in view.
|
|
1375
|
+
const isDown = (s) => S.reach[s.name]?.state === "down";
|
|
1376
|
+
const own = list.filter((s) => s.registered && !isDown(s));
|
|
1377
|
+
const aliases = list.filter((s) => !s.registered && !isDown(s));
|
|
1378
|
+
const down = list.filter(isDown);
|
|
1328
1379
|
const q = S.filter.toLowerCase();
|
|
1329
1380
|
const hit = (s) => !q || s.name.toLowerCase().includes(q) || s.target.toLowerCase().includes(q) || s.name === S.sel;
|
|
1330
1381
|
const item = (s) => {
|
|
1331
1382
|
const tunnels = openTunnels(s.name).length;
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1383
|
+
const r = S.reach[s.name];
|
|
1384
|
+
const why = r?.state === "login" ? `<span class="x"> · <span class="refused">login refused</span></span>` : "";
|
|
1385
|
+
const head = `<button type="button" class="srv ${s.name === S.sel ? "on" : ""} ${r?.state === "down" ? "off" : ""}" data-f="srv:${esc(s.name)}" ${on(() => go(s.name, S.sel === s.name ? S.tab : "sites"))}>
|
|
1386
|
+
${reachDot(s.name)}<div class="n">${esc(s.name)}${s.isDefault ? `<span class="star" title="default server">★</span>` : ""}</div>
|
|
1387
|
+
<div class="t"><span class="h">${esc(s.target)}</span>${why}${tunnels ? `<span class="x"> · <span class="live">${plural(tunnels, "tunnel")}</span></span>` : ""}</div></button>`;
|
|
1335
1388
|
return s.name === S.sel ? `${head}<div class="sections" role="list" aria-label="${esc(s.name)}">${sectionButtons(s.name, "sec")}</div>` : head;
|
|
1336
1389
|
};
|
|
1337
|
-
const group = (title, l) => {
|
|
1390
|
+
const group = (key, title, l) => {
|
|
1391
|
+
if (!l.length) return "";
|
|
1338
1392
|
const shown = l.filter(hit);
|
|
1339
|
-
|
|
1393
|
+
// A closed group still shows the open server; a filter opens every group.
|
|
1394
|
+
const closed = navClosed.has(key) && !q;
|
|
1395
|
+
const rows = closed ? shown.filter((s) => s.name === S.sel) : shown;
|
|
1396
|
+
return `<h3><button type="button" class="grp" aria-expanded="${!closed}" ${on(() => toggleGroup(key))}><span class="caret" aria-hidden="true"></span>${title}</button><span>${q ? `${shown.length}/` : ""}${l.length}</span></h3>
|
|
1397
|
+
${rows.map(item).join("") || (closed ? "" : `<div class="nav-none">no match</div>`)}`;
|
|
1340
1398
|
};
|
|
1341
1399
|
$("#filter").hidden = list.length < 8;
|
|
1342
1400
|
$("#add-server").textContent = list.length < 8 ? "+ Add server" : "+";
|
|
1343
|
-
$("#navlist").innerHTML = `${group("Servers", own)}${group("From ~/.ssh/config", aliases)}
|
|
1401
|
+
$("#navlist").innerHTML = `${group("own", "Servers", own)}${group("ssh", "From ~/.ssh/config", aliases)}${group("down", "Unreachable", down)}
|
|
1344
1402
|
${list.length ? "" : `<div class="nav-none">No servers yet.</div>`}`;
|
|
1403
|
+
$("#navfoot").hidden = !list.length;
|
|
1404
|
+
$("#navfoot").innerHTML = reachSummary(list);
|
|
1345
1405
|
$("#pick").innerHTML = `<select aria-label="Server" onchange="go(this.value || null)">
|
|
1346
|
-
<option value="">Pick a server…</option>${list.map((s) => `<option value="${esc(s.name)}" ${s.name === S.sel ? "selected" : ""}>${esc(s.name)} · ${esc(s.target)}</option>`).join("")}
|
|
1406
|
+
<option value="">Pick a server…</option>${list.map((s) => `<option value="${esc(s.name)}" ${s.name === S.sel ? "selected" : ""}>${esc(s.name)} · ${esc(s.target)}${S.reach[s.name] && S.reach[s.name].state !== "ok" ? ` · ${REACH_DOT[S.reach[s.name].state][1]}` : ""}</option>`).join("")}
|
|
1347
1407
|
</select>`;
|
|
1348
1408
|
}
|
|
1349
1409
|
|
|
1410
|
+
const reachRank = (n) => ({ ok: 0, login: 1, down: 2 })[S.reach[n]?.state] ?? 3;
|
|
1411
|
+
|
|
1412
|
+
/** The sidebar's foot: what the login check is doing, or what it found and when. */
|
|
1413
|
+
function reachSummary(list) {
|
|
1414
|
+
const btnHtml = `<button type="button" class="sm ghost recheck" ${S.reaching ? "disabled" : ""} title="Try landfall's ssh login on every server again (lf server list --check)" ${on(() => loadReach(true))}><span class="${S.reaching ? "spin" : "ico"}" aria-hidden="true">${S.reaching ? "" : "↻"}</span>${S.reaching ? "Checking" : "Check"}</button>`;
|
|
1415
|
+
if (S.reaching) return `<div class="what" role="status">Trying ssh login on ${plural(list.length, "server")}…</div>${btnHtml}`;
|
|
1416
|
+
if (!S.reachedAt) return `<div class="what">Not checked yet</div>${btnHtml}`;
|
|
1417
|
+
const count = { ok: 0, login: 0, down: 0 };
|
|
1418
|
+
for (const s of list) if (S.reach[s.name]) count[S.reach[s.name].state]++;
|
|
1419
|
+
const part = (k, word) => (count[k] ? `<span title="${esc(word)}"><span class="dot ${REACH_DOT[k][0]}"></span>${count[k]} ${word}</span>` : "");
|
|
1420
|
+
const at = S.reachedAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1421
|
+
return `<div class="what" role="status"><div class="counts">${part("ok", "reachable")}${part("login", "refused")}${part("down", "down")}</div>
|
|
1422
|
+
<div class="when">checked ${at} · again every minute</div></div>${btnHtml}`;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Unreachable starts folded on every load; only the other groups remember their state.
|
|
1426
|
+
const navClosed = new Set([...(() => {
|
|
1427
|
+
try {
|
|
1428
|
+
return JSON.parse(localStorage.getItem("lf.navClosed") ?? "[]");
|
|
1429
|
+
} catch {
|
|
1430
|
+
return [];
|
|
1431
|
+
}
|
|
1432
|
+
})(), "down"]);
|
|
1433
|
+
function toggleGroup(key) {
|
|
1434
|
+
navClosed.has(key) ? navClosed.delete(key) : navClosed.add(key);
|
|
1435
|
+
try {
|
|
1436
|
+
localStorage.setItem("lf.navClosed", JSON.stringify([...navClosed].filter((k) => k !== "down")));
|
|
1437
|
+
} catch {}
|
|
1438
|
+
renderNav();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1350
1441
|
/** The open server's sections as buttons, each with its count. */
|
|
1351
1442
|
function sectionButtons(n, f) {
|
|
1352
1443
|
const o = S.overview[n]?.data;
|
|
@@ -1479,14 +1570,14 @@ function renderMain() {
|
|
|
1479
1570
|
/** No server open: the servers to pick from, and how the same works in a terminal. */
|
|
1480
1571
|
function homeView() {
|
|
1481
1572
|
const list = S.state.servers;
|
|
1482
|
-
const own = list.filter((s) => s.registered);
|
|
1573
|
+
const own = list.filter((s) => s.registered).sort((a, b) => reachRank(a.name) - reachRank(b.name));
|
|
1483
1574
|
const tile = (s) => `<button type="button" class="tile" ${on(() => go(s.name, "sites"))}>
|
|
1484
|
-
<div class="n">${esc(s.name)}${s.isDefault ? `<span class="star" title="default server">★</span>` : ""}</div>
|
|
1575
|
+
<div class="n">${reachDot(s.name)}${esc(s.name)}${s.isDefault ? `<span class="star" title="default server">★</span>` : ""}</div>
|
|
1485
1576
|
<div class="t">${esc(s.target)}</div></button>`;
|
|
1486
1577
|
const def = list.find((s) => s.isDefault)?.name;
|
|
1487
1578
|
return `<div class="home">
|
|
1488
1579
|
<h1>${own.length ? "Your servers" : "Pick a server"}</h1>
|
|
1489
|
-
<p>landfall connects
|
|
1580
|
+
<p>The dot shows whether landfall's ssh login works (hover for why not); nothing else connects until you open one. Servers are ssh targets: any ~/.ssh/config alias works${own.length < list.length ? ` (${list.length - own.length} in the list on the left)` : ""}.</p>
|
|
1490
1581
|
<div class="grid">${own.map(tile).join("")}<button type="button" class="tile add" ${on(addServer)}>+ Add server</button></div>
|
|
1491
1582
|
<div class="cli">Everything here runs an <span class="mono">lf</span> command, shown above its output, so you can do the same in a terminal:
|
|
1492
1583
|
<pre>lf deploy ./dist -d blog <span># a folder, Dockerfile project or image, with HTTPS</span>
|
|
@@ -2135,11 +2226,14 @@ $("#filter").addEventListener("input", (e) => {
|
|
|
2135
2226
|
renderNav();
|
|
2136
2227
|
});
|
|
2137
2228
|
$("#add-server").onclick = () => addServer();
|
|
2229
|
+
// Servers go up and down: check again each minute while the page is visible.
|
|
2230
|
+
setInterval(() => document.hidden || loadReach(), 60_000);
|
|
2138
2231
|
$("#app-settings").onclick = () => defaultsDialog();
|
|
2139
2232
|
|
|
2140
2233
|
(async () => {
|
|
2141
2234
|
try {
|
|
2142
2235
|
await refreshState();
|
|
2236
|
+
loadReach();
|
|
2143
2237
|
// Jobs started before a reload keep running on the server: list them, and show one still running.
|
|
2144
2238
|
const old = [];
|
|
2145
2239
|
for (const info of S.state.jobs.slice().reverse()) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{A as e,C as t,D as n,E as r,F as i,M as a,N as o,O as s,P as c,S as l,T as u,_ as d,a as ee,b as te,c as ne,d as f,f as re,g as ie,h as p,i as m,j as h,k as g,l as _,m as v,n as y,o as ae,p as oe,r as se,s as ce,t as b,u as le,v as ue,w as de,x as fe,y as pe}from"./cli.mjs";import{spawn as me,spawnSync as x}from"node:child_process";import S from"node:fs";import{homedir as C}from"node:os";import{dirname as w,isAbsolute as he,join as T,resolve as E}from"node:path";import{createHash as D,randomBytes as O,timingSafeEqual as k}from"node:crypto";import{createServer as A}from"node:http";import{Readable as j,Transform as M}from"node:stream";function N(e){let t=E(!e||e===`~`?C():g(e)),n=S.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&P(T(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:w(t)===t?null:w(t),home:C(),dirs:n,detected:p(t)}}const P=e=>{try{return S.statSync(e).isDirectory()}catch{return!1}},F=T(s,`uploads`),I=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function L(e,t){S.mkdirSync(w(e),{recursive:!0}),S.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function R(e){S.mkdirSync(F,{recursive:!0,mode:448});for(let e of S.readdirSync(F)){let t=T(F,e);Date.now()-S.statSync(t).mtimeMs>864e5&&S.rmSync(t,{recursive:!0,force:!0})}let t=S.mkdtempSync(T(F,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=T(t,`.upload-`+I(n[0].name).pop());await L(e,n[0]);let r=T(t,`files`);S.mkdirSync(r);let i=/\.zip$/i.test(e);if(ge(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let a=i?x(`unzip`,[`-q`,e,`-d`,r]):x(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?x(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return S.rmSync(e),_e(r),{path:z(r),files:ve(r)}}let i=T(t,`files`);for(let[e,t]of n.entries()){let n=I(r[e]||t.name);n.length&&await L(T(i,...n),t)}return{path:z(i),files:n.length}}function ge(e,t){let n=(e,t)=>{let n=x(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
|
|
2
|
+
`).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function _e(e){for(let t of S.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&S.rmSync(T(t.parentPath,t.name),{force:!0})}function z(e){for(;;){let t=S.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!P(T(e,t[0])))return e;e=T(e,t[0])}}function ve(e){return S.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function ye(e){if(e!=null&&e!==``){if(typeof e!=`string`||!he(e)||!P(e))throw Error(`cwd must be an existing folder`);return e}}const B=()=>[process.execPath,...process.execArgv,process.argv[1]],V=5e3,H=1e4,U=e=>e.length>H?e.slice(0,H)+`…`:e;function be(e,t,n={}){let r={...process.env,NO_COLOR:`1`,LANDFALL_REPORT:`1`,...n.env};n.password&&Object.assign(r,_(n.password));let i=me(B()[0],[...B().slice(1),...e],{stdio:[n.stdin===void 0?`ignore`:`pipe`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0});i.stdin?.on(`error`,()=>{}).end(n.stdin);let a={argv:e,lines:[],dropped:0,report:{},stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
3
|
+
`);e=n.pop(),e.length>H&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(a.report,JSON.parse(e.slice(1)))}catch{}else a.lines.push(U(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));a.lines.length>V&&(a.dropped+=a.lines.splice(0,a.lines.length-V).length),s()},flush:()=>e&&a.lines.push(U(e))}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const xe=S.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),Se=512*1024**2,Ce=1024**2,W=/* @__PURE__ */ new Map,we=/* @__PURE__ */ new Set([`ui`,`web`]);function Te(){let e=T(w(u),`ui-token`);try{let t=S.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=O(24).toString(`hex`);return S.mkdirSync(w(e),{recursive:!0,mode:448}),S.writeFileSync(e,t+`
|
|
4
|
+
`,{mode:384}),t}const G=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),K=(e,t=500)=>G({error:e?.message??String(e)},t);function q(e,t){let n=d(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=Ee(e),s=be(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:le(e),job:s,listeners:r,opts:t};W.set(n,c);for(let[e,t]of W){if(W.size<=50)break;t.job.code!==void 0&&W.delete(e)}return c}const J=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,Y=/^[A-Za-z_][A-Za-z0-9_]*=/;function Ee(e){let t=[],n={},r=[];for(let i=0;i<e.length;i++){let a=e[i],s=a.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);s&&(s[2]!==void 0||i+1<e.length)?n[o(s[1])]=s[2]??e[++i]:J(e)&&Y.test(a)&&!a.includes(`
|
|
5
|
+
`)&&!e.includes(`--env-file`)?r.push(a):t.push(a)}return r.length&&t.push(`--env-file`,`-`),{args:t,env:n,stdin:r.length?r.join(`
|
|
6
|
+
`)+`
|
|
7
|
+
`:void 0}}const X=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:J(e)&&Y.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),Z=e=>({id:e.id,argv:X(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function De(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Z(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
8
|
+
|
|
9
|
+
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function Oe(i,o){let[s,c,u]=o,d=i.method;if(s===`state`&&d===`GET`){let r=e();return G({version:b,cwd:process.cwd(),project:h(process.cwd())??null,projects:ie(),defaultImage:pe,rules:{loginName:n.source,minPassword:8},paths:{root:l,sites:t,traefik:de,journal:te,proxy:fe},defaults:y(),servers:f().map(e=>({...e,target:oe(e.name),saved:r.servers[e.name]??null})),jobs:[...W.values()].reverse().map(Z)})}if(s===`reach`&&d===`GET`)return G(await re(f().map(e=>e.name),new URL(i.url).searchParams.get(`fresh`)===`1`));if(s===`servers`&&c){let{server:e}=a(c);if(d===`GET`&&!u)return G(await v(e));if(d===`GET`&&u===`firewall`)return G(await ae(e));if(d===`GET`&&u===`config`)return G(await se(c,e));if(d===`GET`&&u===`history`)return G(await ce(e));if(d===`GET`&&u===`facts`)return G(await ee(c,e));if(d===`GET`&&u===`updates`)return G(await ne(e,new URL(i.url).searchParams.get(`refresh`)===`1`));if(d===`GET`&&u===`dns`){let t=(new URL(i.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>r.test(e)).slice(0,50);return G(await m(e,t))}}if(s===`jobs`){if(d===`POST`&&!c){let e=await i.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||we.has(t[0]))return K(`argv must be a list of landfall arguments`,400);if([...W.values()].filter(e=>e.job.code===void 0).length>=20)return K(`20 jobs are already running: wait for one to finish or stop one`,429);let n;try{n=ye(e.cwd)}catch(e){return K(e,400)}return G(Z(q(t,{server:e.server,password:e.password||void 0,cwd:n})))}let e=c?W.get(c):void 0;if(c&&!e)return K(`no job ${c}`,404);if(e&&d===`GET`&&u===`events`)return De(e,Number(new URL(i.url).searchParams.get(`from`))||0,i.signal);if(e&&d===`GET`&&!u)return G({...Z(e),lines:e.job.lines,from:e.job.dropped});if(e&&d===`POST`&&u===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?K(`only a finished job without --force`,400):[...W.values()].filter(e=>e.job.code===void 0).length>=20?K(`too many jobs running`,429):G(Z(q([...e.argv,`--force`],e.opts)));if(e&&d===`POST`&&u===`stop`)return e.job.stop(),G(Z(e))}if(s===`fs`&&d===`GET`)return G(N(new URL(i.url).searchParams.get(`path`)??void 0));if(s===`upload`&&d===`POST`){let e=await R(await i.formData());return G({...e,detected:N(e.path).detected})}return K(`not found`,404)}const Q=(e,t)=>e.length===t.length&&k(Buffer.from(e),Buffer.from(t)),ke=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>D(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function Ae(e){ue(!0);let t=Te(),n=A(Me(async e=>{let n=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(ke(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(xe,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await Oe(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return K(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),a=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),o=e.port??4777,s=e.port===void 0?[...Array.from({length:20},(e,t)=>o+t),0]:[o],l;for(let n of s){if(!(l=await r(n)))break;if(l.code!==`EADDRINUSE`)throw l;if(await a(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${c.cyan(r)}`),e.open&&i(r),0}}if(l)return console.error(`${c.red(`error:`)} port ${o} is in use by another program; pick another with --port`),1;let u=n.address().port;u!==o&&console.log(c.dim(`port ${o} is in use by another program: using ${u}`));let d=`http://127.0.0.1:${u}/login?t=${t}`;return console.log(`${c.bold(`lf ui`)} ${c.dim(b)} on ${c.cyan(d)}`),console.log(c.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&i(d),new Promise(e=>{let t=()=>{for(let e of W.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function je(e,t){let n=0;return e.pipe(new M({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function Me(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?Se:Ce;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:j.toWeb(je(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=j.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{Ae as webUi};
|
package/package.json
CHANGED
package/dist/server-BxTFpFmw.mjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import{A as e,C as t,D as n,E as r,M as i,N as a,O as o,P as s,S as c,T as l,_ as u,a as d,b as f,c as ee,d as te,f as ne,g as re,h as ie,i as p,j as ae,k as oe,l as se,m as ce,n as m,o as h,p as le,r as ue,s as de,t as g,u as _,v,w as y,x as b,y as fe}from"./cli.mjs";import{spawn as pe,spawnSync as x}from"node:child_process";import S from"node:fs";import{homedir as C}from"node:os";import{dirname as w,isAbsolute as me,join as T,resolve as E}from"node:path";import{createHash as D,randomBytes as O,timingSafeEqual as k}from"node:crypto";import{createServer as A}from"node:http";import{Readable as j,Transform as M}from"node:stream";function N(e){let t=E(!e||e===`~`?C():o(e)),n=S.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&P(T(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:w(t)===t?null:w(t),home:C(),dirs:n,detected:ce(t)}}const P=e=>{try{return S.statSync(e).isDirectory()}catch{return!1}},F=T(n,`uploads`),I=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function L(e,t){S.mkdirSync(w(e),{recursive:!0}),S.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function R(e){S.mkdirSync(F,{recursive:!0,mode:448});for(let e of S.readdirSync(F)){let t=T(F,e);Date.now()-S.statSync(t).mtimeMs>864e5&&S.rmSync(t,{recursive:!0,force:!0})}let t=S.mkdtempSync(T(F,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=T(t,`.upload-`+I(n[0].name).pop());await L(e,n[0]);let r=T(t,`files`);S.mkdirSync(r);let i=/\.zip$/i.test(e);if(z(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let a=i?x(`unzip`,[`-q`,e,`-d`,r]):x(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?x(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return S.rmSync(e),he(r),{path:B(r),files:ge(r)}}let i=T(t,`files`);for(let[e,t]of n.entries()){let n=I(r[e]||t.name);n.length&&await L(T(i,...n),t)}return{path:B(i),files:n.length}}function z(e,t){let n=(e,t)=>{let n=x(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
|
|
2
|
-
`).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function he(e){for(let t of S.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&S.rmSync(T(t.parentPath,t.name),{force:!0})}function B(e){for(;;){let t=S.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!P(T(e,t[0])))return e;e=T(e,t[0])}}function ge(e){return S.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function _e(e){if(e!=null&&e!==``){if(typeof e!=`string`||!me(e)||!P(e))throw Error(`cwd must be an existing folder`);return e}}const V=()=>[process.execPath,...process.execArgv,process.argv[1]],H=5e3,U=1e4,W=e=>e.length>U?e.slice(0,U)+`…`:e;function ve(e,t,n={}){let r={...process.env,NO_COLOR:`1`,LANDFALL_REPORT:`1`,...n.env};n.password&&Object.assign(r,se(n.password));let i=pe(V()[0],[...V().slice(1),...e],{stdio:[n.stdin===void 0?`ignore`:`pipe`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0});i.stdin?.on(`error`,()=>{}).end(n.stdin);let a={argv:e,lines:[],dropped:0,report:{},stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
3
|
-
`);e=n.pop(),e.length>U&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(a.report,JSON.parse(e.slice(1)))}catch{}else a.lines.push(W(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));a.lines.length>H&&(a.dropped+=a.lines.splice(0,a.lines.length-H).length),s()},flush:()=>e&&a.lines.push(W(e))}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const ye=S.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),be=512*1024**2,xe=1024**2,G=/* @__PURE__ */ new Map,Se=/* @__PURE__ */ new Set([`ui`,`web`]);function Ce(){let e=T(w(y),`ui-token`);try{let t=S.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=O(24).toString(`hex`);return S.mkdirSync(w(e),{recursive:!0,mode:448}),S.writeFileSync(e,t+`
|
|
4
|
-
`,{mode:384}),t}const K=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),q=(e,t=500)=>K({error:e?.message??String(e)},t);function J(e,t){let n=re(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=we(e),s=ve(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:_(e),job:s,listeners:r,opts:t};G.set(n,c);for(let[e,t]of G){if(G.size<=50)break;t.job.code!==void 0&&G.delete(e)}return c}const Y=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,X=/^[A-Za-z_][A-Za-z0-9_]*=/;function we(e){let t=[],n={},r=[];for(let a=0;a<e.length;a++){let o=e[a],s=o.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);s&&(s[2]!==void 0||a+1<e.length)?n[i(s[1])]=s[2]??e[++a]:Y(e)&&X.test(o)&&!o.includes(`
|
|
5
|
-
`)&&!e.includes(`--env-file`)?r.push(o):t.push(o)}return r.length&&t.push(`--env-file`,`-`),{args:t,env:n,stdin:r.length?r.join(`
|
|
6
|
-
`)+`
|
|
7
|
-
`:void 0}}const Te=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:Y(e)&&X.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),Z=e=>({id:e.id,argv:Te(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function Ee(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Z(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
8
|
-
|
|
9
|
-
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function De(n,i){let[a,o,s]=i,u=n.method;if(a===`state`&&u===`GET`){let n=oe();return K({version:g,cwd:process.cwd(),project:e(process.cwd())??null,projects:ie(),defaultImage:v,rules:{loginName:r.source,minPassword:8},paths:{root:b,sites:c,traefik:t,journal:fe,proxy:f},defaults:m(),servers:te().map(e=>({...e,target:ne(e.name),saved:n.servers[e.name]??null})),jobs:[...G.values()].reverse().map(Z)})}if(a===`servers`&&o){let{server:e}=ae(o);if(u===`GET`&&!s)return K(await le(e));if(u===`GET`&&s===`firewall`)return K(await h(e));if(u===`GET`&&s===`config`)return K(await ue(o,e));if(u===`GET`&&s===`history`)return K(await de(e));if(u===`GET`&&s===`facts`)return K(await d(o,e));if(u===`GET`&&s===`updates`)return K(await ee(e,new URL(n.url).searchParams.get(`refresh`)===`1`));if(u===`GET`&&s===`dns`){let t=(new URL(n.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>l.test(e)).slice(0,50);return K(await p(e,t))}}if(a===`jobs`){if(u===`POST`&&!o){let e=await n.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||Se.has(t[0]))return q(`argv must be a list of landfall arguments`,400);if([...G.values()].filter(e=>e.job.code===void 0).length>=20)return q(`20 jobs are already running: wait for one to finish or stop one`,429);let r;try{r=_e(e.cwd)}catch(e){return q(e,400)}return K(Z(J(t,{server:e.server,password:e.password||void 0,cwd:r})))}let e=o?G.get(o):void 0;if(o&&!e)return q(`no job ${o}`,404);if(e&&u===`GET`&&s===`events`)return Ee(e,Number(new URL(n.url).searchParams.get(`from`))||0,n.signal);if(e&&u===`GET`&&!s)return K({...Z(e),lines:e.job.lines,from:e.job.dropped});if(e&&u===`POST`&&s===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?q(`only a finished job without --force`,400):[...G.values()].filter(e=>e.job.code===void 0).length>=20?q(`too many jobs running`,429):K(Z(J([...e.argv,`--force`],e.opts)));if(e&&u===`POST`&&s===`stop`)return e.job.stop(),K(Z(e))}if(a===`fs`&&u===`GET`)return K(N(new URL(n.url).searchParams.get(`path`)??void 0));if(a===`upload`&&u===`POST`){let e=await R(await n.formData());return K({...e,detected:N(e.path).detected})}return q(`not found`,404)}const Q=(e,t)=>e.length===t.length&&k(Buffer.from(e),Buffer.from(t)),Oe=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>D(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function ke(e){u(!0);let t=Ce(),n=A(je(async e=>{let n=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(Oe(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(ye,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await De(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return q(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),i=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),o=e.port??4777,c=e.port===void 0?[...Array.from({length:20},(e,t)=>o+t),0]:[o],l;for(let n of c){if(!(l=await r(n)))break;if(l.code!==`EADDRINUSE`)throw l;if(await i(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${a.cyan(r)}`),e.open&&s(r),0}}if(l)return console.error(`${a.red(`error:`)} port ${o} is in use by another program; pick another with --port`),1;let d=n.address().port;d!==o&&console.log(a.dim(`port ${o} is in use by another program: using ${d}`));let f=`http://127.0.0.1:${d}/login?t=${t}`;return console.log(`${a.bold(`lf ui`)} ${a.dim(g)} on ${a.cyan(f)}`),console.log(a.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&s(f),new Promise(e=>{let t=()=>{for(let e of G.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function Ae(e,t){let n=0;return e.pipe(new M({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function je(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?be:xe;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:j.toWeb(Ae(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=j.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{ke as webUi};
|