@cliftonc/cfld 0.2.2 → 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 (2) hide show
  1. package/dist/cli.js +68 -48
  2. package/package.json +1 -1
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,13 +700,15 @@ function startChild(options) {
700
700
  const exited = new Promise((resolve) => {
701
701
  markExited = resolve;
702
702
  });
703
+ const ownPgid = getpgid(process.pid);
703
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);
704
709
  const trackTree = () => {
705
- const pid = child.pid;
706
- if (pid === void 0) return;
707
- for (const g of descendantPgids(pid)) {
708
- if (g > 1 && g !== process.pid) trackedPgids.add(g);
709
- }
710
+ if (child.pid === void 0) return;
711
+ for (const g of descendantPgids(child.pid)) track(g);
710
712
  };
711
713
  const poll = setInterval(trackTree, options.pollTreeMs ?? 1500);
712
714
  poll.unref();
@@ -729,8 +731,6 @@ function startChild(options) {
729
731
  });
730
732
  const signalTree = (signal) => {
731
733
  trackTree();
732
- const pid = child.pid;
733
- if (pid !== void 0) trySignalGroup(pid, signal);
734
734
  for (const g of trackedPgids) trySignalGroup(g, signal);
735
735
  };
736
736
  return {
@@ -738,15 +738,15 @@ function startChild(options) {
738
738
  get pid() {
739
739
  return child.pid;
740
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
+ },
741
747
  stop() {
742
- if (stopped) return;
743
748
  stopped = true;
744
- clearInterval(poll);
745
749
  signalTree("SIGINT");
746
- const t = setTimeout(() => {
747
- if (child.exitCode === null && !child.signalCode) signalTree("SIGKILL");
748
- }, 4e3);
749
- t.unref();
750
750
  },
751
751
  kill() {
752
752
  clearInterval(poll);
@@ -793,18 +793,36 @@ function trySignalGroup(pgid, signal) {
793
793
  } catch {
794
794
  }
795
795
  }
796
- function reapChild(child, graceMs = 3e3) {
796
+ function getpgid(pid) {
797
+ try {
798
+ const n = Number(execSync(`ps -o pgid= -p ${pid}`, { encoding: "utf8" }).trim());
799
+ return Number.isInteger(n) ? n : void 0;
800
+ } 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);
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) {
797
817
  child.stop();
798
- return new Promise((resolve) => {
799
- const done2 = () => {
800
- clearTimeout(grace);
801
- child.kill();
802
- resolve();
803
- };
804
- const grace = setTimeout(done2, graceMs);
805
- grace.unref();
806
- child.exited.then(done2);
807
- });
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);
825
+ }
808
826
  }
809
827
  function wireLines2(child, stream, onLine) {
810
828
  const source = stream === "stdout" ? child.stdout : child.stderr;
@@ -1177,6 +1195,7 @@ async function runCommand(flags) {
1177
1195
  cwd,
1178
1196
  envKey,
1179
1197
  noEnv: flags.noEnv,
1198
+ noUi: flags.noUi,
1180
1199
  port: primary.port,
1181
1200
  exec: flags.exec
1182
1201
  });
@@ -1185,7 +1204,7 @@ async function launch(result, ctx) {
1185
1204
  const metricsPort = await findFreePort();
1186
1205
  const url = `https://${result.hostname}`;
1187
1206
  const target = `localhost:${ctx.port}`;
1188
- const interactive = isInteractive();
1207
+ const interactive = isInteractive() && !ctx.noUi;
1189
1208
  let dashboard;
1190
1209
  if (interactive) {
1191
1210
  const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
@@ -1228,9 +1247,9 @@ async function launch(result, ctx) {
1228
1247
  dashboard?.stop();
1229
1248
  if (opts.reason) note(`
1230
1249
  ${opts.reason}`);
1231
- const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
1232
- if (child) reapChild(child).then(exit);
1233
- else exit();
1250
+ const finish = () => process.exit(opts.code ?? 0);
1251
+ if (child) reapChild(child).then(finish, finish);
1252
+ else finish();
1234
1253
  };
1235
1254
  runner = await startRunner({
1236
1255
  configPath: result.configPath,
@@ -1264,16 +1283,15 @@ ${opts.reason}`);
1264
1283
  }
1265
1284
  });
1266
1285
  }
1267
- if (!dashboard) {
1268
- process.once(
1269
- "SIGINT",
1270
- () => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
1271
- );
1272
- process.once(
1273
- "SIGTERM",
1274
- () => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
1275
- );
1276
- }
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." }));
1277
1295
  const ready = await waitForReady(metricsPort).catch((err) => {
1278
1296
  child?.stop();
1279
1297
  runner?.stop();
@@ -1411,7 +1429,7 @@ async function startQuickTunnel(port, opts = {}) {
1411
1429
  async function quickCommand(flags) {
1412
1430
  const port = await resolvePort2(flags);
1413
1431
  const target = `localhost:${port}`;
1414
- const interactive = isInteractive();
1432
+ const interactive = isInteractive() && !flags.noUi;
1415
1433
  const metricsPort = await findFreePort();
1416
1434
  const envKey = flags.envKey ?? "PUBLIC_URL";
1417
1435
  if (!interactive) step(`Starting an ephemeral tunnel to ${target}\u2026`);
@@ -1444,9 +1462,9 @@ async function quickCommand(flags) {
1444
1462
  dashboard?.stop();
1445
1463
  note(`
1446
1464
  ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1447
- const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
1448
- if (child) reapChild(child).then(exit);
1449
- else exit();
1465
+ const finish = () => process.exit(opts.code ?? 0);
1466
+ if (child) reapChild(child).then(finish, finish);
1467
+ else finish();
1450
1468
  };
1451
1469
  tunnel = await startQuickTunnel(port, { metricsPort, onLine: emitLine });
1452
1470
  const url = tunnel.url;
@@ -1484,10 +1502,9 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1484
1502
  ephemeral: true
1485
1503
  });
1486
1504
  }
1487
- if (!dashboard) {
1488
- process.once("SIGINT", () => shutdown());
1489
- process.once("SIGTERM", () => shutdown());
1490
- }
1505
+ process.once("SIGINT", () => shutdown());
1506
+ process.once("SIGTERM", () => shutdown());
1507
+ process.once("SIGHUP", () => shutdown());
1491
1508
  if (child) {
1492
1509
  if (!dashboard) step(`Waiting for your dev server on :${port}\u2026`);
1493
1510
  const up = await waitForPort(port, { signal: childAbort.signal });
@@ -2237,6 +2254,7 @@ ${pc7.bold("Options")}
2237
2254
  --zone <domain> Cloudflare domain to use (e.g. example.com)
2238
2255
  --env <KEY> .env key to write the URL to (default: PUBLIC_URL)
2239
2256
  --no-env Don't write the URL to .env
2257
+ --no-ui Plain line-by-line output instead of the live dashboard
2240
2258
  --exec <cmd> Dev command to run + supervise (shell form of \`-- <cmd>\`)
2241
2259
  --force Skip confirmation (destroy)
2242
2260
  -h, --help Show this help
@@ -2257,6 +2275,7 @@ async function main(rawArgv) {
2257
2275
  zone: { type: "string" },
2258
2276
  env: { type: "string" },
2259
2277
  "no-env": { type: "boolean" },
2278
+ "no-ui": { type: "boolean" },
2260
2279
  exec: { type: "string" },
2261
2280
  quick: { type: "boolean" },
2262
2281
  force: { type: "boolean" },
@@ -2280,6 +2299,7 @@ async function main(rawArgv) {
2280
2299
  zone: values.zone,
2281
2300
  envKey: values.env,
2282
2301
  noEnv: values["no-env"],
2302
+ noUi: values["no-ui"],
2283
2303
  quick: values.quick,
2284
2304
  force: values.force,
2285
2305
  reauth: values.reauth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.2",
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": {