@cliftonc/cfld 0.2.1 → 0.2.3

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 +2 -0
  2. package/dist/cli.js +99 -72
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # cfld
2
2
 
3
+ **`cfld` = cloudflare local dev.**
4
+
3
5
  **One command to give your localhost a permanent public HTTPS URL — no API tokens, no teardown.**
4
6
 
5
7
  `cfld` wraps [`cloudflared`](https://github.com/cloudflare/cloudflared) into a frictionless dev experience: the **same** custom URL every run (not a random `trycloudflare.com` one), authorized with a **browser login** (zero API tokens), reusing the same named tunnel and DNS record across restarts instead of churning them. It runs a **live dashboard** (URL, connection count, request/latency counters, tailing logs), **auto-reconnects** on edge blips, and **guards** against stealing a DNS record that belongs to another tunnel.
package/dist/cli.js CHANGED
@@ -637,13 +637,13 @@ async function startRunner(options) {
637
637
  );
638
638
  return;
639
639
  }
640
- const delay = BACKOFF_MS[Math.min(rapidFailures, BACKOFF_MS.length - 1)];
640
+ const delay2 = BACKOFF_MS[Math.min(rapidFailures, BACKOFF_MS.length - 1)];
641
641
  options.onStatus?.("reconnecting");
642
642
  restartTimer = setTimeout(() => {
643
643
  if (stopped) return;
644
644
  options.onStatus?.("connecting");
645
645
  spawnOnce();
646
- }, delay);
646
+ }, delay2);
647
647
  restartTimer.unref();
648
648
  });
649
649
  };
@@ -700,9 +700,22 @@ function startChild(options) {
700
700
  const exited = new Promise((resolve) => {
701
701
  markExited = resolve;
702
702
  });
703
+ const ownPgid = getpgid(process.pid);
704
+ const trackedPgids = /* @__PURE__ */ new Set();
705
+ const track = (g) => {
706
+ if (g > 1 && g !== process.pid && g !== ownPgid) trackedPgids.add(g);
707
+ };
708
+ if (child.pid !== void 0) track(child.pid);
709
+ const trackTree = () => {
710
+ if (child.pid === void 0) return;
711
+ for (const g of descendantPgids(child.pid)) track(g);
712
+ };
713
+ const poll = setInterval(trackTree, options.pollTreeMs ?? 1500);
714
+ poll.unref();
703
715
  wireLines2(child, "stdout", options.onLine);
704
716
  wireLines2(child, "stderr", options.onLine);
705
717
  child.on("error", (err) => {
718
+ clearInterval(poll);
706
719
  markExited();
707
720
  if (stopped) return;
708
721
  stopped = true;
@@ -710,93 +723,105 @@ function startChild(options) {
710
723
  options.onLine?.(String(err.message ?? err), "stderr");
711
724
  });
712
725
  child.on("exit", (code, signal) => {
726
+ clearInterval(poll);
713
727
  markExited();
714
728
  if (stopped) return;
715
729
  stopped = true;
716
730
  options.onExit?.(code, signal);
717
731
  });
732
+ const signalTree = (signal) => {
733
+ trackTree();
734
+ for (const g of trackedPgids) trySignalGroup(g, signal);
735
+ };
718
736
  return {
719
737
  exited,
720
738
  get pid() {
721
739
  return child.pid;
722
740
  },
741
+ treeAlive() {
742
+ if (trackedPgids.size === 0) return false;
743
+ const live = livePgids();
744
+ for (const g of trackedPgids) if (live.has(g)) return true;
745
+ return false;
746
+ },
723
747
  stop() {
724
- if (stopped) return;
725
748
  stopped = true;
726
- killGroup(child, "SIGINT");
727
- const t = setTimeout(() => {
728
- if (child.exitCode === null && !child.signalCode) killGroup(child, "SIGKILL");
729
- }, 4e3);
730
- t.unref();
749
+ signalTree("SIGINT");
731
750
  },
732
751
  kill() {
733
- killGroup(child, "SIGKILL");
752
+ clearInterval(poll);
753
+ signalTree("SIGKILL");
734
754
  }
735
755
  };
736
756
  }
737
- function descendantPids(pid) {
757
+ function descendantPgids(pid) {
738
758
  let out = "";
739
759
  try {
740
- out = execSync("ps -A -o pid=,ppid=", { encoding: "utf8" });
760
+ out = execSync("ps -A -o pid=,ppid=,pgid=", { encoding: "utf8" });
741
761
  } catch {
742
762
  return [];
743
763
  }
744
764
  const childrenOf = /* @__PURE__ */ new Map();
765
+ const pgidOf = /* @__PURE__ */ new Map();
745
766
  for (const line of out.split("\n")) {
746
- const m = line.trim().match(/^(\d+)\s+(\d+)$/);
767
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)$/);
747
768
  if (!m) continue;
748
- const child = Number(m[1]);
769
+ const proc = Number(m[1]);
749
770
  const parent = Number(m[2]);
771
+ pgidOf.set(proc, Number(m[3]));
750
772
  const list = childrenOf.get(parent);
751
- if (list) list.push(child);
752
- else childrenOf.set(parent, [child]);
773
+ if (list) list.push(proc);
774
+ else childrenOf.set(parent, [proc]);
753
775
  }
754
- const found = [];
776
+ const pgids = /* @__PURE__ */ new Set();
777
+ const seen = /* @__PURE__ */ new Set();
755
778
  const stack = [pid];
756
779
  while (stack.length) {
757
780
  for (const child of childrenOf.get(stack.pop()) ?? []) {
758
- if (!found.includes(child)) {
759
- found.push(child);
760
- stack.push(child);
761
- }
781
+ if (seen.has(child)) continue;
782
+ seen.add(child);
783
+ stack.push(child);
784
+ const g = pgidOf.get(child);
785
+ if (g !== void 0) pgids.add(g);
762
786
  }
763
787
  }
764
- return found;
788
+ return [...pgids];
765
789
  }
766
- function signalPids(pids, signal) {
767
- for (const pid of pids) {
768
- try {
769
- process.kill(pid, signal);
770
- } catch {
771
- }
790
+ function trySignalGroup(pgid, signal) {
791
+ try {
792
+ process.kill(-pgid, signal);
793
+ } catch {
772
794
  }
773
795
  }
774
- function reapChild(child, graceMs = 3e3) {
775
- const tree = child.pid ? descendantPids(child.pid) : [];
776
- child.stop();
777
- signalPids(tree, "SIGINT");
778
- return new Promise((resolve) => {
779
- const done2 = () => {
780
- clearTimeout(grace);
781
- child.kill();
782
- signalPids(tree, "SIGKILL");
783
- resolve();
784
- };
785
- const grace = setTimeout(done2, graceMs);
786
- grace.unref();
787
- child.exited.then(done2);
788
- });
789
- }
790
- function killGroup(child, signal) {
791
- const pid = child.pid;
792
- if (pid === void 0) return;
796
+ function getpgid(pid) {
793
797
  try {
794
- process.kill(-pid, signal);
798
+ const n = Number(execSync(`ps -o pgid= -p ${pid}`, { encoding: "utf8" }).trim());
799
+ return Number.isInteger(n) ? n : void 0;
795
800
  } catch {
796
- try {
797
- child.kill(signal);
798
- } catch {
801
+ return void 0;
802
+ }
803
+ }
804
+ function livePgids() {
805
+ const live = /* @__PURE__ */ new Set();
806
+ try {
807
+ for (const line of execSync("ps -A -o pgid=", { encoding: "utf8" }).split("\n")) {
808
+ const n = Number(line.trim());
809
+ if (Number.isInteger(n) && n > 0) live.add(n);
799
810
  }
811
+ } catch {
812
+ }
813
+ return live;
814
+ }
815
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
816
+ async function reapChild(child, graceMs = 3e3) {
817
+ child.stop();
818
+ await Promise.race([child.exited, delay(Math.min(graceMs, 1200))]);
819
+ const deadline = Date.now() + graceMs + 5e3;
820
+ for (; ; ) {
821
+ child.kill();
822
+ if (!child.treeAlive()) return;
823
+ if (Date.now() >= deadline) return;
824
+ await delay(200);
800
825
  }
801
826
  }
802
827
  function wireLines2(child, stream, onLine) {
@@ -1170,6 +1195,7 @@ async function runCommand(flags) {
1170
1195
  cwd,
1171
1196
  envKey,
1172
1197
  noEnv: flags.noEnv,
1198
+ noUi: flags.noUi,
1173
1199
  port: primary.port,
1174
1200
  exec: flags.exec
1175
1201
  });
@@ -1178,7 +1204,7 @@ async function launch(result, ctx) {
1178
1204
  const metricsPort = await findFreePort();
1179
1205
  const url = `https://${result.hostname}`;
1180
1206
  const target = `localhost:${ctx.port}`;
1181
- const interactive = isInteractive();
1207
+ const interactive = isInteractive() && !ctx.noUi;
1182
1208
  let dashboard;
1183
1209
  if (interactive) {
1184
1210
  const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
@@ -1221,9 +1247,9 @@ async function launch(result, ctx) {
1221
1247
  dashboard?.stop();
1222
1248
  if (opts.reason) note(`
1223
1249
  ${opts.reason}`);
1224
- const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
1225
- if (child) reapChild(child).then(exit);
1226
- else exit();
1250
+ const finish = () => process.exit(opts.code ?? 0);
1251
+ if (child) reapChild(child).then(finish, finish);
1252
+ else finish();
1227
1253
  };
1228
1254
  runner = await startRunner({
1229
1255
  configPath: result.configPath,
@@ -1257,16 +1283,15 @@ ${opts.reason}`);
1257
1283
  }
1258
1284
  });
1259
1285
  }
1260
- if (!dashboard) {
1261
- process.once(
1262
- "SIGINT",
1263
- () => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
1264
- );
1265
- process.once(
1266
- "SIGTERM",
1267
- () => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
1268
- );
1269
- }
1286
+ process.once(
1287
+ "SIGINT",
1288
+ () => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
1289
+ );
1290
+ process.once(
1291
+ "SIGTERM",
1292
+ () => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
1293
+ );
1294
+ process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
1270
1295
  const ready = await waitForReady(metricsPort).catch((err) => {
1271
1296
  child?.stop();
1272
1297
  runner?.stop();
@@ -1404,7 +1429,7 @@ async function startQuickTunnel(port, opts = {}) {
1404
1429
  async function quickCommand(flags) {
1405
1430
  const port = await resolvePort2(flags);
1406
1431
  const target = `localhost:${port}`;
1407
- const interactive = isInteractive();
1432
+ const interactive = isInteractive() && !flags.noUi;
1408
1433
  const metricsPort = await findFreePort();
1409
1434
  const envKey = flags.envKey ?? "PUBLIC_URL";
1410
1435
  if (!interactive) step(`Starting an ephemeral tunnel to ${target}\u2026`);
@@ -1437,9 +1462,9 @@ async function quickCommand(flags) {
1437
1462
  dashboard?.stop();
1438
1463
  note(`
1439
1464
  ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1440
- const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
1441
- if (child) reapChild(child).then(exit);
1442
- else exit();
1465
+ const finish = () => process.exit(opts.code ?? 0);
1466
+ if (child) reapChild(child).then(finish, finish);
1467
+ else finish();
1443
1468
  };
1444
1469
  tunnel = await startQuickTunnel(port, { metricsPort, onLine: emitLine });
1445
1470
  const url = tunnel.url;
@@ -1477,10 +1502,9 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1477
1502
  ephemeral: true
1478
1503
  });
1479
1504
  }
1480
- if (!dashboard) {
1481
- process.once("SIGINT", () => shutdown());
1482
- process.once("SIGTERM", () => shutdown());
1483
- }
1505
+ process.once("SIGINT", () => shutdown());
1506
+ process.once("SIGTERM", () => shutdown());
1507
+ process.once("SIGHUP", () => shutdown());
1484
1508
  if (child) {
1485
1509
  if (!dashboard) step(`Waiting for your dev server on :${port}\u2026`);
1486
1510
  const up = await waitForPort(port, { signal: childAbort.signal });
@@ -2230,6 +2254,7 @@ ${pc7.bold("Options")}
2230
2254
  --zone <domain> Cloudflare domain to use (e.g. example.com)
2231
2255
  --env <KEY> .env key to write the URL to (default: PUBLIC_URL)
2232
2256
  --no-env Don't write the URL to .env
2257
+ --no-ui Plain line-by-line output instead of the live dashboard
2233
2258
  --exec <cmd> Dev command to run + supervise (shell form of \`-- <cmd>\`)
2234
2259
  --force Skip confirmation (destroy)
2235
2260
  -h, --help Show this help
@@ -2250,6 +2275,7 @@ async function main(rawArgv) {
2250
2275
  zone: { type: "string" },
2251
2276
  env: { type: "string" },
2252
2277
  "no-env": { type: "boolean" },
2278
+ "no-ui": { type: "boolean" },
2253
2279
  exec: { type: "string" },
2254
2280
  quick: { type: "boolean" },
2255
2281
  force: { type: "boolean" },
@@ -2273,6 +2299,7 @@ async function main(rawArgv) {
2273
2299
  zone: values.zone,
2274
2300
  envKey: values.env,
2275
2301
  noEnv: values["no-env"],
2302
+ noUi: values["no-ui"],
2276
2303
  quick: values.quick,
2277
2304
  force: values.force,
2278
2305
  reauth: values.reauth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Zero-token, persistent Cloudflare tunnel CLI for local dev — same public HTTPS URL every run.",
5
5
  "type": "module",
6
6
  "bin": {