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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +49 -14
  3. 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) and dnf (dnf-automatic). Without root or sudo landfall only checks sshd's settings.
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
- dnf|yum|zypper) rpm -q "$1" ;;
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
- # dnf-automatic.timer only downloads unless automatic.conf says apply_updates.
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 systemctl cat dnf-automatic-install.timer >/dev/null 2>&1; then
661
- if ! systemctl is-enabled -q dnf-automatic-install.timer 2>/dev/null; then
662
- $SUDO systemctl enable --now dnf-automatic-install.timer >/dev/null
663
- record "turned on automatic updates" '$SUDO systemctl disable --now dnf-automatic-install.timer'
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
- echo "set apply_updates = yes in /etc/dnf/automatic.conf, then: systemctl enable --now dnf5-automatic.timer" >&2; exit 1
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.3`;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(`
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "landfall",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Set up servers and deploy sites behind Traefik with automatic HTTPS",
5
5
  "author": "ph1p <me@ph1p.dev>",
6
6
  "license": "MIT",