@cliftonc/cfld 0.1.1 → 0.2.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/README.md +54 -0
- package/dist/cli.js +379 -59
- package/dist/dashboard-Q6WI32AK.js +175 -0
- package/package.json +1 -1
- package/dist/dashboard-QUPY25QT.js +0 -85
package/README.md
CHANGED
|
@@ -113,6 +113,60 @@ cfld service uninstall # stop + remove (tunnel + DNS preserved)
|
|
|
113
113
|
|
|
114
114
|
Windows service management is not supported — run `cfld` in the foreground there.
|
|
115
115
|
|
|
116
|
+
## Running alongside your dev server
|
|
117
|
+
|
|
118
|
+
Give `cfld` your dev command after a `--` and it runs both as one managed unit:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cfld 3000 -- next dev
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`cfld` will:
|
|
125
|
+
|
|
126
|
+
- **boot your dev server** as a managed child, with `PUBLIC_URL` already in its environment (so it can read its own public URL for webhooks/OAuth callbacks — no `.env` round-trip needed);
|
|
127
|
+
- **wait for `:3000` to actually listen** before flipping to `● LIVE`, so the URL never serves a 502 the moment you reveal it;
|
|
128
|
+
- **stream your dev server's logs** into the same dashboard as the tunnel — one unified view;
|
|
129
|
+
- **stop both on a single Ctrl-C**, gracefully, leaving the tunnel + DNS intact so the next run reuses the same URL;
|
|
130
|
+
- **exit if your dev server exits**, propagating its exit code (the tunnel is still preserved).
|
|
131
|
+
|
|
132
|
+
The pass-through form (`-- <cmd>`) runs your command directly. If you need shell features (pipes, `&&`, globs), use the string form instead:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
cfld 5173 --exec "npm run dev"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This works with `--quick` too, when you don't have a domain set up yet:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cfld --quick 3000 -- next dev
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Wire it into `package.json` so the whole team gets the same public URL:
|
|
145
|
+
|
|
146
|
+
```jsonc
|
|
147
|
+
{
|
|
148
|
+
"scripts": {
|
|
149
|
+
"dev": "next dev",
|
|
150
|
+
"dev:tunnel": "cfld 3000 -- next dev"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **Tip:** commit a `"cfld": { "zone": "example.com", "name": "myapp" }` block to `package.json` so the URL is deterministic for the whole team — everyone's `dev:tunnel` maps to `myapp.example.com`.
|
|
156
|
+
|
|
157
|
+
<details>
|
|
158
|
+
<summary>Prefer to keep the two processes separate?</summary>
|
|
159
|
+
|
|
160
|
+
`cfld` also works fine standalone — it only needs the port, and tolerates the app not being up yet (502 until it starts, then recovers). Run them with [`concurrently`](https://www.npmjs.com/package/concurrently):
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
"dev:tunnel": "concurrently -k -n app,cfld -c blue,magenta \"npm:dev\" \"cfld 3000\""
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
or plain shell: `cfld 3000 & next dev`.
|
|
167
|
+
|
|
168
|
+
</details>
|
|
169
|
+
|
|
116
170
|
## Development
|
|
117
171
|
|
|
118
172
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
+
import { readFileSync as readFileSync7, realpathSync } from "fs";
|
|
6
|
+
import { dirname, join as join7 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
5
8
|
import pc7 from "picocolors";
|
|
6
9
|
|
|
7
10
|
// src/commands/run.ts
|
|
@@ -476,6 +479,20 @@ async function detectDevPorts() {
|
|
|
476
479
|
);
|
|
477
480
|
return checks.filter((p) => p !== null);
|
|
478
481
|
}
|
|
482
|
+
async function waitForPort(port, opts = {}) {
|
|
483
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
484
|
+
const intervalMs = opts.intervalMs ?? 300;
|
|
485
|
+
const deadline = Date.now() + timeoutMs;
|
|
486
|
+
while (Date.now() < deadline) {
|
|
487
|
+
if (opts.signal?.aborted) return false;
|
|
488
|
+
if (await isPortListening(port)) return true;
|
|
489
|
+
await sleep(intervalMs);
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
function sleep(ms) {
|
|
494
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
495
|
+
}
|
|
479
496
|
function findFreePort() {
|
|
480
497
|
return new Promise((resolve, reject) => {
|
|
481
498
|
const server = createServer();
|
|
@@ -662,6 +679,144 @@ function wireLines(child, stream, onLine) {
|
|
|
662
679
|
});
|
|
663
680
|
}
|
|
664
681
|
|
|
682
|
+
// src/process/child.ts
|
|
683
|
+
import { spawn as spawn3, execSync } from "child_process";
|
|
684
|
+
function describeCommand(cmd) {
|
|
685
|
+
return cmd.shell ? cmd.command : cmd.argv.join(" ");
|
|
686
|
+
}
|
|
687
|
+
function startChild(options) {
|
|
688
|
+
const [command, args, useShell] = options.command.shell ? [options.command.command, [], true] : [options.command.argv[0], options.command.argv.slice(1), false];
|
|
689
|
+
let stopped = false;
|
|
690
|
+
const child = spawn3(command, args, {
|
|
691
|
+
cwd: options.cwd,
|
|
692
|
+
env: { ...process.env, ...options.env },
|
|
693
|
+
// New process group so a terminal Ctrl-C doesn't race us to the child; we
|
|
694
|
+
// stop the whole group ourselves.
|
|
695
|
+
detached: true,
|
|
696
|
+
shell: useShell,
|
|
697
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
698
|
+
});
|
|
699
|
+
let markExited;
|
|
700
|
+
const exited = new Promise((resolve) => {
|
|
701
|
+
markExited = resolve;
|
|
702
|
+
});
|
|
703
|
+
wireLines2(child, "stdout", options.onLine);
|
|
704
|
+
wireLines2(child, "stderr", options.onLine);
|
|
705
|
+
child.on("error", (err) => {
|
|
706
|
+
markExited();
|
|
707
|
+
if (stopped) return;
|
|
708
|
+
stopped = true;
|
|
709
|
+
options.onExit?.(null, null);
|
|
710
|
+
options.onLine?.(String(err.message ?? err), "stderr");
|
|
711
|
+
});
|
|
712
|
+
child.on("exit", (code, signal) => {
|
|
713
|
+
markExited();
|
|
714
|
+
if (stopped) return;
|
|
715
|
+
stopped = true;
|
|
716
|
+
options.onExit?.(code, signal);
|
|
717
|
+
});
|
|
718
|
+
return {
|
|
719
|
+
exited,
|
|
720
|
+
get pid() {
|
|
721
|
+
return child.pid;
|
|
722
|
+
},
|
|
723
|
+
stop() {
|
|
724
|
+
if (stopped) return;
|
|
725
|
+
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();
|
|
731
|
+
},
|
|
732
|
+
kill() {
|
|
733
|
+
killGroup(child, "SIGKILL");
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function descendantPids(pid) {
|
|
738
|
+
let out = "";
|
|
739
|
+
try {
|
|
740
|
+
out = execSync("ps -A -o pid=,ppid=", { encoding: "utf8" });
|
|
741
|
+
} catch {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const line of out.split("\n")) {
|
|
746
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
747
|
+
if (!m) continue;
|
|
748
|
+
const child = Number(m[1]);
|
|
749
|
+
const parent = Number(m[2]);
|
|
750
|
+
const list = childrenOf.get(parent);
|
|
751
|
+
if (list) list.push(child);
|
|
752
|
+
else childrenOf.set(parent, [child]);
|
|
753
|
+
}
|
|
754
|
+
const found = [];
|
|
755
|
+
const stack = [pid];
|
|
756
|
+
while (stack.length) {
|
|
757
|
+
for (const child of childrenOf.get(stack.pop()) ?? []) {
|
|
758
|
+
if (!found.includes(child)) {
|
|
759
|
+
found.push(child);
|
|
760
|
+
stack.push(child);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return found;
|
|
765
|
+
}
|
|
766
|
+
function signalPids(pids, signal) {
|
|
767
|
+
for (const pid of pids) {
|
|
768
|
+
try {
|
|
769
|
+
process.kill(pid, signal);
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
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;
|
|
793
|
+
try {
|
|
794
|
+
process.kill(-pid, signal);
|
|
795
|
+
} catch {
|
|
796
|
+
try {
|
|
797
|
+
child.kill(signal);
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function wireLines2(child, stream, onLine) {
|
|
803
|
+
const source = stream === "stdout" ? child.stdout : child.stderr;
|
|
804
|
+
if (!source || !onLine) return;
|
|
805
|
+
let buffer = "";
|
|
806
|
+
source.setEncoding("utf8");
|
|
807
|
+
source.on("data", (chunk) => {
|
|
808
|
+
buffer += chunk;
|
|
809
|
+
let idx;
|
|
810
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
811
|
+
onLine(buffer.slice(0, idx).replace(/\r$/, ""), stream);
|
|
812
|
+
buffer = buffer.slice(idx + 1);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
source.on("end", () => {
|
|
816
|
+
if (buffer.length > 0) onLine(buffer.replace(/\r$/, ""), stream);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
665
820
|
// src/process/ready.ts
|
|
666
821
|
async function getReady(metricsPort) {
|
|
667
822
|
try {
|
|
@@ -681,7 +836,7 @@ async function waitForReady(metricsPort, opts = {}) {
|
|
|
681
836
|
if (opts.signal?.aborted) throw new Error("aborted");
|
|
682
837
|
const state = await getReady(metricsPort);
|
|
683
838
|
if (state?.ready) return state;
|
|
684
|
-
await
|
|
839
|
+
await sleep2(intervalMs);
|
|
685
840
|
}
|
|
686
841
|
throw new Error("Timed out waiting for the tunnel to connect to Cloudflare.");
|
|
687
842
|
}
|
|
@@ -715,7 +870,7 @@ function sumMetric(text, name) {
|
|
|
715
870
|
}
|
|
716
871
|
return total;
|
|
717
872
|
}
|
|
718
|
-
function
|
|
873
|
+
function sleep2(ms) {
|
|
719
874
|
return new Promise((r) => setTimeout(r, ms));
|
|
720
875
|
}
|
|
721
876
|
|
|
@@ -755,7 +910,7 @@ function colorLevel(level) {
|
|
|
755
910
|
}
|
|
756
911
|
|
|
757
912
|
// src/commands/login.ts
|
|
758
|
-
import { spawn as
|
|
913
|
+
import { spawn as spawn4 } from "child_process";
|
|
759
914
|
import { existsSync as existsSync8 } from "fs";
|
|
760
915
|
|
|
761
916
|
// src/ui/prompts.ts
|
|
@@ -883,7 +1038,7 @@ async function runLogin() {
|
|
|
883
1038
|
step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
|
|
884
1039
|
note(" Pick the domain you want to use. Waiting for you to finish\u2026");
|
|
885
1040
|
await new Promise((resolve, reject) => {
|
|
886
|
-
const child =
|
|
1041
|
+
const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
|
|
887
1042
|
child.on("error", reject);
|
|
888
1043
|
child.on("exit", (code) => {
|
|
889
1044
|
if (code === 0 || existsSync8(defaultCertPath())) resolve();
|
|
@@ -966,6 +1121,12 @@ async function runCommand(flags) {
|
|
|
966
1121
|
const cwd = process.cwd();
|
|
967
1122
|
const interactive = isInteractive();
|
|
968
1123
|
const config = readProjectConfig(cwd);
|
|
1124
|
+
if (flags.exec && flags.port === void 0 && config?.port === void 0 && !config?.routes?.length) {
|
|
1125
|
+
throw new CfldError(
|
|
1126
|
+
"Specify the port your dev command will listen on.",
|
|
1127
|
+
"e.g. `cfld 3000 -- next dev`"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
969
1130
|
const slug = deriveSlug(cwd, flags.name ?? config?.name);
|
|
970
1131
|
const tunnelName = tunnelNameForSlug(slug);
|
|
971
1132
|
const knownZones = Array.from(
|
|
@@ -1005,7 +1166,13 @@ async function runCommand(flags) {
|
|
|
1005
1166
|
envKey
|
|
1006
1167
|
});
|
|
1007
1168
|
if (ensureGitignored(cwd, ".cfld.json")) info("Added .cfld.json to .gitignore.");
|
|
1008
|
-
await launch(result, {
|
|
1169
|
+
await launch(result, {
|
|
1170
|
+
cwd,
|
|
1171
|
+
envKey,
|
|
1172
|
+
noEnv: flags.noEnv,
|
|
1173
|
+
port: primary.port,
|
|
1174
|
+
exec: flags.exec
|
|
1175
|
+
});
|
|
1009
1176
|
}
|
|
1010
1177
|
async function launch(result, ctx) {
|
|
1011
1178
|
const metricsPort = await findFreePort();
|
|
@@ -1014,7 +1181,7 @@ async function launch(result, ctx) {
|
|
|
1014
1181
|
const interactive = isInteractive();
|
|
1015
1182
|
let dashboard;
|
|
1016
1183
|
if (interactive) {
|
|
1017
|
-
const { startDashboard } = await import("./dashboard-
|
|
1184
|
+
const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
|
|
1018
1185
|
dashboard = startDashboard({
|
|
1019
1186
|
url,
|
|
1020
1187
|
target,
|
|
@@ -1022,7 +1189,10 @@ async function launch(result, ctx) {
|
|
|
1022
1189
|
status: "connecting",
|
|
1023
1190
|
connections: 0,
|
|
1024
1191
|
requests: 0,
|
|
1025
|
-
|
|
1192
|
+
split: Boolean(ctx.exec),
|
|
1193
|
+
devLabel: ctx.exec ? describeCommand(ctx.exec) : void 0,
|
|
1194
|
+
tunnelLogs: [],
|
|
1195
|
+
devLogs: []
|
|
1026
1196
|
});
|
|
1027
1197
|
} else {
|
|
1028
1198
|
step("Connecting to Cloudflare edge\u2026");
|
|
@@ -1030,11 +1200,32 @@ async function launch(result, ctx) {
|
|
|
1030
1200
|
const emitLine = (line) => {
|
|
1031
1201
|
const formatted = formatLogLine(line);
|
|
1032
1202
|
if (!formatted) return;
|
|
1033
|
-
if (dashboard) dashboard.log(formatted);
|
|
1203
|
+
if (dashboard) dashboard.log(formatted, "tunnel");
|
|
1034
1204
|
else process.stderr.write(formatted + "\n");
|
|
1035
1205
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1206
|
+
const emitDevLine = (line) => {
|
|
1207
|
+
if (line.trim() === "") return;
|
|
1208
|
+
if (dashboard) dashboard.log(line, "dev");
|
|
1209
|
+
else process.stderr.write(line + "\n");
|
|
1210
|
+
};
|
|
1211
|
+
let runner;
|
|
1212
|
+
let child;
|
|
1213
|
+
let poll;
|
|
1214
|
+
let shuttingDown = false;
|
|
1215
|
+
const childAbort = new AbortController();
|
|
1216
|
+
const shutdown = (opts = {}) => {
|
|
1217
|
+
if (shuttingDown) return;
|
|
1218
|
+
shuttingDown = true;
|
|
1219
|
+
if (poll) clearInterval(poll);
|
|
1220
|
+
runner?.stop();
|
|
1221
|
+
dashboard?.stop();
|
|
1222
|
+
if (opts.reason) note(`
|
|
1223
|
+
${opts.reason}`);
|
|
1224
|
+
const exit = () => setTimeout(() => process.exit(opts.code ?? 0), 100).unref();
|
|
1225
|
+
if (child) reapChild(child).then(exit);
|
|
1226
|
+
else exit();
|
|
1227
|
+
};
|
|
1228
|
+
runner = await startRunner({
|
|
1038
1229
|
configPath: result.configPath,
|
|
1039
1230
|
tunnelName: result.tunnelName,
|
|
1040
1231
|
certPath: result.certPath,
|
|
@@ -1045,32 +1236,53 @@ async function launch(result, ctx) {
|
|
|
1045
1236
|
else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
|
|
1046
1237
|
},
|
|
1047
1238
|
onFatal: (message) => {
|
|
1048
|
-
dashboard?.stop();
|
|
1049
1239
|
warn(message);
|
|
1050
|
-
|
|
1240
|
+
shutdown({ code: 1 });
|
|
1051
1241
|
}
|
|
1052
1242
|
});
|
|
1053
|
-
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1243
|
+
if (ctx.exec) {
|
|
1244
|
+
if (!dashboard) step(`Starting \`${describeCommand(ctx.exec)}\`\u2026`);
|
|
1245
|
+
child = startChild({
|
|
1246
|
+
command: ctx.exec,
|
|
1247
|
+
cwd: ctx.cwd,
|
|
1248
|
+
env: ctx.noEnv ? void 0 : { [ctx.envKey]: url },
|
|
1249
|
+
onLine: (line) => emitDevLine(line),
|
|
1250
|
+
onExit: (code, signal) => {
|
|
1251
|
+
childAbort.abort();
|
|
1252
|
+
const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
|
|
1253
|
+
shutdown({
|
|
1254
|
+
reason: `Your dev command ${how} \u2014 stopping. Your tunnel URL is preserved.`,
|
|
1255
|
+
code: code ?? 1
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1062
1260
|
if (!dashboard) {
|
|
1063
|
-
process.once(
|
|
1064
|
-
|
|
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
|
+
);
|
|
1065
1269
|
}
|
|
1066
1270
|
const ready = await waitForReady(metricsPort).catch((err) => {
|
|
1067
|
-
|
|
1271
|
+
child?.stop();
|
|
1272
|
+
runner?.stop();
|
|
1068
1273
|
dashboard?.stop();
|
|
1069
1274
|
throw new CfldError(
|
|
1070
1275
|
"The tunnel did not connect.",
|
|
1071
1276
|
err instanceof Error ? err.message : void 0
|
|
1072
1277
|
);
|
|
1073
1278
|
});
|
|
1279
|
+
if (child && !shuttingDown) {
|
|
1280
|
+
if (dashboard) dashboard.update({ status: "waiting", connections: ready.connections });
|
|
1281
|
+
else step(`Waiting for your dev server on :${ctx.port}\u2026`);
|
|
1282
|
+
const up = await waitForPort(ctx.port, { signal: childAbort.signal });
|
|
1283
|
+
if (shuttingDown) return;
|
|
1284
|
+
if (!up) warn(`Nothing is listening on :${ctx.port} yet \u2014 going live anyway.`);
|
|
1285
|
+
}
|
|
1074
1286
|
if (!ctx.noEnv) {
|
|
1075
1287
|
const { changed } = upsertEnv(join5(ctx.cwd, ".env"), ctx.envKey, url);
|
|
1076
1288
|
if (changed && !dashboard) info(`Wrote ${ctx.envKey}=${url} to .env`);
|
|
@@ -1080,7 +1292,7 @@ ${reason}`);
|
|
|
1080
1292
|
} else {
|
|
1081
1293
|
summaryBox({ url, target, tunnelName: result.tunnelName, connections: ready.connections });
|
|
1082
1294
|
}
|
|
1083
|
-
|
|
1295
|
+
poll = setInterval(async () => {
|
|
1084
1296
|
const m = await scrapeMetrics(metricsPort);
|
|
1085
1297
|
if (m && dashboard) {
|
|
1086
1298
|
dashboard.update({
|
|
@@ -1093,8 +1305,7 @@ ${reason}`);
|
|
|
1093
1305
|
poll.unref();
|
|
1094
1306
|
if (dashboard) {
|
|
1095
1307
|
await dashboard.waitUntilExit();
|
|
1096
|
-
|
|
1097
|
-
stop("Stopping \u2014 your URL is preserved. Re-run `cfld` to resume.");
|
|
1308
|
+
shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld` to resume." });
|
|
1098
1309
|
}
|
|
1099
1310
|
}
|
|
1100
1311
|
async function resolveZone(flags, configZone, knownZones, interactive) {
|
|
@@ -1142,13 +1353,13 @@ async function resolvePort(flags, configPort, interactive) {
|
|
|
1142
1353
|
}
|
|
1143
1354
|
|
|
1144
1355
|
// src/process/quick.ts
|
|
1145
|
-
import { spawn as
|
|
1356
|
+
import { spawn as spawn5 } from "child_process";
|
|
1146
1357
|
var QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
1147
1358
|
async function startQuickTunnel(port, opts = {}) {
|
|
1148
1359
|
const bin = await resolveBinary();
|
|
1149
1360
|
const args = ["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"];
|
|
1150
1361
|
if (opts.metricsPort) args.push("--metrics", `127.0.0.1:${opts.metricsPort}`);
|
|
1151
|
-
const child =
|
|
1362
|
+
const child = spawn5(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1152
1363
|
return new Promise((resolve, reject) => {
|
|
1153
1364
|
const timeout = setTimeout(() => {
|
|
1154
1365
|
child.kill("SIGKILL");
|
|
@@ -1195,39 +1406,98 @@ async function quickCommand(flags) {
|
|
|
1195
1406
|
const target = `localhost:${port}`;
|
|
1196
1407
|
const interactive = isInteractive();
|
|
1197
1408
|
const metricsPort = await findFreePort();
|
|
1409
|
+
const envKey = flags.envKey ?? "PUBLIC_URL";
|
|
1198
1410
|
if (!interactive) step(`Starting an ephemeral tunnel to ${target}\u2026`);
|
|
1199
1411
|
let dashboard;
|
|
1200
|
-
|
|
1412
|
+
let tunnel;
|
|
1413
|
+
let child;
|
|
1414
|
+
let poll;
|
|
1415
|
+
let shuttingDown = false;
|
|
1416
|
+
const childAbort = new AbortController();
|
|
1417
|
+
const tunnelBuffer = [];
|
|
1418
|
+
const devBuffer = [];
|
|
1201
1419
|
const emitLine = (line) => {
|
|
1202
1420
|
const formatted = formatLogLine(line);
|
|
1203
1421
|
if (!formatted) return;
|
|
1204
|
-
|
|
1205
|
-
if (dashboard) dashboard.log(formatted);
|
|
1422
|
+
tunnelBuffer.push(formatted);
|
|
1423
|
+
if (dashboard) dashboard.log(formatted, "tunnel");
|
|
1206
1424
|
else if (!interactive) process.stderr.write(formatted + "\n");
|
|
1207
1425
|
};
|
|
1208
|
-
const
|
|
1426
|
+
const emitDevLine = (line) => {
|
|
1427
|
+
if (line.trim() === "") return;
|
|
1428
|
+
devBuffer.push(line);
|
|
1429
|
+
if (dashboard) dashboard.log(line, "dev");
|
|
1430
|
+
else if (!interactive) process.stderr.write(line + "\n");
|
|
1431
|
+
};
|
|
1432
|
+
const shutdown = (opts = {}) => {
|
|
1433
|
+
if (shuttingDown) return;
|
|
1434
|
+
shuttingDown = true;
|
|
1435
|
+
if (poll) clearInterval(poll);
|
|
1436
|
+
tunnel?.stop();
|
|
1437
|
+
dashboard?.stop();
|
|
1438
|
+
note(`
|
|
1439
|
+
${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();
|
|
1443
|
+
};
|
|
1444
|
+
tunnel = await startQuickTunnel(port, { metricsPort, onLine: emitLine });
|
|
1445
|
+
const url = tunnel.url;
|
|
1446
|
+
if (flags.exec) {
|
|
1447
|
+
if (!interactive) step(`Starting \`${describeCommand(flags.exec)}\`\u2026`);
|
|
1448
|
+
child = startChild({
|
|
1449
|
+
command: flags.exec,
|
|
1450
|
+
cwd: process.cwd(),
|
|
1451
|
+
env: flags.noEnv ? void 0 : { [envKey]: url },
|
|
1452
|
+
onLine: (line) => emitDevLine(line),
|
|
1453
|
+
onExit: (code, signal) => {
|
|
1454
|
+
childAbort.abort();
|
|
1455
|
+
const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
|
|
1456
|
+
shutdown({
|
|
1457
|
+
reason: `Your dev command ${how} \u2014 stopping the ephemeral tunnel.`,
|
|
1458
|
+
code: code ?? 1
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1209
1463
|
if (interactive) {
|
|
1210
|
-
const { startDashboard } = await import("./dashboard-
|
|
1464
|
+
const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
|
|
1211
1465
|
dashboard = startDashboard({
|
|
1212
|
-
url
|
|
1466
|
+
url,
|
|
1213
1467
|
target,
|
|
1214
1468
|
tunnelName: "quick (ephemeral)",
|
|
1215
|
-
|
|
1216
|
-
|
|
1469
|
+
// Hold at WAITING until the dev server opens the port (if we're running one).
|
|
1470
|
+
status: child ? "waiting" : "live",
|
|
1471
|
+
connections: child ? 0 : 1,
|
|
1217
1472
|
requests: 0,
|
|
1218
|
-
|
|
1473
|
+
split: Boolean(flags.exec),
|
|
1474
|
+
devLabel: flags.exec ? describeCommand(flags.exec) : void 0,
|
|
1475
|
+
tunnelLogs: tunnelBuffer.slice(-200),
|
|
1476
|
+
devLogs: devBuffer.slice(-200),
|
|
1219
1477
|
ephemeral: true
|
|
1220
1478
|
});
|
|
1221
|
-
}
|
|
1479
|
+
}
|
|
1480
|
+
if (!dashboard) {
|
|
1481
|
+
process.once("SIGINT", () => shutdown());
|
|
1482
|
+
process.once("SIGTERM", () => shutdown());
|
|
1483
|
+
}
|
|
1484
|
+
if (child) {
|
|
1485
|
+
if (!dashboard) step(`Waiting for your dev server on :${port}\u2026`);
|
|
1486
|
+
const up = await waitForPort(port, { signal: childAbort.signal });
|
|
1487
|
+
if (shuttingDown) return;
|
|
1488
|
+
if (!up) warn(`Nothing is listening on :${port} yet \u2014 going live anyway.`);
|
|
1489
|
+
dashboard?.update({ status: "live", connections: 1 });
|
|
1490
|
+
}
|
|
1491
|
+
if (!dashboard) {
|
|
1222
1492
|
summaryBox({
|
|
1223
|
-
url
|
|
1493
|
+
url,
|
|
1224
1494
|
target,
|
|
1225
1495
|
tunnelName: "quick (ephemeral)",
|
|
1226
1496
|
connections: 1,
|
|
1227
1497
|
ephemeral: true
|
|
1228
1498
|
});
|
|
1229
1499
|
}
|
|
1230
|
-
|
|
1500
|
+
poll = setInterval(async () => {
|
|
1231
1501
|
const m = await scrapeMetrics(metricsPort);
|
|
1232
1502
|
if (m && dashboard) {
|
|
1233
1503
|
dashboard.update({
|
|
@@ -1238,23 +1508,19 @@ async function quickCommand(flags) {
|
|
|
1238
1508
|
}
|
|
1239
1509
|
}, dashboard ? 1e3 : 15e3);
|
|
1240
1510
|
poll.unref();
|
|
1241
|
-
const stop = () => {
|
|
1242
|
-
clearInterval(poll);
|
|
1243
|
-
tunnel.stop();
|
|
1244
|
-
dashboard?.stop();
|
|
1245
|
-
note("\nStopped ephemeral tunnel.");
|
|
1246
|
-
setTimeout(() => process.exit(0), 100).unref();
|
|
1247
|
-
};
|
|
1248
1511
|
if (dashboard) {
|
|
1249
1512
|
await dashboard.waitUntilExit();
|
|
1250
|
-
|
|
1251
|
-
} else {
|
|
1252
|
-
process.once("SIGINT", stop);
|
|
1253
|
-
process.once("SIGTERM", stop);
|
|
1513
|
+
shutdown();
|
|
1254
1514
|
}
|
|
1255
1515
|
}
|
|
1256
1516
|
async function resolvePort2(flags) {
|
|
1257
1517
|
if (flags.port) return flags.port;
|
|
1518
|
+
if (flags.exec) {
|
|
1519
|
+
throw new CfldError(
|
|
1520
|
+
"Specify the port your dev command will listen on.",
|
|
1521
|
+
"e.g. `cfld --quick 3000 -- next dev`"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1258
1524
|
const detected = await detectDevPorts();
|
|
1259
1525
|
if (detected.length >= 1) {
|
|
1260
1526
|
info(`Detected a local server on port ${detected[0]}.`);
|
|
@@ -1770,6 +2036,30 @@ async function doctorCommand() {
|
|
|
1770
2036
|
}
|
|
1771
2037
|
|
|
1772
2038
|
// src/cli.ts
|
|
2039
|
+
function readVersion() {
|
|
2040
|
+
try {
|
|
2041
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2042
|
+
const pkg = JSON.parse(readFileSync7(join7(here, "../package.json"), "utf8"));
|
|
2043
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
2044
|
+
} catch {
|
|
2045
|
+
return "0.0.0";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function splitAtDoubleDash(rawArgv) {
|
|
2049
|
+
const i = rawArgv.indexOf("--");
|
|
2050
|
+
if (i === -1) return { argv: rawArgv, afterDash: [] };
|
|
2051
|
+
return { argv: rawArgv.slice(0, i), afterDash: rawArgv.slice(i + 1) };
|
|
2052
|
+
}
|
|
2053
|
+
function resolveExec(afterDash, execFlag) {
|
|
2054
|
+
const hasArgv = afterDash.length > 0;
|
|
2055
|
+
const hasShell = execFlag !== void 0 && execFlag.trim() !== "";
|
|
2056
|
+
if (hasArgv && hasShell) {
|
|
2057
|
+
throw new Error('Use either `-- <cmd>` or `--exec "<cmd>"`, not both.');
|
|
2058
|
+
}
|
|
2059
|
+
if (hasArgv) return { shell: false, argv: afterDash };
|
|
2060
|
+
if (hasShell) return { shell: true, command: execFlag.trim() };
|
|
2061
|
+
return void 0;
|
|
2062
|
+
}
|
|
1773
2063
|
var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1774
2064
|
"login",
|
|
1775
2065
|
"init",
|
|
@@ -1785,6 +2075,7 @@ var HELP = `${pc7.bold("cfld")} \u2014 persistent Cloudflare tunnels for local d
|
|
|
1785
2075
|
|
|
1786
2076
|
${pc7.bold("Usage")}
|
|
1787
2077
|
cfld [port] Ensure + run a persistent tunnel (reuses the same URL)
|
|
2078
|
+
cfld [port] -- <cmd> Run + supervise your dev server too (one lifecycle)
|
|
1788
2079
|
cfld --quick [port] Ephemeral trycloudflare.com URL (no domain needed)
|
|
1789
2080
|
cfld login [--reauth] Authorize with Cloudflare in your browser
|
|
1790
2081
|
cfld init Interactive setup wizard (configure, don't run)
|
|
@@ -1801,11 +2092,17 @@ ${pc7.bold("Options")}
|
|
|
1801
2092
|
--zone <domain> Cloudflare domain to use (e.g. example.com)
|
|
1802
2093
|
--env <KEY> .env key to write the URL to (default: PUBLIC_URL)
|
|
1803
2094
|
--no-env Don't write the URL to .env
|
|
2095
|
+
--exec <cmd> Dev command to run + supervise (shell form of \`-- <cmd>\`)
|
|
1804
2096
|
--force Skip confirmation (destroy)
|
|
1805
2097
|
-h, --help Show this help
|
|
1806
2098
|
-v, --version Show version
|
|
2099
|
+
|
|
2100
|
+
${pc7.bold("Examples")}
|
|
2101
|
+
cfld 3000 -- next dev Start Next.js + the tunnel; one Ctrl-C stops both
|
|
2102
|
+
cfld 5173 --exec "npm run dev" Same, via a shell command string
|
|
1807
2103
|
`;
|
|
1808
|
-
async function main(
|
|
2104
|
+
async function main(rawArgv) {
|
|
2105
|
+
const { argv, afterDash } = splitAtDoubleDash(rawArgv);
|
|
1809
2106
|
const { values, positionals } = parseArgs({
|
|
1810
2107
|
args: argv,
|
|
1811
2108
|
allowPositionals: true,
|
|
@@ -1815,6 +2112,7 @@ async function main(argv) {
|
|
|
1815
2112
|
zone: { type: "string" },
|
|
1816
2113
|
env: { type: "string" },
|
|
1817
2114
|
"no-env": { type: "boolean" },
|
|
2115
|
+
exec: { type: "string" },
|
|
1818
2116
|
quick: { type: "boolean" },
|
|
1819
2117
|
force: { type: "boolean" },
|
|
1820
2118
|
reauth: { type: "boolean" },
|
|
@@ -1823,7 +2121,8 @@ async function main(argv) {
|
|
|
1823
2121
|
}
|
|
1824
2122
|
});
|
|
1825
2123
|
if (values.version) {
|
|
1826
|
-
process.stdout.write(
|
|
2124
|
+
process.stdout.write(`cfld ${readVersion()}
|
|
2125
|
+
`);
|
|
1827
2126
|
return;
|
|
1828
2127
|
}
|
|
1829
2128
|
if (values.help) {
|
|
@@ -1838,9 +2137,15 @@ async function main(argv) {
|
|
|
1838
2137
|
noEnv: values["no-env"],
|
|
1839
2138
|
quick: values.quick,
|
|
1840
2139
|
force: values.force,
|
|
1841
|
-
reauth: values.reauth
|
|
2140
|
+
reauth: values.reauth,
|
|
2141
|
+
exec: resolveExec(afterDash, values.exec)
|
|
1842
2142
|
};
|
|
1843
2143
|
const [first, ...rest] = positionals;
|
|
2144
|
+
if (flags.exec && first && SUBCOMMANDS.has(first)) {
|
|
2145
|
+
throw new Error(
|
|
2146
|
+
`A dev command (\`-- \u2026\` / --exec) can't be combined with \`cfld ${first}\`. Use \`cfld [port] -- <cmd>\`.`
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
1844
2149
|
if (first && SUBCOMMANDS.has(first)) {
|
|
1845
2150
|
switch (first) {
|
|
1846
2151
|
case "help":
|
|
@@ -1884,7 +2189,22 @@ async function main(argv) {
|
|
|
1884
2189
|
await runCommand(flags);
|
|
1885
2190
|
}
|
|
1886
2191
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2192
|
+
function invokedDirectly() {
|
|
2193
|
+
const entry = process.argv[1];
|
|
2194
|
+
if (!entry) return false;
|
|
2195
|
+
try {
|
|
2196
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
2197
|
+
} catch {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (invokedDirectly()) {
|
|
2202
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
2203
|
+
printError(err);
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
export {
|
|
2208
|
+
resolveExec,
|
|
2209
|
+
splitAtDoubleDash
|
|
2210
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/ui/dashboard.tsx
|
|
4
|
+
import "react";
|
|
5
|
+
import { Box, render, Text } from "ink";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var MAX_BUFFER = 1e3;
|
|
8
|
+
var HEADER_ROWS = 5;
|
|
9
|
+
function statusDot(status) {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case "live":
|
|
12
|
+
return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF LIVE" });
|
|
13
|
+
case "waiting":
|
|
14
|
+
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CC WAITING" });
|
|
15
|
+
case "reconnecting":
|
|
16
|
+
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CC RECONNECTING" });
|
|
17
|
+
default:
|
|
18
|
+
return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CC CONNECTING" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function Header(state) {
|
|
22
|
+
const latency = state.avgLatencyMs !== void 0 ? ` \u26A1 ${state.avgLatencyMs}ms avg` : "";
|
|
23
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
24
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
25
|
+
statusDot(state.status),
|
|
26
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
27
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: state.url })
|
|
28
|
+
] }),
|
|
29
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
30
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
31
|
+
"\u2192 ",
|
|
32
|
+
state.target
|
|
33
|
+
] }),
|
|
34
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
35
|
+
" ",
|
|
36
|
+
state.connections,
|
|
37
|
+
" edge conns"
|
|
38
|
+
] })
|
|
39
|
+
] }),
|
|
40
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
41
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
42
|
+
"\u2191 ",
|
|
43
|
+
state.requests,
|
|
44
|
+
" reqs",
|
|
45
|
+
latency
|
|
46
|
+
] }),
|
|
47
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
48
|
+
" ",
|
|
49
|
+
state.ephemeral ? "ephemeral \xB7 Ctrl-C stops" : "Ctrl-C stops \xB7 URL preserved"
|
|
50
|
+
] })
|
|
51
|
+
] })
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
function LogColumn({
|
|
55
|
+
title,
|
|
56
|
+
titleColor,
|
|
57
|
+
lines,
|
|
58
|
+
rows,
|
|
59
|
+
placeholder,
|
|
60
|
+
divider
|
|
61
|
+
}) {
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
Box,
|
|
64
|
+
{
|
|
65
|
+
flexDirection: "column",
|
|
66
|
+
flexGrow: 1,
|
|
67
|
+
flexShrink: 1,
|
|
68
|
+
flexBasis: 0,
|
|
69
|
+
paddingLeft: divider ? 1 : 0,
|
|
70
|
+
borderStyle: divider ? "single" : void 0,
|
|
71
|
+
borderColor: "gray",
|
|
72
|
+
borderTop: false,
|
|
73
|
+
borderBottom: false,
|
|
74
|
+
borderRight: false,
|
|
75
|
+
borderLeft: Boolean(divider),
|
|
76
|
+
children: [
|
|
77
|
+
title ? /* @__PURE__ */ jsx(Text, { color: titleColor, bold: true, wrap: "truncate-end", children: title }) : null,
|
|
78
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-end", overflow: "hidden", children: lines.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: line }, i)) })
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function Dashboard(state) {
|
|
84
|
+
const termRows = process.stdout.rows || 24;
|
|
85
|
+
const titleRows = state.split ? 1 : 0;
|
|
86
|
+
const logRows = Math.max(3, termRows - HEADER_ROWS - titleRows - 1);
|
|
87
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [
|
|
88
|
+
/* @__PURE__ */ jsx(Header, { ...state }),
|
|
89
|
+
state.split ? /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "row", paddingX: 1, paddingTop: 1, children: [
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
LogColumn,
|
|
92
|
+
{
|
|
93
|
+
title: state.devLabel ?? "dev server",
|
|
94
|
+
titleColor: "cyan",
|
|
95
|
+
lines: state.devLogs,
|
|
96
|
+
rows: logRows,
|
|
97
|
+
placeholder: "waiting for dev-server output\u2026"
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
LogColumn,
|
|
102
|
+
{
|
|
103
|
+
title: "tunnel",
|
|
104
|
+
titleColor: "blue",
|
|
105
|
+
lines: state.tunnelLogs,
|
|
106
|
+
rows: logRows,
|
|
107
|
+
placeholder: "waiting for tunnel activity\u2026",
|
|
108
|
+
divider: true
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
] }) : /* @__PURE__ */ jsx(Box, { flexGrow: 1, paddingX: 1, paddingTop: 1, children: /* @__PURE__ */ jsx(
|
|
112
|
+
LogColumn,
|
|
113
|
+
{
|
|
114
|
+
lines: state.tunnelLogs,
|
|
115
|
+
rows: logRows,
|
|
116
|
+
placeholder: state.ephemeral ? "waiting for traffic\u2026 (Ctrl-C stops; this URL is temporary)" : "waiting for traffic\u2026 (Ctrl-C stops; your URL is preserved)"
|
|
117
|
+
}
|
|
118
|
+
) })
|
|
119
|
+
] });
|
|
120
|
+
}
|
|
121
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
122
|
+
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
123
|
+
function startDashboard(initial) {
|
|
124
|
+
const state = {
|
|
125
|
+
...initial,
|
|
126
|
+
tunnelLogs: [...initial.tunnelLogs],
|
|
127
|
+
devLogs: [...initial.devLogs]
|
|
128
|
+
};
|
|
129
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
130
|
+
const instance = render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
|
|
131
|
+
stdout: process.stdout,
|
|
132
|
+
// We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
|
|
133
|
+
// waitUntilExit, and the caller then stops the runner (preserving state).
|
|
134
|
+
exitOnCtrlC: true
|
|
135
|
+
});
|
|
136
|
+
const rerender = () => instance.rerender(/* @__PURE__ */ jsx(Dashboard, { ...state }));
|
|
137
|
+
let altActive = true;
|
|
138
|
+
const leaveAltScreen = () => {
|
|
139
|
+
if (!altActive) return;
|
|
140
|
+
altActive = false;
|
|
141
|
+
process.stdout.write(LEAVE_ALT_SCREEN);
|
|
142
|
+
};
|
|
143
|
+
const onResize = () => {
|
|
144
|
+
instance.clear();
|
|
145
|
+
rerender();
|
|
146
|
+
};
|
|
147
|
+
process.stdout.on("resize", onResize);
|
|
148
|
+
process.once("exit", leaveAltScreen);
|
|
149
|
+
return {
|
|
150
|
+
update(partial) {
|
|
151
|
+
Object.assign(state, partial);
|
|
152
|
+
rerender();
|
|
153
|
+
},
|
|
154
|
+
log(line, source = "tunnel") {
|
|
155
|
+
if (source === "dev") {
|
|
156
|
+
state.devLogs = [...state.devLogs, line].slice(-MAX_BUFFER);
|
|
157
|
+
} else {
|
|
158
|
+
state.tunnelLogs = [...state.tunnelLogs, line].slice(-MAX_BUFFER);
|
|
159
|
+
}
|
|
160
|
+
rerender();
|
|
161
|
+
},
|
|
162
|
+
waitUntilExit() {
|
|
163
|
+
return instance.waitUntilExit();
|
|
164
|
+
},
|
|
165
|
+
stop() {
|
|
166
|
+
process.stdout.off("resize", onResize);
|
|
167
|
+
process.removeListener("exit", leaveAltScreen);
|
|
168
|
+
instance.unmount();
|
|
169
|
+
leaveAltScreen();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
startDashboard
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/ui/dashboard.tsx
|
|
4
|
-
import "react";
|
|
5
|
-
import { Box, render, Text } from "ink";
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
var MAX_LOGS = 12;
|
|
8
|
-
function statusDot(status) {
|
|
9
|
-
switch (status) {
|
|
10
|
-
case "live":
|
|
11
|
-
return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF LIVE" });
|
|
12
|
-
case "reconnecting":
|
|
13
|
-
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CC RECONNECTING" });
|
|
14
|
-
default:
|
|
15
|
-
return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CC CONNECTING" });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function Dashboard(state) {
|
|
19
|
-
const latency = state.avgLatencyMs !== void 0 ? ` \u26A1 ${state.avgLatencyMs}ms avg` : "";
|
|
20
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
21
|
-
/* @__PURE__ */ jsxs(
|
|
22
|
-
Box,
|
|
23
|
-
{
|
|
24
|
-
flexDirection: "column",
|
|
25
|
-
borderStyle: "round",
|
|
26
|
-
borderColor: "cyan",
|
|
27
|
-
paddingX: 1,
|
|
28
|
-
children: [
|
|
29
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
30
|
-
statusDot(state.status),
|
|
31
|
-
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
32
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: state.url })
|
|
33
|
-
] }),
|
|
34
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
35
|
-
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
36
|
-
"\u2192 ",
|
|
37
|
-
state.target
|
|
38
|
-
] }),
|
|
39
|
-
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
40
|
-
" ",
|
|
41
|
-
state.connections,
|
|
42
|
-
" edge conns"
|
|
43
|
-
] })
|
|
44
|
-
] }),
|
|
45
|
-
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
46
|
-
"\u2191 ",
|
|
47
|
-
state.requests,
|
|
48
|
-
" reqs",
|
|
49
|
-
latency
|
|
50
|
-
] }) })
|
|
51
|
-
]
|
|
52
|
-
}
|
|
53
|
-
),
|
|
54
|
-
/* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: state.logs.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.ephemeral ? "waiting for traffic\u2026 (Ctrl-C stops; this URL is temporary)" : "waiting for traffic\u2026 (Ctrl-C stops; your URL is preserved)" }) : state.logs.map((line, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: line }, i)) })
|
|
55
|
-
] });
|
|
56
|
-
}
|
|
57
|
-
function startDashboard(initial) {
|
|
58
|
-
const state = { ...initial, logs: [...initial.logs] };
|
|
59
|
-
const instance = render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
|
|
60
|
-
stdout: process.stdout,
|
|
61
|
-
// We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
|
|
62
|
-
// waitUntilExit, and the caller then stops the runner (preserving state).
|
|
63
|
-
exitOnCtrlC: true
|
|
64
|
-
});
|
|
65
|
-
const rerender = () => instance.rerender(/* @__PURE__ */ jsx(Dashboard, { ...state }));
|
|
66
|
-
return {
|
|
67
|
-
update(partial) {
|
|
68
|
-
Object.assign(state, partial);
|
|
69
|
-
rerender();
|
|
70
|
-
},
|
|
71
|
-
log(line) {
|
|
72
|
-
state.logs = [...state.logs, line].slice(-MAX_LOGS);
|
|
73
|
-
rerender();
|
|
74
|
-
},
|
|
75
|
-
waitUntilExit() {
|
|
76
|
-
return instance.waitUntilExit();
|
|
77
|
-
},
|
|
78
|
-
stop() {
|
|
79
|
-
instance.unmount();
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
export {
|
|
84
|
-
startDashboard
|
|
85
|
-
};
|