@cliftonc/cfld 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
@@ -1155,44 +1173,45 @@ async function runCommand(flags) {
1155
1173
  }
1156
1174
  const primary = routes[0];
1157
1175
  const certPath = await ensureCert(zone, { interactive });
1158
- const result = await reconcile(
1159
- { slug, tunnelName, zone, certPath, routes, projectDir: cwd, force: flags.force },
1160
- {
1161
- onStep: (s) => step(s),
1162
- confirmOverwrite: interactive ? (h, t) => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`) : void 0
1163
- }
1164
- );
1165
- done(result.created ? `Created tunnel ${tunnelName}` : `Reusing tunnel ${tunnelName}`);
1166
- for (const r of routes) done(`DNS ${r.hostname} \u2192 :${r.port}`);
1167
- writeProjectConfig(cwd, {
1168
- name: slug,
1169
- zone,
1170
- uuid: result.uuid,
1171
- hostname: primary.hostname,
1172
- port: primary.port,
1173
- envKey
1174
- });
1175
- if (ensureGitignored(cwd, ".cfld.json")) info("Added .cfld.json to .gitignore.");
1176
- await launch(result, {
1176
+ await launch({
1177
1177
  cwd,
1178
+ slug,
1179
+ zone,
1178
1180
  envKey,
1179
1181
  noEnv: flags.noEnv,
1182
+ noUi: flags.noUi,
1183
+ url: `https://${primary.hostname}`,
1180
1184
  port: primary.port,
1181
- exec: flags.exec
1185
+ tunnelName,
1186
+ exec: flags.exec,
1187
+ runReconcile: (events) => reconcile(
1188
+ { slug, tunnelName, zone, certPath, routes, projectDir: cwd, force: flags.force },
1189
+ events
1190
+ ),
1191
+ onReconciled: (result, notify) => {
1192
+ writeProjectConfig(cwd, {
1193
+ name: slug,
1194
+ zone,
1195
+ uuid: result.uuid,
1196
+ hostname: primary.hostname,
1197
+ port: primary.port,
1198
+ envKey
1199
+ });
1200
+ if (ensureGitignored(cwd, ".cfld.json")) notify("Added .cfld.json to .gitignore.");
1201
+ }
1182
1202
  });
1183
1203
  }
1184
- async function launch(result, ctx) {
1185
- const metricsPort = await findFreePort();
1186
- const url = `https://${result.hostname}`;
1204
+ async function launch(ctx) {
1205
+ const url = ctx.url;
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
- const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
1210
+ const { startDashboard } = await import("./dashboard-GA37Z4H4.js");
1192
1211
  dashboard = startDashboard({
1193
1212
  url,
1194
1213
  target,
1195
- tunnelName: result.tunnelName,
1214
+ tunnelName: ctx.tunnelName,
1196
1215
  status: "connecting",
1197
1216
  connections: 0,
1198
1217
  requests: 0,
@@ -1202,8 +1221,10 @@ async function launch(result, ctx) {
1202
1221
  devLogs: []
1203
1222
  });
1204
1223
  } else {
1205
- step("Connecting to Cloudflare edge\u2026");
1224
+ step("Setting up your tunnel\u2026");
1206
1225
  }
1226
+ const setupStep = (s) => dashboard ? dashboard.log(s, "tunnel") : step(s);
1227
+ const setupDone = (s) => dashboard ? dashboard.log(s, "tunnel") : done(s);
1207
1228
  const emitLine = (line) => {
1208
1229
  const formatted = formatLogLine(line);
1209
1230
  if (!formatted) return;
@@ -1228,25 +1249,19 @@ async function launch(result, ctx) {
1228
1249
  dashboard?.stop();
1229
1250
  if (opts.reason) note(`
1230
1251
  ${opts.reason}`);
1231
- const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
1232
- if (child) reapChild(child).then(exit);
1233
- else exit();
1252
+ const finish = () => process.exit(opts.code ?? 0);
1253
+ if (child) reapChild(child).then(finish, finish);
1254
+ else finish();
1234
1255
  };
1235
- runner = await startRunner({
1236
- configPath: result.configPath,
1237
- tunnelName: result.tunnelName,
1238
- certPath: result.certPath,
1239
- metricsPort,
1240
- onLine: (line) => emitLine(line),
1241
- onStatus: (status) => {
1242
- if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
1243
- else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
1244
- },
1245
- onFatal: (message) => {
1246
- warn(message);
1247
- shutdown({ code: 1 });
1248
- }
1249
- });
1256
+ process.once(
1257
+ "SIGINT",
1258
+ () => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
1259
+ );
1260
+ process.once(
1261
+ "SIGTERM",
1262
+ () => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
1263
+ );
1264
+ process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
1250
1265
  if (ctx.exec) {
1251
1266
  if (!dashboard) step(`Starting \`${describeCommand(ctx.exec)}\`\u2026`);
1252
1267
  child = startChild({
@@ -1264,16 +1279,42 @@ ${opts.reason}`);
1264
1279
  }
1265
1280
  });
1266
1281
  }
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
- }
1282
+ const result = await ctx.runReconcile({
1283
+ onStep: (s) => setupStep(s),
1284
+ confirmOverwrite: interactive ? (h, t) => {
1285
+ const ask = () => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`);
1286
+ return dashboard ? dashboard.suspend(ask) : ask();
1287
+ } : void 0
1288
+ }).catch(async (err) => {
1289
+ dashboard?.stop();
1290
+ if (child) await reapChild(child);
1291
+ throw err;
1292
+ });
1293
+ if (shuttingDown) return;
1294
+ setupDone(
1295
+ result.created ? `Created tunnel ${result.tunnelName}` : `Reusing tunnel ${result.tunnelName}`
1296
+ );
1297
+ for (const r of result.routes) setupDone(`DNS ${r.hostname} \u2192 :${r.port}`);
1298
+ ctx.onReconciled?.(
1299
+ result,
1300
+ (msg) => dashboard ? dashboard.log(msg, "tunnel") : info(msg)
1301
+ );
1302
+ const metricsPort = await findFreePort();
1303
+ runner = await startRunner({
1304
+ configPath: result.configPath,
1305
+ tunnelName: result.tunnelName,
1306
+ certPath: result.certPath,
1307
+ metricsPort,
1308
+ onLine: (line) => emitLine(line),
1309
+ onStatus: (status) => {
1310
+ if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
1311
+ else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
1312
+ },
1313
+ onFatal: (message) => {
1314
+ warn(message);
1315
+ shutdown({ code: 1 });
1316
+ }
1317
+ });
1277
1318
  const ready = await waitForReady(metricsPort).catch((err) => {
1278
1319
  child?.stop();
1279
1320
  runner?.stop();
@@ -1411,7 +1452,7 @@ async function startQuickTunnel(port, opts = {}) {
1411
1452
  async function quickCommand(flags) {
1412
1453
  const port = await resolvePort2(flags);
1413
1454
  const target = `localhost:${port}`;
1414
- const interactive = isInteractive();
1455
+ const interactive = isInteractive() && !flags.noUi;
1415
1456
  const metricsPort = await findFreePort();
1416
1457
  const envKey = flags.envKey ?? "PUBLIC_URL";
1417
1458
  if (!interactive) step(`Starting an ephemeral tunnel to ${target}\u2026`);
@@ -1444,9 +1485,9 @@ async function quickCommand(flags) {
1444
1485
  dashboard?.stop();
1445
1486
  note(`
1446
1487
  ${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();
1488
+ const finish = () => process.exit(opts.code ?? 0);
1489
+ if (child) reapChild(child).then(finish, finish);
1490
+ else finish();
1450
1491
  };
1451
1492
  tunnel = await startQuickTunnel(port, { metricsPort, onLine: emitLine });
1452
1493
  const url = tunnel.url;
@@ -1468,7 +1509,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1468
1509
  });
1469
1510
  }
1470
1511
  if (interactive) {
1471
- const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
1512
+ const { startDashboard } = await import("./dashboard-GA37Z4H4.js");
1472
1513
  dashboard = startDashboard({
1473
1514
  url,
1474
1515
  target,
@@ -1484,10 +1525,9 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1484
1525
  ephemeral: true
1485
1526
  });
1486
1527
  }
1487
- if (!dashboard) {
1488
- process.once("SIGINT", () => shutdown());
1489
- process.once("SIGTERM", () => shutdown());
1490
- }
1528
+ process.once("SIGINT", () => shutdown());
1529
+ process.once("SIGTERM", () => shutdown());
1530
+ process.once("SIGHUP", () => shutdown());
1491
1531
  if (child) {
1492
1532
  if (!dashboard) step(`Waiting for your dev server on :${port}\u2026`);
1493
1533
  const up = await waitForPort(port, { signal: childAbort.signal });
@@ -1839,24 +1879,27 @@ async function upCommand(name) {
1839
1879
  }
1840
1880
  const slug = entry.name.replace(/^cfld-/, "");
1841
1881
  const routes = entry.routes?.length ? entry.routes : [{ hostname: entry.hostname, port: entry.port }];
1842
- const result = await reconcile(
1843
- {
1844
- slug,
1845
- tunnelName: entry.name,
1846
- zone: entry.zone,
1847
- certPath: cert,
1848
- routes,
1849
- projectDir: entry.projectDir
1850
- },
1851
- { onStep: (s) => step(s) }
1852
- );
1853
- done(result.created ? `Created tunnel ${entry.name}` : `Reusing tunnel ${entry.name}`);
1854
- await launch(result, {
1882
+ await launch({
1855
1883
  cwd: entry.projectDir,
1884
+ slug,
1885
+ zone: entry.zone,
1856
1886
  envKey: "PUBLIC_URL",
1857
1887
  noEnv: true,
1858
1888
  // don't touch a .env we may not be sitting in
1859
- port: entry.port
1889
+ url: `https://${entry.hostname}`,
1890
+ port: entry.port,
1891
+ tunnelName: entry.name,
1892
+ runReconcile: (events) => reconcile(
1893
+ {
1894
+ slug,
1895
+ tunnelName: entry.name,
1896
+ zone: entry.zone,
1897
+ certPath: cert,
1898
+ routes,
1899
+ projectDir: entry.projectDir
1900
+ },
1901
+ events
1902
+ )
1860
1903
  });
1861
1904
  }
1862
1905
 
@@ -2237,6 +2280,7 @@ ${pc7.bold("Options")}
2237
2280
  --zone <domain> Cloudflare domain to use (e.g. example.com)
2238
2281
  --env <KEY> .env key to write the URL to (default: PUBLIC_URL)
2239
2282
  --no-env Don't write the URL to .env
2283
+ --no-ui Plain line-by-line output instead of the live dashboard
2240
2284
  --exec <cmd> Dev command to run + supervise (shell form of \`-- <cmd>\`)
2241
2285
  --force Skip confirmation (destroy)
2242
2286
  -h, --help Show this help
@@ -2257,6 +2301,7 @@ async function main(rawArgv) {
2257
2301
  zone: { type: "string" },
2258
2302
  env: { type: "string" },
2259
2303
  "no-env": { type: "boolean" },
2304
+ "no-ui": { type: "boolean" },
2260
2305
  exec: { type: "string" },
2261
2306
  quick: { type: "boolean" },
2262
2307
  force: { type: "boolean" },
@@ -2280,6 +2325,7 @@ async function main(rawArgv) {
2280
2325
  zone: values.zone,
2281
2326
  envKey: values.env,
2282
2327
  noEnv: values["no-env"],
2328
+ noUi: values["no-ui"],
2283
2329
  quick: values.quick,
2284
2330
  force: values.force,
2285
2331
  reauth: values.reauth,
@@ -126,14 +126,18 @@ function startDashboard(initial) {
126
126
  tunnelLogs: [...initial.tunnelLogs],
127
127
  devLogs: [...initial.devLogs]
128
128
  };
129
- process.stdout.write(ENTER_ALT_SCREEN);
130
- const instance = render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
129
+ const mount = () => render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
131
130
  stdout: process.stdout,
132
131
  // We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
133
132
  // waitUntilExit, and the caller then stops the runner (preserving state).
134
133
  exitOnCtrlC: true
135
134
  });
136
- const rerender = () => instance.rerender(/* @__PURE__ */ jsx(Dashboard, { ...state }));
135
+ process.stdout.write(ENTER_ALT_SCREEN);
136
+ let instance = mount();
137
+ let paused = false;
138
+ const rerender = () => {
139
+ if (!paused) instance.rerender(/* @__PURE__ */ jsx(Dashboard, { ...state }));
140
+ };
137
141
  let altActive = true;
138
142
  const leaveAltScreen = () => {
139
143
  if (!altActive) return;
@@ -141,6 +145,7 @@ function startDashboard(initial) {
141
145
  process.stdout.write(LEAVE_ALT_SCREEN);
142
146
  };
143
147
  const onResize = () => {
148
+ if (paused) return;
144
149
  instance.clear();
145
150
  rerender();
146
151
  };
@@ -159,6 +164,20 @@ function startDashboard(initial) {
159
164
  }
160
165
  rerender();
161
166
  },
167
+ async suspend(fn) {
168
+ paused = true;
169
+ instance.unmount();
170
+ leaveAltScreen();
171
+ try {
172
+ return await fn();
173
+ } finally {
174
+ process.stdout.write(ENTER_ALT_SCREEN);
175
+ altActive = true;
176
+ instance = mount();
177
+ paused = false;
178
+ rerender();
179
+ }
180
+ },
162
181
  waitUntilExit() {
163
182
  return instance.waitUntilExit();
164
183
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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": {