@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.
- package/README.md +2 -0
- package/dist/cli.js +99 -72
- 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
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
752
|
+
clearInterval(poll);
|
|
753
|
+
signalTree("SIGKILL");
|
|
734
754
|
}
|
|
735
755
|
};
|
|
736
756
|
}
|
|
737
|
-
function
|
|
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
|
|
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(
|
|
752
|
-
else childrenOf.set(parent, [
|
|
773
|
+
if (list) list.push(proc);
|
|
774
|
+
else childrenOf.set(parent, [proc]);
|
|
753
775
|
}
|
|
754
|
-
const
|
|
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 (
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
788
|
+
return [...pgids];
|
|
765
789
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
790
|
+
function trySignalGroup(pgid, signal) {
|
|
791
|
+
try {
|
|
792
|
+
process.kill(-pgid, signal);
|
|
793
|
+
} catch {
|
|
772
794
|
}
|
|
773
795
|
}
|
|
774
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
1225
|
-
if (child) reapChild(child).then(
|
|
1226
|
-
else
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
|
1441
|
-
if (child) reapChild(child).then(
|
|
1442
|
-
else
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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,
|