landfall 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.mjs +49 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -531,7 +531,7 @@ lf keys add --generate -u anna # new pair at ~/.ssh/<server>_anna, its public
|
|
|
531
531
|
|
|
532
532
|
`lf setup` and `lf server add` end with the same advice, and the web UI shows it on the system tab with a
|
|
533
533
|
button per fix. Each fix is journaled, so `lf undo` takes it back. Automatic updates know apt
|
|
534
|
-
(unattended-upgrades)
|
|
534
|
+
(unattended-upgrades), dnf (dnf-automatic) and apk (a daily `apk upgrade` from cron). Without root or sudo landfall only checks sshd's settings.
|
|
535
535
|
|
|
536
536
|
## Web UI
|
|
537
537
|
|
package/dist/cli.mjs
CHANGED
|
@@ -29,6 +29,14 @@ key_users() {
|
|
|
29
29
|
done; return 0
|
|
30
30
|
}
|
|
31
31
|
apt_auto_updates() { dpkg -s unattended-upgrades >/dev/null 2>&1 && apt-config dump 2>/dev/null | grep '^APT::Periodic::Unattended-Upgrade "1"' >/dev/null; }
|
|
32
|
+
# dnf 4's dnf-automatic-install.timer installs; the other timers only download unless automatic.conf says apply_updates.
|
|
33
|
+
dnf_applies() { grep -qiE '^[[:space:]]*apply_updates[[:space:]]*=[[:space:]]*(yes|true|1)' /etc/dnf/automatic.conf 2>/dev/null; }
|
|
34
|
+
dnf_auto_updates() {
|
|
35
|
+
systemctl is-enabled -q dnf-automatic-install.timer 2>/dev/null \
|
|
36
|
+
|| { { systemctl is-enabled -q dnf-automatic.timer 2>/dev/null || systemctl is-enabled -q dnf5-automatic.timer 2>/dev/null; } && dnf_applies; }
|
|
37
|
+
}
|
|
38
|
+
# Alpine: crond runs a job that runs apk upgrade (lf harden updates puts one in /etc/periodic/daily).
|
|
39
|
+
apk_auto_updates() { pidof crond >/dev/null && { S cat /etc/periodic/*/* /etc/crontabs/root 2>/dev/null || true; } | grep 'apk .*upgrade' >/dev/null; }
|
|
32
40
|
`,K=String.raw`
|
|
33
41
|
log() { printf '\033[36m ->\033[0m %s\n' "$*" >&2; }
|
|
34
42
|
SUDO=""; if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi
|
|
@@ -41,7 +49,8 @@ ${it}
|
|
|
41
49
|
pkg_has() {
|
|
42
50
|
case "$(pkg_mgr)" in
|
|
43
51
|
apt-get) dpkg -s "$1" ;;
|
|
44
|
-
|
|
52
|
+
# --whatprovides: dnf5's plugin provides dnf-automatic under another name.
|
|
53
|
+
dnf|yum|zypper) rpm -q --whatprovides "$1" ;;
|
|
45
54
|
apk) apk info -e "$1" ;;
|
|
46
55
|
*) false ;;
|
|
47
56
|
esac >/dev/null 2>&1
|
|
@@ -554,11 +563,10 @@ ${it}
|
|
|
554
563
|
kv reboot "$(reboot_needed && echo 1)"
|
|
555
564
|
if command -v apt-get >/dev/null 2>&1; then
|
|
556
565
|
if apt_auto_updates; then kv auto_updates on; else kv auto_updates off; fi
|
|
566
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
567
|
+
if apk_auto_updates; then kv auto_updates on; else kv auto_updates off; fi
|
|
557
568
|
elif command -v dnf >/dev/null 2>&1 && [ "$init" = systemd ]; then
|
|
558
|
-
|
|
559
|
-
if systemctl is-enabled -q dnf-automatic-install.timer 2>/dev/null \
|
|
560
|
-
|| { { systemctl is-enabled -q dnf-automatic.timer 2>/dev/null || systemctl is-enabled -q dnf5-automatic.timer 2>/dev/null; } \
|
|
561
|
-
&& grep -qiE '^[[:space:]]*apply_updates[[:space:]]*=[[:space:]]*(yes|true|1)' /etc/dnf/automatic.conf 2>/dev/null; }; then kv auto_updates on; else kv auto_updates off; fi
|
|
569
|
+
if dnf_auto_updates; then kv auto_updates on; else kv auto_updates off; fi
|
|
562
570
|
fi
|
|
563
571
|
# Keys of every user with a home: /root and /home/* (deploy users live there).
|
|
564
572
|
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
|
|
@@ -655,17 +663,44 @@ if [ "$H_UPDATES" = 1 ]; then
|
|
|
655
663
|
printf '%s\n' 'APT::Periodic::Update-Package-Lists "1";' 'APT::Periodic::Unattended-Upgrade "1";' | $SUDO tee $f >/dev/null
|
|
656
664
|
record "turned on automatic security updates" "$undo"
|
|
657
665
|
fi ;;
|
|
666
|
+
apk)
|
|
667
|
+
if ! apk_auto_updates; then
|
|
668
|
+
log "turning on automatic updates"
|
|
669
|
+
# A stable Alpine branch only gets fixes, so upgrading all of it is what apt's security updates do.
|
|
670
|
+
f=/etc/periodic/daily/landfall-upgrade
|
|
671
|
+
if [ ! -e $f ]; then
|
|
672
|
+
printf '#!/bin/sh\napk upgrade --no-cache --quiet\n' | $SUDO tee $f >/dev/null && $SUDO chmod 755 $f
|
|
673
|
+
record "turned on automatic updates" "\$SUDO rm -f $f"
|
|
674
|
+
fi
|
|
675
|
+
if ! pidof crond >/dev/null; then
|
|
676
|
+
{ svc enable crond; svc start crond; } >/dev/null 2>&1 || { echo "could not start crond, which runs $f" >&2; exit 1; }
|
|
677
|
+
record "started crond" 'svc stop crond; svc disable crond'
|
|
678
|
+
fi
|
|
679
|
+
fi ;;
|
|
658
680
|
dnf)
|
|
659
681
|
pkg_has dnf-automatic || { log "installing dnf-automatic"; pkg_install dnf-automatic && record "installed dnf-automatic"; }
|
|
660
|
-
if
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
fi
|
|
682
|
+
if dnf_auto_updates; then :
|
|
683
|
+
elif systemctl cat dnf-automatic-install.timer >/dev/null 2>&1; then
|
|
684
|
+
$SUDO systemctl enable --now dnf-automatic-install.timer >/dev/null
|
|
685
|
+
record "turned on automatic updates" '$SUDO systemctl disable --now dnf-automatic-install.timer'
|
|
665
686
|
else
|
|
666
|
-
|
|
687
|
+
# dnf5 (Fedora 41+): one timer, and automatic.conf says whether it installs.
|
|
688
|
+
log "turning on automatic updates"
|
|
689
|
+
f=/etc/dnf/automatic.conf
|
|
690
|
+
if ! dnf_applies; then
|
|
691
|
+
undo=$(saved $f)
|
|
692
|
+
if grep -q '^[[:space:]]*apply_updates' $f 2>/dev/null; then $SUDO sed -i 's/^[[:space:]]*apply_updates.*/apply_updates = yes/' $f
|
|
693
|
+
elif grep -q '^\[commands\]' $f 2>/dev/null; then $SUDO sed -i '/^\[commands\]/a apply_updates = yes' $f
|
|
694
|
+
else printf '[commands]\napply_updates = yes\n' | $SUDO tee -a $f >/dev/null; fi
|
|
695
|
+
record "automatic updates install, not only download" "$undo"
|
|
696
|
+
fi
|
|
697
|
+
t=dnf5-automatic.timer; systemctl cat $t >/dev/null 2>&1 || t=dnf-automatic.timer
|
|
698
|
+
if ! systemctl is-enabled -q $t 2>/dev/null; then
|
|
699
|
+
$SUDO systemctl enable --now $t >/dev/null
|
|
700
|
+
record "turned on automatic updates" "\$SUDO systemctl disable --now $t"
|
|
701
|
+
fi
|
|
667
702
|
fi ;;
|
|
668
|
-
*) echo "automatic updates: landfall knows apt and dnf; on $(pkg_mgr) set them up by hand" >&2; exit 1 ;;
|
|
703
|
+
*) echo "automatic updates: landfall knows apt, apk and dnf; on $(pkg_mgr) set them up by hand" >&2; exit 1 ;;
|
|
669
704
|
esac
|
|
670
705
|
fi
|
|
671
706
|
`;async function Ri(e,t){await J(e,K+Li,{env:{H_SSH:t.includes(`ssh`),H_F2B:t.includes(`fail2ban`),H_UPDATES:t.includes(`updates`)}})}function zi(e,t){/^[a-z_][a-z0-9_.-]*$/i.test(t)||D(`not a user name: ${t}`);let n=m(c(),`.ssh`,`${je(e)}_${t}`),r=pe(),i=Wr(n,`${t}@${e}`,r);return o.existsSync(`${n}.pub`)||D(`${n} exists without ${n}.pub; move it away or pass the public key`),{path:n,created:i,passphrase:i&&r,pub:o.readFileSync(`${n}.pub`,`utf8`).trim()}}async function Q(e,t){let{stdout:n}=await J(e,`{ [ "$(id -u)" = 0 ] || sudo -n true 2>/dev/null; } && echo yes || true`,{capture:!0});if(n.trim()===`yes`)return e;let r=Y(e).user;for(let t of[`root`,e.admin])if(t&&t!==r&&await At({...e,user:t}))return{...e,user:t};return D(`${t} needs root or passwordless sudo; ${r} has neither and root's key login does not work`)}const Bi=String.raw`
|
|
@@ -1207,13 +1242,13 @@ whoever logs in as that user, or keep it if that is you.`,options:{server:M,user
|
|
|
1207
1242
|
Applies to containers created afterwards: redeploy (or lf restart) sites to pick it up.`,options:{server:M,files:{type:`string`,desc:`rotated files per container`,default:`3`},restart:{type:`boolean`,desc:`restart docker now (sites go down for a moment); without, it applies on its next start`},json:Sl},async run([e],t){let{name:n,server:r}=$(t),i=await ti(r),a=i.docker;if(a||D(`no docker on ${n}`),!e){if(t.json)return void console.log(JSON.stringify({driver:a.driver,maxSize:a.maxSize,maxFile:a.maxFile},null,2));console.log(a.maxSize?`${a.driver}: ${a.maxSize} × ${a.maxFile??1} per container`:`${a.driver}: ${x.yellow(`no limit`)}`);return}i.managed&&D(i.managed),a.daemonError&&D(`${a.daemonError}: fix it first`);let o=e===`off`?void 0:e.toLowerCase();o&&!Ci.test(o)&&D(`not a size: ${e} (e.g. 10m, 500k, 1g)`);let s=z(t.files,`--files`,1,100);[`json-file`,`local`].includes(a.driver)||T(`docker logs to ${a.driver}: the limit only takes effect for json-file and local`);let c=!!t.restart&&await k(`Restart docker on ${n}? Every site goes down for a moment.`,!1);await Ei(await Q(r,`changing docker's log limit`),wi(a.daemon,o,s),o?`docker log limit ${o} × ${s}`:`docker log limit off`,c),C(o?`docker keeps at most ${o} × ${s} log per container`:`docker log limit removed`),w(x.dim(c?`new containers get it: redeploy or lf restart sites to apply it to them`:`takes effect when docker restarts (--restart, or a reboot), for containers created after that`))}}),Kl=j({name:`security`,summary:`Check a server's security (ssh passwords, firewall, fail2ban, automatic updates, pending reboot) and say how to fix what is open.`,options:{server:M,json:{type:`boolean`,desc:`machine-readable output`}},async run(e,t){let{name:n,server:r}=$(t),{advice:i,...a}=await Fi(n,r);if(t.json)return void console.log(JSON.stringify({...a.security,fail2ban:a.fail2ban,advice:i},null,2));let o=a.security;A([[x.dim(`ssh login`),o.passwordLogin===void 0?x.dim(`unknown`):o.passwordLogin?x.red(`passwords and keys`):x.green(`keys only`)],[x.dim(`root login`),o.rootLogin===void 0?x.dim(`unknown`):o.rootLogin===`yes`?x.yellow(`yes`):o.rootLogin===`no`?`no`:`key only`],[x.dim(`firewall`),o.firewall===void 0?x.dim(`unknown`):o.firewall===`active`?x.green(`on`):x.yellow(o.firewall===`missing`?`no ufw`:`off`)],[x.dim(`fail2ban`),a.fail2ban===`running`?x.green(`running`):x.yellow(a.fail2ban===`missing`?`not installed`:`stopped`)],[x.dim(`auto updates`),o.autoUpdates===void 0?x.dim(`unknown`):o.autoUpdates?x.green(`on`):x.yellow(`off`)],[x.dim(`reboot`),o.reboot?x.yellow(`pending`):`not needed`]]),ue({advice:i}),console.log(),i.length?Pi(i):C(`nothing to fix`),Dl(a)}}),ql=j({name:`harden`,args:`[ssh|fail2ban|updates…]`,summary:`Close the usual holes; all three without arguments:
|
|
1208
1243
|
ssh keys only: no passwords, root by key only
|
|
1209
1244
|
fail2ban install and start it: bans addresses that keep failing to log in
|
|
1210
|
-
updates install security updates automatically (apt, dnf)
|
|
1245
|
+
updates install security updates automatically (apt, apk, dnf)
|
|
1211
1246
|
Each change is journaled: lf undo takes it back.`,options:{server:M,force:{type:`boolean`,desc:`turn ssh passwords off even if users without a key get locked out`}},async run(e,t){let n=e.filter(e=>!Di.includes(e));n.length&&D(`unknown: ${n.join(`, `)} (ssh, fail2ban or updates)`);let r=e.length?e:[...Di],{name:i,server:a}=$(t),o=await Q(a,`hardening`);if(r.includes(`ssh`)&&!t.force){let e=await Mi(o,Y(a).user);e.length&&D(`${ki(e)} Or --force.`)}(!r.includes(`ssh`)||await k(`Only keys log in to ${i} afterwards (lf undo turns passwords back on). Go on?`))&&(await Ri(o,r),C(`${i}: ${r.join(`, `)} hardened`))}}),Jl=j({name:`ui`,summary:`Web dashboard in your browser: servers, sites, firewall, history, every action.
|
|
1212
1247
|
Runs on 127.0.0.1 only; the printed link logs the browser in.`,options:{port:{type:`string`,short:`p`,desc:`port to listen on (default: 4777, else the next free one)`},open:{type:`boolean`,desc:`open the browser (--no-open to only print the link)`,default:!0}},async run(e,t){let{webUi:n}=await import(`./server-Bbkw_Rxx.mjs`);return n({port:t.port===void 0?void 0:z(t.port,`--port`,1,65535),open:t.open})}}),Yl={current:x.green,update:x.yellow,unknown:x.dim,pinned:x.dim,rebuild:x.dim,foreign:x.dim,missing:x.red};function Xl(e,t){let n=e.updates.filter(e=>e.security).length;if(e.mgr===`none`)return w(`packages: no supported package manager`);let r=e.updates.length?`${x.bold(String(e.updates.length))} package updates${n?`, ${x.yellow(`${n} security`)}`:``}`:x.green(`packages up to date`);console.log(`${r}${e.reboot?x.yellow(` · reboot required`):``}${e.refreshed?``:x.dim(` (package lists not refreshed)`)}`);let i=t?e.updates:e.updates.slice(0,15);A(i.map(e=>[` `+e.name,x.dim(e.from??``),`→ `+e.to,e.security?x.yellow(`security`):``])),i.length<e.updates.length&&console.log(x.dim(` … ${e.updates.length-i.length} more (--all)`))}function Zl(e){e.length&&(console.log(`\n${x.bold(`Images`)}`),A(e.map(e=>[` `+e.name,x.dim(e.ref||`-`),Yl[e.state](e.state===`update`?`newer image`:e.state),x.dim(e.note??``)])))}function Ql(e){let t=ia(e);console.log(`\n${x.bold(`Docker`)} ${e.docker?`${e.docker} · compose ${e.compose||x.red(`missing`)}`:x.red(`not installed`)}`);for(let e of[t.dockerNote,t.composeNote])e&&console.log(` `+x.yellow(e));!t.dockerNote&&!t.composeNote&&e.docker&&console.log(x.dim(` newest releases`))}const $l=j({name:`updates`,summary:`Show pending OS package updates (security, reboot needed) and newer images for traefik and sites. Changes nothing.`,options:{server:M,refresh:{type:`boolean`,desc:`refresh package lists first (apt update; --no-refresh to skip)`,default:!0},all:{type:`boolean`,desc:`list every package`},json:{type:`boolean`,desc:`machine-readable output`}},async run(e,t){let{server:n}=R(t.server);if(t.json)return void console.log(JSON.stringify(await Ac(n,t.refresh),null,2));let r=await Hi(n,t.refresh),i=await sa(n),a=await ra(n);Xl(r,t.all),Ql(a),Zl(i);let o=t.server?` -s ${t.server}`:``,s=i.filter(e=>e.state===`update`).length;(r.updates.length||s)&&console.log(x.dim(`\napply with: lf update${o}`)),ia(a).composeUpdate&&console.log(x.dim(`newest compose: lf update --compose${o}`))}}),eu=j({name:`update`,args:`[site...]`,summary:`Apply updates: OS packages and/or newer images (traefik, image and static sites).
|
|
1213
1248
|
|
|
1214
1249
|
Without --packages/--images both are updated. Package upgrades cannot be undone;
|
|
1215
1250
|
image updates can (\`lf undo\` puts the old image back). --compose replaces the
|
|
1216
1251
|
compose plugin setup installed with the newest release (undoable too).`,options:{server:M,packages:{type:`boolean`,desc:`upgrade OS packages`},security:{type:`boolean`,desc:`only security updates (apt, dnf/yum); implies --packages`},reboot:{type:`boolean`,desc:`reboot afterwards if the upgrade needs it`},images:{type:`boolean`,desc:`pull newer images and recreate their containers`},compose:{type:`boolean`,desc:`update the compose plugin landfall installed to the newest release`}},async run(e,t){let{name:n,server:r}=R(t.server);if(t.compose){(e.length||t.packages||t.images)&&D(`--compose goes alone`);let i=await Q(r,`updating docker compose`),a=await ra(i),o=ia(a);if(!o.composeUpdate)return void(o.composeNote?w(o.composeNote):C(`compose ${a.compose} is the newest`));await k(`Update compose ${a.compose} to ${a.latestCompose} on ${n}?`)&&(S(`downloading docker compose`),await aa(i),C(`compose updated (lf undo puts the old one back)`));return}let i=!t.packages&&!t.images&&!t.security&&!t.reboot;e.length&&t.packages&&D(`sites only apply to --images`);let a=!e.length&&(t.packages||i||t.security),o=t.images||i||e.length>0;if(a){let e=await Q(r,`upgrading packages`),i=await Hi(e),a=t.security?i.updates.filter(e=>e.security):i.updates;i.mgr===`none`?T(`no supported package manager, skipping packages`):a.length?(Xl({...i,updates:a},!1),t.security&&(i.mgr===`apk`||i.mgr===`zypper`)&&D(`${i.mgr} has no security-only upgrades here; drop --security`),await k(`Upgrade ${a.length} package${a.length===1?``:`s`} on ${n}?`)&&(S(`upgrading packages on ${n}`),await Wi(e,{only:t.security?a.map(e=>e.name):void 0,what:t.security?`${a.length} security packages`:`${a.length} packages`,reboot:t.reboot}),C(`packages upgraded`),t.reboot&&w(`rebooting if required; give it a minute`))):C(t.security?`no security updates`:`packages up to date`)}if(o){let t=await sa(r);if(e.length){let r=e.filter(e=>!t.some(t=>t.name===e));r.length&&D(`no site or service ${r.join(`, `)} on ${n}`)}let i=e.length?t.filter(t=>e.includes(t.name)):t;for(let e of i.filter(e=>e.state!==`update`&&e.state!==`current`&&e.state!==`unknown`))w(x.dim(`${e.name}: ${e.note??e.state}`));let a=i.filter(t=>t.state===`update`||e.includes(t.name)&&t.state===`unknown`);a.length?(Zl(a),await k(`Pull and recreate ${a.map(e=>e.name).join(`, `)}?`)&&(S(`updating images`),await ca(r,a),C(`images updated (lf undo puts the old ones back)`))):C(`images up to date`)}}}),tu=()=>[process.execPath,...process.execArgv,process.argv[1]];function nu(){let e=Number(process.env.LANDFALL_PARENT);delete process.env.LANDFALL_PARENT,e&&setInterval(()=>{if(process.ppid!==e)try{process.kill(-process.pid,`SIGTERM`)}catch{process.exit(1)}},1e3).unref()}const ru=5e3,iu=1e4,au=e=>e.length>iu?e.slice(0,iu)+`…`:e;function ou(e,t,r={}){let i={...process.env,NO_COLOR:`1`,LANDFALL_REPORT:`1`,LANDFALL_PARENT:String(process.pid),...r.env};r.password&&Object.assign(i,Br(r.password));let a=n(tu()[0],[...tu().slice(1),...e],{stdio:[r.stdin===void 0?`ignore`:`pipe`,`pipe`,`pipe`],env:i,cwd:r.cwd,detached:!0});a.stdin?.on(`error`,()=>{}).end(r.stdin);let o={argv:e,lines:[],dropped:0,report:{},stop(){if(o.code===void 0){o.stopped=!0;try{process.kill(-a.pid,`SIGTERM`)}catch{}}}},s,c=()=>s??=setTimeout(()=>(s=void 0,t()),50),l=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
1217
|
-
`);e=n.pop(),e.length>iu&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(o.report,JSON.parse(e.slice(1)))}catch{}else o.lines.push(au(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));o.lines.length>ru&&(o.dropped+=o.lines.splice(0,o.lines.length-ru).length),c()},flush:()=>e&&o.lines.push(au(e))}},u=l(),d=l();return a.stdout.setEncoding(`utf8`).on(`data`,u.push),a.stderr.setEncoding(`utf8`).on(`data`,d.push),a.on(`error`,e=>{o.lines.push(`error: ${e.message}`),o.code=1,t()}),a.on(`close`,e=>{u.flush(),d.flush(),o.code=o.stopped?null:e??1,clearTimeout(s),t()}),o}var su=`0.6.
|
|
1252
|
+
`);e=n.pop(),e.length>iu&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(o.report,JSON.parse(e.slice(1)))}catch{}else o.lines.push(au(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));o.lines.length>ru&&(o.dropped+=o.lines.splice(0,o.lines.length-ru).length),c()},flush:()=>e&&o.lines.push(au(e))}},u=l(),d=l();return a.stdout.setEncoding(`utf8`).on(`data`,u.push),a.stderr.setEncoding(`utf8`).on(`data`,d.push),a.on(`error`,e=>{o.lines.push(`error: ${e.message}`),o.code=1,t()}),a.on(`close`,e=>{u.flush(),d.flush(),o.code=o.stopped?null:e??1,clearTimeout(s),t()}),o}var su=`0.6.4`;const cu=[Kc,$c,Zc,so,co,lo,vo,ho,fo,po,mo,_o,go,yo,Fc,Ic,Lc,$l,eu,vs,ys,Ts,al,sl,pl,gl,vl,bl,xl,Ol,Kl,ql,kl,jl,Ml,Nl,Pl,Il,Rl,zl,Bl,Vl,Hl,Ul,Wl,Gl,Ds,js,Cs,bs,Ss,ws,Qa,$a,to,no,ro,eo,io,Bo,Vo,Ho,Ro,zo,Wo,Uo,ss,cs,Hc,Vc,Jl],lu={ls:`sites list`,sites:`sites list`,servers:`server list`,"server ls":`server list`,"server ssh":`ssh`,"sites logs":`logs`,"sites env":`env`,"sites rollback":`rollback`,"sites restart":`restart`,"sites stop":`stop`,"sites start":`start`,"sites ls":`sites list`,"sites remove":`sites rm`,"server remove":`server rm`,uninstall:`server uninstall`,"traefik remove":`traefik rm`,traefik:`traefik status`,firewall:`firewall status`,"firewall remove":`firewall rm`,log:`history`,revert:`undo`,web:`ui`,info:`server info`,"sites domains":`domains`,"sites auth":`auth`,"sites open":`open`,"sites tunnel":`tunnel`,upgrade:`update`,"server stats":`stats`,"server disk":`disk`,"server traffic":`traffic`,"server certs":`certs`,"sites check":`check`,"server prune":`prune`,"server reboot":`reboot`,"server system":`system`,"services ls":`services`,"services list":`services`,"keys ls":`keys`,"keys list":`keys`,"keys remove":`keys rm`,"bans unban":`unban`,"server swap":`swap`,"server hostname":`hostname`,"server timezone":`timezone`},uu=new Map(cu.map(e=>[e.name,e]));function du(e){for(let t of[2,1]){let n=e.slice(0,t).join(` `),r=uu.get(lu[n]??n);if(r&&e.length>=t)return{cmd:r,rest:e.slice(t)}}return{rest:e}}function fu(){let e=[[`Servers`,[`setup`,`server add`,`server list`,`server info`,`stats`,`disk`,`prune`,`reboot`,`server config`,`server domain`,`server use`,`server rm`,`server uninstall`,`ssh`,`keygen`]],[`System`,[`system`,`services`,`services restart`,`services start`,`services stop`,`services logs`,`keys`,`keys add`,`keys rm`,`bans`,`unban`,`swap`,`hostname`,`timezone`,`log-limit`]],[`Traefik`,[`traefik setup`,`traefik status`,`traefik dashboard`,`traefik logs`,`traefik restart`,`traefik rm`]],[`Firewall`,[`firewall status`,`firewall enable`,`firewall disable`,`firewall allow`,`firewall deny`,`firewall update`,`firewall rm`]],[`Sites`,[`deploy`,`sites list`,`sites info`,`domains`,`auth`,`open`,`tunnel`,`logs`,`env`,`rollback`,`restart`,`stop`,`start`,`sites rm`]],[`Monitoring`,[`traffic`,`certs`,`check`]],[`Updates`,[`updates`,`update`]],[`History`,[`history`,`undo`]],[`landfall`,[`config`,`ui`]]],t=[`${x.bold(`landfall`)} ${x.dim(su)} — set up servers and deploy sites behind Traefik + HTTPS`,``,`${x.bold(`Usage:`)} lf <command> [options] ${x.dim(`lf <command> --help for details`)}`,` lf ui ${x.dim(`web dashboard in your browser`)}`];for(let[n,r]of e){t.push(``,x.bold(n));for(let e of r){let n=uu.get(e);t.push(` ${(e+(n.args?` `+n.args:``)).padEnd(32)} ${n.summary.split(`
|
|
1218
1253
|
`)[0]}`)}}t.push(``,x.bold(`Quick start`),` lf setup vps --check ${x.dim(`# what is on vps, what setup would change`)}`,` lf setup vps ${x.dim("# docker + traefik on a server you reach with `ssh vps`")}`,` lf setup box --host 203.0.113.7 ${x.dim("# fresh box (root password once) → `ssh box` works")}`,` lf deploy ./dist -d example.com -s vps ${x.dim(`# publish a static site with HTTPS`)}`,` lf deploy ${x.dim(`# redeploy using ./.landfall.json`)}`,` lf deploy ./dist -y ${x.dim(`# no domain yet: serve at <name>.<ip>.sslip.io`)}`,` lf auth my-site --user anna ${x.dim(`# put a password in front of a site`)}`,` lf updates -s vps && lf update -s vps ${x.dim(`# pending OS/image updates, then apply them`)}`,` lf ls ${x.dim(`# what runs where`)}`,` lf history -s vps && lf undo -s vps ${x.dim(`# what landfall changed, and revert the last of it`)}`,``,x.dim(`Servers are ssh targets: any ~/.ssh/config alias works as --server.`)),console.log(t.join(`
|
|
1219
1254
|
`))}async function pu(e){if(!e.length||e[0]===`help`||e[0]===`-h`||e[0]===`--help`)return fu(),0;if(e[0]===`-v`||e[0]===`--version`||e[0]===`version`)return console.log(su),0;let{cmd:t,rest:n}=du(e);if(!t)return console.error(`${x.red(`error:`)} unknown command "${e.join(` `)}"\n`),fu(),2;let{values:r,positionals:i,rest:a}=xe(t,n);if(r.help)return console.log(Se(t)),0;fe(!!r.yes),kt.cmd=N(t,i,r);let o=await t.run(i,r,a);return typeof o==`number`?o:0}async function mu(e){try{return await pu(e)}catch(e){return e instanceof E?(console.error(`${x.red(`error:`)} ${e.message}`),/--force\b/.test(e.message)&&ue({force:e.message})):console.error(e),1}}nu(),mu(process.argv.slice(2)).then(e=>process.stdout.write(``,()=>process.exit(e)));export{H as A,be as B,nr as C,G as D,Ze as E,De as F,ve as H,F as I,L,P as M,Me as N,W as O,Ne as P,B as R,er as S,gt as T,x as V,_s as _,Cc as a,gs as b,wc as c,Dc as d,Tc as f,Qs as g,Ac as h,Nc as i,U as j,V as k,Sc as l,Mc as m,ou as n,Ec as o,Oc as p,Bc as r,jc as s,su as t,kc as u,ps as v,Ot as w,vr as x,hs as y,R as z};
|