landfall 0.1.0 → 0.2.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 +53 -35
- package/dist/cli.mjs +186 -143
- package/dist/index.html +30 -12
- package/dist/{job-CypEf1ud.mjs → job-i5GF9h93.mjs} +2 -2
- package/dist/server-CqBtraHg.mjs +4 -0
- package/dist/tui-BDvEukfL.mjs +8 -0
- package/package.json +1 -1
- package/dist/server-CkG3c5Za.mjs +0 -4
- package/dist/tui-B0KECXvO.mjs +0 -8
package/dist/index.html
CHANGED
|
@@ -617,7 +617,7 @@ function addServer() {
|
|
|
617
617
|
fields: [
|
|
618
618
|
{ name: "user", label: "User", placeholder: "root" },
|
|
619
619
|
{ name: "port", label: "SSH port", placeholder: "22" },
|
|
620
|
-
{ name: "key", label: "Private key file (optional)", placeholder: "~/.ssh/id_ed25519", hint: "Without one, landfall creates ~/.ssh/
|
|
620
|
+
{ name: "key", label: "Private key file (optional)", placeholder: "~/.ssh/id_ed25519", hint: "Without one, landfall creates ~/.ssh/landfall_<name> and installs it with the password." },
|
|
621
621
|
{ name: "password", label: "Password (optional)", type: "password", hint: "Used once to install the key, never stored. Leave empty if the key already works." },
|
|
622
622
|
],
|
|
623
623
|
},
|
|
@@ -656,7 +656,7 @@ const tilde = (p) => p.replace(/^\/(Users|home)\/[^/]+/, "~");
|
|
|
656
656
|
function describe(d) {
|
|
657
657
|
const built = d.build ? ` · builds with <code>${esc(d.build)}</code> first` : "";
|
|
658
658
|
const what =
|
|
659
|
-
d.kind === "image" ? `Docker image <code>${esc(d.project?.image ?? "")}</code> (from
|
|
659
|
+
d.kind === "image" ? `Docker image <code>${esc(d.project?.image ?? "")}</code> (from .landfall.json)`
|
|
660
660
|
: d.kind === "dockerfile" ? `Dockerfile: built on the server, served on port ${esc(d.port)}`
|
|
661
661
|
: d.dir ? `Static files from <code>${esc(d.dir === "." ? "./" : d.dir + "/")}</code>${d.spa ? " (single-page app)" : ""}${built}`
|
|
662
662
|
: d.build ? `Static site, built with <code>${esc(d.build)}</code> first`
|
|
@@ -665,7 +665,7 @@ function describe(d) {
|
|
|
665
665
|
return what + before;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
/** Deploy a local folder (det from /api/fs or an upload). upload: a temporary folder, so no
|
|
668
|
+
/** Deploy a local folder (det from /api/fs or an upload). upload: a temporary folder, so no .landfall.json. */
|
|
669
669
|
function deployDialog(det, o = {}) {
|
|
670
670
|
const p = det.project;
|
|
671
671
|
const servers = S.state.servers.map((s) => ({ value: s.name, label: s.name }));
|
|
@@ -674,7 +674,7 @@ function deployDialog(det, o = {}) {
|
|
|
674
674
|
const main = mainDomain(target);
|
|
675
675
|
dialog({
|
|
676
676
|
title: `Deploy ${name}`,
|
|
677
|
-
text: o.upload ? `Uploaded ${plural(o.files ?? 0, "file")}. Not remembered: drop it again to redeploy.` : `From ${tilde(det.path)}${p ? " (settings from
|
|
677
|
+
text: o.upload ? `Uploaded ${plural(o.files ?? 0, "file")}. Not remembered: drop it again to redeploy.` : `From ${tilde(det.path)}${p ? " (settings from .landfall.json)" : ""}`,
|
|
678
678
|
extra: `<div class="detected">${describe(det)}</div>`,
|
|
679
679
|
fields: [
|
|
680
680
|
{ name: "name", label: "Site name", value: name },
|
|
@@ -683,7 +683,7 @@ function deployDialog(det, o = {}) {
|
|
|
683
683
|
name: "domains", label: "Domains", value: (p?.domains ?? []).join(" "),
|
|
684
684
|
placeholder: autoDomain(target, name) || "example.com www.example.com",
|
|
685
685
|
hint: main
|
|
686
|
-
? `Empty: <name>.${main}. A bare name like blog means blog.${main}. Other domains need an A record pointing at the server.`
|
|
686
|
+
? `Empty: <name>.${main}. A bare name like blog means blog.${main}, @ means ${main} itself (another site serving it hands it over). Other domains need an A record pointing at the server.`
|
|
687
687
|
: "Empty: a free <name>.<ip>.sslip.io address, no DNS needed. Own domains need an A record pointing at the server.",
|
|
688
688
|
},
|
|
689
689
|
...(det.kind === "static"
|
|
@@ -833,6 +833,8 @@ function copy(text) {
|
|
|
833
833
|
navigator.clipboard?.writeText(text).then(() => toast(`Copied ${text}`), () => toast(text)) ?? toast(text);
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
const servedByOther = (n, site, d) => S.overview[n]?.data?.sites.find((s) => s.meta.name !== site && s.meta.domains.includes(d))?.meta.name;
|
|
837
|
+
|
|
836
838
|
function domainsDialog(n, m) {
|
|
837
839
|
const main = mainDomain(n);
|
|
838
840
|
const free = autoDomain(n, m.name);
|
|
@@ -842,12 +844,14 @@ function domainsDialog(n, m) {
|
|
|
842
844
|
text: `Each domain needs an A record pointing at ${ips.join(", ") || "the server"}. Traefik fetches the certificate once it resolves.`,
|
|
843
845
|
fields: [
|
|
844
846
|
{ name: "domains", label: "Domains (space separated)", value: m.domains.join(" ") },
|
|
847
|
+
...(main && !m.domains.includes(main) ? [{ name: "apex", label: `Also serve ${main}`, type: "check", value: false, hint: servedByOther(n, m.name, main) ? `${servedByOther(n, m.name, main)} serves it now and hands it over.` : "The main domain itself." }] : []),
|
|
845
848
|
...(free && !m.domains.includes(free) ? [{ name: "free", label: main ? `Also add ${free}` : `Also add the free address ${free}`, type: "check", value: false, hint: main ? `Under the main domain: works once *.${main} points at the server.` : "Works right away, no DNS setup." }] : []),
|
|
846
849
|
],
|
|
847
850
|
ok: "Save",
|
|
848
851
|
onOk: (v) => {
|
|
849
852
|
const domains = v.domains.split(/[\s,]+/).filter(Boolean);
|
|
850
853
|
if (v.free) domains.push(free);
|
|
854
|
+
if (v.apex) domains.push(main);
|
|
851
855
|
if (!domains.length) return void toast("A site needs at least one domain"), false;
|
|
852
856
|
run(["domains", m.name, ...sv(n), ...domains.flatMap((d) => ["--set", d]), "-y"], { server: n });
|
|
853
857
|
},
|
|
@@ -952,7 +956,7 @@ function renderHeader() {
|
|
|
952
956
|
$("#project").innerHTML = `
|
|
953
957
|
<span class="path" title="${esc(st.cwd)}">${p ? `<b>${esc(p.name)}</b> · ${esc(p.domains.join(", "))}` : esc(st.cwd.replace(/^\/(Users|home)\/[^/]+/, "~"))}</span>
|
|
954
958
|
${btn("Deploy folder…", () => folderDialog(st.cwd), "", "Pick a folder, a recent project, or drop one here")}
|
|
955
|
-
${btn(p ? "Deploy" : "Deploy this folder", deployHere, "primary", p ? `lf deploy (
|
|
959
|
+
${btn(p ? "Deploy" : "Deploy this folder", deployHere, "primary", p ? `lf deploy (.landfall.json in ${st.cwd})` : `lf deploy from ${st.cwd}`)}`;
|
|
956
960
|
}
|
|
957
961
|
|
|
958
962
|
function errorHint(error) {
|
|
@@ -1086,10 +1090,10 @@ function sitesView(n, o) {
|
|
|
1086
1090
|
const toggle = () => (open ? S.open.delete(`${n}/${name}`) : S.open.add(`${n}/${name}`), render());
|
|
1087
1091
|
const detail = open
|
|
1088
1092
|
? `<tr class="detail"><td colspan="5"><dl class="kv">
|
|
1089
|
-
<dt>Container</dt><dd class="mono">
|
|
1093
|
+
<dt>Container</dt><dd class="mono">landfall-${esc(name)} · ${esc(status)}</dd>
|
|
1090
1094
|
<dt>On ${esc(n)}</dt><dd>${siteFiles(m, cur)}</dd>
|
|
1091
1095
|
<dt>Source</dt><dd>${proj
|
|
1092
|
-
? `<span class="mono">${esc(tilde(proj.path))}</span> <span class="dim">· settings in</span> <span class="mono"
|
|
1096
|
+
? `<span class="mono">${esc(tilde(proj.path))}</span> <span class="dim">· settings in</span> <span class="mono">.landfall.json</span> <span class="dim">· redeploy runs there</span>`
|
|
1093
1097
|
: `<span class="dim">not deployed from this machine${m.kind === "image" ? "" : " (or the folder moved)"}</span>`}</dd>
|
|
1094
1098
|
<dt>Kind</dt><dd>${esc(m.kind)}${m.spa ? " (single-page app)" : ""} · port ${esc(m.port)}</dd>
|
|
1095
1099
|
<dt>Login</dt><dd>${m.auth?.length ? `${esc(m.auth.map((a) => a.split(":")[0]).join(", "))} (basic auth)` : `<span class="dim">public</span>`}</dd>
|
|
@@ -1352,6 +1356,20 @@ function settingsView(n) {
|
|
|
1352
1356
|
promptDlg(`Traefik dashboard on ${n}`, "Serves the dashboard behind a password (shown in the output). `off` removes it.\nRuns lf traefik setup, which reconfigures landfall's Traefik.", { label: "Domain", type: "text", value: x.traefik.dashboard ?? (x.domains[0] ? `traefik.${x.domains[0]}` : "") }, (d) => {
|
|
1353
1357
|
if (d) run(["traefik", "setup", ...sv(n), "--dashboard", d], reload);
|
|
1354
1358
|
}, "Apply");
|
|
1359
|
+
const sites = S.overview[n]?.data?.sites ?? [];
|
|
1360
|
+
const servedBy = (d) => sites.find((s) => s.meta.domains.includes(d))?.meta.name;
|
|
1361
|
+
const serve = (d) => {
|
|
1362
|
+
const cur = servedBy(d);
|
|
1363
|
+
const pick = sites.filter((s) => s.meta.name !== cur);
|
|
1364
|
+
if (!pick.length) return void toast(cur ? `${cur} is the only site on ${n}` : `No sites on ${n} yet: deploy one with @ as its domain`);
|
|
1365
|
+
dialog({
|
|
1366
|
+
title: `Serve a site on ${d}`,
|
|
1367
|
+
text: cur ? `${cur} serves ${d} now; it moves to the site you pick, which keeps its other domains.` : `The site keeps its other domains. ${d} needs an A record pointing at the server.`,
|
|
1368
|
+
fields: [{ name: "site", label: "Site", type: "select", options: pick.map((s) => ({ value: s.meta.name, label: `${s.meta.name} (${s.meta.domains.join(", ")})` })), value: pick[0].meta.name }],
|
|
1369
|
+
ok: "Serve it",
|
|
1370
|
+
onOk: (v) => run(["domains", v.site, ...sv(n), "--add", d, "-y"], { server: n, after: (j) => (j.info.code === 0 ? void (load(n, "settings", true), load(n, "sites", true), render()) : undefined) }),
|
|
1371
|
+
});
|
|
1372
|
+
};
|
|
1355
1373
|
const P = S.state.paths;
|
|
1356
1374
|
const ip = x.dns[0]?.ips[0] ?? "<server ip>";
|
|
1357
1375
|
const records = x.dns.flatMap((r) => [r.apex, r.wildcard]).filter((d) => !d.ok).map((d) => `${esc(d.domain)} A ${esc(ip)}`);
|
|
@@ -1361,10 +1379,10 @@ function settingsView(n) {
|
|
|
1361
1379
|
<h2>Main domains ${btn("Add…", addDomain, "sm")}</h2>
|
|
1362
1380
|
<div class="body">${x.domains.length
|
|
1363
1381
|
? `<table><tbody>${x.domains.map((d, i) => `<tr>
|
|
1364
|
-
<td>${dnsRow(x.dns[i].apex)}${i === 0 ? ` <span class="badge">primary</span>` : ""}<div class="faint">sites: ${dnsRow(x.dns[i].wildcard)}</div></td>
|
|
1365
|
-
<td><div class="actions">${i ? btn("Make primary", () => run(["server", "domain", n, "--primary", d], reload), "ghost sm") : ""}${btn("Remove", () => confirmDlg({ title: `Remove ${d} from ${n}?`, text: "New sites stop getting names under it. Sites keep their domains.", ok: "Remove it", danger: true, onOk: () => run(["server", "domain", n, "--rm", d, "-y"], reload) }), "ghost sm")}</div></td>
|
|
1382
|
+
<td>${dnsRow(x.dns[i].apex)}${i === 0 ? ` <span class="badge">primary</span>` : ""}<div class="faint">sites: ${dnsRow(x.dns[i].wildcard)}</div><div class="faint">${servedBy(d) ? `served by <b>${esc(servedBy(d))}</b>` : "no site serves it"}</div></td>
|
|
1383
|
+
<td><div class="actions">${btn(servedBy(d) ? "Serve another site…" : "Serve a site…", () => serve(d), "ghost sm")}${i ? btn("Make primary", () => run(["server", "domain", n, "--primary", d], reload), "ghost sm") : ""}${btn("Remove", () => confirmDlg({ title: `Remove ${d} from ${n}?`, text: "New sites stop getting names under it. Sites keep their domains.", ok: "Remove it", danger: true, onOk: () => run(["server", "domain", n, "--rm", d, "-y"], reload) }), "ghost sm")}</div></td>
|
|
1366
1384
|
</tr>`).join("")}</tbody></table>
|
|
1367
|
-
<p class="faint">New sites get <span class="mono"><site>.${esc(x.domains[0])}</span>; a bare name like <span class="mono">blog</span> means <span class="mono">blog.${esc(x.domains[0])}</span
|
|
1385
|
+
<p class="faint">New sites get <span class="mono"><site>.${esc(x.domains[0])}</span>; a bare name like <span class="mono">blog</span> means <span class="mono">blog.${esc(x.domains[0])}</span>, and <span class="mono">@</span> means <span class="mono">${esc(x.domains[0])}</span> itself.</p>
|
|
1368
1386
|
${records.length ? `<div class="note">Add at your DNS provider:<div class="mono">${records.join("<br>")}</div></div>` : ""}`
|
|
1369
1387
|
: `<p class="dim">No main domain set in landfall. New sites get a free <span class="mono"><site>.<ip>.sslip.io</span> address until you set one.</p>`}
|
|
1370
1388
|
${x.suggest.length ? `<div class="note">${x.domains.length ? "Other domains" : "Domains"} your sites on this server already use: ${x.suggest.map((g) => `<b>${esc(g.domain)}</b> <span class="faint">(${g.hosts})</span> ${btn("Add", () => run(["server", "domain", n, "--add", g.domain], reload), "sm")}`).join(" ")}</div>` : ""}</div>
|
|
@@ -1442,7 +1460,7 @@ function historyView(n) {
|
|
|
1442
1460
|
.join("");
|
|
1443
1461
|
return `
|
|
1444
1462
|
<div class="card">
|
|
1445
|
-
<h2>Journal <span class="dim" style="font-weight:400">/opt/
|
|
1463
|
+
<h2>Journal <span class="dim" style="font-weight:400">/opt/landfall/journal.log · newest first</span>
|
|
1446
1464
|
<span class="actions">${total ? btn("Undo everything", () => confirmDlg({ title: `Undo everything landfall did on ${n}?`, text: `${plural(total, "change")}, newest first. Sites, Traefik, the deploy user and landfall's firewall rules go away.`, ok: "Revert everything", danger: true, onOk: () => undo(["--all"]) }), "sm danger") : ""}</span></h2>
|
|
1447
1465
|
${items}
|
|
1448
1466
|
</div>`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
2
|
-
`);e=n.pop();for(let e of n)s.lines.push(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``));s.lines.length>d&&(s.dropped+=s.lines.splice(0,s.lines.length-d).length),l()},flush:()=>e&&s.lines.push(e)}},p=f(),m=f();return o.stdout.setEncoding(`utf8`).on(`data`,p.push),o.stderr.setEncoding(`utf8`).on(`data`,m.push),o.on(`error`,e=>{s.lines.push(`error: ${e.message}`),s.code=1,
|
|
1
|
+
import{C as e,c as t,j as n,k as r,w as i}from"./cli.mjs";import{spawn as a}from"node:child_process";const o=/^(github\.com|gitlab\.com|bitbucket\.org|codeberg\.org)$/;function s(){let e=r(),t=Object.keys(e.servers).map(t=>({name:t,registered:!0,isDefault:e.default===t})),n=i().filter(t=>!e.servers[t]&&!o.test(t)).map(e=>({name:e,registered:!1,isDefault:!1}));return[...t,...n]}function c(t){try{let r=e(n(t).server);return`${r.user}@${r.hostname}${r.port===22?``:`:${r.port}`}`}catch{return`cannot resolve host`}}function l(e){let[t,n]=e;return e.includes(`--check`)||e.includes(`--key-only`)?!1:t===`firewall`?[`enable`,`disable`,`allow`,`deny`,`update`,`rm`,`remove`].includes(n):t===`traefik`?n===`setup`||n===`use`:t===`env`?e.some(e=>e.includes(`=`)||e===`-u`||e===`--unset`):t===`domains`?e.some(e=>[`-a`,`--add`,`--rm`,`--set`].includes(e)):t===`auth`?e.some(e=>[`-u`,`--user`,`--rm-user`,`--off`].includes(e)):t===`update`||t===`upgrade`||[`setup`,`deploy`,`rollback`,`stop`,`start`].includes(t)}const u=()=>[process.execPath,...process.execArgv,process.argv[1]],d=5e3;function f(e,n,r={}){let i={...process.env,NO_COLOR:`1`,...r.env};r.password&&Object.assign(i,t(r.password));let o=a(u()[0],[...u().slice(1),...e],{stdio:[`ignore`,`pipe`,`pipe`],env:i,cwd:r.cwd,detached:!0}),s={argv:e,lines:[],dropped:0,stop(){if(s.code===void 0){s.stopped=!0;try{process.kill(-o.pid,`SIGTERM`)}catch{}}}},c,l=()=>c??=setTimeout(()=>(c=void 0,n()),50),f=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
2
|
+
`);e=n.pop();for(let e of n)s.lines.push(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``));s.lines.length>d&&(s.dropped+=s.lines.splice(0,s.lines.length-d).length),l()},flush:()=>e&&s.lines.push(e)}},p=f(),m=f();return o.stdout.setEncoding(`utf8`).on(`data`,p.push),o.stderr.setEncoding(`utf8`).on(`data`,m.push),o.on(`error`,e=>{s.lines.push(`error: ${e.message}`),s.code=1,n()}),o.on(`close`,e=>{p.flush(),m.flush(),s.code=s.stopped?null:e??1,clearTimeout(c),n()}),s}export{c as i,l as n,s as r,f as t};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{A as e,C as t,D as n,E as r,F as i,H as a,I as o,L as s,M as c,N as l,O as u,P as ee,R as d,S as f,T as p,V as m,_ as h,a as g,b as _,d as te,f as ne,h as re,i as ie,j as v,k as y,l as b,n as ae,o as x,p as S,r as C,s as w,t as T,u as oe,v as E,x as se,y as ce,z as D}from"./cli.mjs";import{i as O,n as k,r as A,t as j}from"./job-i5GF9h93.mjs";import{spawnSync as M}from"node:child_process";import N from"node:fs";import{homedir as P}from"node:os";import{dirname as F,isAbsolute as I,join as L,resolve as R}from"node:path";import{randomBytes as z,timingSafeEqual as B}from"node:crypto";import{createServer as V}from"node:http";import{Readable as H}from"node:stream";function U(e){let t=R(!e||e===`~`?P():u(e)),n=N.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&W(L(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:F(t)===t?null:F(t),home:P(),dirs:n,detected:h(t)}}const W=e=>{try{return N.statSync(e).isDirectory()}catch{return!1}},G=L(n,`uploads`),K=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function q(e,t){N.mkdirSync(F(e),{recursive:!0}),N.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function le(e){N.mkdirSync(G,{recursive:!0,mode:448});for(let e of N.readdirSync(G)){let t=L(G,e);Date.now()-N.statSync(t).mtimeMs>864e5&&N.rmSync(t,{recursive:!0,force:!0})}let t=N.mkdtempSync(L(G,`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=L(t,`.upload-`+K(n[0].name).pop());await q(e,n[0]);let r=L(t,`files`);N.mkdirSync(r);let i=/\.zip$/i.test(e),a=i?M(`unzip`,[`-q`,e,`-d`,r]):M(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?M(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return N.rmSync(e),{path:J(r),files:ue(r)}}let i=L(t,`files`);for(let[e,t]of n.entries()){let n=K(r[e]||t.name);n.length&&await q(L(i,...n),t)}return{path:J(i),files:n.length}}function J(e){for(;;){let t=N.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!W(L(e,t[0])))return e;e=L(e,t[0])}}function ue(e){return N.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function de(e){if(e!=null&&e!==``){if(typeof e!=`string`||!I(e)||!W(e))throw Error(`cwd must be an existing folder`);return e}}const fe=N.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),Y=/* @__PURE__ */ new Map,pe=/* @__PURE__ */ new Set([`ui`,`web`,`tui`]);function me(){let e=L(F(p),`ui-token`);try{let t=N.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=z(24).toString(`hex`);return N.mkdirSync(F(e),{recursive:!0,mode:448}),N.writeFileSync(e,t+`
|
|
2
|
+
`,{mode:384}),t}const X=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),Z=(e,t=500)=>X({error:e?.message??String(e)},t);function he(e,t){let n=`${Date.now().toString(36)}${Math.random().toString(36).slice(2,6)}`,r=/* @__PURE__ */ new Set,i=j(e,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,env:{LANDFALL_RUN:n}}),a={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:k(e),job:i,listeners:r};Y.set(n,a);for(let[e,t]of Y){if(Y.size<=50)break;t.job.code!==void 0&&Y.delete(e)}return a}const ge=e=>e.map((t,n)=>[`-p`,`--password`].includes(e[n-1])?`***`:t.replace(/^(--password=).*/,`$1***`)),Q=e=>({id:e.id,argv:ge(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped});function _e(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)})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Q(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
3
|
+
|
|
4
|
+
`),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 ve(n,u){let[p,m,h]=u,D=n.method;if(p===`state`&&D===`GET`){let t=y();return X({version:T,cwd:process.cwd(),project:e(process.cwd())??null,projects:E(),defaultImage:l,paths:{root:o,sites:s,traefik:d,journal:ee,proxy:i},servers:A().map(e=>({...e,target:O(e.name),saved:t.servers[e.name]??null})),jobs:[...Y.values()].reverse().map(Q)})}if(p===`servers`&&m){let{server:e}=v(m);if(D===`GET`&&!h)return X(await re(e));if(D===`GET`&&h===`firewall`){let{login:n,probe:r,...i}=await ie(e);return X({...i,asRoot:t(n).user===`root`})}if(D===`GET`&&h===`config`){let t=x(m),[n,r]=await Promise.all([Promise.all(t.domains.map(t=>g(e,t))),w(e)]);return X({...t,dns:n,suggest:r})}if(D===`GET`&&h===`history`){let t=(await C(e)).reverse();return X(t.map(e=>({...e,pending:ae(e).length})))}if(D===`GET`&&h===`facts`){let t=await _(e),n=await f(e,t);return X({...t,ips:n,sslipIp:se(n)??null})}if(D===`GET`&&h===`updates`){let t=new URL(n.url).searchParams.get(`refresh`)===`1`,r=await S(e,t),i=await te(e);return X({packages:r,images:await ne(e),engine:{...i,...oe(i)}})}if(D===`GET`&&h===`dns`){let t=(new URL(n.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>r.test(e)).slice(0,50),i=await f(e),[a,o]=await Promise.all([ce(t,i),Promise.all(t.map(b))]);return X({ips:i,domains:a.map((e,t)=>({...e,https:o[t]}))})}if(D===`POST`&&h==="default"){let e=y();return e.servers[m]??={host:m},e.default=m,c(e),X({ok:!0})}}if(p===`jobs`){if(D===`POST`&&!m){let e=await n.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||pe.has(t[0]))return Z(`argv must be a list of landfall arguments`,400);let r;try{r=de(e.cwd)}catch(e){return Z(e,400)}return X(Q(he(t,{server:e.server,password:e.password||void 0,cwd:r})))}let e=m?Y.get(m):void 0;if(m&&!e)return Z(`no job ${m}`,404);if(e&&D===`GET`&&h===`events`)return _e(e,Number(new URL(n.url).searchParams.get(`from`))||0,n.signal);if(e&&D===`GET`&&!h)return X({...Q(e),lines:e.job.lines,from:e.job.dropped});if(e&&D===`POST`&&h===`stop`)return e.job.stop(),X(Q(e))}if(p===`fs`&&D===`GET`)return X(U(new URL(n.url).searchParams.get(`path`)??void 0));if(p===`projects`&&D===`GET`)return X(E());if(p===`upload`&&D===`POST`){let e=await le(await n.formData());return X({...e,detected:U(e.path).detected})}if(p===`open`&&D===`POST`){let{url:e}=await n.json();return!e||!/^https?:\/\//.test(e)?Z(`not a web address`,400):(a(e),X({ok:!0}))}return Z(`not found`,404)}const $=(e,t)=>e.length===t.length&&B(Buffer.from(e),Buffer.from(t)),ye=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``;async function be(e){D(!0);let t=me(),n=V(xe(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===`/login`)return $(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(!$(ye(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(fe,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`))try{return await ve(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return Z(e)}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=await new Promise(t=>{n.once(`error`,t),n.listen(e.port,`127.0.0.1`,()=>t(void 0))});if(r){if(r.code!==`EADDRINUSE`)throw r;let n=`http://127.0.0.1:${e.port}/login?t=${t}`;return await fetch(`http://127.0.0.1:${e.port}/api/state`,{headers:{cookie:`landfall=${t}`}}).then(e=>e.ok,()=>!1)?(console.log(`lf ui already runs: ${m.cyan(n)}`),e.open&&a(n),0):(console.error(`${m.red(`error:`)} port ${e.port} is in use; pick another with --port`),1)}let i=`http://127.0.0.1:${n.address().port}/login?t=${t}`;return console.log(`${m.bold(`lf ui`)} ${m.dim(T)} on ${m.cyan(i)}`),console.log(m.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&a(i),new Promise(e=>{let t=()=>{for(let e of Y.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function xe(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let n=new Headers;for(let[e,r]of Object.entries(t.headers))for(let t of[r??[]].flat())n.append(e,t);let a=t.method===`GET`||t.method===`HEAD`?void 0:H.toWeb(t);i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:n,body:a,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=H.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{be as webUi};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{A as e,B as t,C as n,M as r,V as i,g as a,h as o,i as s,j as c,k as l,m as u,n as d,r as f,t as p,w as ee,z as m}from"./cli.mjs";import{i as te,n as ne,r as h,t as re}from"./job-i5GF9h93.mjs";import{spawn as ie}from"node:child_process";import g from"string-width";import{basename as ae}from"node:path";import{Box as _,Text as v,render as y,useApp as oe,useInput as se,useStdout as b}from"ink";import{useEffect as x,useRef as S,useState as C}from"react";import{Fragment as ce,jsx as w,jsxs as T}from"react/jsx-runtime";function E(e){return/permission denied/i.test(e)?`No key login yet. Press u to set it up: landfall asks for the password once and installs a key.`:/resolve|not known|nodename/i.test(e)?`Host name not found. Check the entry in ~/.ssh/config.`:/timed out|refused|no route|unreachable/i.test(e)?`Server not reachable. Is it running, and is the ssh port right?`:`Press R to retry.`}const D=e=>e.startsWith(`Up`);function le({load:e}){return e?e.kind===`loading`?/* @__PURE__ */ w(O,{}):e.kind===`error`?/* @__PURE__ */ w(v,{color:`red`,children:`●`}):e.data.ready?/* @__PURE__ */ w(v,{color:`green`,children:`●`}):/* @__PURE__ */ w(v,{color:`yellow`,children:`●`}):/* @__PURE__ */ w(v,{dimColor:!0,children:`○`})}function O(){let[e,t]=C(0);return x(()=>{let e=setInterval(()=>t(e=>(e+1)%10),80);return()=>clearInterval(e)},[]),/* @__PURE__ */ w(v,{color:`cyan`,children:`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`[e]})}function ue(){let e=b().stdout,[t,n]=C({cols:e.columns||80,rows:e.rows||24});return x(()=>{let t=()=>n({cols:e.columns,rows:e.rows});return e.on(`resize`,t),()=>void e.off(`resize`,t)},[e]),t}function k(e,t,n,r){let i=r.current;return t<i&&(i=t),t>=i+n&&(i=t-n+1),i=Math.max(0,Math.min(i,e.length-n)),r.current=i,{items:e.slice(i,i+n),offset:i}}function A({title:e,focused:t,width:n,height:r,children:i}){return/* @__PURE__ */ T(_,{flexDirection:`column`,borderStyle:`round`,borderColor:t?`cyan`:`gray`,width:n,flexGrow:+!n,height:r,paddingX:1,overflow:`hidden`,children:[/* @__PURE__ */ w(_,{marginBottom:1,children:e}),i]})}function de(e,t,n){let r=[[]],i=0;for(let a of e){let e=g(`${a[0]} ${a[1]}`)+3;i&&i+e-3>t&&r.length<n&&(r.push([]),i=0),r[r.length-1].push(a),i+=e}return r}function fe({lines:e}){return e.map((e,t)=>/* @__PURE__ */ w(v,{wrap:`truncate`,children:e.map(([e,t],n)=>/* @__PURE__ */ T(v,{children:[n?` `:` `,/* @__PURE__ */ w(v,{color:`cyan`,bold:!0,children:e}),` `,t]},e))},t))}function j({state:n,onAction:i}){let{exit:a}=oe(),{cols:m,rows:g}=ue(),[y,b]=C(h),[E,O]=C(Math.min(n.server,y.length)),[j,M]=C(n.site),[N,P]=C(n.focus),[F,I]=C({kind:`browse`}),[L,R]=C(``),[,z]=C(0),Se=S(0),Ce=S(0),we=S(0),Te=S(0),B=S(void 0),V=e(process.cwd()),H=y[E],U=H&&n.cache.get(H.name),W=U?.kind===`ok`?U.data.sites:[],G=W[Math.min(j,W.length-1)];x(()=>{Object.assign(n,{server:E,site:j,focus:N})},[E,j,N]),x(()=>{if(!H||!n.connect.has(H.name)||n.cache.has(H.name))return;let e=H.name;n.cache.set(e,{kind:`loading`}),z(e=>e+1),o(c(e).server).then(t=>n.cache.set(e,{kind:`ok`,data:t}),t=>n.cache.set(e,{kind:`error`,error:t.message??String(t)})).finally(()=>z(e=>e+1))},[H?.name,U,n.connect.has(H?.name??``)]);let K=(e,t={})=>{let r=t.server??H?.name,i=`${Date.now().toString(36)}${Math.random().toString(36).slice(2,6)}`,a=!1,o=re(e,()=>{if(o.code!==void 0&&!a){a=!0,r&&n.cache.delete(r),b(h());let s=t.after?.(o)||(r&&ne(e)?De(i,r,e):void 0);I(e=>e.kind===`run`&&e.job===o?{...e,next:s}:e)}z(e=>e+1)},{password:t.password,env:{LANDFALL_RUN:i}});I({kind:`run`,job:o})},Ee=e=>{i({argv:e,server:H?.name}),a()},q=()=>{let e=B.current;if(!e)return I({kind:`browse`});e.kind===`firewall`?Ne(e.server,e.index):Fe(e.server,e.index,e.id)},De=(e,t,n)=>[{key:`z`,label:`undo this`,run:()=>J(`Undo \`lf ${n.join(` `)}\` on ${t}?`,`Reverts the changes this command recorded in the server's journal.`,`Undo it`,()=>K([`undo`,e,`-s`,t,`-y`],{server:t,after:()=>[]}))}],J=(e,t,n,r)=>I({kind:`choose`,title:e,body:t,index:0,options:[{label:n,pick:r},{label:`Cancel`,pick:q}]}),Y=(e,t,n,r=!1,i=``)=>I({kind:`prompt`,title:e,hint:t,value:i,secret:r,submit:n});function Oe(e,t=e){K([`setup`,...e,`--check`],{server:e[0],after:n=>{if(n.code!==0)return;if(n.lines.some(e=>e.includes(`no key login yet`)))return[{key:`p`,label:`enter the password to install a key`,run:()=>Ae(e,t)}];let r=n.lines.filter(e=>/^\s+→ /.test(e)).length;return[{key:`y`,label:r?`apply ${r} change${r===1?``:`s`}`:`finish setup (nothing to install)`,run:()=>K([`setup`,...e,`-y`],{server:e[0]})},{key:`o`,label:`change options (traefik, firewall, ssh, deploy user)`,run:()=>ke(t)}]}})}function ke(e){let t=l().servers[e[0]]??{host:e[0]},n=[],r=(...e)=>n.push(...e),i=(e,t,n,r=0)=>I({kind:`choose`,title:e,body:t,options:n,index:r}),a=(e,t,n,i,a)=>Y(e,t,e=>(e.trim()&&r(`--${i}=${e.trim()}`),a()),!1,n),o=()=>i(`Setup 1/4 · Traefik`,`Reverse proxy with automatic HTTPS for your sites.
|
|
2
|
+
If the server already runs a Traefik on 80/443, setup routes sites through it instead.`,[{label:`Own Traefik, ports published by Docker`,hint:`default; Docker forwards 80/443 to the container`,pick:()=>(r(`--no-host-network`),s())},{label:`Own Traefik on the host network`,hint:`sees real client IPs (also IPv6); ufw guards 80/443`,pick:()=>(r(`--host-network`),s())},{label:`No Traefik`,hint:`add it later with lf traefik setup`,pick:()=>(r(`--no-traefik`),d())}],+!!t.hostNetwork),s=()=>a(`Setup · Traefik image`,`Docker image to run. Enter keeps it.`,t.traefikImage??`traefik:v3`,`image`,c),c=()=>a(`Setup · Let's Encrypt e-mail`,`Optional contact for the certificate account. Empty skips.`,t.email??``,`email`,u),u=()=>a(`Setup · Traefik dashboard`,`Optional domain for the dashboard behind basic auth; the password is shown after setup. Empty skips.`,``,`dashboard`,d),d=()=>i(`Setup 2/4 · Firewall`,`ufw: deny incoming except ssh, 80 and 443.
|
|
3
|
+
Skipped automatically if it would block ports in use.`,[{label:`Enable ufw`,pick:f},{label:`Leave the firewall alone`,hint:`manage it later with f`,pick:()=>(r(`--no-firewall`),f())}]),f=()=>i(`Setup 3/4 · SSH`,`Turn off password login so only keys work.
|
|
4
|
+
Skipped automatically if other users still log in with a password.`,[{label:`Key login only`,pick:p},{label:`Leave sshd alone`,pick:()=>(r(`--no-harden`),p())}]),p=()=>Y(`Setup 4/4 · Deploy user`,`landfall deploys as this user (docker group, your key). Empty keeps the login user.`,e=>(r(`--deploy-user=${e.trim()}`),ee()),!1,`deploy`),ee=()=>Oe([...e,...n],e);o()}function Ae(e,t=e){let n=e.includes(`--host`)?`root@${e[e.indexOf(`--host`)+1]}`:te(e[0]);Y(`Password for ${n}`,`Used once to install landfall's ssh key (ssh-copy-id). It is not stored; afterwards only the key is used.`,n=>{if(!n)return q();K([`setup`,...e,`--key-only`,`-y`],{password:n,server:e[0],after:n=>n.code===0?Oe(e,t):[{key:`p`,label:`try the password again`,run:()=>Ae(e,t)}]})},!0)}function je(){Y(`Add a server`,`Its name. Use an alias from ~/.ssh/config (like het), or pick a new name for a fresh machine.`,e=>{if(e=e.trim(),!e)return q();ee().includes(e)||l().servers[e]?I({kind:`choose`,title:`${e} is in your ssh config`,index:0,options:[{label:`Set it up now`,hint:`choose options, see what it would change, then apply`,pick:()=>ke([e])},{label:`Only add it to landfall`,hint:`no changes on the server`,pick:()=>K([`server`,`add`,e],{server:e})},{label:`Cancel`,pick:q}]}):Y(`Add ${e}`,`IP address or hostname. lf logs in as root once with its password and installs an ssh key.`,t=>t.trim()?ke([e,`--host`,t.trim()]):q())})}function Me(){let e=V?.server??H?.name;if(!e)return R(`Pick a server on the left first.`);if(V)return J(`Deploy ${V.name} to ${e}?`,`From ${process.cwd()}\nServed on ${V.domains.join(`, `)} (settings from .landfall.json)`,`Deploy`,()=>K([`deploy`,`-y`,...V.server?[]:[`-s`,e]],{server:e}));Y(`Deploy ${ae(process.cwd())} to ${e}`,`Domain(s) to serve it on, e.g. example.com www.example.com. Their DNS must point at the server.`,t=>{let n=t.split(/[\s,]+/).filter(Boolean);if(!n.length)return q();K([`deploy`,`-y`,`-s`,e,...n.flatMap(e=>[`-d`,e])],{server:e})})}function Ne(e,t=0){B.current={kind:`firewall`,server:e,index:t},I({kind:`firewall`,server:e,index:t});let n=t=>I(n=>n.kind===`firewall`&&n.server===e?{...n,...t,index:Math.min(n.index,t.data?.rules.length??0)}:n);s(c(e).server).then(e=>n({data:e}),e=>n({error:e.message??String(e)}))}function Pe(e,t){if(F.kind!==`firewall`)return;let{server:n,data:r,index:i}=F;if(t.escape||t.leftArrow)return B.current=void 0,q();if(e===`R`)return Ne(n,i);if(!r)return;let a=[`-s`,n],o=r.rules[i],s=String(i+1),c=e=>K(e,{server:n,after:t=>t.code!==0&&!e.includes(`--force`)&&t.lines.some(e=>e.includes(`--force`))?[{key:`f`,label:`do it anyway`,run:()=>c([...e,`--force`])}]:void 0}),l=()=>Y(`Open a port`,`port[/proto], e.g. 5432, 51820/udp or 6000:6007/tcp`,e=>{if(!e.trim())return q();Y(`Who may reach ${e.trim()}?`,`An address or network like 10.0.0.0/8. Leave empty for anyone.`,t=>c([`firewall`,`allow`,e.trim(),...a,...t.trim()?[`--from`,t.trim()]:[]]))}),u=e=>{B.current={kind:`firewall`,server:n,index:e},I({...F,index:e})};if(t.upArrow||e===`k`)return u(Math.max(0,i-1));if(t.downArrow||e===`j`)return u(Math.min(r.rules.length,i+1));if(e===`t`)return r.ufw===`active`?J(`Turn the firewall on ${n} off?`,`Every listening port becomes reachable from anywhere.`,`Turn it off`,()=>c([`firewall`,`disable`,...a,`-y`])):c([`firewall`,`enable`,...a]);if(e===`a`||!o&&t.return)return l();if(o){if(e===` `)return c([`firewall`,`update`,s,`--action`,o.action===`deny`||o.action===`reject`?`allow`:`deny`,...a]);if(e===`e`||t.return){let e=o.port?`${o.port}${o.proto?`/${o.proto}`:``}`:``;return Y(`Edit rule ${s}: ${o.spec}`,`port[/proto]`,t=>Y(`Who may reach ${t.trim()||e}?`,`An address or network like 10.0.0.0/8. Leave empty for anyone.`,n=>c([`firewall`,`update`,s,...a,...t.trim()&&t.trim()!==e?[`--port`,t.trim()]:[],`--from`,n.trim()||`any`]),!1,o.from??``),!1,e)}if(t.delete||t.backspace)return J(`Remove rule ${s}?`,o.spec,`Remove it`,()=>c([`firewall`,`rm`,s,...a,`-y`]))}}function Fe(e,t=0,n){B.current={kind:`history`,server:e,index:t,id:n},I({kind:`history`,server:e,index:t});let r=t=>I(r=>{if(r.kind!==`history`||r.server!==e)return r;let i=n?t.runs?.findIndex(e=>e.id===n)??-1:-1;return{...r,...t,index:i>=0?i:Math.min(r.index,Math.max(0,(t.runs?.length??1)-1))}});f(c(e).server).then(e=>r({runs:e.reverse()}),e=>r({error:e.message??String(e)}))}function Ie(e,t){if(F.kind!==`history`)return;let{server:n,runs:r,index:i}=F;if(t.escape||t.leftArrow)return B.current=void 0,q();if(e===`R`)return Fe(n,i,r?.[i]?.id);if(!r?.length)return;let a=e=>{B.current={kind:`history`,server:n,index:e,id:r[e].id},I({...F,index:e})};if(t.upArrow||e===`k`)return a(Math.max(0,i-1));if(t.downArrow||e===`j`)return a(Math.min(r.length-1,i+1));let o=r[i],s=e=>K([`undo`,...e,`-s`,n,`-y`],{server:n,after:()=>[]});if(e===`z`||t.return){let e=d(o);if(!e.length)return R(`#${o.n} has nothing left to undo`);let t=r.filter(e=>e.n>o.n&&d(e).length),i=[...e.map(e=>`↺ ${e.what}`).reverse(),...t.length?[``,`Later runs build on it and stay: ${t.map(e=>`#${e.n}`).join(`, `)}`]:[]].join(`
|
|
5
|
+
`);return J(`Undo #${o.n} ${o.cmd} on ${n}?`,i,`Revert ${e.length} change${e.length>1?`s`:``}`,()=>s([String(o.n)]))}if(e===`U`){let e=r.flatMap(d);return e.length?J(`Undo everything landfall did on ${n}?`,`${e.length} changes, newest first. Sites, Traefik, the deploy user and landfall's firewall rules go away.`,`Revert everything`,()=>s([`--all`])):R(`nothing left to undo`)}}function Le(e,t){if(!H){(t.return||e===`a`)&&je();return}let i=H.name;if(t.return||t.rightArrow||t.tab){if(!n.connect.has(i))return n.connect.add(i),z(e=>e+1);W.length&&P(`sites`)}else{if((t.delete||t.backspace)&&H.registered)return J(`Forget ${i}?`,`Removes it from landfall (and the Host block landfall wrote to ~/.ssh/config).
|
|
6
|
+
Nothing on the server is changed.`,`Forget ${i}`,()=>K([`server`,`rm`,i,`-y`]));switch(e){case`c`:case`u`:return Oe([i]);case`i`:return K([`server`,`info`,i]);case`t`:return K([`traefik`,`status`,`-s`,i]);case`f`:return Ne(i);case`h`:return Fe(i);case`x`:return Ee([`ssh`,i]);case`a`:return je();case`*`:{let e=l();return e.servers[i]??={host:i},e.default=i,r(e),b(h()),R(`${i} is now the default server`)}}}}function Re(e,n){if(!H||!G)return;let r=[`-s`,H.name],i=G.meta.name;if(n.return)return I({kind:`detail`});if(n.delete||n.backspace)return J(`Delete ${i} from ${H.name}?`,`Stops it and removes all its files and releases.
|
|
7
|
+
This cannot be undone.`,`Delete ${i}`,()=>K([`sites`,`rm`,i,...r,`-y`]));switch(e){case`l`:return K([`logs`,i,...r,`-f`]);case`r`:return K([`restart`,i,...r]);case`s`:return K([D(G.status)?`stop`:`start`,i,...r]);case`b`:{let e=u(G.meta).filter(e=>e.at<(G.meta.releases.find(e=>e.id===G.meta.current)?.at??``));return e.length?J(`Roll back ${i}?`,`From ${G.meta.current} to ${e[0].id} (${t(e[0].at)})`,`Roll back`,()=>K([`rollback`,i,...r,`-y`])):R(`${i} has no older release to go back to`)}case`e`:return G.meta.kind===`static`?R(`static sites have no environment variables`):Y(`Environment of ${i}`,`KEY=VALUE pairs to set (the container restarts). Leave empty to just show them.`,e=>K([`env`,i,...r,...e.split(/\s+/).filter(Boolean)]));case`o`:{let e=`https://${G.meta.domains[0]}`;return ie(process.platform===`darwin`?`open`:`xdg-open`,[e],{stdio:`ignore`,detached:!0}).unref(),R(`opened ${e}`)}}}let ze=F.kind===`run`?F.job.code===void 0?[[`esc`,`stop`],[`↑↓ space`,`scroll`],[`g G`,`top / end`]]:[...(F.next??[]).map(e=>[e.key,e.label]),[`esc`,`back`],[`↑↓ space`,`scroll`],[`g G`,`top / end`]]:F.kind===`firewall`?[[`esc`,`back`],[`↑↓`,`rule`],[`t`,F.data?.ufw===`active`?`turn off`:`turn on`],[`a`,`add`],[`e`,`edit`],[`space`,`allow ↔ deny`],[`⌫`,`remove`],[`R`,`reload`]]:F.kind===`history`?[[`esc`,`back`],[`↑↓`,`run`],[`z ↵`,`undo run`],[`U`,`undo everything`],[`R`,`reload`]]:F.kind===`choose`?[[`↑↓`,`choose`],[`↵`,`ok`],[`esc`,`back`]]:F.kind===`prompt`?[[`↵`,`ok`],[`esc`,`cancel`]]:F.kind===`help`?[[`↑↓ space`,`scroll`],[`any key`,`close`]]:H?N===`servers`&&F.kind===`browse`?[[`?`,`help`],[`q`,`quit`],[`↑↓`,`server`],[`↵`,H&&n.connect.has(H.name)?`sites`:`connect`],[`u`,`check & set up`],[`d`,`deploy here`],[`i`,`info`],[`t`,`traefik`],[`f`,`firewall`],[`h`,`history`],[`x`,`shell ↗`],[`*`,`default`],[`a`,`add`],...H.registered?[[`⌫`,`forget`]]:[]]:[[`?`,`help`],[`←`,`servers`],[`↑↓`,`site`],[`↵`,`details`],[`l`,`logs`],[`r`,`restart`],[`s`,G&&D(G.status)?`stop`:`start`],[`b`,`roll back`],[`e`,`env`],[`o`,`open`],[`⌫`,`delete`]]:[[`↵`,`add a server`],[`d`,`deploy this folder`],[`?`,`help`],[`q`,`quit`]],X=m<72,Be=de(ze,m-1,g<16?2:3),Z=Math.max(6,g-3-Be.length),Ve=Math.min(36,Math.floor(m*.4),Math.max(26,...y.map(e=>e.name.length+14))),Q=X&&Z<16,He=X&&!Q?Math.max(5,Math.min(y.length+5,Math.floor(Z*.4))):Z,Ue=X&&!Q?Z-He:Z,We=X?m:m-Ve;se((e,t)=>{R(``);let r=F.kind===`run`&&F.job.code===void 0;if(t.ctrl&&e===`c`)return r?F.job.stop():a();if(F.kind===`run`){let n=be(Z),i=Math.max(0,ye(F.job,m).length-n),a=e=>I({...F,top:e>=i?void 0:Math.max(0,e)}),o=Math.min(F.top??i,i);return t.escape||e===`q`||t.leftArrow?r?F.job.stop():q():t.upArrow||e===`k`?a(o-1):t.downArrow||e===`j`?a(o+1):t.pageUp?a(o-n):t.pageDown||e===` `?a(o+n):e===`g`||t.home?a(0):e===`G`||t.end?a(i):F.next?.find(t=>t.key===e)?.run()}if(F.kind===`prompt`){if(t.escape)return q();if(t.return)return F.submit(F.value);if(t.backspace||t.delete)return I({...F,value:F.value.slice(0,-1)});e&&!t.ctrl&&!t.meta&&I({...F,value:F.value+e})}else{if(F.kind===`choose`){if(t.escape)return q();if(t.upArrow||t.downArrow){let e=t.upArrow?-1:1;return I({...F,index:(F.index+e+F.options.length)%F.options.length})}return t.return?F.options[F.index].pick():void 0}if(F.kind===`help`){let n=Math.max(0,ge(m).length-(Z-4)),r=e=>I({...F,top:Math.max(0,Math.min(n,e))});return t.upArrow||e===`k`?r(F.top-1):t.downArrow||e===`j`?r(F.top+1):e===` `||t.pageDown?r(F.top+Z-4):t.pageUp?r(F.top-(Z-4)):q()}if(F.kind===`firewall`&&e!==`q`&&e!==`?`)return Pe(e,t);if(F.kind===`history`&&e!==`q`&&e!==`?`)return Ie(e,t);if(e===`q`)return a();if(e===`?`)return I({kind:`help`,top:0});if(e===`d`)return Me();if(e===`R`)return n.cache.clear(),b(h()),z(e=>e+1);if(F.kind===`detail`)return t.escape||t.leftArrow?q():Re(e,t);if(N===`servers`)return t.upArrow||e===`k`?(M(0),O(e=>Math.max(0,e-1))):t.downArrow||e===`j`?(M(0),O(e=>Math.min(y.length,e+1))):t.escape?a():Le(e,t);if(t.escape||t.leftArrow)return P(`servers`);if(t.upArrow||e===`k`)return M(e=>Math.max(0,e-1));if(t.downArrow||e===`j`)return M(e=>Math.min(W.length-1,e+1));Re(e,t)}});let Ge=/* @__PURE__ */ T(_,{justifyContent:`space-between`,children:[/* @__PURE__ */ w(_,{flexShrink:0,children:/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,color:`cyan`,children:`landfall`}),` `,/* @__PURE__ */ w(v,{dimColor:!0,children:p})]})}),/* @__PURE__ */ w(_,{flexShrink:1,flexGrow:0,marginLeft:2,overflow:`hidden`,children:/* @__PURE__ */ w(v,{dimColor:!0,wrap:`truncate`,children:V?`this folder: ${V.name} → ${V.server??`?`} · ${V.domains.join(`, `)}`:`this folder: ${ae(process.cwd())} (not deployed yet · d to deploy)`})})]}),$;if(m<40||g<12)$=/* @__PURE__ */ T(v,{color:`yellow`,children:[`Make the terminal bigger: `,m,`×`,g,`, landfall needs 40×12.`]});else if(F.kind===`help`)$=/* @__PURE__ */ w(_e,{height:Z,width:m,top:F.top});else if(F.kind===`firewall`)$=/* @__PURE__ */ w(he,{mode:F,height:Z,top:we});else if(F.kind===`history`)$=/* @__PURE__ */ w(ve,{mode:F,height:Z,top:Te});else if(F.kind===`run`)$=/* @__PURE__ */ w(xe,{job:F.job,top:F.top,height:Z,width:m});else if(F.kind===`choose`||F.kind===`prompt`)$=/* @__PURE__ */ w(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:F.title}),focused:!0,height:Z,children:F.kind===`choose`?/* @__PURE__ */ T(_,{flexDirection:`column`,children:[F.body&&/* @__PURE__ */ w(_,{marginBottom:1,children:/* @__PURE__ */ w(v,{children:F.body})}),F.options.map((e,t)=>/* @__PURE__ */ T(v,{color:t===F.index?`cyan`:void 0,children:[t===F.index?`› `:` `,/* @__PURE__ */ w(v,{bold:t===F.index,children:e.label}),e.hint&&/* @__PURE__ */ T(v,{dimColor:!0,children:[` `,e.hint]})]},e.label))]}):/* @__PURE__ */ T(_,{flexDirection:`column`,children:[/* @__PURE__ */ w(v,{dimColor:!0,children:F.hint}),/* @__PURE__ */ w(_,{marginTop:1,borderStyle:`single`,borderColor:`cyan`,paddingX:1,children:/* @__PURE__ */ T(v,{children:[F.secret?`•`.repeat(F.value.length):F.value,/* @__PURE__ */ w(v,{inverse:!0,children:` `})]})})]})});else{let e=k([...y,void 0],E,He-4,Se),t=y.findIndex(e=>!e.registered);$=/* @__PURE__ */ T(_,{flexDirection:X?`column`:`row`,children:[!(Q&&N===`sites`)&&/* @__PURE__ */ w(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:`Servers`}),focused:N===`servers`,width:X?void 0:Ve,height:He,children:e.items.map((r,i)=>{let a=e.offset+i,o=a===E,s=r?/* @__PURE__ */ T(ce,{children:[/* @__PURE__ */ w(le,{load:n.cache.get(r.name)}),` `,/* @__PURE__ */ w(v,{bold:o,children:r.name}),r.isDefault&&/* @__PURE__ */ w(v,{dimColor:!0,children:` default`}),!r.registered&&a===t&&/* @__PURE__ */ w(v,{dimColor:!0,children:` ssh config`})]}):/* @__PURE__ */ w(v,{color:`green`,children:`+ add a server`});return/* @__PURE__ */ T(v,{wrap:`truncate`,color:o&&N===`servers`?`cyan`:void 0,children:[o?`› `:` `,s]},r?.name??`+`)})}),!(Q&&N===`servers`)&&/* @__PURE__ */ w(me,{entry:H,load:U,sites:W,siteIdx:j,detail:F.kind===`detail`,focused:N===`sites`,height:Ue,width:We,top:Ce})]})}let Ke=L||(F.kind===`run`?F.job.code===void 0?`running… esc stops it`:`finished · esc goes back`:H?pe(H,U):`Servers are machines you reach with ssh. Sites run on them behind Traefik with HTTPS.`);return/* @__PURE__ */ T(_,{flexDirection:`column`,width:m,height:g-1,children:[Ge,$,/* @__PURE__ */ w(v,{dimColor:!L,color:L?`yellow`:void 0,wrap:`truncate`,children:` `+Ke}),/* @__PURE__ */ w(fe,{lines:Be})]})}function pe(e,t){let n=[te(e.name)];return e.registered||n.push(`from ~/.ssh/config`),t?.kind===`ok`&&n.push(t.data.ready?`${t.data.sites.length} site${t.data.sites.length===1?``:`s`}`:`not set up for landfall yet`),n.join(` · `)}function me(e){if(!e.entry)return/* @__PURE__ */ T(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:`Add a server`}),height:e.height,children:[/* @__PURE__ */ w(v,{children:`Press ↵ to add one. Two ways:`}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,children:`A server you already reach with ssh`}),` (e.g. `,/* @__PURE__ */ w(v,{color:`cyan`,children:`ssh het`}),`): enter its alias.`]}),/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,children:`A fresh machine`}),`: enter a new name and its IP. landfall asks for the root password once and installs a key.`]}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ w(v,{dimColor:!0,children:`Setup installs Docker and Traefik (HTTPS). It checks the server first and never touches what already runs there.`})]});let n=e.entry.name,r=e.load;if(!r)return/* @__PURE__ */ T(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:n}),height:e.height,children:[/* @__PURE__ */ w(v,{dimColor:!0,children:te(n)}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ T(v,{children:[`Not connected. Press`,` `,/* @__PURE__ */ w(v,{color:`cyan`,bold:!0,children:`↵`}),` `,`to connect and show its sites.`]})]});if(r.kind===`loading`)return/* @__PURE__ */ w(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:n}),height:e.height,children:/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(O,{}),` connecting…`]})});if(r.kind===`error`)return/* @__PURE__ */ T(A,{title:/* @__PURE__ */ w(v,{bold:!0,children:n}),height:e.height,children:[/* @__PURE__ */ w(v,{color:`red`,children:r.error}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ w(v,{children:E(r.error)})]});let{ready:i,traefik:a,proxy:o,sites:s}=r.data,c=/* @__PURE__ */ T(v,{wrap:`truncate`,children:[/* @__PURE__ */ T(v,{bold:!0,children:[`Sites on `,n]}),/* @__PURE__ */ T(v,{dimColor:!0,children:[` `,o,` `]}),a?/* @__PURE__ */ w(v,{color:D(a)?`green`:`red`,children:a}):/* @__PURE__ */ w(v,{color:`yellow`,children:`not installed`})]});if(!i)return/* @__PURE__ */ T(A,{title:c,height:e.height,children:[/* @__PURE__ */ T(v,{children:[n,` is reachable, but landfall has not set it up yet.`]}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ T(v,{children:[`Press`,` `,/* @__PURE__ */ w(v,{color:`cyan`,bold:!0,children:`u`}),` `,`to check what is installed. Nothing changes until you confirm the list (Docker, firewall, deploy user, Traefik).`]})]});if(!s.length)return/* @__PURE__ */ T(A,{title:c,height:e.height,children:[/* @__PURE__ */ w(v,{children:`No sites yet.`}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ T(v,{children:[`Start landfall inside a project folder and press`,` `,/* @__PURE__ */ w(v,{color:`cyan`,bold:!0,children:`d`}),` `,`to deploy it here. A folder with index.html, dist/ or build/ is served as a static site; a Dockerfile is built on the server.`]})]});let l=Math.min(e.siteIdx,s.length-1);if(e.detail)return/* @__PURE__ */ w(N,{s:s[l],height:e.height});let u=k(s,l,e.height-4,e.top),d=e.width-8,f=Math.min(Math.max(...s.map(e=>e.meta.name.length))+2,Math.max(8,Math.floor(d*.4))),p=d-f,ee=p>=40,m=p>=24;return/* @__PURE__ */ w(A,{title:c,focused:e.focused,height:e.height,children:u.items.map((n,r)=>{let i=u.offset+r===l&&e.focused;return/* @__PURE__ */ T(_,{children:[/* @__PURE__ */ w(_,{width:2,flexShrink:0,children:/* @__PURE__ */ w(v,{color:i?`cyan`:void 0,children:i?`›`:` `})}),/* @__PURE__ */ w(_,{width:2,flexShrink:0,children:/* @__PURE__ */ w(v,{color:D(n.status)?`green`:`red`,children:D(n.status)?`●`:`○`})}),/* @__PURE__ */ w(_,{width:f,flexShrink:0,paddingRight:1,children:/* @__PURE__ */ w(v,{bold:i,color:i?`cyan`:void 0,wrap:`truncate`,children:n.meta.name})}),ee&&/* @__PURE__ */ w(_,{width:12,children:/* @__PURE__ */ w(v,{dimColor:!0,children:n.meta.kind})}),/* @__PURE__ */ w(_,{flexGrow:1,flexShrink:1,children:/* @__PURE__ */ w(v,{wrap:`truncate`,children:n.meta.domains.join(` `)})}),m&&/* @__PURE__ */ w(_,{width:10,flexShrink:0,justifyContent:`flex-end`,children:/* @__PURE__ */ w(v,{dimColor:!0,children:t(n.meta.updatedAt)})})]},n.meta.name)})})}const M={open:`green`,limited:`cyan`,blocked:`red`,local:void 0};function he({mode:e,height:t,top:r}){let i=e.data,a=/* @__PURE__ */ T(v,{bold:!0,children:[`Firewall `,/* @__PURE__ */ w(v,{dimColor:!0,children:`on`}),` `,e.server]});if(!i)return/* @__PURE__ */ w(A,{title:a,focused:!0,height:t,children:e.error?/* @__PURE__ */ w(v,{color:`red`,children:e.error}):/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(O,{}),` reading ufw…`]})});let o=t-4,s=[...i.rules,void 0],c=k(s,e.index,Math.max(2,Math.min(s.length,Math.floor((o-4)/2))),r),l=Math.max(0,o-c.items.length-5),u=i.ports.slice(0,l),d=i.ufw===`active`?/* @__PURE__ */ w(v,{color:`green`,children:`on`}):/* @__PURE__ */ w(v,{color:`yellow`,children:i.ufw===`missing`?`not installed`:`off`});return/* @__PURE__ */ T(A,{title:a,focused:!0,height:t,children:[/* @__PURE__ */ T(v,{wrap:`truncate`,children:[`ufw `,d,/* @__PURE__ */ T(v,{dimColor:!0,children:[` · `,`incoming without a rule: `,i.policy===`ACCEPT`?`allowed`:`blocked`,` · `,`ssh port `,i.sshPort,n(i.login).user===`root`?` · as root`:``]})]}),/* @__PURE__ */ T(_,{marginTop:1,flexDirection:`column`,children:[/* @__PURE__ */ w(v,{bold:!0,children:`Rules`}),c.items.map((t,n)=>{let r=c.offset+n,i=r===e.index;return/* @__PURE__ */ T(v,{wrap:`truncate`,color:i?`cyan`:void 0,children:[i?`› `:` `,t?/* @__PURE__ */ T(ce,{children:[/* @__PURE__ */ T(v,{dimColor:!0,children:[String(r+1).padStart(2),` `]}),/* @__PURE__ */ w(v,{color:t.action===`deny`||t.action===`reject`?`red`:`green`,children:t.action.padEnd(6)}),t.spec.replace(/^\w+\s+/,``)]}):/* @__PURE__ */ w(v,{color:`green`,children:`+ add a rule`})]},t?.spec??`+`)})]}),/* @__PURE__ */ T(_,{marginTop:1,flexDirection:`column`,children:[/* @__PURE__ */ w(v,{bold:!0,children:`Listening ports`}),u.map(e=>/* @__PURE__ */ T(v,{wrap:`truncate`,children:[` `,e.port.padEnd(8),e.proc.padEnd(17),/* @__PURE__ */ w(v,{color:M[e.state],dimColor:e.state===`local`,children:e.why})]},`${e.port} ${e.proc}`)),i.ports.length>u.length&&/* @__PURE__ */ T(v,{dimColor:!0,children:[` … `,i.ports.length-u.length,` more (lf firewall status -s `,e.server,`)`]})]})]})}function N({s:e,height:n}){let r=e.meta,i=u(r).slice(0,Math.max(1,n-10));return/* @__PURE__ */ T(A,{title:/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,children:r.name}),` `,/* @__PURE__ */ w(v,{dimColor:!0,children:r.kind}),` `,/* @__PURE__ */ w(v,{color:D(e.status)?`green`:`red`,children:e.status})]}),focused:!0,height:n,children:[r.domains.map(e=>/* @__PURE__ */ T(v,{color:`cyan`,children:[`https://`,e]},e)),/* @__PURE__ */ T(v,{dimColor:!0,children:[a(r.name),` · port `,r.port,r.spa?` · single-page app`:``]}),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ w(v,{bold:!0,children:`Releases`}),i.map(e=>/* @__PURE__ */ T(v,{children:[e.id===r.current?/* @__PURE__ */ w(v,{color:`green`,children:`● `}):` `,e.id,` `,/* @__PURE__ */ w(v,{dimColor:!0,children:t(e.at)}),e.image?/* @__PURE__ */ T(v,{dimColor:!0,children:[` `,e.image]}):null,e.id===r.current?/* @__PURE__ */ w(v,{dimColor:!0,children:` (live)`}):null]},e.id))]})}const P=[[`u`,`check, then set up / repair on y`],[`i`,`OS, memory, disk, containers`],[`t`,`traefik status and routes`],[`f`,`firewall: rules, ports, on / off`],[`h`,`history: what landfall changed, undo it`],[`x`,`open a shell (leaves the dashboard)`],[`*`,`make default server`],[`a`,`add a server`]],F=[[`↵`,`details and releases`],[`l`,`follow logs (esc stops)`],[`r / s`,`restart / stop or start`],[`b`,`roll back to previous release`],[`e`,`environment variables`],[`o`,`open in browser`],[`⌫`,`delete site / forget server`]],I=[`Servers are machines you reach with ssh (anything in ~/.ssh/config works). Set up installs Docker and Traefik there; Traefik gets HTTPS certificates from Let's Encrypt for every site automatically.`,`Sites are deployed from a project folder: static files, a Dockerfile, or an image. Each deploy is a release you can roll back to. .landfall.json in the folder remembers where it goes.`,`Every change landfall makes is written to /opt/landfall/journal.log on the server. h shows it; z reverts a run, also right after a command finished.`];function L(e,t){let n=[],r=``;for(let i of e.split(` `))r&&g(`${r} ${i}`)>t?(n.push(r),r=i):r=r?`${r} ${i}`:i;return[...n,r]}function ge(e){let t=e-4,n=(e,t,n=10)=>/* @__PURE__ */ T(v,{wrap:`truncate`,children:[/* @__PURE__ */ w(v,{color:`cyan`,bold:!0,children:e.padEnd(n)}),t]}),r=[];for(let e of I)r.push(...L(e,t).map(e=>/* @__PURE__ */ w(v,{wrap:`truncate`,children:e})),/* @__PURE__ */ w(v,{children:` `}));if(t>=92){r.push(/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,children:`Servers`.padEnd(46)}),/* @__PURE__ */ w(v,{bold:!0,children:`Sites`})]}));for(let e=0;e<Math.max(P.length,F.length);e++){let[t,i]=[P[e],F[e]];r.push(/* @__PURE__ */ T(_,{children:[/* @__PURE__ */ w(_,{width:46,children:t?n(...t):/* @__PURE__ */ w(v,{children:` `})}),i?n(...i):/* @__PURE__ */ w(v,{children:` `})]}))}}else r.push(/* @__PURE__ */ w(v,{bold:!0,children:`Servers`}),...P.map(e=>n(...e)),/* @__PURE__ */ w(v,{children:` `}),/* @__PURE__ */ w(v,{bold:!0,children:`Sites`}),...F.map(e=>n(...e)));return r.push(/* @__PURE__ */ w(v,{children:` `}),n(`d`,`deploy the folder landfall was started in`),n(`R`,`reload`),/* @__PURE__ */ w(v,{children:` `})),r.push(...L("Commands run inside the dashboard and show their output; esc stops or goes back. Each is a normal landfall command, shown as `$ lf …`, so the same thing works without the dashboard.",t).map(e=>/* @__PURE__ */ w(v,{dimColor:!0,wrap:`truncate`,children:e}))),r}function _e({height:e,width:t,top:n}){let r=ge(t),i=Math.max(1,e-4),a=Math.min(n,Math.max(0,r.length-i)),o=r.length-a-i;return/* @__PURE__ */ w(A,{title:/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(v,{bold:!0,children:`How landfall works`}),o>0&&/* @__PURE__ */ T(v,{dimColor:!0,children:[` ↓ `,o,` more`]})]}),focused:!0,height:e,children:r.slice(a,a+i).map((e,t)=>/* @__PURE__ */ w(_,{children:e},a+t))})}function ve({mode:e,height:n,top:r}){let i=/* @__PURE__ */ T(v,{bold:!0,children:[`History `,/* @__PURE__ */ w(v,{dimColor:!0,children:`on`}),` `,e.server,` `,/* @__PURE__ */ w(v,{dimColor:!0,children:`· /opt/landfall/journal.log`})]}),{runs:a}=e;if(!a?.length)return/* @__PURE__ */ w(A,{title:i,focused:!0,height:n,children:e.error?/* @__PURE__ */ w(v,{color:`red`,children:e.error}):a?/* @__PURE__ */ T(v,{children:[`landfall has not changed anything on `,e.server,` yet.`]}):/* @__PURE__ */ T(v,{children:[/* @__PURE__ */ w(O,{}),` reading the journal…`]})});let o=n-4,s=a[e.index],c=k(a,e.index,Math.max(1,Math.min(a.length,Math.max(Math.ceil(o/2),o-s.steps.length-1))),r),l=Math.max(1,o-c.items.length-2),u=s.steps.slice(0,s.steps.length>l?l-1:l);return/* @__PURE__ */ T(A,{title:i,focused:!0,height:n,children:[c.items.map((n,r)=>{let i=c.offset+r===e.index,a=d(n).length;return/* @__PURE__ */ T(v,{wrap:`truncate`,color:i?`cyan`:void 0,children:[i?`› `:` `,/* @__PURE__ */ w(v,{bold:!0,children:`#${n.n}`.padEnd(5)}),/* @__PURE__ */ w(v,{dimColor:!0,children:t(n.time).padEnd(9)}),/* @__PURE__ */ w(v,{bold:i,dimColor:!a,children:n.cmd||`?`}),a?/* @__PURE__ */ T(v,{color:`cyan`,children:[` ↺`,a]}):null,/* @__PURE__ */ T(v,{dimColor:!0,children:[` `,n.actor]})]},n.id)}),/* @__PURE__ */ T(_,{marginTop:1,flexDirection:`column`,children:[u.map(e=>/* @__PURE__ */ T(v,{wrap:`truncate`,children:[` `,e.reverts?/* @__PURE__ */ T(v,{dimColor:!0,children:[`↩ `,e.what]}):e.undone?/* @__PURE__ */ T(v,{dimColor:!0,children:[`✓ `,e.what,` (undone)`]}):e.undo?/* @__PURE__ */ T(ce,{children:[/* @__PURE__ */ w(v,{color:`cyan`,children:`↺`}),` `,e.what]}):/* @__PURE__ */ T(v,{dimColor:!0,children:[`· `,e.what]})]},e.line)),s.steps.length>u.length&&/* @__PURE__ */ T(v,{dimColor:!0,children:[` … `,s.steps.length-u.length,` more (lf history -s `,e.server,`)`]})]})]})}const R=e=>/^==> /.test(e)?`cyan`:/^\s*✓/.test(e)?`green`:/^\s*!/.test(e)?`yellow`:/^\s*(error|✗)/i.test(e)?`red`:void 0;function z(e,t){let n=[],r=``;for(let i of e)g(r+i)>t&&(n.push(r),r=``),r+=i;return n.push(r),n}const ye=(e,t)=>e.lines.flatMap(e=>z(e,Math.max(10,t-4)).map(t=>({r:t,color:R(e)}))),be=e=>Math.max(1,e-4);function xe({job:e,top:t,height:n,width:r}){let i=ye(e,r),a=be(n),o=Math.max(0,i.length-a),s=Math.min(t??o,o),c=i.slice(s,s+a),l=i.length-s-c.length,u=e.code===void 0?/* @__PURE__ */ w(O,{}):e.code===0?/* @__PURE__ */ w(v,{color:`green`,children:`✓ done`}):e.code===null?/* @__PURE__ */ w(v,{color:`yellow`,children:`stopped`}):/* @__PURE__ */ T(v,{color:`red`,children:[`✗ exit `,e.code]});return/* @__PURE__ */ w(A,{title:/* @__PURE__ */ T(v,{wrap:`truncate`,children:[/* @__PURE__ */ T(v,{bold:!0,children:[`$ lf `,e.argv.join(` `)]}),` `,u,l>0&&/* @__PURE__ */ T(v,{dimColor:!0,children:[` ↓ `,l,` more line`,l===1?``:`s`]})]}),focused:!0,height:n,children:c.map(({r:e,color:t},n)=>/* @__PURE__ */ w(v,{color:t,wrap:`truncate`,children:e||` `},n))})}async function Se(e){let t={server:0,site:0,focus:`servers`,cache:/* @__PURE__ */ new Map,connect:/* @__PURE__ */ new Set};for(;;){let n;m(!0),process.stdout.write(`\x1B[?1049h\x1B[H`);try{await y(/* @__PURE__ */ w(j,{state:t,onAction:e=>n=e}),{exitOnCtrlC:!1}).waitUntilExit()}finally{process.stdout.write(`\x1B[?1049l`),m(!1)}if(!n)return 0;console.log(i.dim(`$ lf ${n.argv.join(` `)}\n`));let r=()=>{};process.on(`SIGINT`,r);let a=await e(n.argv).finally(()=>process.off(`SIGINT`,r));n.server&&t.cache.delete(n.server),await Ce(a===0?i.green(`✓ done`):i.red(`✗ failed`))}}function Ce(e){process.stdout.write(`\n${e} ${i.dim(`press any key to go back to the dashboard`)}`);let{stdin:t}=process;return new Promise(e=>{t.setRawMode(!0),t.resume(),t.once(`data`,()=>{t.setRawMode(!1),t.pause(),process.stdout.write(`
|
|
8
|
+
`),e()})})}export{Se as dashboard};
|
package/package.json
CHANGED
package/dist/server-CkG3c5Za.mjs
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import{C as e,D as t,E as n,F as r,H as i,I as a,L as o,M as s,N as c,O as ee,P as l,R as te,S as u,T as d,V as f,_ as p,a as m,b as h,d as g,f as _,h as v,i as y,j as b,k as x,l as ne,n as re,o as ie,p as ae,r as oe,s as se,t as S,u as ce,v as C,w as le,x as w,y as T,z as E}from"./cli.mjs";import{i as D,n as O,r as k,t as A}from"./job-CypEf1ud.mjs";import{spawnSync as j}from"node:child_process";import M from"node:fs";import{homedir as N}from"node:os";import{dirname as P,isAbsolute as F,join as I,resolve as L}from"node:path";import{randomBytes as R,timingSafeEqual as z}from"node:crypto";import{createServer as B}from"node:http";import{Readable as V}from"node:stream";function H(e){let t=L(!e||e===`~`?N():r(e)),n=M.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&U(I(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:P(t)===t?null:P(t),home:N(),dirs:n,detected:p(t)}}const U=e=>{try{return M.statSync(e).isDirectory()}catch{return!1}},W=I(l,`uploads`),G=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function K(e,t){M.mkdirSync(P(e),{recursive:!0}),M.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function ue(e){M.mkdirSync(W,{recursive:!0,mode:448});for(let e of M.readdirSync(W)){let t=I(W,e);Date.now()-M.statSync(t).mtimeMs>864e5&&M.rmSync(t,{recursive:!0,force:!0})}let t=M.mkdtempSync(I(W,`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=I(t,`.upload-`+G(n[0].name).pop());await K(e,n[0]);let r=I(t,`files`);M.mkdirSync(r);let i=/\.zip$/i.test(e),a=i?j(`unzip`,[`-q`,e,`-d`,r]):j(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?j(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return M.rmSync(e),{path:q(r),files:de(r)}}let i=I(t,`files`);for(let[e,t]of n.entries()){let n=G(r[e]||t.name);n.length&&await K(I(i,...n),t)}return{path:q(i),files:n.length}}function q(e){for(;;){let t=M.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!U(I(e,t[0])))return e;e=I(e,t[0])}}function de(e){return M.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function fe(e){if(e!=null&&e!==``){if(typeof e!=`string`||!F(e)||!U(e))throw Error(`cwd must be an existing folder`);return e}}const pe=M.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),J=/* @__PURE__ */ new Map,me=/* @__PURE__ */ new Set([`ui`,`web`,`tui`]);function he(){let e=I(P(s),`ui-token`);try{let t=M.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=R(24).toString(`hex`);return M.mkdirSync(P(e),{recursive:!0,mode:448}),M.writeFileSync(e,t+`
|
|
2
|
-
`,{mode:384}),t}const Y=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),X=(e,t=500)=>Y({error:e?.message??String(e)},t);function Z(e,t){let n=`${Date.now().toString(36)}${Math.random().toString(36).slice(2,6)}`,r=/* @__PURE__ */ new Set,i=A(e,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,env:{SRV_RUN:n}}),a={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:O(e),job:i,listeners:r};J.set(n,a);for(let[e,t]of J){if(J.size<=50)break;t.job.code!==void 0&&J.delete(e)}return a}const ge=e=>e.map((t,n)=>[`-p`,`--password`].includes(e[n-1])?`***`:t.replace(/^(--password=).*/,`$1***`)),Q=e=>({id:e.id,argv:ge(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped});function _e(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)})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Q(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
3
|
-
|
|
4
|
-
`),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 ve(r,s){let[l,f,p]=s,b=r.method;if(l===`state`&&b===`GET`){let r=a();return Y({version:S,cwd:process.cwd(),project:o(process.cwd())??null,projects:C(),defaultImage:e,paths:{root:n,sites:t,traefik:ee,journal:le,proxy:d},servers:k().map(e=>({...e,target:D(e.name),saved:r.servers[e.name]??null})),jobs:[...J.values()].reverse().map(Q)})}if(l===`servers`&&f){let{server:e}=te(f);if(b===`GET`&&!p)return Y(await v(e));if(b===`GET`&&p===`firewall`){let{login:t,probe:n,...r}=await y(e);return Y({...r,asRoot:x(t).user===`root`})}if(b===`GET`&&p===`config`){let t=ie(f),[n,r]=await Promise.all([Promise.all(t.domains.map(t=>m(e,t))),se(e)]);return Y({...t,dns:n,suggest:r})}if(b===`GET`&&p===`history`){let t=(await oe(e)).reverse();return Y(t.map(e=>({...e,pending:re(e).length})))}if(b===`GET`&&p===`facts`){let t=await h(e),n=await u(e,t);return Y({...t,ips:n,sslipIp:w(n)??null})}if(b===`GET`&&p===`updates`){let t=new URL(r.url).searchParams.get(`refresh`)===`1`,n=await ae(e,t),i=await g(e);return Y({packages:n,images:await _(e),engine:{...i,...ce(i)}})}if(b===`GET`&&p===`dns`){let t=(new URL(r.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>c.test(e)).slice(0,50),n=await u(e),[i,a]=await Promise.all([T(t,n),Promise.all(t.map(ne))]);return Y({ips:n,domains:i.map((e,t)=>({...e,https:a[t]}))})}if(b===`POST`&&p==="default"){let e=a();return e.servers[f]??={host:f},e.default=f,E(e),Y({ok:!0})}}if(l===`jobs`){if(b===`POST`&&!f){let e=await r.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||me.has(t[0]))return X(`argv must be a list of landfall arguments`,400);let n;try{n=fe(e.cwd)}catch(e){return X(e,400)}return Y(Q(Z(t,{server:e.server,password:e.password||void 0,cwd:n})))}let e=f?J.get(f):void 0;if(f&&!e)return X(`no job ${f}`,404);if(e&&b===`GET`&&p===`events`)return _e(e,Number(new URL(r.url).searchParams.get(`from`))||0,r.signal);if(e&&b===`GET`&&!p)return Y({...Q(e),lines:e.job.lines,from:e.job.dropped});if(e&&b===`POST`&&p===`stop`)return e.job.stop(),Y(Q(e))}if(l===`fs`&&b===`GET`)return Y(H(new URL(r.url).searchParams.get(`path`)??void 0));if(l===`projects`&&b===`GET`)return Y(C());if(l===`upload`&&b===`POST`){let e=await ue(await r.formData());return Y({...e,detected:H(e.path).detected})}if(l===`open`&&b===`POST`){let{url:e}=await r.json();return!e||!/^https?:\/\//.test(e)?X(`not a web address`,400):(i(e),Y({ok:!0}))}return X(`not found`,404)}const $=(e,t)=>e.length===t.length&&z(Buffer.from(e),Buffer.from(t)),ye=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`srv=`))?.slice(4)??``;async function be(e){b(!0);let t=he(),n=B(xe(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===`/login`)return $(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`srv=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!$(ye(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(pe,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`))try{return await ve(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return X(e)}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=await new Promise(t=>{n.once(`error`,t),n.listen(e.port,`127.0.0.1`,()=>t(void 0))});if(r){if(r.code!==`EADDRINUSE`)throw r;let n=`http://127.0.0.1:${e.port}/login?t=${t}`;return await fetch(`http://127.0.0.1:${e.port}/api/state`,{headers:{cookie:`srv=${t}`}}).then(e=>e.ok,()=>!1)?(console.log(`lf ui already runs: ${f.cyan(n)}`),e.open&&i(n),0):(console.error(`${f.red(`error:`)} port ${e.port} is in use; pick another with --port`),1)}let a=`http://127.0.0.1:${n.address().port}/login?t=${t}`;return console.log(`${f.bold(`lf ui`)} ${f.dim(S)} on ${f.cyan(a)}`),console.log(f.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&i(a),new Promise(e=>{let t=()=>{for(let e of J.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function xe(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let n=new Headers;for(let[e,r]of Object.entries(t.headers))for(let t of[r??[]].flat())n.append(e,t);let a=t.method===`GET`||t.method===`HEAD`?void 0:V.toWeb(t);i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:n,body:a,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=V.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{be as webUi};
|
package/dist/tui-B0KECXvO.mjs
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{A as e,B as t,I as n,L as r,R as i,V as a,g as o,h as s,i as c,j as l,k as u,m as d,n as f,r as p,t as ee,z as te}from"./cli.mjs";import{i as ne,n as re,r as m,t as ie}from"./job-CypEf1ud.mjs";import{spawn as ae}from"node:child_process";import h from"string-width";import{basename as oe}from"node:path";import{Box as g,Text as _,render as v,useApp as se,useInput as ce,useStdout as y}from"ink";import{useEffect as b,useRef as x,useState as S}from"react";import{Fragment as le,jsx as C,jsxs as w}from"react/jsx-runtime";function T(e){return/permission denied/i.test(e)?`No key login yet. Press u to set it up: landfall asks for the password once and installs a key.`:/resolve|not known|nodename/i.test(e)?`Host name not found. Check the entry in ~/.ssh/config.`:/timed out|refused|no route|unreachable/i.test(e)?`Server not reachable. Is it running, and is the ssh port right?`:`Press R to retry.`}const E=e=>e.startsWith(`Up`);function ue({load:e}){return e?e.kind===`loading`?/* @__PURE__ */ C(D,{}):e.kind===`error`?/* @__PURE__ */ C(_,{color:`red`,children:`●`}):e.data.ready?/* @__PURE__ */ C(_,{color:`green`,children:`●`}):/* @__PURE__ */ C(_,{color:`yellow`,children:`●`}):/* @__PURE__ */ C(_,{dimColor:!0,children:`○`})}function D(){let[e,t]=S(0);return b(()=>{let e=setInterval(()=>t(e=>(e+1)%10),80);return()=>clearInterval(e)},[]),/* @__PURE__ */ C(_,{color:`cyan`,children:`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`[e]})}function de(){let e=y().stdout,[t,n]=S({cols:e.columns||80,rows:e.rows||24});return b(()=>{let t=()=>n({cols:e.columns,rows:e.rows});return e.on(`resize`,t),()=>void e.off(`resize`,t)},[e]),t}function O(e,t,n,r){let i=r.current;return t<i&&(i=t),t>=i+n&&(i=t-n+1),i=Math.max(0,Math.min(i,e.length-n)),r.current=i,{items:e.slice(i,i+n),offset:i}}function k({title:e,focused:t,width:n,height:r,children:i}){return/* @__PURE__ */ w(g,{flexDirection:`column`,borderStyle:`round`,borderColor:t?`cyan`:`gray`,width:n,flexGrow:+!n,height:r,paddingX:1,overflow:`hidden`,children:[/* @__PURE__ */ C(g,{marginBottom:1,children:e}),i]})}function fe(e,t,n){let r=[[]],i=0;for(let a of e){let e=h(`${a[0]} ${a[1]}`)+3;i&&i+e-3>t&&r.length<n&&(r.push([]),i=0),r[r.length-1].push(a),i+=e}return r}function pe({lines:e}){return e.map((e,t)=>/* @__PURE__ */ C(_,{wrap:`truncate`,children:e.map(([e,t],n)=>/* @__PURE__ */ w(_,{children:[n?` `:` `,/* @__PURE__ */ C(_,{color:`cyan`,bold:!0,children:e}),` `,t]},e))},t))}function A({state:a,onAction:o}){let{exit:l}=se(),{cols:u,rows:h}=de(),[v,y]=S(m),[T,D]=S(Math.min(a.server,v.length)),[A,j]=S(a.site),[M,N]=S(a.focus),[P,F]=S({kind:`browse`}),[I,L]=S(``),[,R]=S(0),Ce=x(0),we=x(0),Te=x(0),Ee=x(0),z=x(void 0),B=r(process.cwd()),V=v[T],H=V&&a.cache.get(V.name),U=H?.kind===`ok`?H.data.sites:[],W=U[Math.min(A,U.length-1)];b(()=>{Object.assign(a,{server:T,site:A,focus:M})},[T,A,M]),b(()=>{if(!V||!a.connect.has(V.name)||a.cache.has(V.name))return;let e=V.name;a.cache.set(e,{kind:`loading`}),R(e=>e+1),s(i(e).server).then(t=>a.cache.set(e,{kind:`ok`,data:t}),t=>a.cache.set(e,{kind:`error`,error:t.message??String(t)})).finally(()=>R(e=>e+1))},[V?.name,H,a.connect.has(V?.name??``)]);let G=(e,t={})=>{let n=t.server??V?.name,r=`${Date.now().toString(36)}${Math.random().toString(36).slice(2,6)}`,i=!1,o=ie(e,()=>{if(o.code!==void 0&&!i){i=!0,n&&a.cache.delete(n),y(m());let s=t.after?.(o)||(n&&re(e)?Oe(r,n,e):void 0);F(e=>e.kind===`run`&&e.job===o?{...e,next:s}:e)}R(e=>e+1)},{password:t.password,env:{SRV_RUN:r}});F({kind:`run`,job:o})},De=e=>{o({argv:e,server:V?.name}),l()},K=()=>{let e=z.current;if(!e)return F({kind:`browse`});e.kind===`firewall`?Pe(e.server,e.index):Y(e.server,e.index,e.id)},Oe=(e,t,n)=>[{key:`z`,label:`undo this`,run:()=>q(`Undo \`lf ${n.join(` `)}\` on ${t}?`,`Reverts the changes this command recorded in the server's journal.`,`Undo it`,()=>G([`undo`,e,`-s`,t,`-y`],{server:t,after:()=>[]}))}],q=(e,t,n,r)=>F({kind:`choose`,title:e,body:t,index:0,options:[{label:n,pick:r},{label:`Cancel`,pick:K}]}),J=(e,t,n,r=!1,i=``)=>F({kind:`prompt`,title:e,hint:t,value:i,secret:r,submit:n});function ke(e,t=e){G([`setup`,...e,`--check`],{server:e[0],after:n=>{if(n.code!==0)return;if(n.lines.some(e=>e.includes(`no key login yet`)))return[{key:`p`,label:`enter the password to install a key`,run:()=>je(e,t)}];let r=n.lines.filter(e=>/^\s+→ /.test(e)).length;return[{key:`y`,label:r?`apply ${r} change${r===1?``:`s`}`:`finish setup (nothing to install)`,run:()=>G([`setup`,...e,`-y`],{server:e[0]})},{key:`o`,label:`change options (traefik, firewall, ssh, deploy user)`,run:()=>Ae(t)}]}})}function Ae(e){let t=n().servers[e[0]]??{host:e[0]},r=[],i=(...e)=>r.push(...e),a=(e,t,n,r=0)=>F({kind:`choose`,title:e,body:t,options:n,index:r}),o=(e,t,n,r,a)=>J(e,t,e=>(e.trim()&&i(`--${r}=${e.trim()}`),a()),!1,n),s=()=>a(`Setup 1/4 · Traefik`,`Reverse proxy with automatic HTTPS for your sites.
|
|
2
|
-
If the server already runs a Traefik on 80/443, setup routes sites through it instead.`,[{label:`Own Traefik, ports published by Docker`,hint:`default; Docker forwards 80/443 to the container`,pick:()=>(i(`--no-host-network`),c())},{label:`Own Traefik on the host network`,hint:`sees real client IPs (also IPv6); ufw guards 80/443`,pick:()=>(i(`--host-network`),c())},{label:`No Traefik`,hint:`add it later with lf traefik setup`,pick:()=>(i(`--no-traefik`),d())}],+!!t.hostNetwork),c=()=>o(`Setup · Traefik image`,`Docker image to run. Enter keeps it.`,t.traefikImage??`traefik:v3`,`image`,l),l=()=>o(`Setup · Let's Encrypt e-mail`,`Optional contact for the certificate account. Empty skips.`,t.email??``,`email`,u),u=()=>o(`Setup · Traefik dashboard`,`Optional domain for the dashboard behind basic auth; the password is shown after setup. Empty skips.`,``,`dashboard`,d),d=()=>a(`Setup 2/4 · Firewall`,`ufw: deny incoming except ssh, 80 and 443.
|
|
3
|
-
Skipped automatically if it would block ports in use.`,[{label:`Enable ufw`,pick:f},{label:`Leave the firewall alone`,hint:`manage it later with f`,pick:()=>(i(`--no-firewall`),f())}]),f=()=>a(`Setup 3/4 · SSH`,`Turn off password login so only keys work.
|
|
4
|
-
Skipped automatically if other users still log in with a password.`,[{label:`Key login only`,pick:p},{label:`Leave sshd alone`,pick:()=>(i(`--no-harden`),p())}]),p=()=>J(`Setup 4/4 · Deploy user`,`landfall deploys as this user (docker group, your key). Empty keeps the login user.`,e=>(i(`--deploy-user=${e.trim()}`),ee()),!1,`deploy`),ee=()=>ke([...e,...r],e);s()}function je(e,t=e){let n=e.includes(`--host`)?`root@${e[e.indexOf(`--host`)+1]}`:ne(e[0]);J(`Password for ${n}`,`Used once to install landfall's ssh key (ssh-copy-id). It is not stored; afterwards only the key is used.`,n=>{if(!n)return K();G([`setup`,...e,`--key-only`,`-y`],{password:n,server:e[0],after:n=>n.code===0?ke(e,t):[{key:`p`,label:`try the password again`,run:()=>je(e,t)}]})},!0)}function Me(){J(`Add a server`,`Its name. Use an alias from ~/.ssh/config (like het), or pick a new name for a fresh machine.`,t=>{if(t=t.trim(),!t)return K();e().includes(t)||n().servers[t]?F({kind:`choose`,title:`${t} is in your ssh config`,index:0,options:[{label:`Set it up now`,hint:`choose options, see what it would change, then apply`,pick:()=>Ae([t])},{label:`Only add it to landfall`,hint:`no changes on the server`,pick:()=>G([`server`,`add`,t],{server:t})},{label:`Cancel`,pick:K}]}):J(`Add ${t}`,`IP address or hostname. lf logs in as root once with its password and installs an ssh key.`,e=>e.trim()?Ae([t,`--host`,e.trim()]):K())})}function Ne(){let e=B?.server??V?.name;if(!e)return L(`Pick a server on the left first.`);if(B)return q(`Deploy ${B.name} to ${e}?`,`From ${process.cwd()}\nServed on ${B.domains.join(`, `)} (settings from srv.json)`,`Deploy`,()=>G([`deploy`,`-y`,...B.server?[]:[`-s`,e]],{server:e}));J(`Deploy ${oe(process.cwd())} to ${e}`,`Domain(s) to serve it on, e.g. example.com www.example.com. Their DNS must point at the server.`,t=>{let n=t.split(/[\s,]+/).filter(Boolean);if(!n.length)return K();G([`deploy`,`-y`,`-s`,e,...n.flatMap(e=>[`-d`,e])],{server:e})})}function Pe(e,t=0){z.current={kind:`firewall`,server:e,index:t},F({kind:`firewall`,server:e,index:t});let n=t=>F(n=>n.kind===`firewall`&&n.server===e?{...n,...t,index:Math.min(n.index,t.data?.rules.length??0)}:n);c(i(e).server).then(e=>n({data:e}),e=>n({error:e.message??String(e)}))}function Fe(e,t){if(P.kind!==`firewall`)return;let{server:n,data:r,index:i}=P;if(t.escape||t.leftArrow)return z.current=void 0,K();if(e===`R`)return Pe(n,i);if(!r)return;let a=[`-s`,n],o=r.rules[i],s=String(i+1),c=e=>G(e,{server:n,after:t=>t.code!==0&&!e.includes(`--force`)&&t.lines.some(e=>e.includes(`--force`))?[{key:`f`,label:`do it anyway`,run:()=>c([...e,`--force`])}]:void 0}),l=()=>J(`Open a port`,`port[/proto], e.g. 5432, 51820/udp or 6000:6007/tcp`,e=>{if(!e.trim())return K();J(`Who may reach ${e.trim()}?`,`An address or network like 10.0.0.0/8. Leave empty for anyone.`,t=>c([`firewall`,`allow`,e.trim(),...a,...t.trim()?[`--from`,t.trim()]:[]]))}),u=e=>{z.current={kind:`firewall`,server:n,index:e},F({...P,index:e})};if(t.upArrow||e===`k`)return u(Math.max(0,i-1));if(t.downArrow||e===`j`)return u(Math.min(r.rules.length,i+1));if(e===`t`)return r.ufw===`active`?q(`Turn the firewall on ${n} off?`,`Every listening port becomes reachable from anywhere.`,`Turn it off`,()=>c([`firewall`,`disable`,...a,`-y`])):c([`firewall`,`enable`,...a]);if(e===`a`||!o&&t.return)return l();if(o){if(e===` `)return c([`firewall`,`update`,s,`--action`,o.action===`deny`||o.action===`reject`?`allow`:`deny`,...a]);if(e===`e`||t.return){let e=o.port?`${o.port}${o.proto?`/${o.proto}`:``}`:``;return J(`Edit rule ${s}: ${o.spec}`,`port[/proto]`,t=>J(`Who may reach ${t.trim()||e}?`,`An address or network like 10.0.0.0/8. Leave empty for anyone.`,n=>c([`firewall`,`update`,s,...a,...t.trim()&&t.trim()!==e?[`--port`,t.trim()]:[],`--from`,n.trim()||`any`]),!1,o.from??``),!1,e)}if(t.delete||t.backspace)return q(`Remove rule ${s}?`,o.spec,`Remove it`,()=>c([`firewall`,`rm`,s,...a,`-y`]))}}function Y(e,t=0,n){z.current={kind:`history`,server:e,index:t,id:n},F({kind:`history`,server:e,index:t});let r=t=>F(r=>{if(r.kind!==`history`||r.server!==e)return r;let i=n?t.runs?.findIndex(e=>e.id===n)??-1:-1;return{...r,...t,index:i>=0?i:Math.min(r.index,Math.max(0,(t.runs?.length??1)-1))}});p(i(e).server).then(e=>r({runs:e.reverse()}),e=>r({error:e.message??String(e)}))}function Ie(e,t){if(P.kind!==`history`)return;let{server:n,runs:r,index:i}=P;if(t.escape||t.leftArrow)return z.current=void 0,K();if(e===`R`)return Y(n,i,r?.[i]?.id);if(!r?.length)return;let a=e=>{z.current={kind:`history`,server:n,index:e,id:r[e].id},F({...P,index:e})};if(t.upArrow||e===`k`)return a(Math.max(0,i-1));if(t.downArrow||e===`j`)return a(Math.min(r.length-1,i+1));let o=r[i],s=e=>G([`undo`,...e,`-s`,n,`-y`],{server:n,after:()=>[]});if(e===`z`||t.return){let e=f(o);if(!e.length)return L(`#${o.n} has nothing left to undo`);let t=r.filter(e=>e.n>o.n&&f(e).length),i=[...e.map(e=>`↺ ${e.what}`).reverse(),...t.length?[``,`Later runs build on it and stay: ${t.map(e=>`#${e.n}`).join(`, `)}`]:[]].join(`
|
|
5
|
-
`);return q(`Undo #${o.n} ${o.cmd} on ${n}?`,i,`Revert ${e.length} change${e.length>1?`s`:``}`,()=>s([String(o.n)]))}if(e===`U`){let e=r.flatMap(f);return e.length?q(`Undo everything landfall did on ${n}?`,`${e.length} changes, newest first. Sites, Traefik, the deploy user and landfall's firewall rules go away.`,`Revert everything`,()=>s([`--all`])):L(`nothing left to undo`)}}function Le(e,t){if(!V){(t.return||e===`a`)&&Me();return}let r=V.name;if(t.return||t.rightArrow||t.tab){if(!a.connect.has(r))return a.connect.add(r),R(e=>e+1);U.length&&N(`sites`)}else{if((t.delete||t.backspace)&&V.registered)return q(`Forget ${r}?`,`Removes it from landfall (and the Host block landfall wrote to ~/.ssh/config).
|
|
6
|
-
Nothing on the server is changed.`,`Forget ${r}`,()=>G([`server`,`rm`,r,`-y`]));switch(e){case`c`:case`u`:return ke([r]);case`i`:return G([`server`,`info`,r]);case`t`:return G([`traefik`,`status`,`-s`,r]);case`f`:return Pe(r);case`h`:return Y(r);case`x`:return De([`ssh`,r]);case`a`:return Me();case`*`:{let e=n();return e.servers[r]??={host:r},e.default=r,te(e),y(m()),L(`${r} is now the default server`)}}}}function Re(e,n){if(!V||!W)return;let r=[`-s`,V.name],i=W.meta.name;if(n.return)return F({kind:`detail`});if(n.delete||n.backspace)return q(`Delete ${i} from ${V.name}?`,`Stops it and removes all its files and releases.
|
|
7
|
-
This cannot be undone.`,`Delete ${i}`,()=>G([`sites`,`rm`,i,...r,`-y`]));switch(e){case`l`:return G([`logs`,i,...r,`-f`]);case`r`:return G([`restart`,i,...r]);case`s`:return G([E(W.status)?`stop`:`start`,i,...r]);case`b`:{let e=d(W.meta).filter(e=>e.at<(W.meta.releases.find(e=>e.id===W.meta.current)?.at??``));return e.length?q(`Roll back ${i}?`,`From ${W.meta.current} to ${e[0].id} (${t(e[0].at)})`,`Roll back`,()=>G([`rollback`,i,...r,`-y`])):L(`${i} has no older release to go back to`)}case`e`:return W.meta.kind===`static`?L(`static sites have no environment variables`):J(`Environment of ${i}`,`KEY=VALUE pairs to set (the container restarts). Leave empty to just show them.`,e=>G([`env`,i,...r,...e.split(/\s+/).filter(Boolean)]));case`o`:{let e=`https://${W.meta.domains[0]}`;return ae(process.platform===`darwin`?`open`:`xdg-open`,[e],{stdio:`ignore`,detached:!0}).unref(),L(`opened ${e}`)}}}let ze=P.kind===`run`?P.job.code===void 0?[[`esc`,`stop`],[`↑↓ space`,`scroll`],[`g G`,`top / end`]]:[...(P.next??[]).map(e=>[e.key,e.label]),[`esc`,`back`],[`↑↓ space`,`scroll`],[`g G`,`top / end`]]:P.kind===`firewall`?[[`esc`,`back`],[`↑↓`,`rule`],[`t`,P.data?.ufw===`active`?`turn off`:`turn on`],[`a`,`add`],[`e`,`edit`],[`space`,`allow ↔ deny`],[`⌫`,`remove`],[`R`,`reload`]]:P.kind===`history`?[[`esc`,`back`],[`↑↓`,`run`],[`z ↵`,`undo run`],[`U`,`undo everything`],[`R`,`reload`]]:P.kind===`choose`?[[`↑↓`,`choose`],[`↵`,`ok`],[`esc`,`back`]]:P.kind===`prompt`?[[`↵`,`ok`],[`esc`,`cancel`]]:P.kind===`help`?[[`↑↓ space`,`scroll`],[`any key`,`close`]]:V?M===`servers`&&P.kind===`browse`?[[`?`,`help`],[`q`,`quit`],[`↑↓`,`server`],[`↵`,V&&a.connect.has(V.name)?`sites`:`connect`],[`u`,`check & set up`],[`d`,`deploy here`],[`i`,`info`],[`t`,`traefik`],[`f`,`firewall`],[`h`,`history`],[`x`,`shell ↗`],[`*`,`default`],[`a`,`add`],...V.registered?[[`⌫`,`forget`]]:[]]:[[`?`,`help`],[`←`,`servers`],[`↑↓`,`site`],[`↵`,`details`],[`l`,`logs`],[`r`,`restart`],[`s`,W&&E(W.status)?`stop`:`start`],[`b`,`roll back`],[`e`,`env`],[`o`,`open`],[`⌫`,`delete`]]:[[`↵`,`add a server`],[`d`,`deploy this folder`],[`?`,`help`],[`q`,`quit`]],X=u<72,Be=fe(ze,u-1,h<16?2:3),Z=Math.max(6,h-3-Be.length),Ve=Math.min(36,Math.floor(u*.4),Math.max(26,...v.map(e=>e.name.length+14))),Q=X&&Z<16,He=X&&!Q?Math.max(5,Math.min(v.length+5,Math.floor(Z*.4))):Z,Ue=X&&!Q?Z-He:Z,We=X?u:u-Ve;ce((e,t)=>{L(``);let n=P.kind===`run`&&P.job.code===void 0;if(t.ctrl&&e===`c`)return n?P.job.stop():l();if(P.kind===`run`){let r=xe(Z),i=Math.max(0,be(P.job,u).length-r),a=e=>F({...P,top:e>=i?void 0:Math.max(0,e)}),o=Math.min(P.top??i,i);return t.escape||e===`q`||t.leftArrow?n?P.job.stop():K():t.upArrow||e===`k`?a(o-1):t.downArrow||e===`j`?a(o+1):t.pageUp?a(o-r):t.pageDown||e===` `?a(o+r):e===`g`||t.home?a(0):e===`G`||t.end?a(i):P.next?.find(t=>t.key===e)?.run()}if(P.kind===`prompt`){if(t.escape)return K();if(t.return)return P.submit(P.value);if(t.backspace||t.delete)return F({...P,value:P.value.slice(0,-1)});e&&!t.ctrl&&!t.meta&&F({...P,value:P.value+e})}else{if(P.kind===`choose`){if(t.escape)return K();if(t.upArrow||t.downArrow){let e=t.upArrow?-1:1;return F({...P,index:(P.index+e+P.options.length)%P.options.length})}return t.return?P.options[P.index].pick():void 0}if(P.kind===`help`){let n=Math.max(0,_e(u).length-(Z-4)),r=e=>F({...P,top:Math.max(0,Math.min(n,e))});return t.upArrow||e===`k`?r(P.top-1):t.downArrow||e===`j`?r(P.top+1):e===` `||t.pageDown?r(P.top+Z-4):t.pageUp?r(P.top-(Z-4)):K()}if(P.kind===`firewall`&&e!==`q`&&e!==`?`)return Fe(e,t);if(P.kind===`history`&&e!==`q`&&e!==`?`)return Ie(e,t);if(e===`q`)return l();if(e===`?`)return F({kind:`help`,top:0});if(e===`d`)return Ne();if(e===`R`)return a.cache.clear(),y(m()),R(e=>e+1);if(P.kind===`detail`)return t.escape||t.leftArrow?K():Re(e,t);if(M===`servers`)return t.upArrow||e===`k`?(j(0),D(e=>Math.max(0,e-1))):t.downArrow||e===`j`?(j(0),D(e=>Math.min(v.length,e+1))):t.escape?l():Le(e,t);if(t.escape||t.leftArrow)return N(`servers`);if(t.upArrow||e===`k`)return j(e=>Math.max(0,e-1));if(t.downArrow||e===`j`)return j(e=>Math.min(U.length-1,e+1));Re(e,t)}});let Ge=/* @__PURE__ */ w(g,{justifyContent:`space-between`,children:[/* @__PURE__ */ C(g,{flexShrink:0,children:/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,color:`cyan`,children:`landfall`}),` `,/* @__PURE__ */ C(_,{dimColor:!0,children:ee})]})}),/* @__PURE__ */ C(g,{flexShrink:1,flexGrow:0,marginLeft:2,overflow:`hidden`,children:/* @__PURE__ */ C(_,{dimColor:!0,wrap:`truncate`,children:B?`this folder: ${B.name} → ${B.server??`?`} · ${B.domains.join(`, `)}`:`this folder: ${oe(process.cwd())} (not deployed yet · d to deploy)`})})]}),$;if(u<40||h<12)$=/* @__PURE__ */ w(_,{color:`yellow`,children:[`Make the terminal bigger: `,u,`×`,h,`, landfall needs 40×12.`]});else if(P.kind===`help`)$=/* @__PURE__ */ C(ve,{height:Z,width:u,top:P.top});else if(P.kind===`firewall`)$=/* @__PURE__ */ C(ge,{mode:P,height:Z,top:Te});else if(P.kind===`history`)$=/* @__PURE__ */ C(ye,{mode:P,height:Z,top:Ee});else if(P.kind===`run`)$=/* @__PURE__ */ C(Se,{job:P.job,top:P.top,height:Z,width:u});else if(P.kind===`choose`||P.kind===`prompt`)$=/* @__PURE__ */ C(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:P.title}),focused:!0,height:Z,children:P.kind===`choose`?/* @__PURE__ */ w(g,{flexDirection:`column`,children:[P.body&&/* @__PURE__ */ C(g,{marginBottom:1,children:/* @__PURE__ */ C(_,{children:P.body})}),P.options.map((e,t)=>/* @__PURE__ */ w(_,{color:t===P.index?`cyan`:void 0,children:[t===P.index?`› `:` `,/* @__PURE__ */ C(_,{bold:t===P.index,children:e.label}),e.hint&&/* @__PURE__ */ w(_,{dimColor:!0,children:[` `,e.hint]})]},e.label))]}):/* @__PURE__ */ w(g,{flexDirection:`column`,children:[/* @__PURE__ */ C(_,{dimColor:!0,children:P.hint}),/* @__PURE__ */ C(g,{marginTop:1,borderStyle:`single`,borderColor:`cyan`,paddingX:1,children:/* @__PURE__ */ w(_,{children:[P.secret?`•`.repeat(P.value.length):P.value,/* @__PURE__ */ C(_,{inverse:!0,children:` `})]})})]})});else{let e=O([...v,void 0],T,He-4,Ce),t=v.findIndex(e=>!e.registered);$=/* @__PURE__ */ w(g,{flexDirection:X?`column`:`row`,children:[!(Q&&M===`sites`)&&/* @__PURE__ */ C(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:`Servers`}),focused:M===`servers`,width:X?void 0:Ve,height:He,children:e.items.map((n,r)=>{let i=e.offset+r,o=i===T,s=n?/* @__PURE__ */ w(le,{children:[/* @__PURE__ */ C(ue,{load:a.cache.get(n.name)}),` `,/* @__PURE__ */ C(_,{bold:o,children:n.name}),n.isDefault&&/* @__PURE__ */ C(_,{dimColor:!0,children:` default`}),!n.registered&&i===t&&/* @__PURE__ */ C(_,{dimColor:!0,children:` ssh config`})]}):/* @__PURE__ */ C(_,{color:`green`,children:`+ add a server`});return/* @__PURE__ */ w(_,{wrap:`truncate`,color:o&&M===`servers`?`cyan`:void 0,children:[o?`› `:` `,s]},n?.name??`+`)})}),!(Q&&M===`servers`)&&/* @__PURE__ */ C(he,{entry:V,load:H,sites:U,siteIdx:A,detail:P.kind===`detail`,focused:M===`sites`,height:Ue,width:We,top:we})]})}let Ke=I||(P.kind===`run`?P.job.code===void 0?`running… esc stops it`:`finished · esc goes back`:V?me(V,H):`Servers are machines you reach with ssh. Sites run on them behind Traefik with HTTPS.`);return/* @__PURE__ */ w(g,{flexDirection:`column`,width:u,height:h-1,children:[Ge,$,/* @__PURE__ */ C(_,{dimColor:!I,color:I?`yellow`:void 0,wrap:`truncate`,children:` `+Ke}),/* @__PURE__ */ C(pe,{lines:Be})]})}function me(e,t){let n=[ne(e.name)];return e.registered||n.push(`from ~/.ssh/config`),t?.kind===`ok`&&n.push(t.data.ready?`${t.data.sites.length} site${t.data.sites.length===1?``:`s`}`:`not set up for landfall yet`),n.join(` · `)}function he(e){if(!e.entry)return/* @__PURE__ */ w(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:`Add a server`}),height:e.height,children:[/* @__PURE__ */ C(_,{children:`Press ↵ to add one. Two ways:`}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,children:`A server you already reach with ssh`}),` (e.g. `,/* @__PURE__ */ C(_,{color:`cyan`,children:`ssh het`}),`): enter its alias.`]}),/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,children:`A fresh machine`}),`: enter a new name and its IP. landfall asks for the root password once and installs a key.`]}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ C(_,{dimColor:!0,children:`Setup installs Docker and Traefik (HTTPS). It checks the server first and never touches what already runs there.`})]});let n=e.entry.name,r=e.load;if(!r)return/* @__PURE__ */ w(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:n}),height:e.height,children:[/* @__PURE__ */ C(_,{dimColor:!0,children:ne(n)}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ w(_,{children:[`Not connected. Press`,` `,/* @__PURE__ */ C(_,{color:`cyan`,bold:!0,children:`↵`}),` `,`to connect and show its sites.`]})]});if(r.kind===`loading`)return/* @__PURE__ */ C(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:n}),height:e.height,children:/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(D,{}),` connecting…`]})});if(r.kind===`error`)return/* @__PURE__ */ w(k,{title:/* @__PURE__ */ C(_,{bold:!0,children:n}),height:e.height,children:[/* @__PURE__ */ C(_,{color:`red`,children:r.error}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ C(_,{children:T(r.error)})]});let{ready:i,traefik:a,proxy:o,sites:s}=r.data,c=/* @__PURE__ */ w(_,{wrap:`truncate`,children:[/* @__PURE__ */ w(_,{bold:!0,children:[`Sites on `,n]}),/* @__PURE__ */ w(_,{dimColor:!0,children:[` `,o,` `]}),a?/* @__PURE__ */ C(_,{color:E(a)?`green`:`red`,children:a}):/* @__PURE__ */ C(_,{color:`yellow`,children:`not installed`})]});if(!i)return/* @__PURE__ */ w(k,{title:c,height:e.height,children:[/* @__PURE__ */ w(_,{children:[n,` is reachable, but landfall has not set it up yet.`]}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ w(_,{children:[`Press`,` `,/* @__PURE__ */ C(_,{color:`cyan`,bold:!0,children:`u`}),` `,`to check what is installed. Nothing changes until you confirm the list (Docker, firewall, deploy user, Traefik).`]})]});if(!s.length)return/* @__PURE__ */ w(k,{title:c,height:e.height,children:[/* @__PURE__ */ C(_,{children:`No sites yet.`}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ w(_,{children:[`Start landfall inside a project folder and press`,` `,/* @__PURE__ */ C(_,{color:`cyan`,bold:!0,children:`d`}),` `,`to deploy it here. A folder with index.html, dist/ or build/ is served as a static site; a Dockerfile is built on the server.`]})]});let l=Math.min(e.siteIdx,s.length-1);if(e.detail)return/* @__PURE__ */ C(M,{s:s[l],height:e.height});let u=O(s,l,e.height-4,e.top),d=e.width-8,f=Math.min(Math.max(...s.map(e=>e.meta.name.length))+2,Math.max(8,Math.floor(d*.4))),p=d-f,ee=p>=40,te=p>=24;return/* @__PURE__ */ C(k,{title:c,focused:e.focused,height:e.height,children:u.items.map((n,r)=>{let i=u.offset+r===l&&e.focused;return/* @__PURE__ */ w(g,{children:[/* @__PURE__ */ C(g,{width:2,flexShrink:0,children:/* @__PURE__ */ C(_,{color:i?`cyan`:void 0,children:i?`›`:` `})}),/* @__PURE__ */ C(g,{width:2,flexShrink:0,children:/* @__PURE__ */ C(_,{color:E(n.status)?`green`:`red`,children:E(n.status)?`●`:`○`})}),/* @__PURE__ */ C(g,{width:f,flexShrink:0,paddingRight:1,children:/* @__PURE__ */ C(_,{bold:i,color:i?`cyan`:void 0,wrap:`truncate`,children:n.meta.name})}),ee&&/* @__PURE__ */ C(g,{width:12,children:/* @__PURE__ */ C(_,{dimColor:!0,children:n.meta.kind})}),/* @__PURE__ */ C(g,{flexGrow:1,flexShrink:1,children:/* @__PURE__ */ C(_,{wrap:`truncate`,children:n.meta.domains.join(` `)})}),te&&/* @__PURE__ */ C(g,{width:10,flexShrink:0,justifyContent:`flex-end`,children:/* @__PURE__ */ C(_,{dimColor:!0,children:t(n.meta.updatedAt)})})]},n.meta.name)})})}const j={open:`green`,limited:`cyan`,blocked:`red`,local:void 0};function ge({mode:e,height:t,top:n}){let r=e.data,i=/* @__PURE__ */ w(_,{bold:!0,children:[`Firewall `,/* @__PURE__ */ C(_,{dimColor:!0,children:`on`}),` `,e.server]});if(!r)return/* @__PURE__ */ C(k,{title:i,focused:!0,height:t,children:e.error?/* @__PURE__ */ C(_,{color:`red`,children:e.error}):/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(D,{}),` reading ufw…`]})});let a=t-4,o=[...r.rules,void 0],s=O(o,e.index,Math.max(2,Math.min(o.length,Math.floor((a-4)/2))),n),c=Math.max(0,a-s.items.length-5),l=r.ports.slice(0,c),d=r.ufw===`active`?/* @__PURE__ */ C(_,{color:`green`,children:`on`}):/* @__PURE__ */ C(_,{color:`yellow`,children:r.ufw===`missing`?`not installed`:`off`});return/* @__PURE__ */ w(k,{title:i,focused:!0,height:t,children:[/* @__PURE__ */ w(_,{wrap:`truncate`,children:[`ufw `,d,/* @__PURE__ */ w(_,{dimColor:!0,children:[` · `,`incoming without a rule: `,r.policy===`ACCEPT`?`allowed`:`blocked`,` · `,`ssh port `,r.sshPort,u(r.login).user===`root`?` · as root`:``]})]}),/* @__PURE__ */ w(g,{marginTop:1,flexDirection:`column`,children:[/* @__PURE__ */ C(_,{bold:!0,children:`Rules`}),s.items.map((t,n)=>{let r=s.offset+n,i=r===e.index;return/* @__PURE__ */ w(_,{wrap:`truncate`,color:i?`cyan`:void 0,children:[i?`› `:` `,t?/* @__PURE__ */ w(le,{children:[/* @__PURE__ */ w(_,{dimColor:!0,children:[String(r+1).padStart(2),` `]}),/* @__PURE__ */ C(_,{color:t.action===`deny`||t.action===`reject`?`red`:`green`,children:t.action.padEnd(6)}),t.spec.replace(/^\w+\s+/,``)]}):/* @__PURE__ */ C(_,{color:`green`,children:`+ add a rule`})]},t?.spec??`+`)})]}),/* @__PURE__ */ w(g,{marginTop:1,flexDirection:`column`,children:[/* @__PURE__ */ C(_,{bold:!0,children:`Listening ports`}),l.map(e=>/* @__PURE__ */ w(_,{wrap:`truncate`,children:[` `,e.port.padEnd(8),e.proc.padEnd(17),/* @__PURE__ */ C(_,{color:j[e.state],dimColor:e.state===`local`,children:e.why})]},`${e.port} ${e.proc}`)),r.ports.length>l.length&&/* @__PURE__ */ w(_,{dimColor:!0,children:[` … `,r.ports.length-l.length,` more (lf firewall status -s `,e.server,`)`]})]})]})}function M({s:e,height:n}){let r=e.meta,i=d(r).slice(0,Math.max(1,n-10));return/* @__PURE__ */ w(k,{title:/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,children:r.name}),` `,/* @__PURE__ */ C(_,{dimColor:!0,children:r.kind}),` `,/* @__PURE__ */ C(_,{color:E(e.status)?`green`:`red`,children:e.status})]}),focused:!0,height:n,children:[r.domains.map(e=>/* @__PURE__ */ w(_,{color:`cyan`,children:[`https://`,e]},e)),/* @__PURE__ */ w(_,{dimColor:!0,children:[o(r.name),` · port `,r.port,r.spa?` · single-page app`:``]}),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ C(_,{bold:!0,children:`Releases`}),i.map(e=>/* @__PURE__ */ w(_,{children:[e.id===r.current?/* @__PURE__ */ C(_,{color:`green`,children:`● `}):` `,e.id,` `,/* @__PURE__ */ C(_,{dimColor:!0,children:t(e.at)}),e.image?/* @__PURE__ */ w(_,{dimColor:!0,children:[` `,e.image]}):null,e.id===r.current?/* @__PURE__ */ C(_,{dimColor:!0,children:` (live)`}):null]},e.id))]})}const N=[[`u`,`check, then set up / repair on y`],[`i`,`OS, memory, disk, containers`],[`t`,`traefik status and routes`],[`f`,`firewall: rules, ports, on / off`],[`h`,`history: what landfall changed, undo it`],[`x`,`open a shell (leaves the dashboard)`],[`*`,`make default server`],[`a`,`add a server`]],P=[[`↵`,`details and releases`],[`l`,`follow logs (esc stops)`],[`r / s`,`restart / stop or start`],[`b`,`roll back to previous release`],[`e`,`environment variables`],[`o`,`open in browser`],[`⌫`,`delete site / forget server`]],F=[`Servers are machines you reach with ssh (anything in ~/.ssh/config works). Set up installs Docker and Traefik there; Traefik gets HTTPS certificates from Let's Encrypt for every site automatically.`,`Sites are deployed from a project folder: static files, a Dockerfile, or an image. Each deploy is a release you can roll back to. srv.json in the folder remembers where it goes.`,`Every change landfall makes is written to /opt/srv/journal.log on the server. h shows it; z reverts a run, also right after a command finished.`];function I(e,t){let n=[],r=``;for(let i of e.split(` `))r&&h(`${r} ${i}`)>t?(n.push(r),r=i):r=r?`${r} ${i}`:i;return[...n,r]}function _e(e){let t=e-4,n=(e,t,n=10)=>/* @__PURE__ */ w(_,{wrap:`truncate`,children:[/* @__PURE__ */ C(_,{color:`cyan`,bold:!0,children:e.padEnd(n)}),t]}),r=[];for(let e of F)r.push(...I(e,t).map(e=>/* @__PURE__ */ C(_,{wrap:`truncate`,children:e})),/* @__PURE__ */ C(_,{children:` `}));if(t>=92){r.push(/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,children:`Servers`.padEnd(46)}),/* @__PURE__ */ C(_,{bold:!0,children:`Sites`})]}));for(let e=0;e<Math.max(N.length,P.length);e++){let[t,i]=[N[e],P[e]];r.push(/* @__PURE__ */ w(g,{children:[/* @__PURE__ */ C(g,{width:46,children:t?n(...t):/* @__PURE__ */ C(_,{children:` `})}),i?n(...i):/* @__PURE__ */ C(_,{children:` `})]}))}}else r.push(/* @__PURE__ */ C(_,{bold:!0,children:`Servers`}),...N.map(e=>n(...e)),/* @__PURE__ */ C(_,{children:` `}),/* @__PURE__ */ C(_,{bold:!0,children:`Sites`}),...P.map(e=>n(...e)));return r.push(/* @__PURE__ */ C(_,{children:` `}),n(`d`,`deploy the folder landfall was started in`),n(`R`,`reload`),/* @__PURE__ */ C(_,{children:` `})),r.push(...I("Commands run inside the dashboard and show their output; esc stops or goes back. Each is a normal landfall command, shown as `$ lf …`, so the same thing works without the dashboard.",t).map(e=>/* @__PURE__ */ C(_,{dimColor:!0,wrap:`truncate`,children:e}))),r}function ve({height:e,width:t,top:n}){let r=_e(t),i=Math.max(1,e-4),a=Math.min(n,Math.max(0,r.length-i)),o=r.length-a-i;return/* @__PURE__ */ C(k,{title:/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(_,{bold:!0,children:`How landfall works`}),o>0&&/* @__PURE__ */ w(_,{dimColor:!0,children:[` ↓ `,o,` more`]})]}),focused:!0,height:e,children:r.slice(a,a+i).map((e,t)=>/* @__PURE__ */ C(g,{children:e},a+t))})}function ye({mode:e,height:n,top:r}){let i=/* @__PURE__ */ w(_,{bold:!0,children:[`History `,/* @__PURE__ */ C(_,{dimColor:!0,children:`on`}),` `,e.server,` `,/* @__PURE__ */ C(_,{dimColor:!0,children:`· /opt/srv/journal.log`})]}),{runs:a}=e;if(!a?.length)return/* @__PURE__ */ C(k,{title:i,focused:!0,height:n,children:e.error?/* @__PURE__ */ C(_,{color:`red`,children:e.error}):a?/* @__PURE__ */ w(_,{children:[`landfall has not changed anything on `,e.server,` yet.`]}):/* @__PURE__ */ w(_,{children:[/* @__PURE__ */ C(D,{}),` reading the journal…`]})});let o=n-4,s=a[e.index],c=O(a,e.index,Math.max(1,Math.min(a.length,Math.max(Math.ceil(o/2),o-s.steps.length-1))),r),l=Math.max(1,o-c.items.length-2),u=s.steps.slice(0,s.steps.length>l?l-1:l);return/* @__PURE__ */ w(k,{title:i,focused:!0,height:n,children:[c.items.map((n,r)=>{let i=c.offset+r===e.index,a=f(n).length;return/* @__PURE__ */ w(_,{wrap:`truncate`,color:i?`cyan`:void 0,children:[i?`› `:` `,/* @__PURE__ */ C(_,{bold:!0,children:`#${n.n}`.padEnd(5)}),/* @__PURE__ */ C(_,{dimColor:!0,children:t(n.time).padEnd(9)}),/* @__PURE__ */ C(_,{bold:i,dimColor:!a,children:n.cmd||`?`}),a?/* @__PURE__ */ w(_,{color:`cyan`,children:[` ↺`,a]}):null,/* @__PURE__ */ w(_,{dimColor:!0,children:[` `,n.actor]})]},n.id)}),/* @__PURE__ */ w(g,{marginTop:1,flexDirection:`column`,children:[u.map(e=>/* @__PURE__ */ w(_,{wrap:`truncate`,children:[` `,e.reverts?/* @__PURE__ */ w(_,{dimColor:!0,children:[`↩ `,e.what]}):e.undone?/* @__PURE__ */ w(_,{dimColor:!0,children:[`✓ `,e.what,` (undone)`]}):e.undo?/* @__PURE__ */ w(le,{children:[/* @__PURE__ */ C(_,{color:`cyan`,children:`↺`}),` `,e.what]}):/* @__PURE__ */ w(_,{dimColor:!0,children:[`· `,e.what]})]},e.line)),s.steps.length>u.length&&/* @__PURE__ */ w(_,{dimColor:!0,children:[` … `,s.steps.length-u.length,` more (lf history -s `,e.server,`)`]})]})]})}const L=e=>/^==> /.test(e)?`cyan`:/^\s*✓/.test(e)?`green`:/^\s*!/.test(e)?`yellow`:/^\s*(error|✗)/i.test(e)?`red`:void 0;function R(e,t){let n=[],r=``;for(let i of e)h(r+i)>t&&(n.push(r),r=``),r+=i;return n.push(r),n}const be=(e,t)=>e.lines.flatMap(e=>R(e,Math.max(10,t-4)).map(t=>({r:t,color:L(e)}))),xe=e=>Math.max(1,e-4);function Se({job:e,top:t,height:n,width:r}){let i=be(e,r),a=xe(n),o=Math.max(0,i.length-a),s=Math.min(t??o,o),c=i.slice(s,s+a),l=i.length-s-c.length,u=e.code===void 0?/* @__PURE__ */ C(D,{}):e.code===0?/* @__PURE__ */ C(_,{color:`green`,children:`✓ done`}):e.code===null?/* @__PURE__ */ C(_,{color:`yellow`,children:`stopped`}):/* @__PURE__ */ w(_,{color:`red`,children:[`✗ exit `,e.code]});return/* @__PURE__ */ C(k,{title:/* @__PURE__ */ w(_,{wrap:`truncate`,children:[/* @__PURE__ */ w(_,{bold:!0,children:[`$ lf `,e.argv.join(` `)]}),` `,u,l>0&&/* @__PURE__ */ w(_,{dimColor:!0,children:[` ↓ `,l,` more line`,l===1?``:`s`]})]}),focused:!0,height:n,children:c.map(({r:e,color:t},n)=>/* @__PURE__ */ C(_,{color:t,wrap:`truncate`,children:e||` `},n))})}async function Ce(e){let t={server:0,site:0,focus:`servers`,cache:/* @__PURE__ */ new Map,connect:/* @__PURE__ */ new Set};for(;;){let n;l(!0),process.stdout.write(`\x1B[?1049h\x1B[H`);try{await v(/* @__PURE__ */ C(A,{state:t,onAction:e=>n=e}),{exitOnCtrlC:!1}).waitUntilExit()}finally{process.stdout.write(`\x1B[?1049l`),l(!1)}if(!n)return 0;console.log(a.dim(`$ lf ${n.argv.join(` `)}\n`));let r=()=>{};process.on(`SIGINT`,r);let i=await e(n.argv).finally(()=>process.off(`SIGINT`,r));n.server&&t.cache.delete(n.server),await we(i===0?a.green(`✓ done`):a.red(`✗ failed`))}}function we(e){process.stdout.write(`\n${e} ${a.dim(`press any key to go back to the dashboard`)}`);let{stdin:t}=process;return new Promise(e=>{t.setRawMode(!0),t.resume(),t.once(`data`,()=>{t.setRawMode(!1),t.pause(),process.stdout.write(`
|
|
8
|
-
`),e()})})}export{Ce as dashboard};
|