@iamken/cloudtunnel 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  listZones,
4
4
  resolveZone
5
- } from "./chunk-NB3OEY2P.js";
5
+ } from "./chunk-OT3UCBBF.js";
6
6
  import {
7
7
  createCname,
8
8
  deleteDnsRecord,
9
9
  findCname,
10
10
  isManagedDns
11
- } from "./chunk-IDTF7SUY.js";
11
+ } from "./chunk-ABLZ7XRZ.js";
12
12
  import {
13
13
  CliError,
14
14
  DEFAULT_API_BASE,
@@ -41,7 +41,7 @@ import {
41
41
  say,
42
42
  scanCacheFile,
43
43
  selectOne
44
- } from "./chunk-ZIUYW2HQ.js";
44
+ } from "./chunk-7XACBYL7.js";
45
45
 
46
46
  // src/index.ts
47
47
  import { Command } from "commander";
@@ -143,18 +143,22 @@ function buildUpArgs(p) {
143
143
  "-d",
144
144
  p.zone,
145
145
  ...p.proto === "https" ? ["--proto", "https"] : [],
146
+ "--keep-alive",
146
147
  "-f",
147
148
  "-y"
148
149
  ];
149
150
  }
150
151
  const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
152
+ const proto = p.protocol ?? "http2";
151
153
  return [
152
154
  "up",
153
155
  spec,
154
156
  "-d",
155
157
  p.zone,
156
158
  ...p.proto === "https" ? ["--proto", "https"] : [],
157
- ...p.protocol ? ["--protocol", p.protocol] : [],
159
+ "--protocol",
160
+ proto,
161
+ "--keep-alive",
158
162
  "-f",
159
163
  "-y"
160
164
  ];
@@ -204,6 +208,7 @@ function buildUnit(d) {
204
208
  `Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
205
209
  "After=network-online.target",
206
210
  "Wants=network-online.target",
211
+ "StartLimitIntervalSec=0",
207
212
  "",
208
213
  "[Service]",
209
214
  "Type=simple",
@@ -211,7 +216,7 @@ function buildUnit(d) {
211
216
  `Environment=HOME=${d.home}`,
212
217
  `Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
213
218
  `ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
214
- "Restart=on-failure",
219
+ "Restart=always",
215
220
  "RestartSec=5",
216
221
  "",
217
222
  "[Install]",
@@ -381,7 +386,7 @@ function buildTaskXml(d) {
381
386
  " <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
382
387
  " <StartWhenAvailable>true</StartWhenAvailable>",
383
388
  " <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
384
- " <RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
389
+ " <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
385
390
  " <Enabled>true</Enabled>",
386
391
  " </Settings>",
387
392
  ' <Actions Context="Author">',
@@ -937,6 +942,7 @@ function extractTgz(bytes) {
937
942
 
938
943
  // src/core/up-runner.ts
939
944
  import { join as join7 } from "path";
945
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
940
946
  import * as clack2 from "@clack/prompts";
941
947
 
942
948
  // src/connector/process.ts
@@ -1063,7 +1069,7 @@ async function reconcile() {
1063
1069
  // src/connector/process.ts
1064
1070
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1065
1071
  function startConnector(opts) {
1066
- const args = ["tunnel", "run"];
1072
+ const args = ["tunnel", "run", "--metrics", "127.0.0.1:0"];
1067
1073
  if (opts.protocol) args.push("--protocol", opts.protocol);
1068
1074
  const env = { ...process.env, TUNNEL_TOKEN: opts.token };
1069
1075
  const fd = openSync(opts.logFile, "a", 384);
@@ -1171,6 +1177,34 @@ async function waitHealthy(cf, tunnelId, opts = {}) {
1171
1177
  }
1172
1178
  return opts.signal?.aborted ? "dead" : "provisioning";
1173
1179
  }
1180
+ function extractMetricsPort(log) {
1181
+ const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
1182
+ if (!matches.length) return void 0;
1183
+ const last = matches[matches.length - 1];
1184
+ const port = Number(last?.[1]);
1185
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1186
+ }
1187
+ async function checkLocalReady(port, opts = {}) {
1188
+ const controller = new AbortController();
1189
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
1190
+ try {
1191
+ const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
1192
+ if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
1193
+ const data = await res.json().catch(() => ({}));
1194
+ const readyConnections = Number(data.readyConnections) || 0;
1195
+ return { healthy: readyConnections > 0, readyConnections, status: res.status };
1196
+ } catch {
1197
+ return { healthy: false, readyConnections: 0 };
1198
+ } finally {
1199
+ clearTimeout(timer);
1200
+ }
1201
+ }
1202
+ async function probeEntryState(entry, gone) {
1203
+ if (gone || entry.state !== "running" || !entry.pid) return "down";
1204
+ if (!entry.metricsPort) return "up";
1205
+ const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
1206
+ return ready.healthy ? "up" : "reconnecting";
1207
+ }
1174
1208
 
1175
1209
  // src/core/orchestrator-create.ts
1176
1210
  import { randomInt as randomInt2 } from "crypto";
@@ -1432,8 +1466,8 @@ async function resolveRemoteTarget(cf, target) {
1432
1466
  }
1433
1467
  const tunnel = matches[0];
1434
1468
  if (!tunnel) return null;
1435
- const { listCargoCnames } = await import("./dns-2F5SYQNX.js");
1436
- const { listZones: listZones3 } = await import("./zones-PCUEPXHD.js");
1469
+ const { listCargoCnames } = await import("./dns-DKPXBRCH.js");
1470
+ const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
1437
1471
  for (const zone of await listZones3(cf.token)) {
1438
1472
  const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
1439
1473
  if (rec) return { tunnel, fqdn: rec.name };
@@ -1454,24 +1488,25 @@ async function removeTunnelById(cf, tunnel, opts = {}) {
1454
1488
  async function listAll(cf, opts = {}) {
1455
1489
  const entries = await reconcile();
1456
1490
  const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
1457
- const rows = entries.map((e) => {
1491
+ const rows = await Promise.all(entries.map(async (e) => {
1458
1492
  const fqdn = entryFqdn(e);
1459
1493
  const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
1460
1494
  const svc = serviceState(fqdn);
1495
+ const state4 = await probeEntryState(e, gone);
1461
1496
  return {
1462
1497
  num: e.index ? String(e.index) : "-",
1463
1498
  url: `https://${fqdn}`,
1464
1499
  target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
1465
1500
  protocol: e.protocol ?? "auto",
1466
- state: !gone && e.state === "running" ? "up" : "down",
1501
+ state: state4,
1467
1502
  service: svc === "none" ? "-" : svc,
1468
1503
  pid: e.state === "running" && e.pid ? String(e.pid) : "-",
1469
1504
  managed: true
1470
1505
  };
1471
- });
1506
+ }));
1472
1507
  if (opts.all) {
1473
- const { listCargoCnames } = await import("./dns-2F5SYQNX.js");
1474
- const { listZones: listZones3 } = await import("./zones-PCUEPXHD.js");
1508
+ const { listCargoCnames } = await import("./dns-DKPXBRCH.js");
1509
+ const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
1475
1510
  const tracked = new Set(entries.map(entryFqdn));
1476
1511
  const unmanaged = [];
1477
1512
  for (const zone of await listZones3(cf.token)) {
@@ -1492,9 +1527,141 @@ async function listAll(cf, opts = {}) {
1492
1527
  return rows;
1493
1528
  }
1494
1529
 
1530
+ // src/core/watchdog.ts
1531
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
1532
+ var activeWatchdogs = /* @__PURE__ */ new Map();
1533
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1534
+ function resolveMetricsPort(logFile, fallback) {
1535
+ if (existsSync6(logFile)) {
1536
+ try {
1537
+ const port = extractMetricsPort(readFileSync5(logFile, "utf8"));
1538
+ if (port) return port;
1539
+ } catch {
1540
+ }
1541
+ }
1542
+ return fallback;
1543
+ }
1544
+ async function verifyRemoteTunnel(cf, tunnelId) {
1545
+ try {
1546
+ await getTunnel(cf, tunnelId);
1547
+ return true;
1548
+ } catch (err) {
1549
+ if (err instanceof CliError && err.status === 404) return false;
1550
+ return true;
1551
+ }
1552
+ }
1553
+ async function tier1Restart(ctx, state4) {
1554
+ state4.lastRestartAt = Date.now();
1555
+ state4.tier1Restarts++;
1556
+ say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
1557
+ await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
1558
+ const conn = startConnector({
1559
+ bin: ctx.bin,
1560
+ token: ctx.token,
1561
+ detach: false,
1562
+ logFile: ctx.logFile,
1563
+ protocol: ctx.protocol,
1564
+ onExit: () => {
1565
+ void onConnectorExited(ctx.fqdn);
1566
+ }
1567
+ });
1568
+ ctx.pid = conn.pid;
1569
+ await sleep3(1e3);
1570
+ ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
1571
+ await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId() });
1572
+ state4.consecutiveFailures = 0;
1573
+ }
1574
+ async function tier2Reprovision(ctx, state4) {
1575
+ state4.lastRestartAt = Date.now();
1576
+ state4.tier2Restarts++;
1577
+ say.warn(`[watchdog] Tunnel missing or unrecoverable for ${ctx.fqdn} -> re-provisioning (Tier 2)...`);
1578
+ await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
1579
+ const result = await createTunnelSubdomain(ctx.cf, { ...ctx.item, force: true, yes: true });
1580
+ ctx.tunnelId = result.tunnelId;
1581
+ ctx.token = result.token;
1582
+ const conn = startConnector({
1583
+ bin: ctx.bin,
1584
+ token: ctx.token,
1585
+ detach: false,
1586
+ logFile: ctx.logFile,
1587
+ protocol: ctx.protocol,
1588
+ onExit: () => {
1589
+ void onConnectorExited(ctx.fqdn);
1590
+ }
1591
+ });
1592
+ ctx.pid = conn.pid;
1593
+ await sleep3(1e3);
1594
+ ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
1595
+ await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId(), tunnelId: ctx.tunnelId });
1596
+ state4.consecutiveFailures = 0;
1597
+ state4.tier1Restarts = 0;
1598
+ }
1599
+ async function onConnectorExited(fqdn) {
1600
+ const item = activeWatchdogs.get(fqdn);
1601
+ if (!item) return;
1602
+ const { ctx, state: state4 } = item;
1603
+ if (Date.now() - state4.lastRestartAt < 5e3) return;
1604
+ await tier1Restart(ctx, state4);
1605
+ }
1606
+ function startTunnelWatchdog(ctx, opts = {}) {
1607
+ stopTunnelWatchdog(ctx.fqdn);
1608
+ const intervalMs = opts.intervalMs ?? 15e3;
1609
+ const cooldownMs = opts.cooldownMs ?? 5e3;
1610
+ const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0 };
1611
+ const check = async () => {
1612
+ if (!ctx.metricsPort) ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
1613
+ let healthy = false;
1614
+ if (ctx.metricsPort) {
1615
+ const res = await checkLocalReady(ctx.metricsPort);
1616
+ healthy = res.healthy;
1617
+ }
1618
+ if (healthy) {
1619
+ state4.consecutiveFailures = 0;
1620
+ return;
1621
+ }
1622
+ state4.consecutiveFailures++;
1623
+ if (state4.consecutiveFailures >= 3 && Date.now() - state4.lastRestartAt >= cooldownMs) {
1624
+ const remoteExists = await verifyRemoteTunnel(ctx.cf, ctx.tunnelId);
1625
+ if (!remoteExists || state4.tier1Restarts >= 3) {
1626
+ await tier2Reprovision(ctx, state4);
1627
+ } else {
1628
+ await tier1Restart(ctx, state4);
1629
+ }
1630
+ }
1631
+ };
1632
+ const timer = setInterval(() => {
1633
+ void check();
1634
+ }, intervalMs);
1635
+ if (typeof timer.unref === "function") timer.unref();
1636
+ activeWatchdogs.set(ctx.fqdn, { timer, ctx, state: state4 });
1637
+ }
1638
+ function stopTunnelWatchdog(fqdn) {
1639
+ const item = activeWatchdogs.get(fqdn);
1640
+ if (item) {
1641
+ clearInterval(item.timer);
1642
+ activeWatchdogs.delete(fqdn);
1643
+ }
1644
+ }
1645
+ function stopAllWatchdogs() {
1646
+ for (const [fqdn] of activeWatchdogs) stopTunnelWatchdog(fqdn);
1647
+ }
1648
+
1495
1649
  // src/core/up-runner.ts
1496
- function logFileFor(subdomain) {
1497
- return join7(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
1650
+ var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
1651
+ function logFileFor(fqdn) {
1652
+ return join7(logDir, `${serviceSlug(fqdn)}.log`);
1653
+ }
1654
+ async function createWithBootRetry(cf, item, keepAlive) {
1655
+ const deadline = Date.now() + (keepAlive ? 3e4 : 0);
1656
+ for (let attempt = 0; ; attempt++) {
1657
+ try {
1658
+ return await createTunnelSubdomain(cf, item);
1659
+ } catch (err) {
1660
+ if (!keepAlive || Date.now() >= deadline) throw err;
1661
+ say.debug(`[boot-retry] Waiting for network to create ${item.name ?? "tunnel"}...`);
1662
+ await sleep4(Math.min(2 ** attempt * 1e3, 5e3));
1663
+ }
1664
+ }
1498
1665
  }
1499
1666
  async function startTunnels(cf, bin, items, opts = {}) {
1500
1667
  const started = [];
@@ -1502,14 +1669,19 @@ async function startTunnels(cf, bin, items, opts = {}) {
1502
1669
  const teardownAll = async (code) => {
1503
1670
  if (tornDown) return;
1504
1671
  tornDown = true;
1672
+ stopAllWatchdogs();
1505
1673
  try {
1506
1674
  for (const s of started) {
1507
- try {
1508
- await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
1509
- } catch {
1675
+ if (opts.keepAlive) {
1676
+ await stopConnector({ pid: s.pid, bootId: currentBootId() });
1677
+ } else {
1678
+ try {
1679
+ await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
1680
+ } catch {
1681
+ }
1510
1682
  }
1511
1683
  }
1512
- if (process.stdout.isTTY) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
1684
+ if (process.stdout.isTTY && !opts.keepAlive) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
1513
1685
  } catch (err) {
1514
1686
  reportError(err);
1515
1687
  } finally {
@@ -1520,9 +1692,9 @@ async function startTunnels(cf, bin, items, opts = {}) {
1520
1692
  spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
1521
1693
  for (const item of items) {
1522
1694
  spin.message(`Creating ${item.name ?? "tunnel"} (:${item.port})\u2026`);
1523
- const result = await createTunnelSubdomain(cf, item);
1695
+ const result = await createWithBootRetry(cf, item, opts.keepAlive);
1524
1696
  const fqdn = result.host.hostname;
1525
- const logFile = logFileFor(result.host.subdomain);
1697
+ const logFile = logFileFor(fqdn);
1526
1698
  const conn = startConnector({
1527
1699
  bin,
1528
1700
  token: result.token,
@@ -1530,13 +1702,21 @@ async function startTunnels(cf, bin, items, opts = {}) {
1530
1702
  logFile,
1531
1703
  protocol: opts.protocol,
1532
1704
  onExit: opts.detach ? void 0 : (code) => {
1533
- if (!tornDown) {
1705
+ if (!tornDown && !opts.keepAlive) {
1534
1706
  say.warn(`Connector for ${fqdn} exited.`);
1535
1707
  void teardownAll(code ?? 1);
1536
1708
  }
1537
1709
  }
1538
1710
  });
1539
- await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile, protocol: opts.protocol });
1711
+ await sleep4(600);
1712
+ let metricsPort;
1713
+ if (existsSync7(logFile)) {
1714
+ try {
1715
+ metricsPort = extractMetricsPort(readFileSync6(logFile, "utf8"));
1716
+ } catch {
1717
+ }
1718
+ }
1719
+ await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile, metricsPort, protocol: opts.protocol });
1540
1720
  started.push({
1541
1721
  fqdn,
1542
1722
  subdomain: result.host.subdomain,
@@ -1544,6 +1724,20 @@ async function startTunnels(cf, bin, items, opts = {}) {
1544
1724
  target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
1545
1725
  pid: conn.pid
1546
1726
  });
1727
+ if (!opts.detach) {
1728
+ startTunnelWatchdog({
1729
+ cf,
1730
+ bin,
1731
+ fqdn,
1732
+ tunnelId: result.tunnelId,
1733
+ token: result.token,
1734
+ logFile,
1735
+ item,
1736
+ pid: conn.pid,
1737
+ metricsPort,
1738
+ protocol: opts.protocol
1739
+ });
1740
+ }
1547
1741
  }
1548
1742
  if (opts.detach) {
1549
1743
  spin.stop(`${started.length} tunnel(s) started in the background`);
@@ -1561,7 +1755,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
1561
1755
  spin.stop(`${started.length} tunnel(s) started`);
1562
1756
  const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
1563
1757
  clack2.note(lines.join("\n"), `${live}/${started.length} live`);
1564
- say.dim("Ctrl-C stops and releases them.");
1758
+ say.dim(opts.keepAlive ? "Service running with auto-heal watchdog." : "Ctrl-C stops and releases them.");
1565
1759
  }
1566
1760
 
1567
1761
  // src/core/resolve-domain.ts
@@ -1642,7 +1836,7 @@ async function runUp(specArgs, opts) {
1642
1836
  await registerServices(cf, items, domain, opts.proto, protocol);
1643
1837
  return;
1644
1838
  }
1645
- await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
1839
+ await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
1646
1840
  }
1647
1841
  var SERVICE_UP_TIMEOUT_MS = 2e4;
1648
1842
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -1688,7 +1882,7 @@ async function waitServicesUp(fqdns, timeoutMs) {
1688
1882
  return [...pending];
1689
1883
  }
1690
1884
  function registerUp(program) {
1691
- program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--service", "register each subdomain as a boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
1885
+ program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--keep-alive", "supervised mode with auto-heal watchdog").option("--service", "register each subdomain as a boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
1692
1886
  }
1693
1887
 
1694
1888
  // src/commands/ls.ts
@@ -1763,9 +1957,9 @@ function registerDelete(program) {
1763
1957
  }
1764
1958
 
1765
1959
  // src/commands/logs.ts
1766
- import { closeSync, existsSync as existsSync6, openSync as openSync2, readFileSync as readFileSync5, readSync, statSync, watch } from "fs";
1960
+ import { closeSync, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync7, readSync, statSync, watch } from "fs";
1767
1961
  function printTail(file, n) {
1768
- const lines = readFileSync5(file, "utf8").split("\n");
1962
+ const lines = readFileSync7(file, "utf8").split("\n");
1769
1963
  const tail = lines.slice(-n).join("\n");
1770
1964
  process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
1771
1965
  `);
@@ -1797,7 +1991,7 @@ function follow(file, fromPos) {
1797
1991
  function registerLogs(program) {
1798
1992
  program.command("logs").argument("<target>", "subdomain name / hostname / id / #").description("Show the connector log for a subdomain (use -f to follow)").option("-f, --follow", "keep printing new log lines (like tail -f)").option("-n, --lines <n>", "number of lines to show", "50").action((name, opts) => {
1799
1993
  const { fqdn, entry } = resolveTarget(name);
1800
- if (!entry?.logFile || !existsSync6(entry.logFile)) {
1994
+ if (!entry?.logFile || !existsSync8(entry.logFile)) {
1801
1995
  throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
1802
1996
  }
1803
1997
  const n = Math.max(1, Number(opts.lines) || 50);