@fabioplunser/epd 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/cli.js +228 -228
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1484,9 +1484,9 @@ function declFor(cfg) {
1484
1484
  extra: cfg.proxy.extra
1485
1485
  };
1486
1486
  }
1487
- async function readDecls(host2, cfg) {
1487
+ async function readDecls(host, cfg) {
1488
1488
  const dir = paths(cfg).proxyAppsDir;
1489
- const out = await host2.capture(`for f in ${shq(dir)}/*.json; do [ -f "$f" ] || continue; echo "###EPD_FILE"; cat "$f"; done`, { allowFailure: true });
1489
+ const out = await host.capture(`for f in ${shq(dir)}/*.json; do [ -f "$f" ] || continue; echo "###EPD_FILE"; cat "$f"; done`, { allowFailure: true });
1490
1490
  const decls = [];
1491
1491
  for (const chunk of out.split("###EPD_FILE")) {
1492
1492
  const t = chunk.trim();
@@ -1495,7 +1495,7 @@ async function readDecls(host2, cfg) {
1495
1495
  try {
1496
1496
  decls.push(JSON.parse(t));
1497
1497
  } catch {
1498
- log.warn(`${host2.name}: ignoring an unreadable proxy declaration`);
1498
+ log.warn(`${host.name}: ignoring an unreadable proxy declaration`);
1499
1499
  }
1500
1500
  }
1501
1501
  return decls;
@@ -1647,18 +1647,18 @@ function dynamicConfig(cfg, endpoints, accessories = []) {
1647
1647
  middlewares[key] = {
1648
1648
  redirectRegex: { regex: "^https?://[^/]+(.*)", replacement: `${route.redirect.to.replace(/\/$/, "")}$1`, permanent: route.redirect.permanent }
1649
1649
  };
1650
- const svcKey2 = `${owner.key}-${route.port}`;
1651
- ensureService(services, svcKey2, owner.urlsFor(route.port), route, owner.health);
1650
+ const svcKey = `${owner.key}-${route.port}`;
1651
+ ensureService(services, svcKey, owner.urlsFor(route.port), route, owner.health);
1652
1652
  routers[routerKey] = {
1653
1653
  rule,
1654
1654
  entryPoints: [entrypoint],
1655
1655
  middlewares: [...mws, key],
1656
- service: svcKey2,
1656
+ service: svcKey,
1657
1657
  ...route.ssl ? { tls: tlsFor(route) } : {},
1658
1658
  ...route.priority !== undefined ? { priority: route.priority } : {}
1659
1659
  };
1660
1660
  if (route.ssl && cfg.proxy.httpsRedirect) {
1661
- routers[`${routerKey}-http`] = { rule, entryPoints: ["web"], middlewares: [key], service: svcKey2 };
1661
+ routers[`${routerKey}-http`] = { rule, entryPoints: ["web"], middlewares: [key], service: svcKey };
1662
1662
  }
1663
1663
  return;
1664
1664
  }
@@ -1743,18 +1743,18 @@ function containerSpec(d, cfg, staticHash) {
1743
1743
  args.push(d.image);
1744
1744
  return args;
1745
1745
  }
1746
- async function ensureProxy(host2, cfg, opts = {}) {
1746
+ async function ensureProxy(host, cfg, opts = {}) {
1747
1747
  if (!cfg.proxy.enabled)
1748
1748
  return;
1749
1749
  const p = paths(cfg);
1750
1750
  const containerMode = cfg.mode === "docker";
1751
- await host2.writeFile(p.proxyApp, JSON.stringify(declFor(cfg), null, 2), "644");
1752
- const merged = mergeDecls(await readDecls(host2, cfg));
1751
+ await host.writeFile(p.proxyApp, JSON.stringify(declFor(cfg), null, 2), "644");
1752
+ const merged = mergeDecls(await readDecls(host, cfg));
1753
1753
  if (!Object.keys(merged.entrypoints).length)
1754
1754
  merged.entrypoints = cfg.proxy.entrypoints;
1755
1755
  const where = containerMode ? CONTAINER_PATHS : { dynamicDir: p.proxyDynamicDir, acmeFile: `${p.acmeDir}/acme.json` };
1756
1756
  const staticJson = JSON.stringify(staticConfig(merged, where), null, 2);
1757
- await host2.writeFile(p.proxyStatic, staticJson, "644");
1757
+ await host.writeFile(p.proxyStatic, staticJson, "644");
1758
1758
  if (merged.dnsEnv.length) {
1759
1759
  const lines = merged.dnsEnv.map((name) => {
1760
1760
  const value = opts.secrets?.[name];
@@ -1763,7 +1763,7 @@ async function ensureProxy(host2, cfg, opts = {}) {
1763
1763
  }
1764
1764
  return `${name}=${value}`;
1765
1765
  });
1766
- await host2.writeFile(p.proxyEnv, lines.join(`
1766
+ await host.writeFile(p.proxyEnv, lines.join(`
1767
1767
  `), "600");
1768
1768
  }
1769
1769
  if (merged.dashboard && merged.dashboardHost) {
@@ -1779,23 +1779,23 @@ async function ensureProxy(host2, cfg, opts = {}) {
1779
1779
  }
1780
1780
  }
1781
1781
  };
1782
- await host2.writeFile(`${p.proxyDynamicDir}/_dashboard.yml`, JSON.stringify(dash, null, 2), "644");
1782
+ await host.writeFile(`${p.proxyDynamicDir}/_dashboard.yml`, JSON.stringify(dash, null, 2), "644");
1783
1783
  }
1784
1784
  const specHash = hash32(staticJson + JSON.stringify(merged)).toString(36);
1785
1785
  if (containerMode)
1786
- await ensureProxyContainer(host2, cfg, merged, specHash, opts);
1786
+ await ensureProxyContainer(host, cfg, merged, specHash, opts);
1787
1787
  else
1788
- await ensureProxyProcess(host2, cfg, merged, specHash, opts);
1788
+ await ensureProxyProcess(host, cfg, merged, specHash, opts);
1789
1789
  }
1790
- async function ensureProxyContainer(host2, cfg, merged, specHash, opts) {
1790
+ async function ensureProxyContainer(host, cfg, merged, specHash, opts) {
1791
1791
  const spec = containerSpec(merged, cfg, specHash);
1792
- const state = await host2.capture(`docker inspect -f '{{.State.Running}} {{index .Config.Labels "epd.spec"}}' ${PROXY_CONTAINER} 2>/dev/null || echo "missing"`);
1792
+ const state = await host.capture(`docker inspect -f '{{.State.Running}} {{index .Config.Labels "epd.spec"}}' ${PROXY_CONTAINER} 2>/dev/null || echo "missing"`);
1793
1793
  const [running, currentHash] = state.split(/\s+/);
1794
1794
  if (!opts.force && running === "true" && currentHash === specHash) {
1795
- log.host(host2.name, "proxy is up to date");
1795
+ log.host(host.name, "proxy is up to date");
1796
1796
  return;
1797
1797
  }
1798
- await host2.exec(`docker network inspect ${shq(merged.network)} >/dev/null 2>&1 || docker network create ${shq(merged.network)} >/dev/null
1798
+ await host.exec(`docker network inspect ${shq(merged.network)} >/dev/null 2>&1 || docker network create ${shq(merged.network)} >/dev/null
1799
1799
  docker rm -f ${PROXY_CONTAINER} >/dev/null 2>&1 || true
1800
1800
  docker pull ${shq(merged.image)} >/dev/null
1801
1801
  ${spec.map((a) => shq(a)).join(" ")} >/dev/null
@@ -1804,15 +1804,15 @@ docker inspect -f '{{.State.Running}}' ${PROXY_CONTAINER} | grep -q true || {
1804
1804
  echo "traefik failed to start:" >&2
1805
1805
  docker logs --tail 40 ${PROXY_CONTAINER} >&2 || true
1806
1806
  exit 1
1807
- }`, { label: `${host2.name}: start proxy` });
1808
- log.host(host2.name, `proxy running (${entrypointSummary(merged)})`);
1807
+ }`, { label: `${host.name}: start proxy` });
1808
+ log.host(host.name, `proxy running (${entrypointSummary(merged)})`);
1809
1809
  }
1810
1810
  var TRAEFIK_FALLBACK_VERSION = "v3.3.7";
1811
- async function ensureProxyProcess(host2, cfg, merged, specHash, opts) {
1811
+ async function ensureProxyProcess(host, cfg, merged, specHash, opts) {
1812
1812
  const p = paths(cfg);
1813
1813
  const wanted = merged.version ?? (/^traefik:(v\d+\.\d+\.\d+)$/.exec(merged.image)?.[1] ?? "");
1814
1814
  const needsPrivilegedPort = Object.values(merged.entrypoints).some((port) => port < 1024);
1815
- await host2.exec(`mkdir -p ${shq(`${p.proxyDir}/bin`)} ${shq(p.proxyLogDir)}
1815
+ await host.exec(`mkdir -p ${shq(`${p.proxyDir}/bin`)} ${shq(p.proxyLogDir)}
1816
1816
  want=${shq(wanted)}
1817
1817
  if [ -z "$want" ]; then
1818
1818
  # Piping curl straight into grep -m1 closes the pipe early, which pipefail
@@ -1845,7 +1845,7 @@ if [ "$have" != "$want" ]; then
1845
1845
  rm -rf "$tmp"
1846
1846
  fi
1847
1847
  ${needsPrivilegedPort ? `command -v setcap >/dev/null 2>&1 && setcap 'cap_net_bind_service=+ep' ${shq(p.proxyBin)} 2>/dev/null || echo "note: could not grant the traefik binary permission to bind ports below 1024" >&2` : ""}
1848
- true`, { stream: true, label: `${host2.name}: install traefik` });
1848
+ true`, { stream: true, label: `${host.name}: install traefik` });
1849
1849
  const ecosystem = {
1850
1850
  apps: [
1851
1851
  {
@@ -1865,17 +1865,17 @@ true`, { stream: true, label: `${host2.name}: install traefik` });
1865
1865
  }
1866
1866
  ]
1867
1867
  };
1868
- await host2.writeFile(p.proxyEcosystem, JSON.stringify(ecosystem, null, 2), "600");
1869
- const jlist = await host2.capture(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null || echo '[]'`, {
1868
+ await host.writeFile(p.proxyEcosystem, JSON.stringify(ecosystem, null, 2), "600");
1869
+ const jlist = await host.capture(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null || echo '[]'`, {
1870
1870
  sudo: false,
1871
1871
  allowFailure: true
1872
1872
  });
1873
- const running = await host2.test(`${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} >/dev/null 2>&1`);
1873
+ const running = await host.test(`${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} >/dev/null 2>&1`);
1874
1874
  if (!opts.force && running && jlist.includes(specHash)) {
1875
- log.host(host2.name, "proxy is up to date");
1875
+ log.host(host.name, "proxy is up to date");
1876
1876
  return;
1877
1877
  }
1878
- await host2.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true
1878
+ await host.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true
1879
1879
  ${pathPrelude(cfg)} pm2 start ${shq(p.proxyEcosystem)} --update-env
1880
1880
  ${pathPrelude(cfg)} pm2 save --force >/dev/null 2>&1 || true
1881
1881
  sleep 2
@@ -1883,29 +1883,29 @@ ${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} | grep -q online || {
1883
1883
  echo "traefik failed to start:" >&2
1884
1884
  tail -n 40 ${shq(`${p.proxyLogDir}/err.log`)} >&2 2>/dev/null || true
1885
1885
  exit 1
1886
- }`, { sudo: false, label: `${host2.name}: start proxy` });
1887
- log.host(host2.name, `proxy running (${entrypointSummary(merged)})`);
1886
+ }`, { sudo: false, label: `${host.name}: start proxy` });
1887
+ log.host(host.name, `proxy running (${entrypointSummary(merged)})`);
1888
1888
  }
1889
1889
  function entrypointSummary(d) {
1890
1890
  return Object.entries(d.entrypoints).map(([name, port]) => `${name}:${port}`).join(" ");
1891
1891
  }
1892
- async function writeRoutes(host2, cfg, endpoints, accessories = []) {
1892
+ async function writeRoutes(host, cfg, endpoints, accessories = []) {
1893
1893
  if (!cfg.proxy.enabled)
1894
1894
  return;
1895
1895
  const doc = dynamicConfig(cfg, endpoints, accessories);
1896
- await host2.writeFile(paths(cfg).proxyDynamic, JSON.stringify(doc, null, 2), "644");
1897
- await host2.exec(`sleep ${Math.max(1, cfg.proxy.reloadWait)}`);
1896
+ await host.writeFile(paths(cfg).proxyDynamic, JSON.stringify(doc, null, 2), "644");
1897
+ await host.exec(`sleep ${Math.max(1, cfg.proxy.reloadWait)}`);
1898
1898
  }
1899
- async function removeRoutes(host2, cfg) {
1900
- await host2.exec(`rm -f ${shq(paths(cfg).proxyDynamic)} ${shq(paths(cfg).proxyApp)}`, { allowFailure: true });
1899
+ async function removeRoutes(host, cfg) {
1900
+ await host.exec(`rm -f ${shq(paths(cfg).proxyDynamic)} ${shq(paths(cfg).proxyApp)}`, { allowFailure: true });
1901
1901
  }
1902
- async function proxyState(host2, cfg) {
1902
+ async function proxyState(host, cfg) {
1903
1903
  if (cfg.mode === "docker") {
1904
- const out2 = await host2.capture(`docker inspect -f '{{.State.Running}}|{{.Config.Image}}|{{.State.Status}}' ${PROXY_CONTAINER} 2>/dev/null || echo 'false||absent'`, { allowFailure: true });
1905
- const [running, image, status2] = out2.split("|");
1906
- return { running: running === "true", image: image ?? "", status: status2 ?? "absent" };
1904
+ const out = await host.capture(`docker inspect -f '{{.State.Running}}|{{.Config.Image}}|{{.State.Status}}' ${PROXY_CONTAINER} 2>/dev/null || echo 'false||absent'`, { allowFailure: true });
1905
+ const [running, image, status] = out.split("|");
1906
+ return { running: running === "true", image: image ?? "", status: status ?? "absent" };
1907
1907
  }
1908
- const out = await host2.capture(`describe=$(${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} 2>/dev/null || true)
1908
+ const out = await host.capture(`describe=$(${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} 2>/dev/null || true)
1909
1909
  printf '%s' "$describe" | awk -F'\u2502' '/status/ {gsub(/ /,"",$3); print $3; exit}' || true
1910
1910
  version_out=$(${shq(paths(cfg).proxyBin)} version 2>/dev/null || true)
1911
1911
  printf '%s' "$version_out" | awk '/[Vv]ersion:/ {print "traefik " $2; exit}' || true`, { sudo: false, allowFailure: true });
@@ -1931,8 +1931,8 @@ class DeployLock {
1931
1931
  async acquire(what) {
1932
1932
  const dir = paths(this.cfg).lock;
1933
1933
  const info = { by: who(), at: new Date().toISOString(), what };
1934
- for (const host2 of this.hosts) {
1935
- const res = await host2.exec(`mkdir -p ${shq(paths(this.cfg).app)}
1934
+ for (const host of this.hosts) {
1935
+ const res = await host.exec(`mkdir -p ${shq(paths(this.cfg).app)}
1936
1936
  if mkdir ${shq(dir)} 2>/dev/null; then
1937
1937
  printf '%s' ${shq(JSON.stringify(info))} > ${shq(dir)}/info
1938
1938
  echo acquired
@@ -1946,27 +1946,27 @@ fi`, { allowFailure: true });
1946
1946
  try {
1947
1947
  held = JSON.parse(res.stdout || "{}");
1948
1948
  } catch {}
1949
- throw new EpdError(`${this.cfg.name} is locked on ${host2.name}`, held.by ? `Held by ${held.by} since ${held.at} (${held.what}).
1949
+ throw new EpdError(`${this.cfg.name} is locked on ${host.name}`, held.by ? `Held by ${held.by} since ${held.at} (${held.what}).
1950
1950
  If that deploy died, run: epd lock release` : "Run `epd lock release` if a previous deploy was interrupted.");
1951
1951
  }
1952
- this.held.push(host2);
1952
+ this.held.push(host);
1953
1953
  }
1954
1954
  log.debug(`lock acquired on ${this.held.length} host(s)`);
1955
1955
  }
1956
1956
  async release() {
1957
1957
  const dir = paths(this.cfg).lock;
1958
- for (const host2 of this.held.splice(0)) {
1959
- await host2.exec(`rm -rf ${shq(dir)}`, { allowFailure: true });
1958
+ for (const host of this.held.splice(0)) {
1959
+ await host.exec(`rm -rf ${shq(dir)}`, { allowFailure: true });
1960
1960
  }
1961
1961
  }
1962
1962
  static async forceRelease(cfg, hosts) {
1963
- for (const host2 of hosts)
1964
- await host2.exec(`rm -rf ${shq(paths(cfg).lock)}`, { allowFailure: true });
1963
+ for (const host of hosts)
1964
+ await host.exec(`rm -rf ${shq(paths(cfg).lock)}`, { allowFailure: true });
1965
1965
  }
1966
1966
  static async status(cfg, hosts) {
1967
1967
  const out = [];
1968
- for (const host2 of hosts) {
1969
- const raw = await host2.readFile(`${paths(cfg).lock}/info`);
1968
+ for (const host of hosts) {
1969
+ const raw = await host.readFile(`${paths(cfg).lock}/info`);
1970
1970
  let info = null;
1971
1971
  if (raw) {
1972
1972
  try {
@@ -1975,7 +1975,7 @@ If that deploy died, run: epd lock release` : "Run `epd lock release` if a previ
1975
1975
  info = { by: "unknown", at: "unknown", what: "unknown" };
1976
1976
  }
1977
1977
  }
1978
- out.push({ host: host2.name, info });
1978
+ out.push({ host: host.name, info });
1979
1979
  }
1980
1980
  return out;
1981
1981
  }
@@ -2074,34 +2074,34 @@ EPD_TARGETS
2074
2074
  done
2075
2075
  `;
2076
2076
  }
2077
- async function waitHealthy(host2, targets, health) {
2077
+ async function waitHealthy(host, targets, health) {
2078
2078
  if (!targets.length)
2079
2079
  return;
2080
- const res = await host2.exec(healthScript(targets, health), {
2080
+ const res = await host.exec(healthScript(targets, health), {
2081
2081
  allowFailure: true,
2082
- label: `${host2.name}: health check`
2082
+ label: `${host.name}: health check`
2083
2083
  });
2084
2084
  if (res.code === 0 && res.stdout.includes("EPD_HEALTHY"))
2085
2085
  return;
2086
2086
  const detail = res.stdout.replace("EPD_UNHEALTHY:", "").trim() || res.stderr;
2087
- throw new EpdError(`${host2.name}: new version did not become healthy within ${health.timeout}s`, `Still failing:${detail ? ` ${detail}` : ""}
2087
+ throw new EpdError(`${host.name}: new version did not become healthy within ${health.timeout}s`, `Still failing:${detail ? ` ${detail}` : ""}
2088
2088
  Check \`epd logs\` \u2014 the old version is still serving traffic.`);
2089
2089
  }
2090
2090
 
2091
2091
  // src/core/state.ts
2092
- async function readState(host2, cfg) {
2093
- const raw = await host2.readFile(paths(cfg).state);
2092
+ async function readState(host, cfg) {
2093
+ const raw = await host.readFile(paths(cfg).state);
2094
2094
  if (!raw)
2095
2095
  return null;
2096
2096
  try {
2097
2097
  return JSON.parse(raw);
2098
2098
  } catch {
2099
- log.warn(`${host2.name}: state file is corrupt, treating this host as fresh`);
2099
+ log.warn(`${host.name}: state file is corrupt, treating this host as fresh`);
2100
2100
  return null;
2101
2101
  }
2102
2102
  }
2103
- async function writeState(host2, cfg, state) {
2104
- await host2.writeFile(paths(cfg).state, JSON.stringify(state, null, 2), "644");
2103
+ async function writeState(host, cfg, state) {
2104
+ await host.writeFile(paths(cfg).state, JSON.stringify(state, null, 2), "644");
2105
2105
  }
2106
2106
  function emptyState(cfg) {
2107
2107
  return {
@@ -2210,7 +2210,7 @@ async function ensureBuilder() {
2210
2210
  log.info('creating buildx builder "epd"');
2211
2211
  await run(["docker", "buildx", "create", "--name", "epd", "--use", "--bootstrap"], { stream: true });
2212
2212
  }
2213
- async function loginRegistry(host2, cfg, env) {
2213
+ async function loginRegistry(host, cfg, env) {
2214
2214
  if (!cfg.registry)
2215
2215
  return;
2216
2216
  const password = env[cfg.registry.passwordEnv];
@@ -2218,25 +2218,25 @@ async function loginRegistry(host2, cfg, env) {
2218
2218
  throw new EpdError(`registry password: ${cfg.registry.passwordEnv} is not set`, "Export it, or put it in .env next to epd.yml.");
2219
2219
  }
2220
2220
  const server = cfg.registry.server ?? "";
2221
- await host2.exec(`printf '%s' ${shq(password)} | docker login ${server ? shq(server) + " " : ""}--username ${shq(cfg.registry.username)} --password-stdin >/dev/null`, { label: `${host2.name}: docker login` });
2221
+ await host.exec(`printf '%s' ${shq(password)} | docker login ${server ? shq(server) + " " : ""}--username ${shq(cfg.registry.username)} --password-stdin >/dev/null`, { label: `${host.name}: docker login` });
2222
2222
  }
2223
- async function ensureImage(host2, cfg, version) {
2223
+ async function ensureImage(host, cfg, version) {
2224
2224
  const ref = imageRef(cfg, version);
2225
- if (await host2.test(`docker image inspect ${shq(ref)} >/dev/null 2>&1`)) {
2226
- log.host(host2.name, `image ${version} already present`);
2225
+ if (await host.test(`docker image inspect ${shq(ref)} >/dev/null 2>&1`)) {
2226
+ log.host(host.name, `image ${version} already present`);
2227
2227
  return;
2228
2228
  }
2229
2229
  if (cfg.registry) {
2230
- log.host(host2.name, `pulling ${ref}`);
2231
- await host2.exec(`docker pull ${shq(ref)} >/dev/null`, { label: `${host2.name}: docker pull` });
2230
+ log.host(host.name, `pulling ${ref}`);
2231
+ await host.exec(`docker pull ${shq(ref)} >/dev/null`, { label: `${host.name}: docker pull` });
2232
2232
  } else {
2233
- log.host(host2.name, `shipping ${ref} over ssh (no registry configured)`);
2234
- await host2.pipeInto("gunzip | docker load", ["/bin/sh", "-c", `docker save ${shq(ref)} | gzip -1`]);
2233
+ log.host(host.name, `shipping ${ref} over ssh (no registry configured)`);
2234
+ await host.pipeInto("gunzip | docker load", ["/bin/sh", "-c", `docker save ${shq(ref)} | gzip -1`]);
2235
2235
  }
2236
2236
  }
2237
- async function pruneImages(host2, cfg, keep, inUse) {
2237
+ async function pruneImages(host, cfg, keep, inUse) {
2238
2238
  const keepRefs = inUse.map((v) => imageRef(cfg, v));
2239
- await host2.exec(`keep=${shq(keepRefs.join(" "))}
2239
+ await host.exec(`keep=${shq(keepRefs.join(" "))}
2240
2240
  docker images --filter "label=epd.app=${cfg.name}" --format '{{.Repository}}:{{.Tag}} {{.CreatedAt}}' 2>/dev/null \\
2241
2241
  | sort -k2 -r | tail -n +$(( ${keep} + 1 )) | awk '{print $1}' | while read -r img; do
2242
2242
  case " $keep " in *" $img "*) continue;; esac
@@ -2271,10 +2271,10 @@ function envFileContent(vars) {
2271
2271
  return Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(`
2272
2272
  `);
2273
2273
  }
2274
- async function writeServiceEnv(host2, cfg, svc, env) {
2274
+ async function writeServiceEnv(host, cfg, svc, env) {
2275
2275
  const secrets = resolveSecrets(svc.secrets, env, `servers.${svc.name}`);
2276
2276
  const file = `${paths(cfg).app}/env.${svc.name}`;
2277
- await host2.writeFile(file, envFileContent({ ...svc.env, ...secrets }), "600");
2277
+ await host.writeFile(file, envFileContent({ ...svc.env, ...secrets }), "600");
2278
2278
  return file;
2279
2279
  }
2280
2280
  function dockerRunArgs(o) {
@@ -2301,7 +2301,7 @@ function dockerRunArgs(o) {
2301
2301
  args.push(...parseCommand(svc.command));
2302
2302
  return args;
2303
2303
  }
2304
- async function startSlot(host2, cfg, svc, slot, version, envFile) {
2304
+ async function startSlot(host, cfg, svc, slot, version, envFile) {
2305
2305
  const names = [];
2306
2306
  const lines = [
2307
2307
  `old=$(docker ps -a -q --filter "label=epd.app=${cfg.name}" --filter "label=epd.service=${svc.name}" --filter "label=epd.slot=${slot}")`,
@@ -2310,19 +2310,19 @@ async function startSlot(host2, cfg, svc, slot, version, envFile) {
2310
2310
  for (let i = 0;i < svc.replicas; i++) {
2311
2311
  const name = containerName(cfg.name, svc.name, slot, i);
2312
2312
  names.push(name);
2313
- const args = dockerRunArgs({ cfg, svc, slot, version, replica: i, serverHost: host2.name, envFile });
2313
+ const args = dockerRunArgs({ cfg, svc, slot, version, replica: i, serverHost: host.name, envFile });
2314
2314
  lines.push(`docker rm -f ${shq(name)} >/dev/null 2>&1 || true`);
2315
2315
  lines.push(`${args.map((a) => shq(a)).join(" ")} >/dev/null`);
2316
2316
  }
2317
- await host2.exec(lines.join(`
2318
- `), { label: `${host2.name}: start ${svc.name}/${slot}` });
2317
+ await host.exec(lines.join(`
2318
+ `), { label: `${host.name}: start ${svc.name}/${slot}` });
2319
2319
  return names;
2320
2320
  }
2321
- async function containerIps(host2, cfg, names) {
2321
+ async function containerIps(host, cfg, names) {
2322
2322
  if (!names.length)
2323
2323
  return {};
2324
2324
  const tpl = `{{ with (index .NetworkSettings.Networks ${JSON.stringify(cfg.proxy.network)}) }}{{ .IPAddress }}{{ end }}`;
2325
- const out = await host2.capture(names.map((n) => `echo "${n} $(docker inspect -f ${shq(tpl)} ${shq(n)} 2>/dev/null)"`).join(`
2325
+ const out = await host.capture(names.map((n) => `echo "${n} $(docker inspect -f ${shq(tpl)} ${shq(n)} 2>/dev/null)"`).join(`
2326
2326
  `));
2327
2327
  const map = {};
2328
2328
  for (const line of out.split(`
@@ -2333,21 +2333,21 @@ async function containerIps(host2, cfg, names) {
2333
2333
  }
2334
2334
  return map;
2335
2335
  }
2336
- async function stopSlot(host2, cfg, svc, slot, opts) {
2336
+ async function stopSlot(host, cfg, svc, slot, opts) {
2337
2337
  const teardown = `names=$(docker ps -a -q --filter "label=epd.app=${cfg.name}" --filter "label=epd.service=${svc.name}" --filter "label=epd.slot=${slot}")
2338
2338
  if [ -n "$names" ]; then
2339
2339
  sleep ${opts.drain}
2340
2340
  docker stop --timeout ${svc.stopTimeout} $names >/dev/null 2>&1 || true
2341
2341
  ${opts.remove ? "docker rm -f $names >/dev/null 2>&1 || true" : ""}
2342
2342
  fi`;
2343
- await host2.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
2344
- disown 2>/dev/null || true` : teardown, { allowFailure: true, label: `${host2.name}: stop ${svc.name}/${slot}` });
2343
+ await host.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
2344
+ disown 2>/dev/null || true` : teardown, { allowFailure: true, label: `${host.name}: stop ${svc.name}/${slot}` });
2345
2345
  }
2346
- async function removeApp(host2, cfg) {
2347
- await host2.exec(`ids=$(docker ps -a -q --filter "label=epd.app=${cfg.name}")
2346
+ async function removeApp(host, cfg) {
2347
+ await host.exec(`ids=$(docker ps -a -q --filter "label=epd.app=${cfg.name}")
2348
2348
  [ -n "$ids" ] && docker rm -f $ids >/dev/null 2>&1 || true`, { allowFailure: true });
2349
2349
  }
2350
- async function listContainers(host2, cfg) {
2350
+ async function listContainers(host, cfg) {
2351
2351
  const fmt = [
2352
2352
  "{{.Names}}",
2353
2353
  '{{.Label "epd.service"}}',
@@ -2359,7 +2359,7 @@ async function listContainers(host2, cfg) {
2359
2359
  "{{.Image}}",
2360
2360
  "{{.CreatedAt}}"
2361
2361
  ].join("\t");
2362
- const out = await host2.capture(`docker ps -a --filter "label=epd.app=${cfg.name}" --format ${shq(fmt)} 2>/dev/null || true`, { allowFailure: true });
2362
+ const out = await host.capture(`docker ps -a --filter "label=epd.app=${cfg.name}" --format ${shq(fmt)} 2>/dev/null || true`, { allowFailure: true });
2363
2363
  return out.split(`
2364
2364
  `).filter(Boolean).map((line) => {
2365
2365
  const [name, service, slot, version, replica, status, state, image, created] = line.split("\t");
@@ -2376,11 +2376,11 @@ async function listContainers(host2, cfg) {
2376
2376
  };
2377
2377
  });
2378
2378
  }
2379
- async function startAccessory(host2, cfg, acc, env, opts) {
2379
+ async function startAccessory(host, cfg, acc, env, opts) {
2380
2380
  const name = accessoryContainer(cfg.name, acc.name);
2381
2381
  const secrets = resolveSecrets(acc.secrets, env, `accessories.${acc.name}`);
2382
2382
  const envFile = `${paths(cfg).app}/env.acc.${acc.name}`;
2383
- await host2.writeFile(envFile, envFileContent({ ...acc.env, ...secrets }), "600");
2383
+ await host.writeFile(envFile, envFileContent({ ...acc.env, ...secrets }), "600");
2384
2384
  const args = [
2385
2385
  "docker",
2386
2386
  "run",
@@ -2411,12 +2411,12 @@ async function startAccessory(host2, cfg, acc, env, opts) {
2411
2411
  args.push(...acc.dockerOptions, acc.image);
2412
2412
  if (acc.command)
2413
2413
  args.push(...parseCommand(acc.command));
2414
- await host2.exec(`if docker inspect ${shq(name)} >/dev/null 2>&1; then
2414
+ await host.exec(`if docker inspect ${shq(name)} >/dev/null 2>&1; then
2415
2415
  ${opts.recreate ? `docker rm -f ${shq(name)} >/dev/null` : `docker start ${shq(name)} >/dev/null 2>&1 || true; exit 0`}
2416
2416
  fi
2417
2417
  docker pull ${shq(acc.image)} >/dev/null
2418
- ${args.map((a) => shq(a)).join(" ")} >/dev/null`, { label: `${host2.name}: accessory ${acc.name}` });
2419
- log.host(host2.name, `accessory ${acc.name} running`);
2418
+ ${args.map((a) => shq(a)).join(" ")} >/dev/null`, { label: `${host.name}: accessory ${acc.name}` });
2419
+ log.host(host.name, `accessory ${acc.name} running`);
2420
2420
  }
2421
2421
 
2422
2422
  // src/deploy/process.ts
@@ -2427,34 +2427,34 @@ var pm2 = (cfg, args) => `${pathPrelude(cfg)} pm2 ${args}`;
2427
2427
  function releaseDir(cfg, version) {
2428
2428
  return `${paths(cfg).releases}/${version}`;
2429
2429
  }
2430
- async function uploadSource(host2, cfg, version) {
2430
+ async function uploadSource(host, cfg, version) {
2431
2431
  const dest = releaseDir(cfg, version);
2432
2432
  const p = paths(cfg);
2433
- await host2.exec(`mkdir -p ${shq(p.releases)} ${shq(p.shared)}`, asUser);
2433
+ await host.exec(`mkdir -p ${shq(p.releases)} ${shq(p.shared)}`, asUser);
2434
2434
  if (cfg.process.source === "git") {
2435
2435
  if (!existsSync4(join5(cfg.root, ".git")))
2436
2436
  throw new EpdError('process.source is "git" but this is not a git repository');
2437
- log.host(host2.name, `uploading HEAD as ${version}`);
2438
- await host2.pipeInto(`mkdir -p ${shq(dest)} && tar -x -C ${shq(dest)}`, ["/bin/sh", "-c", `git -C ${shq(cfg.root)} archive --format=tar HEAD`]);
2437
+ log.host(host.name, `uploading HEAD as ${version}`);
2438
+ await host.pipeInto(`mkdir -p ${shq(dest)} && tar -x -C ${shq(dest)}`, ["/bin/sh", "-c", `git -C ${shq(cfg.root)} archive --format=tar HEAD`]);
2439
2439
  return;
2440
2440
  }
2441
- const previous = await host2.capture(`ls -1dt ${shq(p.releases)}/*/ 2>/dev/null | head -n 1 || true`, { ...asUser, allowFailure: true });
2441
+ const previous = await host.capture(`ls -1dt ${shq(p.releases)}/*/ 2>/dev/null | head -n 1 || true`, { ...asUser, allowFailure: true });
2442
2442
  const args = ["rsync", "-az", "--delete", "--human-readable", "--info=stats1"];
2443
2443
  for (const ex of cfg.process.exclude)
2444
2444
  args.push("--exclude", ex);
2445
2445
  if (previous)
2446
2446
  args.push("--link-dest", previous.replace(/\/$/, ""));
2447
- args.push("-e", host2.rsyncShell());
2448
- args.push(`${cfg.root.replace(/\/$/, "")}/`, `${host2.target}:${dest}/`);
2449
- await host2.exec(`mkdir -p ${shq(dest)}`, asUser);
2450
- log.host(host2.name, `uploading ${version}`);
2451
- await run(args, { cwd: cfg.root, label: `rsync \u2192 ${host2.name}` });
2447
+ args.push("-e", host.rsyncShell());
2448
+ args.push(`${cfg.root.replace(/\/$/, "")}/`, `${host.target}:${dest}/`);
2449
+ await host.exec(`mkdir -p ${shq(dest)}`, asUser);
2450
+ log.host(host.name, `uploading ${version}`);
2451
+ await run(args, { cwd: cfg.root, label: `rsync \u2192 ${host.name}` });
2452
2452
  }
2453
- async function buildRelease(host2, cfg, version, env) {
2453
+ async function buildRelease(host, cfg, version, env) {
2454
2454
  const dest = releaseDir(cfg, version);
2455
2455
  const secrets = resolveSecrets(cfg.secrets, env, "env.secret");
2456
2456
  const envFile = paths(cfg).envFile;
2457
- await host2.writeFile(envFile, Object.entries({ ...cfg.env, ...secrets }).map(([k, v]) => `${k}=${v}`).join(`
2457
+ await host.writeFile(envFile, Object.entries({ ...cfg.env, ...secrets }).map(([k, v]) => `${k}=${v}`).join(`
2458
2458
  `), "600");
2459
2459
  const steps = [`cd ${shq(dest)}`, pathPrelude(cfg), `set -a; . ${shq(envFile)}; set +a`];
2460
2460
  if (cfg.process.install)
@@ -2462,9 +2462,9 @@ async function buildRelease(host2, cfg, version, env) {
2462
2462
  if (cfg.process.build)
2463
2463
  steps.push(cfg.process.build);
2464
2464
  if (steps.length > 3) {
2465
- log.host(host2.name, "installing and building");
2466
- await host2.exec(steps.join(`
2467
- `), { ...asUser, stream: true, label: `${host2.name}: build` });
2465
+ log.host(host.name, "installing and building");
2466
+ await host.exec(steps.join(`
2467
+ `), { ...asUser, stream: true, label: `${host.name}: build` });
2468
2468
  }
2469
2469
  }
2470
2470
  function ecosystem(cfg, svc, slot, version, env) {
@@ -2502,34 +2502,34 @@ function ecosystem(cfg, svc, slot, version, env) {
2502
2502
  }
2503
2503
  return { apps };
2504
2504
  }
2505
- async function startSlot2(host2, cfg, svc, slot, version, env) {
2505
+ async function startSlot2(host, cfg, svc, slot, version, env) {
2506
2506
  const secrets = resolveSecrets(svc.secrets, env, `servers.${svc.name}`);
2507
2507
  const doc = ecosystem(cfg, svc, slot, version, { ...svc.env, ...secrets });
2508
2508
  const file = `${paths(cfg).app}/pm2.${svc.name}.${slot}.json`;
2509
- await host2.writeFile(file, JSON.stringify(doc, null, 2), "600");
2509
+ await host.writeFile(file, JSON.stringify(doc, null, 2), "600");
2510
2510
  const stale = Array.from({ length: 16 }, (_, i) => processName(cfg.name, svc.name, slot, i));
2511
- await host2.exec(`for name in ${stale.map((n) => shq(n)).join(" ")}; do ${pathPrelude(cfg)} pm2 delete "$name" >/dev/null 2>&1 || true; done
2511
+ await host.exec(`for name in ${stale.map((n) => shq(n)).join(" ")}; do ${pathPrelude(cfg)} pm2 delete "$name" >/dev/null 2>&1 || true; done
2512
2512
  ${pm2(cfg, `start ${shq(file)} --update-env`)}
2513
- ${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, label: `${host2.name}: pm2 start ${svc.name}/${slot}` });
2513
+ ${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, label: `${host.name}: pm2 start ${svc.name}/${slot}` });
2514
2514
  }
2515
- async function stopSlot2(host2, cfg, svc, slot, drain, opts = {}) {
2515
+ async function stopSlot2(host, cfg, svc, slot, drain, opts = {}) {
2516
2516
  const names = Array.from({ length: 16 }, (_, i) => processName(cfg.name, svc.name, slot, i));
2517
2517
  const teardown = `sleep ${drain}
2518
2518
  for name in ${names.map((n) => shq(n)).join(" ")}; do
2519
2519
  ${pathPrelude(cfg)} pm2 delete "$name" >/dev/null 2>&1 || true
2520
2520
  done
2521
2521
  ${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`;
2522
- await host2.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
2523
- disown 2>/dev/null || true` : teardown, { ...asUser, allowFailure: true, label: `${host2.name}: pm2 stop ${svc.name}/${slot}` });
2522
+ await host.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
2523
+ disown 2>/dev/null || true` : teardown, { ...asUser, allowFailure: true, label: `${host.name}: pm2 stop ${svc.name}/${slot}` });
2524
2524
  }
2525
- async function removeApp2(host2, cfg) {
2526
- await host2.exec(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null | tr ',' '\\n' | grep -o '"name":"epd-${cfg.name}-[^"]*"' | cut -d'"' -f4 | sort -u | while read -r n; do
2525
+ async function removeApp2(host, cfg) {
2526
+ await host.exec(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null | tr ',' '\\n' | grep -o '"name":"epd-${cfg.name}-[^"]*"' | cut -d'"' -f4 | sort -u | while read -r n; do
2527
2527
  ${pathPrelude(cfg)} pm2 delete "$n" >/dev/null 2>&1 || true
2528
2528
  done
2529
2529
  ${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, allowFailure: true });
2530
2530
  }
2531
- async function listProcesses(host2, cfg) {
2532
- const out = await host2.capture(`${pm2(cfg, "jlist")} 2>/dev/null || echo '[]'`, { ...asUser, allowFailure: true });
2531
+ async function listProcesses(host, cfg) {
2532
+ const out = await host.capture(`${pm2(cfg, "jlist")} 2>/dev/null || echo '[]'`, { ...asUser, allowFailure: true });
2533
2533
  const jsonStart = out.indexOf("[");
2534
2534
  let parsed = [];
2535
2535
  try {
@@ -2546,9 +2546,9 @@ async function listProcesses(host2, cfg) {
2546
2546
  memory: p.monit?.memory ?? 0
2547
2547
  }));
2548
2548
  }
2549
- async function finalizeRelease(host2, cfg, version) {
2549
+ async function finalizeRelease(host, cfg, version) {
2550
2550
  const p = paths(cfg);
2551
- await host2.exec(`ln -sfn ${shq(releaseDir(cfg, version))} ${shq(p.current)}.tmp && mv -Tf ${shq(p.current)}.tmp ${shq(p.current)}
2551
+ await host.exec(`ln -sfn ${shq(releaseDir(cfg, version))} ${shq(p.current)}.tmp && mv -Tf ${shq(p.current)}.tmp ${shq(p.current)}
2552
2552
  ls -1dt ${shq(p.releases)}/*/ 2>/dev/null | tail -n +$(( ${cfg.keepReleases} + 1 )) | while read -r d; do
2553
2553
  case "$d" in *${shq(version)}*) continue;; esac
2554
2554
  rm -rf "$d"
@@ -2558,7 +2558,7 @@ done`, { ...asUser, allowFailure: true });
2558
2558
  // src/deploy/deploy.ts
2559
2559
  async function deploy(loaded, opts = {}) {
2560
2560
  const { config: cfg, env } = loaded;
2561
- const started2 = Date.now();
2561
+ const started = Date.now();
2562
2562
  const services = selectServices(cfg, opts.onlyServices);
2563
2563
  const targetHosts = selectHosts(cfg, services, opts.onlyHosts);
2564
2564
  const hosts = targetHosts.map((h) => host(h, cfg.ssh));
@@ -2575,15 +2575,15 @@ async function deploy(loaded, opts = {}) {
2575
2575
  await hook(cfg, "pre_deploy", cfg.hooks.preDeploy, version);
2576
2576
  await withLock(cfg, hosts, `deploy ${version}`, opts.skipLock ?? false, async () => {
2577
2577
  log.step("Preparing servers");
2578
- await pool(hosts, 8, async (host2) => {
2579
- await verifyHost(host2, cfg);
2578
+ await pool(hosts, 8, async (host) => {
2579
+ await verifyHost(host, cfg);
2580
2580
  if (cfg.mode === "docker")
2581
- await ensureNetwork(host2, cfg);
2581
+ await ensureNetwork(host, cfg);
2582
2582
  });
2583
2583
  await deployAccessories(cfg, env, opts.recreateAccessories ?? false);
2584
2584
  if (cfg.proxy.enabled && proxyHosts(cfg).length) {
2585
2585
  log.step("Checking proxy");
2586
- await pool(proxyHosts(cfg).filter((h) => targetHosts.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host2) => ensureProxy(host2, cfg, { secrets: env }));
2586
+ await pool(proxyHosts(cfg).filter((h) => targetHosts.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host) => ensureProxy(host, cfg, { secrets: env }));
2587
2587
  }
2588
2588
  const live = new Map;
2589
2589
  for (const h of allHosts(cfg)) {
@@ -2593,61 +2593,61 @@ async function deploy(loaded, opts = {}) {
2593
2593
  m.set(svc, s.slot);
2594
2594
  live.set(h, m);
2595
2595
  }
2596
- const deployHost = async (host2) => {
2597
- await deployToHost({ cfg, env, host: host2, services, version, opts, live });
2596
+ const deployHost = async (host) => {
2597
+ await deployToHost({ cfg, env, host, services, version, opts, live });
2598
2598
  };
2599
2599
  if (cfg.strategy === "parallel") {
2600
2600
  await pool(hosts, hosts.length, deployHost);
2601
2601
  } else {
2602
- for (const host2 of hosts)
2603
- await deployHost(host2);
2602
+ for (const host of hosts)
2603
+ await deployHost(host);
2604
2604
  }
2605
2605
  if (cfg.proxy.crossHost) {
2606
2606
  log.step("Syncing routes across hosts");
2607
- await pool(proxyHosts(cfg).map((h) => host(h, cfg.ssh)), 4, async (host2) => {
2608
- await writeRoutes(host2, cfg, buildEndpoints(cfg, host2.name, live), buildAccessoryEndpoints(cfg, host2.name));
2607
+ await pool(proxyHosts(cfg).map((h) => host(h, cfg.ssh)), 4, async (host) => {
2608
+ await writeRoutes(host, cfg, buildEndpoints(cfg, host.name, live), buildAccessoryEndpoints(cfg, host.name));
2609
2609
  });
2610
2610
  }
2611
2611
  });
2612
2612
  if (!opts.skipHooks && cfg.hooks.postDeploy)
2613
2613
  await hook(cfg, "post_deploy", cfg.hooks.postDeploy, version);
2614
- log.ok(`Deployed ${cfg.name} ${version} in ${dur(Date.now() - started2)}`);
2614
+ log.ok(`Deployed ${cfg.name} ${version} in ${dur(Date.now() - started)}`);
2615
2615
  printUrls(cfg);
2616
2616
  }
2617
2617
  async function deployToHost(d) {
2618
- const { cfg, env, host: host2, version, opts, live } = d;
2619
- const services = d.services.filter((s) => s.hosts.includes(host2.name));
2618
+ const { cfg, env, host, version, opts, live } = d;
2619
+ const services = d.services.filter((s) => s.hosts.includes(host.name));
2620
2620
  if (!services.length)
2621
2621
  return;
2622
- log.step(`${host2.name}: deploying ${services.map((s) => s.name).join(", ")}`);
2622
+ log.step(`${host.name}: deploying ${services.map((s) => s.name).join(", ")}`);
2623
2623
  if (cfg.mode === "docker") {
2624
- await loginRegistry(host2, cfg, env);
2625
- await ensureImage(host2, cfg, version);
2624
+ await loginRegistry(host, cfg, env);
2625
+ await ensureImage(host, cfg, version);
2626
2626
  } else if (!opts.reuse) {
2627
- await uploadSource(host2, cfg, version);
2628
- await buildRelease(host2, cfg, version, env);
2627
+ await uploadSource(host, cfg, version);
2628
+ await buildRelease(host, cfg, version, env);
2629
2629
  } else {
2630
2630
  const dir = releaseDir(cfg, version);
2631
- if (!await host2.test(`[ -d ${JSON.stringify(dir)} ]`)) {
2632
- throw new EpdError(`${host2.name}: release ${version} is not on this server`, "Deploy it normally first.");
2631
+ if (!await host.test(`[ -d ${JSON.stringify(dir)} ]`)) {
2632
+ throw new EpdError(`${host.name}: release ${version} is not on this server`, "Deploy it normally first.");
2633
2633
  }
2634
2634
  }
2635
- const state = await readState(host2, cfg) ?? emptyState(cfg);
2635
+ const state = await readState(host, cfg) ?? emptyState(cfg);
2636
2636
  const previous = new Map;
2637
- const started2 = [];
2637
+ const started = [];
2638
2638
  try {
2639
2639
  for (const svc of services) {
2640
2640
  const current = state.services[svc.name]?.slot;
2641
2641
  if (current)
2642
2642
  previous.set(svc.name, current);
2643
2643
  const slot = current ? otherSlot(current) : "blue";
2644
- log.host(host2.name, `${svc.name}: starting ${svc.replicas} replica(s) on ${slot}`);
2644
+ log.host(host.name, `${svc.name}: starting ${svc.replicas} replica(s) on ${slot}`);
2645
2645
  let probes = [];
2646
2646
  if (cfg.mode === "docker") {
2647
- const envFile = await writeServiceEnv(host2, cfg, svc, env);
2648
- const names = await startSlot(host2, cfg, svc, slot, version, envFile);
2649
- started2.push({ svc, slot });
2650
- const ips = usesHostPorts(cfg) ? {} : await containerIps(host2, cfg, names);
2647
+ const envFile = await writeServiceEnv(host, cfg, svc, env);
2648
+ const names = await startSlot(host, cfg, svc, slot, version, envFile);
2649
+ started.push({ svc, slot });
2650
+ const ips = usesHostPorts(cfg) ? {} : await containerIps(host, cfg, names);
2651
2651
  probes = names.map((name, i) => ({
2652
2652
  label: name,
2653
2653
  address: usesHostPorts(cfg) ? "127.0.0.1" : ips[name] ?? "",
@@ -2655,10 +2655,10 @@ async function deployToHost(d) {
2655
2655
  }));
2656
2656
  const noIp = probes.find((p) => !p.address);
2657
2657
  if (noIp)
2658
- throw new EpdError(`${host2.name}: ${noIp.label} did not get an IP on the ${cfg.proxy.network} network`, "Check `epd logs`.");
2658
+ throw new EpdError(`${host.name}: ${noIp.label} did not get an IP on the ${cfg.proxy.network} network`, "Check `epd logs`.");
2659
2659
  } else {
2660
- await startSlot2(host2, cfg, svc, slot, version, env);
2661
- started2.push({ svc, slot });
2660
+ await startSlot2(host, cfg, svc, slot, version, env);
2661
+ started.push({ svc, slot });
2662
2662
  probes = Array.from({ length: svc.replicas }, (_, i) => ({
2663
2663
  label: `${svc.name}#${i}`,
2664
2664
  address: "127.0.0.1",
@@ -2666,52 +2666,52 @@ async function deployToHost(d) {
2666
2666
  }));
2667
2667
  }
2668
2668
  if (!opts.skipHealth && svc.proxied) {
2669
- log.host(host2.name, `${svc.name}: waiting for health check${svc.health.path ? ` ${svc.health.path}` : " (tcp)"}`);
2670
- await waitHealthy(host2, probes, svc.health);
2671
- log.host(host2.name, `${svc.name}: healthy`);
2669
+ log.host(host.name, `${svc.name}: waiting for health check${svc.health.path ? ` ${svc.health.path}` : " (tcp)"}`);
2670
+ await waitHealthy(host, probes, svc.health);
2671
+ log.host(host.name, `${svc.name}: healthy`);
2672
2672
  } else if (!opts.skipHealth) {
2673
- await confirmUp(host2, cfg, svc, slot);
2673
+ await confirmUp(host, cfg, svc, slot);
2674
2674
  }
2675
- live.get(host2.name)?.set(svc.name, slot) ?? live.set(host2.name, new Map([[svc.name, slot]]));
2675
+ live.get(host.name)?.set(svc.name, slot) ?? live.set(host.name, new Map([[svc.name, slot]]));
2676
2676
  state.services[svc.name] = {
2677
2677
  slot,
2678
2678
  replicas: svc.replicas,
2679
2679
  version,
2680
- endpoints: endpointsFor(cfg, svc, slot, host2.name).map((e) => e.url)
2680
+ endpoints: endpointsFor(cfg, svc, slot, host.name).map((e) => e.url)
2681
2681
  };
2682
2682
  }
2683
2683
  const hasSvcRoutes = services.some((s) => s.routes.length);
2684
- const hasAccRoutes = cfg.accessories.some((a) => a.routes.length && (cfg.proxy.crossHost || a.host === host2.name));
2684
+ const hasAccRoutes = cfg.accessories.some((a) => a.routes.length && (cfg.proxy.crossHost || a.host === host.name));
2685
2685
  if (cfg.proxy.enabled && (hasSvcRoutes || hasAccRoutes)) {
2686
- await writeRoutes(host2, cfg, buildEndpoints(cfg, host2.name, live), buildAccessoryEndpoints(cfg, host2.name));
2687
- log.host(host2.name, "routes updated");
2686
+ await writeRoutes(host, cfg, buildEndpoints(cfg, host.name, live), buildAccessoryEndpoints(cfg, host.name));
2687
+ log.host(host.name, "routes updated");
2688
2688
  }
2689
2689
  for (const svc of services) {
2690
2690
  const old = previous.get(svc.name);
2691
2691
  if (!old)
2692
2692
  continue;
2693
2693
  const drain = Math.max(svc.drain, cfg.proxy.reloadWait + 2);
2694
- log.host(host2.name, `${svc.name}: retiring ${old} (${drain}s drain, in the background)`);
2694
+ log.host(host.name, `${svc.name}: retiring ${old} (${drain}s drain, in the background)`);
2695
2695
  if (cfg.mode === "docker")
2696
- await stopSlot(host2, cfg, svc, old, { drain, remove: true, detach: true });
2696
+ await stopSlot(host, cfg, svc, old, { drain, remove: true, detach: true });
2697
2697
  else
2698
- await stopSlot2(host2, cfg, svc, old, drain, { detach: true });
2698
+ await stopSlot2(host, cfg, svc, old, drain, { detach: true });
2699
2699
  }
2700
- await writeState(host2, cfg, recordDeploy(state, version, process.env.USER ?? "epd", cfg.keepReleases));
2700
+ await writeState(host, cfg, recordDeploy(state, version, process.env.USER ?? "epd", cfg.keepReleases));
2701
2701
  if (cfg.mode === "docker") {
2702
- await pruneImages(host2, cfg, cfg.keepReleases, [version, ...state.history.slice(0, 2).map((h) => h.version)]);
2702
+ await pruneImages(host, cfg, cfg.keepReleases, [version, ...state.history.slice(0, 2).map((h) => h.version)]);
2703
2703
  } else {
2704
- await finalizeRelease(host2, cfg, version);
2704
+ await finalizeRelease(host, cfg, version);
2705
2705
  }
2706
- log.host(host2.name, c.green("done"));
2706
+ log.host(host.name, c.green("done"));
2707
2707
  } catch (error) {
2708
- log.error(`${host2.name}: deploy failed, rolling back this host`);
2709
- for (const { svc, slot } of started2) {
2708
+ log.error(`${host.name}: deploy failed, rolling back this host`);
2709
+ for (const { svc, slot } of started) {
2710
2710
  try {
2711
2711
  if (cfg.mode === "docker")
2712
- await stopSlot(host2, cfg, svc, slot, { drain: 0, remove: true });
2712
+ await stopSlot(host, cfg, svc, slot, { drain: 0, remove: true });
2713
2713
  else
2714
- await stopSlot2(host2, cfg, svc, slot, 0);
2714
+ await stopSlot2(host, cfg, svc, slot, 0);
2715
2715
  } catch {}
2716
2716
  }
2717
2717
  if (!d.opts.skipHooks && cfg.hooks.onFailure) {
@@ -2722,18 +2722,18 @@ async function deployToHost(d) {
2722
2722
  throw error;
2723
2723
  }
2724
2724
  }
2725
- async function confirmUp(host2, cfg, svc, slot) {
2725
+ async function confirmUp(host, cfg, svc, slot) {
2726
2726
  if (cfg.mode === "docker") {
2727
- const containers = await listContainers(host2, cfg);
2727
+ const containers = await listContainers(host, cfg);
2728
2728
  const bad = containers.filter((ct) => ct.service === svc.name && ct.slot === slot && !ct.running);
2729
2729
  if (bad.length) {
2730
- throw new EpdError(`${host2.name}: ${bad.map((b) => b.name).join(", ")} exited right after starting`, `Run: epd logs --service ${svc.name}`);
2730
+ throw new EpdError(`${host.name}: ${bad.map((b) => b.name).join(", ")} exited right after starting`, `Run: epd logs --service ${svc.name}`);
2731
2731
  }
2732
2732
  } else {
2733
- const procs = await listProcesses(host2, cfg);
2733
+ const procs = await listProcesses(host, cfg);
2734
2734
  const bad = procs.filter((p) => p.name.includes(`-${svc.name}-${slot}-`) && p.status !== "online");
2735
2735
  if (bad.length) {
2736
- throw new EpdError(`${host2.name}: ${bad.map((b) => b.name).join(", ")} is ${bad[0].status}`, `Run: epd logs --service ${svc.name}`);
2736
+ throw new EpdError(`${host.name}: ${bad.map((b) => b.name).join(", ")} is ${bad[0].status}`, `Run: epd logs --service ${svc.name}`);
2737
2737
  }
2738
2738
  }
2739
2739
  }
@@ -2874,22 +2874,22 @@ Options
2874
2874
  log.step(`Preparing ${hosts.length} host(s) for ${c.bold(cfg.name)}`);
2875
2875
  if (cfg.mode === "docker")
2876
2876
  await requireLocal("docker", "Install Docker Desktop or the docker engine to build images.");
2877
- await pool(hosts, 4, async (host2) => {
2878
- await host2.ping();
2879
- log.host(host2.name, "reachable");
2877
+ await pool(hosts, 4, async (host) => {
2878
+ await host.ping();
2879
+ log.host(host.name, "reachable");
2880
2880
  if (cfg.mode === "docker") {
2881
- await installDocker(host2);
2882
- await ensureNetwork(host2, cfg);
2881
+ await installDocker(host);
2882
+ await ensureNetwork(host, cfg);
2883
2883
  } else {
2884
- await installSystemPackages(host2);
2885
- await installProcessRuntime(host2, cfg);
2884
+ await installSystemPackages(host);
2885
+ await installProcessRuntime(host, cfg);
2886
2886
  }
2887
- await ensureDirs(host2, cfg);
2888
- await reservePorts(host2, cfg);
2887
+ await ensureDirs(host, cfg);
2888
+ await reservePorts(host, cfg);
2889
2889
  });
2890
2890
  if (cfg.proxy.enabled && proxyHosts(cfg).length) {
2891
2891
  log.step("Starting the proxy");
2892
- await pool(proxyHosts(cfg).filter((h) => names.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host2) => ensureProxy(host2, cfg, { secrets: loaded.env, force: true }));
2892
+ await pool(proxyHosts(cfg).filter((h) => names.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host) => ensureProxy(host, cfg, { secrets: loaded.env, force: true }));
2893
2893
  }
2894
2894
  log.ok("servers are ready");
2895
2895
  if (ctx.values["skip-deploy"]) {
@@ -3250,12 +3250,12 @@ Examples
3250
3250
  const known = cfg.accessories.map((a) => a.name);
3251
3251
  throw new EpdError(`no such accessory "${accName}"`, known.length ? `Known: ${known.join(", ")}` : "No accessories configured.");
3252
3252
  }
3253
- const host3 = host(acc.host, cfg.ssh);
3253
+ const host2 = host(acc.host, cfg.ssh);
3254
3254
  const container = accessoryContainer(cfg.name, acc.name);
3255
- log.info(`${host3.name}: ${container} $ ${command}`);
3256
- const code2 = await host3.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
3257
- if (code2 !== 0)
3258
- process.exitCode = code2;
3255
+ log.info(`${host2.name}: ${container} $ ${command}`);
3256
+ const code = await host2.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
3257
+ if (code !== 0)
3258
+ process.exitCode = code;
3259
3259
  return;
3260
3260
  }
3261
3261
  const host2 = await pickHost(ctx, cfg);
@@ -3270,18 +3270,18 @@ Examples
3270
3270
  const dir = `${paths(cfg).current}`;
3271
3271
  const script = `cd ${shq(dir)} && ${pathPrelude(cfg)} && set -a && . ${shq(paths(cfg).envFile)} && set +a && ${command}`;
3272
3272
  log.info(`${host2.name}: ${command}`);
3273
- const code2 = await host2.interactive(script, { sudo: false });
3274
- if (code2 !== 0)
3275
- process.exitCode = code2;
3273
+ const code = await host2.interactive(script, { sudo: false });
3274
+ if (code !== 0)
3275
+ process.exitCode = code;
3276
3276
  return;
3277
3277
  }
3278
3278
  if (ctx.values.reuse) {
3279
3279
  const slot = state.services[svc.name]?.slot;
3280
3280
  const container = `epd-${cfg.name}-${svc.name}-${slot}-0`;
3281
3281
  log.info(`${host2.name}: ${container} $ ${command}`);
3282
- const code2 = await host2.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
3283
- if (code2 !== 0)
3284
- process.exitCode = code2;
3282
+ const code = await host2.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
3283
+ if (code !== 0)
3284
+ process.exitCode = code;
3285
3285
  return;
3286
3286
  }
3287
3287
  const envFile = `${paths(cfg).app}/env.${svc.name}`;
@@ -3360,12 +3360,12 @@ Options
3360
3360
  const hosts = names.map((h) => host(h, cfg.ssh));
3361
3361
  switch (sub) {
3362
3362
  case "status": {
3363
- for (const host2 of hosts) {
3364
- const s = await proxyState(host2, cfg);
3365
- const listening = cfg.mode === "docker" ? await host2.capture(`docker port ${PROXY_CONTAINER} 2>/dev/null | sort -u || true`, { allowFailure: true }) : Object.entries(cfg.proxy.entrypoints).map(([n, p]) => `${n} -> :${p}`).join(`
3363
+ for (const host of hosts) {
3364
+ const s = await proxyState(host, cfg);
3365
+ const listening = cfg.mode === "docker" ? await host.capture(`docker port ${PROXY_CONTAINER} 2>/dev/null | sort -u || true`, { allowFailure: true }) : Object.entries(cfg.proxy.entrypoints).map(([n, p]) => `${n} -> :${p}`).join(`
3366
3366
  `);
3367
- const apps = await host2.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)} 2>/dev/null | sed 's/\\.yml$//' || true`, { allowFailure: true });
3368
- log.plain(`${c.magenta(host2.name)} ${s.running ? c.green("running") : c.red(s.status)} ${c.gray(s.image)}`);
3367
+ const apps = await host.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)} 2>/dev/null | sed 's/\\.yml$//' || true`, { allowFailure: true });
3368
+ log.plain(`${c.magenta(host.name)} ${s.running ? c.green("running") : c.red(s.status)} ${c.gray(s.image)}`);
3369
3369
  if (listening)
3370
3370
  log.plain(listening.split(`
3371
3371
  `).map((l) => ` ${l}`).join(`
@@ -3377,8 +3377,8 @@ Options
3377
3377
  return;
3378
3378
  }
3379
3379
  case "reboot": {
3380
- await pool(hosts, 4, async (host2) => {
3381
- await ensureProxy(host2, cfg, { force: true, secrets: loaded.env });
3380
+ await pool(hosts, 4, async (host) => {
3381
+ await ensureProxy(host, cfg, { force: true, secrets: loaded.env });
3382
3382
  });
3383
3383
  log.ok("proxy restarted");
3384
3384
  return;
@@ -3386,40 +3386,40 @@ Options
3386
3386
  case "logs": {
3387
3387
  const lines = String(ctx.values.lines ?? "100");
3388
3388
  const follow = Boolean(ctx.values.follow);
3389
- for (const host2 of hosts) {
3389
+ for (const host of hosts) {
3390
3390
  const cmd = cfg.mode === "docker" ? `docker logs ${follow ? "-f " : ""}--tail ${lines} ${PROXY_CONTAINER} 2>&1` : `${pathPrelude(cfg)} pm2 logs ${PROXY_PROCESS} --lines ${lines}${follow ? "" : " --nostream"} 2>&1`;
3391
3391
  if (follow) {
3392
- log.info(`following proxy on ${host2.name} \u2014 Ctrl-C to stop`);
3393
- await host2.interactive(cmd, { sudo: cfg.mode === "docker" ? undefined : false });
3392
+ log.info(`following proxy on ${host.name} \u2014 Ctrl-C to stop`);
3393
+ await host.interactive(cmd, { sudo: cfg.mode === "docker" ? undefined : false });
3394
3394
  } else {
3395
- const out = await host2.exec(`${cmd} || true`, { allowFailure: true, sudo: cfg.mode === "docker" ? undefined : false });
3396
- log.plain(c.magenta(host2.name));
3395
+ const out = await host.exec(`${cmd} || true`, { allowFailure: true, sudo: cfg.mode === "docker" ? undefined : false });
3396
+ log.plain(c.magenta(host.name));
3397
3397
  log.plain(out.stdout || c.gray(" no output"));
3398
3398
  }
3399
3399
  }
3400
3400
  return;
3401
3401
  }
3402
3402
  case "routes": {
3403
- for (const host2 of hosts) {
3404
- const doc = await host2.readFile(paths(cfg).proxyDynamic);
3405
- log.plain(c.magenta(host2.name));
3403
+ for (const host of hosts) {
3404
+ const doc = await host.readFile(paths(cfg).proxyDynamic);
3405
+ log.plain(c.magenta(host.name));
3406
3406
  log.plain(doc ?? c.gray(" no routes written yet"));
3407
3407
  }
3408
3408
  return;
3409
3409
  }
3410
3410
  case "remove": {
3411
- for (const host2 of hosts) {
3412
- await removeRoutes(host2, cfg);
3413
- const left = await host2.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)}/*.yml 2>/dev/null | wc -l`, { allowFailure: true });
3411
+ for (const host of hosts) {
3412
+ await removeRoutes(host, cfg);
3413
+ const left = await host.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)}/*.yml 2>/dev/null | wc -l`, { allowFailure: true });
3414
3414
  if (Number(left.trim()) === 0) {
3415
3415
  if (cfg.mode === "docker") {
3416
- await host2.exec(`docker rm -f ${PROXY_CONTAINER} >/dev/null 2>&1 || true`, { allowFailure: true });
3416
+ await host.exec(`docker rm -f ${PROXY_CONTAINER} >/dev/null 2>&1 || true`, { allowFailure: true });
3417
3417
  } else {
3418
- await host2.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true`, { allowFailure: true, sudo: false });
3418
+ await host.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true`, { allowFailure: true, sudo: false });
3419
3419
  }
3420
- log.host(host2.name, "routes removed, proxy stopped (no apps left)");
3420
+ log.host(host.name, "routes removed, proxy stopped (no apps left)");
3421
3421
  } else {
3422
- log.host(host2.name, `routes removed, proxy still serving ${left.trim()} other app(s)`);
3422
+ log.host(host.name, `routes removed, proxy still serving ${left.trim()} other app(s)`);
3423
3423
  }
3424
3424
  }
3425
3425
  return;
@@ -3560,8 +3560,8 @@ If a deploy was killed halfway, release it by hand.`,
3560
3560
  }
3561
3561
  if (sub !== "status")
3562
3562
  throw new EpdError(`unknown subcommand "${sub}"`, "Try: status, release");
3563
- for (const { host: host2, info } of await DeployLock.status(cfg, hosts)) {
3564
- log.plain(info ? `${c.magenta(host2)} ${c.yellow("locked")} by ${info.by} ${c.gray(`${info.at} \u2014 ${info.what}`)}` : `${c.magenta(host2)} ${c.green("free")}`);
3563
+ for (const { host, info } of await DeployLock.status(cfg, hosts)) {
3564
+ log.plain(info ? `${c.magenta(host)} ${c.yellow("locked")} by ${info.by} ${c.gray(`${info.at} \u2014 ${info.what}`)}` : `${c.magenta(host)} ${c.green("free")}`);
3565
3565
  }
3566
3566
  }
3567
3567
  };
@@ -3593,16 +3593,16 @@ Options
3593
3593
  return;
3594
3594
  }
3595
3595
  }
3596
- await pool(names.map((h) => host(h, cfg.ssh)), 4, async (host2) => {
3597
- await removeRoutes(host2, cfg);
3596
+ await pool(names.map((h) => host(h, cfg.ssh)), 4, async (host) => {
3597
+ await removeRoutes(host, cfg);
3598
3598
  if (cfg.mode === "docker")
3599
- await removeApp(host2, cfg);
3599
+ await removeApp(host, cfg);
3600
3600
  else
3601
- await removeApp2(host2, cfg);
3601
+ await removeApp2(host, cfg);
3602
3602
  if (!ctx.values["keep-data"]) {
3603
- await host2.exec(`rm -rf ${shq(paths(cfg).app)} ${shq(`${paths(cfg).portsDir}/${cfg.portBase}`)}`, { allowFailure: true });
3603
+ await host.exec(`rm -rf ${shq(paths(cfg).app)} ${shq(`${paths(cfg).portsDir}/${cfg.portBase}`)}`, { allowFailure: true });
3604
3604
  }
3605
- log.host(host2.name, "removed");
3605
+ log.host(host.name, "removed");
3606
3606
  });
3607
3607
  log.ok(`${cfg.name} removed from ${names.length} host(s)`);
3608
3608
  log.info(`the shared proxy is still running \u2014 ${c.cyan("epd proxy remove")} stops it if nothing else uses it`);
@@ -3755,7 +3755,7 @@ var commands = [
3755
3755
  ];
3756
3756
 
3757
3757
  // src/cli.ts
3758
- var VERSION = "0.1.0";
3758
+ var VERSION = "0.1.1";
3759
3759
  var USAGE = `${c.bold("epd")} \u2014 Easy Project Deployer ${c.gray(`v${VERSION}`)}
3760
3760
 
3761
3761
  ${c.bold("Usage")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabioplunser/epd",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Easy Project Deployer — deploy any project to one or many servers with Docker or PM2, fronted by Traefik.",
5
5
  "type": "module",
6
6
  "author": "Fabio Plünser",
@@ -8,7 +8,7 @@
8
8
  "homepage": "https://github.com/FabioPlunser/epd#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/FabioPlunser/epd.git"
11
+ "url": "https://github.com/FabioPlunser/epd"
12
12
  },
13
13
  "bugs": {
14
14
  "url": "https://github.com/FabioPlunser/epd/issues"