@cliftonc/cfld 0.2.3 → 0.2.5
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 +148 -78
- package/dist/dashboard-ZJVAPOTE.js +272 -0
- package/package.json +1 -1
- package/dist/dashboard-Q6WI32AK.js +0 -175
package/dist/cli.js
CHANGED
|
@@ -1173,56 +1173,50 @@ async function runCommand(flags) {
|
|
|
1173
1173
|
}
|
|
1174
1174
|
const primary = routes[0];
|
|
1175
1175
|
const certPath = await ensureCert(zone, { interactive });
|
|
1176
|
-
|
|
1177
|
-
{ slug, tunnelName, zone, certPath, routes, projectDir: cwd, force: flags.force },
|
|
1178
|
-
{
|
|
1179
|
-
onStep: (s) => step(s),
|
|
1180
|
-
confirmOverwrite: interactive ? (h, t) => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`) : void 0
|
|
1181
|
-
}
|
|
1182
|
-
);
|
|
1183
|
-
done(result.created ? `Created tunnel ${tunnelName}` : `Reusing tunnel ${tunnelName}`);
|
|
1184
|
-
for (const r of routes) done(`DNS ${r.hostname} \u2192 :${r.port}`);
|
|
1185
|
-
writeProjectConfig(cwd, {
|
|
1186
|
-
name: slug,
|
|
1187
|
-
zone,
|
|
1188
|
-
uuid: result.uuid,
|
|
1189
|
-
hostname: primary.hostname,
|
|
1190
|
-
port: primary.port,
|
|
1191
|
-
envKey
|
|
1192
|
-
});
|
|
1193
|
-
if (ensureGitignored(cwd, ".cfld.json")) info("Added .cfld.json to .gitignore.");
|
|
1194
|
-
await launch(result, {
|
|
1176
|
+
await launch({
|
|
1195
1177
|
cwd,
|
|
1178
|
+
slug,
|
|
1179
|
+
zone,
|
|
1196
1180
|
envKey,
|
|
1197
1181
|
noEnv: flags.noEnv,
|
|
1198
1182
|
noUi: flags.noUi,
|
|
1183
|
+
url: `https://${primary.hostname}`,
|
|
1199
1184
|
port: primary.port,
|
|
1200
|
-
|
|
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
|
+
}
|
|
1201
1202
|
});
|
|
1202
1203
|
}
|
|
1203
|
-
async function launch(
|
|
1204
|
-
const
|
|
1205
|
-
const url = `https://${result.hostname}`;
|
|
1204
|
+
async function launch(ctx) {
|
|
1205
|
+
const url = ctx.url;
|
|
1206
1206
|
const target = `localhost:${ctx.port}`;
|
|
1207
1207
|
const interactive = isInteractive() && !ctx.noUi;
|
|
1208
1208
|
let dashboard;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
tunnelLogs: [],
|
|
1221
|
-
devLogs: []
|
|
1222
|
-
});
|
|
1223
|
-
} else {
|
|
1224
|
-
step("Connecting to Cloudflare edge\u2026");
|
|
1225
|
-
}
|
|
1209
|
+
let runner;
|
|
1210
|
+
let child;
|
|
1211
|
+
let result;
|
|
1212
|
+
let poll;
|
|
1213
|
+
let metricsPort = 0;
|
|
1214
|
+
let shuttingDown = false;
|
|
1215
|
+
let started = false;
|
|
1216
|
+
let busy = false;
|
|
1217
|
+
const childAbort = new AbortController();
|
|
1218
|
+
const setupStep = (s) => dashboard ? dashboard.log(s, "tunnel") : step(s);
|
|
1219
|
+
const setupDone = (s) => dashboard ? dashboard.log(s, "tunnel") : done(s);
|
|
1226
1220
|
const emitLine = (line) => {
|
|
1227
1221
|
const formatted = formatLogLine(line);
|
|
1228
1222
|
if (!formatted) return;
|
|
@@ -1234,11 +1228,6 @@ async function launch(result, ctx) {
|
|
|
1234
1228
|
if (dashboard) dashboard.log(line, "dev");
|
|
1235
1229
|
else process.stderr.write(line + "\n");
|
|
1236
1230
|
};
|
|
1237
|
-
let runner;
|
|
1238
|
-
let child;
|
|
1239
|
-
let poll;
|
|
1240
|
-
let shuttingDown = false;
|
|
1241
|
-
const childAbort = new AbortController();
|
|
1242
1231
|
const shutdown = (opts = {}) => {
|
|
1243
1232
|
if (shuttingDown) return;
|
|
1244
1233
|
shuttingDown = true;
|
|
@@ -1251,11 +1240,11 @@ ${opts.reason}`);
|
|
|
1251
1240
|
if (child) reapChild(child).then(finish, finish);
|
|
1252
1241
|
else finish();
|
|
1253
1242
|
};
|
|
1254
|
-
|
|
1243
|
+
const startTunnel = (port) => startRunner({
|
|
1255
1244
|
configPath: result.configPath,
|
|
1256
1245
|
tunnelName: result.tunnelName,
|
|
1257
1246
|
certPath: result.certPath,
|
|
1258
|
-
metricsPort,
|
|
1247
|
+
metricsPort: port,
|
|
1259
1248
|
onLine: (line) => emitLine(line),
|
|
1260
1249
|
onStatus: (status) => {
|
|
1261
1250
|
if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
|
|
@@ -1266,23 +1255,50 @@ ${opts.reason}`);
|
|
|
1266
1255
|
shutdown({ code: 1 });
|
|
1267
1256
|
}
|
|
1268
1257
|
});
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1258
|
+
const startDevServer = () => startChild({
|
|
1259
|
+
command: ctx.exec,
|
|
1260
|
+
cwd: ctx.cwd,
|
|
1261
|
+
env: ctx.noEnv ? void 0 : { [ctx.envKey]: url },
|
|
1262
|
+
onLine: (line) => emitDevLine(line),
|
|
1263
|
+
onExit: (code, signal) => {
|
|
1264
|
+
childAbort.abort();
|
|
1265
|
+
const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
|
|
1266
|
+
shutdown({
|
|
1267
|
+
reason: `Your dev command ${how} \u2014 stopping. Your tunnel URL is preserved.`,
|
|
1268
|
+
code: code ?? 1
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
const doRestartDev = async () => {
|
|
1273
|
+
if (!ctx.exec || !child) return;
|
|
1274
|
+
dashboard?.log("\u21BB restarting dev server\u2026", "dev");
|
|
1275
|
+
await reapChild(child);
|
|
1276
|
+
if (shuttingDown) return;
|
|
1277
|
+
child = startDevServer();
|
|
1278
|
+
};
|
|
1279
|
+
const doRestartTunnel = async () => {
|
|
1280
|
+
if (!runner || !result) return;
|
|
1281
|
+
dashboard?.log("\u21BB restarting tunnel\u2026", "tunnel");
|
|
1282
|
+
dashboard?.update({ status: "reconnecting" });
|
|
1283
|
+
runner.stop();
|
|
1284
|
+
metricsPort = await findFreePort();
|
|
1285
|
+
runner = await startTunnel(metricsPort);
|
|
1286
|
+
const r = await waitForReady(metricsPort).catch(() => void 0);
|
|
1287
|
+
if (r && !shuttingDown) dashboard?.update({ status: "live", connections: r.connections });
|
|
1288
|
+
};
|
|
1289
|
+
const guard = (fn) => () => {
|
|
1290
|
+
if (busy || shuttingDown || !started) return;
|
|
1291
|
+
busy = true;
|
|
1292
|
+
void Promise.resolve(fn()).finally(() => {
|
|
1293
|
+
busy = false;
|
|
1284
1294
|
});
|
|
1285
|
-
}
|
|
1295
|
+
};
|
|
1296
|
+
const restartDev = guard(doRestartDev);
|
|
1297
|
+
const restartTunnel = guard(doRestartTunnel);
|
|
1298
|
+
const restartAll = guard(async () => {
|
|
1299
|
+
await doRestartTunnel();
|
|
1300
|
+
await doRestartDev();
|
|
1301
|
+
});
|
|
1286
1302
|
process.once(
|
|
1287
1303
|
"SIGINT",
|
|
1288
1304
|
() => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
|
|
@@ -1292,6 +1308,56 @@ ${opts.reason}`);
|
|
|
1292
1308
|
() => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
|
|
1293
1309
|
);
|
|
1294
1310
|
process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
|
|
1311
|
+
if (interactive) {
|
|
1312
|
+
const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
|
|
1313
|
+
dashboard = startDashboard(
|
|
1314
|
+
{
|
|
1315
|
+
url,
|
|
1316
|
+
target,
|
|
1317
|
+
tunnelName: ctx.tunnelName,
|
|
1318
|
+
status: "connecting",
|
|
1319
|
+
connections: 0,
|
|
1320
|
+
requests: 0,
|
|
1321
|
+
split: Boolean(ctx.exec),
|
|
1322
|
+
devLabel: ctx.exec ? describeCommand(ctx.exec) : void 0,
|
|
1323
|
+
tunnelLogs: [],
|
|
1324
|
+
devLogs: []
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
restartAll,
|
|
1328
|
+
restartTunnel,
|
|
1329
|
+
restartDev: ctx.exec ? restartDev : void 0
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
} else {
|
|
1333
|
+
step("Setting up your tunnel\u2026");
|
|
1334
|
+
}
|
|
1335
|
+
if (ctx.exec) {
|
|
1336
|
+
if (!dashboard) step(`Starting \`${describeCommand(ctx.exec)}\`\u2026`);
|
|
1337
|
+
child = startDevServer();
|
|
1338
|
+
}
|
|
1339
|
+
result = await ctx.runReconcile({
|
|
1340
|
+
onStep: (s) => setupStep(s),
|
|
1341
|
+
confirmOverwrite: interactive ? (h, t) => {
|
|
1342
|
+
const ask = () => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`);
|
|
1343
|
+
return dashboard ? dashboard.suspend(ask) : ask();
|
|
1344
|
+
} : void 0
|
|
1345
|
+
}).catch(async (err) => {
|
|
1346
|
+
dashboard?.stop();
|
|
1347
|
+
if (child) await reapChild(child);
|
|
1348
|
+
throw err;
|
|
1349
|
+
});
|
|
1350
|
+
if (shuttingDown) return;
|
|
1351
|
+
setupDone(
|
|
1352
|
+
result.created ? `Created tunnel ${result.tunnelName}` : `Reusing tunnel ${result.tunnelName}`
|
|
1353
|
+
);
|
|
1354
|
+
for (const r of result.routes) setupDone(`DNS ${r.hostname} \u2192 :${r.port}`);
|
|
1355
|
+
ctx.onReconciled?.(
|
|
1356
|
+
result,
|
|
1357
|
+
(msg) => dashboard ? dashboard.log(msg, "tunnel") : info(msg)
|
|
1358
|
+
);
|
|
1359
|
+
metricsPort = await findFreePort();
|
|
1360
|
+
runner = await startTunnel(metricsPort);
|
|
1295
1361
|
const ready = await waitForReady(metricsPort).catch((err) => {
|
|
1296
1362
|
child?.stop();
|
|
1297
1363
|
runner?.stop();
|
|
@@ -1301,6 +1367,7 @@ ${opts.reason}`);
|
|
|
1301
1367
|
err instanceof Error ? err.message : void 0
|
|
1302
1368
|
);
|
|
1303
1369
|
});
|
|
1370
|
+
started = true;
|
|
1304
1371
|
if (child && !shuttingDown) {
|
|
1305
1372
|
if (dashboard) dashboard.update({ status: "waiting", connections: ready.connections });
|
|
1306
1373
|
else step(`Waiting for your dev server on :${ctx.port}\u2026`);
|
|
@@ -1486,7 +1553,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
|
|
|
1486
1553
|
});
|
|
1487
1554
|
}
|
|
1488
1555
|
if (interactive) {
|
|
1489
|
-
const { startDashboard } = await import("./dashboard-
|
|
1556
|
+
const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
|
|
1490
1557
|
dashboard = startDashboard({
|
|
1491
1558
|
url,
|
|
1492
1559
|
target,
|
|
@@ -1856,24 +1923,27 @@ async function upCommand(name) {
|
|
|
1856
1923
|
}
|
|
1857
1924
|
const slug = entry.name.replace(/^cfld-/, "");
|
|
1858
1925
|
const routes = entry.routes?.length ? entry.routes : [{ hostname: entry.hostname, port: entry.port }];
|
|
1859
|
-
|
|
1860
|
-
{
|
|
1861
|
-
slug,
|
|
1862
|
-
tunnelName: entry.name,
|
|
1863
|
-
zone: entry.zone,
|
|
1864
|
-
certPath: cert,
|
|
1865
|
-
routes,
|
|
1866
|
-
projectDir: entry.projectDir
|
|
1867
|
-
},
|
|
1868
|
-
{ onStep: (s) => step(s) }
|
|
1869
|
-
);
|
|
1870
|
-
done(result.created ? `Created tunnel ${entry.name}` : `Reusing tunnel ${entry.name}`);
|
|
1871
|
-
await launch(result, {
|
|
1926
|
+
await launch({
|
|
1872
1927
|
cwd: entry.projectDir,
|
|
1928
|
+
slug,
|
|
1929
|
+
zone: entry.zone,
|
|
1873
1930
|
envKey: "PUBLIC_URL",
|
|
1874
1931
|
noEnv: true,
|
|
1875
1932
|
// don't touch a .env we may not be sitting in
|
|
1876
|
-
|
|
1933
|
+
url: `https://${entry.hostname}`,
|
|
1934
|
+
port: entry.port,
|
|
1935
|
+
tunnelName: entry.name,
|
|
1936
|
+
runReconcile: (events) => reconcile(
|
|
1937
|
+
{
|
|
1938
|
+
slug,
|
|
1939
|
+
tunnelName: entry.name,
|
|
1940
|
+
zone: entry.zone,
|
|
1941
|
+
certPath: cert,
|
|
1942
|
+
routes,
|
|
1943
|
+
projectDir: entry.projectDir
|
|
1944
|
+
},
|
|
1945
|
+
events
|
|
1946
|
+
)
|
|
1877
1947
|
});
|
|
1878
1948
|
}
|
|
1879
1949
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/ui/dashboard.tsx
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import "react";
|
|
6
|
+
import { Box, render, Text, useApp, useInput } from "ink";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var VIEW_ORDER = ["dev", "split", "tunnel"];
|
|
9
|
+
var MAX_BUFFER = 1e3;
|
|
10
|
+
var HEADER_ROWS = 5;
|
|
11
|
+
function statusDot(status) {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case "live":
|
|
14
|
+
return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF LIVE" });
|
|
15
|
+
case "waiting":
|
|
16
|
+
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CC WAITING" });
|
|
17
|
+
case "reconnecting":
|
|
18
|
+
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CC RECONNECTING" });
|
|
19
|
+
default:
|
|
20
|
+
return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CC CONNECTING" });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function hotkeyHints(state) {
|
|
24
|
+
const dev = Boolean(state.split);
|
|
25
|
+
const row1 = state.canRestart ? "r restart all" : "";
|
|
26
|
+
const row2 = state.canRestart ? dev ? "d restart dev \xB7 t restart tunnel" : "t restart tunnel" : "";
|
|
27
|
+
const row3 = [dev ? "\u2039 \u203A cols" : "", "o open", "^C quit"].filter(Boolean).join(" \xB7 ");
|
|
28
|
+
return [row1, row2, row3];
|
|
29
|
+
}
|
|
30
|
+
function Header(state) {
|
|
31
|
+
const latency = state.avgLatencyMs !== void 0 ? ` \u26A1 ${state.avgLatencyMs}ms avg` : "";
|
|
32
|
+
const [h1, h2, h3] = hotkeyHints(state);
|
|
33
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
34
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
35
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
36
|
+
statusDot(state.status),
|
|
37
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
38
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: state.url })
|
|
39
|
+
] }),
|
|
40
|
+
h1 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
41
|
+
" ",
|
|
42
|
+
h1
|
|
43
|
+
] }) : null
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
46
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
47
|
+
"\u2192 ",
|
|
48
|
+
state.target,
|
|
49
|
+
" ",
|
|
50
|
+
state.connections,
|
|
51
|
+
" edge conns",
|
|
52
|
+
state.ephemeral ? " \xB7 ephemeral" : ""
|
|
53
|
+
] }),
|
|
54
|
+
h2 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
55
|
+
" ",
|
|
56
|
+
h2
|
|
57
|
+
] }) : null
|
|
58
|
+
] }),
|
|
59
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
60
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
61
|
+
"\u2191 ",
|
|
62
|
+
state.requests,
|
|
63
|
+
" reqs",
|
|
64
|
+
latency
|
|
65
|
+
] }),
|
|
66
|
+
h3 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
67
|
+
" ",
|
|
68
|
+
h3
|
|
69
|
+
] }) : null
|
|
70
|
+
] })
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
function LogColumn({
|
|
74
|
+
title,
|
|
75
|
+
titleColor,
|
|
76
|
+
lines,
|
|
77
|
+
rows,
|
|
78
|
+
placeholder,
|
|
79
|
+
divider
|
|
80
|
+
}) {
|
|
81
|
+
return /* @__PURE__ */ jsxs(
|
|
82
|
+
Box,
|
|
83
|
+
{
|
|
84
|
+
flexDirection: "column",
|
|
85
|
+
flexGrow: 1,
|
|
86
|
+
flexShrink: 1,
|
|
87
|
+
flexBasis: 0,
|
|
88
|
+
paddingLeft: divider ? 1 : 0,
|
|
89
|
+
borderStyle: divider ? "single" : void 0,
|
|
90
|
+
borderColor: "gray",
|
|
91
|
+
borderTop: false,
|
|
92
|
+
borderBottom: false,
|
|
93
|
+
borderRight: false,
|
|
94
|
+
borderLeft: Boolean(divider),
|
|
95
|
+
children: [
|
|
96
|
+
title ? /* @__PURE__ */ jsx(Text, { color: titleColor, bold: true, wrap: "truncate-end", children: title }) : null,
|
|
97
|
+
/* @__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)) })
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function Dashboard(state) {
|
|
103
|
+
const termRows = process.stdout.rows || 24;
|
|
104
|
+
const view = state.split ? state.view ?? "split" : "tunnel";
|
|
105
|
+
const titleRows = state.split ? 1 : 0;
|
|
106
|
+
const logRows = Math.max(3, termRows - HEADER_ROWS - titleRows - 1);
|
|
107
|
+
const devColumn = (divider) => /* @__PURE__ */ jsx(
|
|
108
|
+
LogColumn,
|
|
109
|
+
{
|
|
110
|
+
title: state.devLabel ?? "dev server",
|
|
111
|
+
titleColor: "cyan",
|
|
112
|
+
lines: state.devLogs,
|
|
113
|
+
rows: logRows,
|
|
114
|
+
placeholder: "waiting for dev-server output\u2026",
|
|
115
|
+
divider
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
const tunnelColumn = (divider) => /* @__PURE__ */ jsx(
|
|
119
|
+
LogColumn,
|
|
120
|
+
{
|
|
121
|
+
title: state.split ? "tunnel" : void 0,
|
|
122
|
+
titleColor: "blue",
|
|
123
|
+
lines: state.tunnelLogs,
|
|
124
|
+
rows: logRows,
|
|
125
|
+
placeholder: state.split ? "waiting for tunnel activity\u2026" : state.ephemeral ? "waiting for traffic\u2026 (Ctrl-C stops; this URL is temporary)" : "waiting for traffic\u2026 (Ctrl-C stops; your URL is preserved)",
|
|
126
|
+
divider
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
let body;
|
|
130
|
+
if (view === "split") {
|
|
131
|
+
body = /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "row", paddingX: 1, paddingTop: 1, children: [
|
|
132
|
+
devColumn(),
|
|
133
|
+
tunnelColumn(true)
|
|
134
|
+
] });
|
|
135
|
+
} else {
|
|
136
|
+
body = /* @__PURE__ */ jsx(Box, { flexGrow: 1, paddingX: 1, paddingTop: 1, children: view === "dev" ? devColumn() : tunnelColumn() });
|
|
137
|
+
}
|
|
138
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [
|
|
139
|
+
/* @__PURE__ */ jsx(Header, { ...state }),
|
|
140
|
+
body
|
|
141
|
+
] });
|
|
142
|
+
}
|
|
143
|
+
function App({
|
|
144
|
+
state,
|
|
145
|
+
actions,
|
|
146
|
+
onCycleView,
|
|
147
|
+
onOpen
|
|
148
|
+
}) {
|
|
149
|
+
const { exit } = useApp();
|
|
150
|
+
useInput((input, key) => {
|
|
151
|
+
if (key.leftArrow || input === "<" || input === ",") return onCycleView(-1);
|
|
152
|
+
if (key.rightArrow || input === ">" || input === ".") return onCycleView(1);
|
|
153
|
+
switch (input.toLowerCase()) {
|
|
154
|
+
case "r":
|
|
155
|
+
return actions.restartAll?.();
|
|
156
|
+
case "d":
|
|
157
|
+
return actions.restartDev?.();
|
|
158
|
+
case "t":
|
|
159
|
+
return actions.restartTunnel?.();
|
|
160
|
+
case "o":
|
|
161
|
+
return onOpen();
|
|
162
|
+
case "q":
|
|
163
|
+
return exit();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return /* @__PURE__ */ jsx(Dashboard, { ...state });
|
|
167
|
+
}
|
|
168
|
+
function openInBrowser(url) {
|
|
169
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
170
|
+
try {
|
|
171
|
+
spawn(cmd, [url], {
|
|
172
|
+
stdio: "ignore",
|
|
173
|
+
detached: true,
|
|
174
|
+
shell: process.platform === "win32"
|
|
175
|
+
}).unref();
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
180
|
+
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
181
|
+
function startDashboard(initial, actions = {}) {
|
|
182
|
+
const state = {
|
|
183
|
+
...initial,
|
|
184
|
+
view: initial.view ?? (initial.split ? "split" : "tunnel"),
|
|
185
|
+
canRestart: Boolean(actions.restartAll || actions.restartTunnel),
|
|
186
|
+
tunnelLogs: [...initial.tunnelLogs],
|
|
187
|
+
devLogs: [...initial.devLogs]
|
|
188
|
+
};
|
|
189
|
+
const cycleView = (delta) => {
|
|
190
|
+
if (!state.split) return;
|
|
191
|
+
const i = VIEW_ORDER.indexOf(state.view ?? "split");
|
|
192
|
+
const next = VIEW_ORDER[Math.min(VIEW_ORDER.length - 1, Math.max(0, i + delta))];
|
|
193
|
+
if (next !== state.view) {
|
|
194
|
+
state.view = next;
|
|
195
|
+
rerender();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const element = () => /* @__PURE__ */ jsx(
|
|
199
|
+
App,
|
|
200
|
+
{
|
|
201
|
+
state,
|
|
202
|
+
actions,
|
|
203
|
+
onCycleView: cycleView,
|
|
204
|
+
onOpen: () => openInBrowser(state.url)
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
const mount = () => render(element(), {
|
|
208
|
+
stdout: process.stdout,
|
|
209
|
+
// We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
|
|
210
|
+
// waitUntilExit, and the caller then stops the runner (preserving state).
|
|
211
|
+
exitOnCtrlC: true
|
|
212
|
+
});
|
|
213
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
214
|
+
let instance = mount();
|
|
215
|
+
let paused = false;
|
|
216
|
+
const rerender = () => {
|
|
217
|
+
if (!paused) instance.rerender(element());
|
|
218
|
+
};
|
|
219
|
+
let altActive = true;
|
|
220
|
+
const leaveAltScreen = () => {
|
|
221
|
+
if (!altActive) return;
|
|
222
|
+
altActive = false;
|
|
223
|
+
process.stdout.write(LEAVE_ALT_SCREEN);
|
|
224
|
+
};
|
|
225
|
+
const onResize = () => {
|
|
226
|
+
if (paused) return;
|
|
227
|
+
instance.clear();
|
|
228
|
+
rerender();
|
|
229
|
+
};
|
|
230
|
+
process.stdout.on("resize", onResize);
|
|
231
|
+
process.once("exit", leaveAltScreen);
|
|
232
|
+
return {
|
|
233
|
+
update(partial) {
|
|
234
|
+
Object.assign(state, partial);
|
|
235
|
+
rerender();
|
|
236
|
+
},
|
|
237
|
+
log(line, source = "tunnel") {
|
|
238
|
+
if (source === "dev") {
|
|
239
|
+
state.devLogs = [...state.devLogs, line].slice(-MAX_BUFFER);
|
|
240
|
+
} else {
|
|
241
|
+
state.tunnelLogs = [...state.tunnelLogs, line].slice(-MAX_BUFFER);
|
|
242
|
+
}
|
|
243
|
+
rerender();
|
|
244
|
+
},
|
|
245
|
+
async suspend(fn) {
|
|
246
|
+
paused = true;
|
|
247
|
+
instance.unmount();
|
|
248
|
+
leaveAltScreen();
|
|
249
|
+
try {
|
|
250
|
+
return await fn();
|
|
251
|
+
} finally {
|
|
252
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
253
|
+
altActive = true;
|
|
254
|
+
instance = mount();
|
|
255
|
+
paused = false;
|
|
256
|
+
rerender();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
waitUntilExit() {
|
|
260
|
+
return instance.waitUntilExit();
|
|
261
|
+
},
|
|
262
|
+
stop() {
|
|
263
|
+
process.stdout.off("resize", onResize);
|
|
264
|
+
process.removeListener("exit", leaveAltScreen);
|
|
265
|
+
instance.unmount();
|
|
266
|
+
leaveAltScreen();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export {
|
|
271
|
+
startDashboard
|
|
272
|
+
};
|
package/package.json
CHANGED
|
@@ -1,175 +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_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
|
-
};
|