@cliftonc/cfld 0.2.4 → 0.2.6
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 +272 -125
- package/dist/dashboard-Z4AN7H5V.js +277 -0
- package/package.json +1 -1
- package/dist/dashboard-GA37Z4H4.js +0 -194
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
import { readFileSync as readFileSync7, realpathSync } from "fs";
|
|
6
|
-
import { dirname, join as
|
|
6
|
+
import { dirname, join as join8 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import pc7 from "picocolors";
|
|
9
9
|
|
|
10
10
|
// src/commands/run.ts
|
|
11
|
-
import { join as
|
|
11
|
+
import { join as join6 } from "path";
|
|
12
12
|
|
|
13
13
|
// src/core/state-machine.ts
|
|
14
14
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -392,6 +392,24 @@ function certHasZone(certPath) {
|
|
|
392
392
|
const token = readCertToken(certPath);
|
|
393
393
|
return Boolean(token?.zoneID);
|
|
394
394
|
}
|
|
395
|
+
async function certZoneName(certPath) {
|
|
396
|
+
const token = readCertToken(certPath);
|
|
397
|
+
if (!token?.zoneID || !token.apiToken) return void 0;
|
|
398
|
+
try {
|
|
399
|
+
const res = await fetch(
|
|
400
|
+
`https://api.cloudflare.com/client/v4/zones/${token.zoneID}`,
|
|
401
|
+
{
|
|
402
|
+
headers: { Authorization: `Bearer ${token.apiToken}` },
|
|
403
|
+
signal: AbortSignal.timeout(5e3)
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
if (!res.ok) return void 0;
|
|
407
|
+
const body = await res.json();
|
|
408
|
+
return body.result?.name?.toLowerCase();
|
|
409
|
+
} catch {
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
395
413
|
|
|
396
414
|
// src/core/cert-store.ts
|
|
397
415
|
function ensureStoreDir() {
|
|
@@ -408,8 +426,19 @@ function listStoredZones() {
|
|
|
408
426
|
return [];
|
|
409
427
|
}
|
|
410
428
|
}
|
|
429
|
+
function storedZoneClaiming(certPath, except) {
|
|
430
|
+
const zoneID = readCertToken(certPath)?.zoneID;
|
|
431
|
+
if (!zoneID) return void 0;
|
|
432
|
+
return listStoredZones().find(
|
|
433
|
+
(z) => z !== except && readCertToken(certPathForZone(z))?.zoneID === zoneID
|
|
434
|
+
);
|
|
435
|
+
}
|
|
411
436
|
function resolveCert(zone) {
|
|
412
|
-
|
|
437
|
+
const stored = storedCertForZone(zone);
|
|
438
|
+
if (stored) return stored;
|
|
439
|
+
const def = defaultCertPath();
|
|
440
|
+
if (!certExists(def) || storedZoneClaiming(def, zone)) return void 0;
|
|
441
|
+
return def;
|
|
413
442
|
}
|
|
414
443
|
function importCert(zone, fromPath = defaultCertPath()) {
|
|
415
444
|
ensureStoreDir();
|
|
@@ -936,7 +965,8 @@ function colorLevel(level) {
|
|
|
936
965
|
|
|
937
966
|
// src/commands/login.ts
|
|
938
967
|
import { spawn as spawn4 } from "child_process";
|
|
939
|
-
import { existsSync as existsSync8 } from "fs";
|
|
968
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3, renameSync, rmSync } from "fs";
|
|
969
|
+
import { join as join4 } from "path";
|
|
940
970
|
|
|
941
971
|
// src/ui/prompts.ts
|
|
942
972
|
async function clack() {
|
|
@@ -1031,23 +1061,73 @@ function normalizeZone(input) {
|
|
|
1031
1061
|
return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1032
1062
|
}
|
|
1033
1063
|
async function loginCommand(flags) {
|
|
1034
|
-
const interactive = isInteractive();
|
|
1035
1064
|
const haveCert = certExists(defaultCertPath());
|
|
1065
|
+
let certPath = defaultCertPath();
|
|
1036
1066
|
if (flags.reauth || !haveCert) {
|
|
1037
|
-
await runLogin();
|
|
1067
|
+
certPath = await runLogin();
|
|
1038
1068
|
} else {
|
|
1039
1069
|
info("A Cloudflare certificate already exists. Use --reauth to authorize another domain.");
|
|
1040
1070
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1071
|
+
try {
|
|
1072
|
+
if (!certHasZone(certPath)) {
|
|
1073
|
+
printZeroZoneGuidance();
|
|
1074
|
+
throw new CfldError(
|
|
1075
|
+
"Logged in, but your account has no domain to authorize.",
|
|
1076
|
+
"Add a domain (see above), then run `cfld login --reauth`."
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const zone = await saveCert(flags.zone ? normalizeZone(flags.zone) : void 0, certPath);
|
|
1080
|
+
done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
|
|
1081
|
+
} finally {
|
|
1082
|
+
discardLoginCert(certPath);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function runLogin() {
|
|
1086
|
+
const bin = await resolveBinary();
|
|
1087
|
+
const def = defaultCertPath();
|
|
1088
|
+
const backup = existsSync8(def) ? `${def}.cfld-backup` : void 0;
|
|
1089
|
+
if (backup) renameSync(def, backup);
|
|
1090
|
+
try {
|
|
1091
|
+
step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
|
|
1092
|
+
note(" Pick the domain you want to use. Waiting for you to finish\u2026");
|
|
1093
|
+
await new Promise((resolve, reject) => {
|
|
1094
|
+
const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
|
|
1095
|
+
child.on("error", reject);
|
|
1096
|
+
child.on("exit", (code) => {
|
|
1097
|
+
if (code === 0 || existsSync8(def)) resolve();
|
|
1098
|
+
else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
if (!existsSync8(def)) {
|
|
1102
|
+
throw new CfldError(
|
|
1103
|
+
"Login finished but no certificate was written.",
|
|
1104
|
+
"Re-run `cfld login` and complete the browser step."
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
if (!backup) return def;
|
|
1108
|
+
mkdirSync3(cfldDir(), { recursive: true });
|
|
1109
|
+
const fresh = join4(cfldDir(), "login-cert.tmp");
|
|
1110
|
+
copyFileSync2(def, fresh);
|
|
1111
|
+
rmSync(def);
|
|
1112
|
+
return fresh;
|
|
1113
|
+
} finally {
|
|
1114
|
+
if (backup) renameSync(backup, def);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function discardLoginCert(certPath) {
|
|
1118
|
+
if (certPath !== defaultCertPath()) rmSync(certPath, { force: true });
|
|
1119
|
+
}
|
|
1120
|
+
async function saveCert(expected, certPath) {
|
|
1121
|
+
const actual = await certZoneName(certPath);
|
|
1122
|
+
if (expected && actual && actual !== expected) {
|
|
1043
1123
|
throw new CfldError(
|
|
1044
|
-
|
|
1045
|
-
|
|
1124
|
+
`You authorized ${actual} in the browser, not ${expected}.`,
|
|
1125
|
+
`Run \`cfld login --reauth --zone ${expected}\` and pick ${expected} this time.`
|
|
1046
1126
|
);
|
|
1047
1127
|
}
|
|
1048
|
-
let zone =
|
|
1128
|
+
let zone = expected ?? actual;
|
|
1049
1129
|
if (!zone) {
|
|
1050
|
-
if (!
|
|
1130
|
+
if (!isInteractive()) {
|
|
1051
1131
|
throw new CfldError(
|
|
1052
1132
|
"Pass --zone to record which domain this cert is for.",
|
|
1053
1133
|
"e.g. `cfld login --zone example.com`"
|
|
@@ -1055,48 +1135,57 @@ async function loginCommand(flags) {
|
|
|
1055
1135
|
}
|
|
1056
1136
|
zone = normalizeZone(await askText("Which domain did you authorize?", "example.com"));
|
|
1057
1137
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
async function runLogin() {
|
|
1062
|
-
const bin = await resolveBinary();
|
|
1063
|
-
step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
|
|
1064
|
-
note(" Pick the domain you want to use. Waiting for you to finish\u2026");
|
|
1065
|
-
await new Promise((resolve, reject) => {
|
|
1066
|
-
const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
|
|
1067
|
-
child.on("error", reject);
|
|
1068
|
-
child.on("exit", (code) => {
|
|
1069
|
-
if (code === 0 || existsSync8(defaultCertPath())) resolve();
|
|
1070
|
-
else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
if (!existsSync8(defaultCertPath())) {
|
|
1138
|
+
const clash = storedZoneClaiming(certPath, zone);
|
|
1139
|
+
if (clash && !actual) {
|
|
1074
1140
|
throw new CfldError(
|
|
1075
|
-
|
|
1076
|
-
|
|
1141
|
+
`That cert is for the same domain as your saved ${clash} cert, not ${zone}.`,
|
|
1142
|
+
`Run \`cfld login --reauth --zone ${zone}\` and pick ${zone} in the browser.`
|
|
1077
1143
|
);
|
|
1078
1144
|
}
|
|
1145
|
+
importCert(zone, certPath);
|
|
1146
|
+
return zone;
|
|
1079
1147
|
}
|
|
1080
1148
|
async function ensureCert(zone, opts = { interactive: true }) {
|
|
1081
|
-
const
|
|
1082
|
-
if (
|
|
1149
|
+
const usable = await usableCert(zone);
|
|
1150
|
+
if (usable) return usable;
|
|
1083
1151
|
if (!opts.interactive) {
|
|
1084
1152
|
throw new CfldError(
|
|
1085
1153
|
`No Cloudflare certificate found for ${zone}.`,
|
|
1086
|
-
|
|
1154
|
+
`Run \`cfld login --reauth --zone ${zone}\` first (needs a browser), or set TUNNEL_ORIGIN_CERT.`
|
|
1087
1155
|
);
|
|
1088
1156
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1157
|
+
info(`No certificate for ${zone} yet \u2014 authorize it in the browser.`);
|
|
1158
|
+
const fresh = await runLogin();
|
|
1159
|
+
try {
|
|
1160
|
+
if (!certHasZone(fresh)) {
|
|
1161
|
+
printZeroZoneGuidance();
|
|
1162
|
+
throw new CfldError(
|
|
1163
|
+
"Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
|
|
1164
|
+
"Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
await saveCert(zone, fresh);
|
|
1168
|
+
} finally {
|
|
1169
|
+
discardLoginCert(fresh);
|
|
1096
1170
|
}
|
|
1097
|
-
const stored = importCert(zone);
|
|
1098
1171
|
info(`Saved cert for ${zone}.`);
|
|
1099
|
-
return
|
|
1172
|
+
return certPathForZone(zone);
|
|
1173
|
+
}
|
|
1174
|
+
async function usableCert(zone) {
|
|
1175
|
+
const stored = storedCertForZone(zone);
|
|
1176
|
+
if (stored) {
|
|
1177
|
+
if (!storedZoneClaiming(stored, zone)) return stored;
|
|
1178
|
+
const actual2 = await certZoneName(stored);
|
|
1179
|
+
if (!actual2 || actual2 === zone) return stored;
|
|
1180
|
+
warn(`The saved cert for ${zone} is actually for ${actual2} \u2014 discarding it.`);
|
|
1181
|
+
rmSync(stored);
|
|
1182
|
+
}
|
|
1183
|
+
const def = defaultCertPath();
|
|
1184
|
+
if (!certHasZone(def)) return void 0;
|
|
1185
|
+
const actual = await certZoneName(def);
|
|
1186
|
+
if (actual === zone) return importCert(zone, def);
|
|
1187
|
+
if (!actual && !storedZoneClaiming(def, zone)) return def;
|
|
1188
|
+
return void 0;
|
|
1100
1189
|
}
|
|
1101
1190
|
function printZeroZoneGuidance() {
|
|
1102
1191
|
warn("cfld can't create a permanent custom URL yet \u2014 your account has no domains.");
|
|
@@ -1123,11 +1212,11 @@ function printZeroZoneGuidance() {
|
|
|
1123
1212
|
|
|
1124
1213
|
// src/util/gitignore.ts
|
|
1125
1214
|
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
1126
|
-
import { join as
|
|
1215
|
+
import { join as join5 } from "path";
|
|
1127
1216
|
function ensureGitignored(cwd, pattern) {
|
|
1128
|
-
const gitignorePath =
|
|
1217
|
+
const gitignorePath = join5(cwd, ".gitignore");
|
|
1129
1218
|
const hasGitignore = existsSync9(gitignorePath);
|
|
1130
|
-
const inRepo = hasGitignore || existsSync9(
|
|
1219
|
+
const inRepo = hasGitignore || existsSync9(join5(cwd, ".git"));
|
|
1131
1220
|
if (!inRepo) return false;
|
|
1132
1221
|
const existing = hasGitignore ? readFileSync6(gitignorePath, "utf8") : "";
|
|
1133
1222
|
const present = existing.split("\n").map((l) => l.trim()).includes(pattern);
|
|
@@ -1206,23 +1295,15 @@ async function launch(ctx) {
|
|
|
1206
1295
|
const target = `localhost:${ctx.port}`;
|
|
1207
1296
|
const interactive = isInteractive() && !ctx.noUi;
|
|
1208
1297
|
let dashboard;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
split: Boolean(ctx.exec),
|
|
1219
|
-
devLabel: ctx.exec ? describeCommand(ctx.exec) : void 0,
|
|
1220
|
-
tunnelLogs: [],
|
|
1221
|
-
devLogs: []
|
|
1222
|
-
});
|
|
1223
|
-
} else {
|
|
1224
|
-
step("Setting up your tunnel\u2026");
|
|
1225
|
-
}
|
|
1298
|
+
let runner;
|
|
1299
|
+
let child;
|
|
1300
|
+
let result;
|
|
1301
|
+
let poll;
|
|
1302
|
+
let metricsPort = 0;
|
|
1303
|
+
let shuttingDown = false;
|
|
1304
|
+
let started = false;
|
|
1305
|
+
let busy = false;
|
|
1306
|
+
const childAbort = new AbortController();
|
|
1226
1307
|
const setupStep = (s) => dashboard ? dashboard.log(s, "tunnel") : step(s);
|
|
1227
1308
|
const setupDone = (s) => dashboard ? dashboard.log(s, "tunnel") : done(s);
|
|
1228
1309
|
const emitLine = (line) => {
|
|
@@ -1236,11 +1317,6 @@ async function launch(ctx) {
|
|
|
1236
1317
|
if (dashboard) dashboard.log(line, "dev");
|
|
1237
1318
|
else process.stderr.write(line + "\n");
|
|
1238
1319
|
};
|
|
1239
|
-
let runner;
|
|
1240
|
-
let child;
|
|
1241
|
-
let poll;
|
|
1242
|
-
let shuttingDown = false;
|
|
1243
|
-
const childAbort = new AbortController();
|
|
1244
1320
|
const shutdown = (opts = {}) => {
|
|
1245
1321
|
if (shuttingDown) return;
|
|
1246
1322
|
shuttingDown = true;
|
|
@@ -1253,6 +1329,65 @@ ${opts.reason}`);
|
|
|
1253
1329
|
if (child) reapChild(child).then(finish, finish);
|
|
1254
1330
|
else finish();
|
|
1255
1331
|
};
|
|
1332
|
+
const startTunnel = (port) => startRunner({
|
|
1333
|
+
configPath: result.configPath,
|
|
1334
|
+
tunnelName: result.tunnelName,
|
|
1335
|
+
certPath: result.certPath,
|
|
1336
|
+
metricsPort: port,
|
|
1337
|
+
onLine: (line) => emitLine(line),
|
|
1338
|
+
onStatus: (status) => {
|
|
1339
|
+
if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
|
|
1340
|
+
else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
|
|
1341
|
+
},
|
|
1342
|
+
onFatal: (message) => {
|
|
1343
|
+
warn(message);
|
|
1344
|
+
shutdown({ code: 1 });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
const startDevServer = () => startChild({
|
|
1348
|
+
command: ctx.exec,
|
|
1349
|
+
cwd: ctx.cwd,
|
|
1350
|
+
env: ctx.noEnv ? void 0 : { [ctx.envKey]: url },
|
|
1351
|
+
onLine: (line) => emitDevLine(line),
|
|
1352
|
+
onExit: (code, signal) => {
|
|
1353
|
+
childAbort.abort();
|
|
1354
|
+
const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
|
|
1355
|
+
shutdown({
|
|
1356
|
+
reason: `Your dev command ${how} \u2014 stopping. Your tunnel URL is preserved.`,
|
|
1357
|
+
code: code ?? 1
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
const doRestartDev = async () => {
|
|
1362
|
+
if (!ctx.exec || !child) return;
|
|
1363
|
+
dashboard?.log("\u21BB restarting dev server\u2026", "dev");
|
|
1364
|
+
await reapChild(child);
|
|
1365
|
+
if (shuttingDown) return;
|
|
1366
|
+
child = startDevServer();
|
|
1367
|
+
};
|
|
1368
|
+
const doRestartTunnel = async () => {
|
|
1369
|
+
if (!runner || !result) return;
|
|
1370
|
+
dashboard?.log("\u21BB restarting tunnel\u2026", "tunnel");
|
|
1371
|
+
dashboard?.update({ status: "reconnecting" });
|
|
1372
|
+
runner.stop();
|
|
1373
|
+
metricsPort = await findFreePort();
|
|
1374
|
+
runner = await startTunnel(metricsPort);
|
|
1375
|
+
const r = await waitForReady(metricsPort).catch(() => void 0);
|
|
1376
|
+
if (r && !shuttingDown) dashboard?.update({ status: "live", connections: r.connections });
|
|
1377
|
+
};
|
|
1378
|
+
const guard = (fn) => () => {
|
|
1379
|
+
if (busy || shuttingDown || !started) return;
|
|
1380
|
+
busy = true;
|
|
1381
|
+
void Promise.resolve(fn()).finally(() => {
|
|
1382
|
+
busy = false;
|
|
1383
|
+
});
|
|
1384
|
+
};
|
|
1385
|
+
const restartDev = guard(doRestartDev);
|
|
1386
|
+
const restartTunnel = guard(doRestartTunnel);
|
|
1387
|
+
const restartAll = guard(async () => {
|
|
1388
|
+
await doRestartTunnel();
|
|
1389
|
+
await doRestartDev();
|
|
1390
|
+
});
|
|
1256
1391
|
process.once(
|
|
1257
1392
|
"SIGINT",
|
|
1258
1393
|
() => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
|
|
@@ -1262,24 +1397,35 @@ ${opts.reason}`);
|
|
|
1262
1397
|
() => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
|
|
1263
1398
|
);
|
|
1264
1399
|
process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
|
|
1400
|
+
if (interactive) {
|
|
1401
|
+
const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
|
|
1402
|
+
dashboard = startDashboard(
|
|
1403
|
+
{
|
|
1404
|
+
url,
|
|
1405
|
+
target,
|
|
1406
|
+
tunnelName: ctx.tunnelName,
|
|
1407
|
+
status: "connecting",
|
|
1408
|
+
connections: 0,
|
|
1409
|
+
requests: 0,
|
|
1410
|
+
split: Boolean(ctx.exec),
|
|
1411
|
+
devLabel: ctx.exec ? describeCommand(ctx.exec) : void 0,
|
|
1412
|
+
tunnelLogs: [],
|
|
1413
|
+
devLogs: []
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
restartAll,
|
|
1417
|
+
restartTunnel,
|
|
1418
|
+
restartDev: ctx.exec ? restartDev : void 0
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
} else {
|
|
1422
|
+
step("Setting up your tunnel\u2026");
|
|
1423
|
+
}
|
|
1265
1424
|
if (ctx.exec) {
|
|
1266
1425
|
if (!dashboard) step(`Starting \`${describeCommand(ctx.exec)}\`\u2026`);
|
|
1267
|
-
child =
|
|
1268
|
-
command: ctx.exec,
|
|
1269
|
-
cwd: ctx.cwd,
|
|
1270
|
-
env: ctx.noEnv ? void 0 : { [ctx.envKey]: url },
|
|
1271
|
-
onLine: (line) => emitDevLine(line),
|
|
1272
|
-
onExit: (code, signal) => {
|
|
1273
|
-
childAbort.abort();
|
|
1274
|
-
const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
|
|
1275
|
-
shutdown({
|
|
1276
|
-
reason: `Your dev command ${how} \u2014 stopping. Your tunnel URL is preserved.`,
|
|
1277
|
-
code: code ?? 1
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1426
|
+
child = startDevServer();
|
|
1281
1427
|
}
|
|
1282
|
-
|
|
1428
|
+
result = await ctx.runReconcile({
|
|
1283
1429
|
onStep: (s) => setupStep(s),
|
|
1284
1430
|
confirmOverwrite: interactive ? (h, t) => {
|
|
1285
1431
|
const ask = () => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`);
|
|
@@ -1299,22 +1445,8 @@ ${opts.reason}`);
|
|
|
1299
1445
|
result,
|
|
1300
1446
|
(msg) => dashboard ? dashboard.log(msg, "tunnel") : info(msg)
|
|
1301
1447
|
);
|
|
1302
|
-
|
|
1303
|
-
runner = await
|
|
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
|
-
});
|
|
1448
|
+
metricsPort = await findFreePort();
|
|
1449
|
+
runner = await startTunnel(metricsPort);
|
|
1318
1450
|
const ready = await waitForReady(metricsPort).catch((err) => {
|
|
1319
1451
|
child?.stop();
|
|
1320
1452
|
runner?.stop();
|
|
@@ -1324,6 +1456,7 @@ ${opts.reason}`);
|
|
|
1324
1456
|
err instanceof Error ? err.message : void 0
|
|
1325
1457
|
);
|
|
1326
1458
|
});
|
|
1459
|
+
started = true;
|
|
1327
1460
|
if (child && !shuttingDown) {
|
|
1328
1461
|
if (dashboard) dashboard.update({ status: "waiting", connections: ready.connections });
|
|
1329
1462
|
else step(`Waiting for your dev server on :${ctx.port}\u2026`);
|
|
@@ -1332,7 +1465,7 @@ ${opts.reason}`);
|
|
|
1332
1465
|
if (!up) warn(`Nothing is listening on :${ctx.port} yet \u2014 going live anyway.`);
|
|
1333
1466
|
}
|
|
1334
1467
|
if (!ctx.noEnv) {
|
|
1335
|
-
const { changed } = upsertEnv(
|
|
1468
|
+
const { changed } = upsertEnv(join6(ctx.cwd, ".env"), ctx.envKey, url);
|
|
1336
1469
|
if (changed && !dashboard) info(`Wrote ${ctx.envKey}=${url} to .env`);
|
|
1337
1470
|
}
|
|
1338
1471
|
if (dashboard) {
|
|
@@ -1509,7 +1642,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
|
|
|
1509
1642
|
});
|
|
1510
1643
|
}
|
|
1511
1644
|
if (interactive) {
|
|
1512
|
-
const { startDashboard } = await import("./dashboard-
|
|
1645
|
+
const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
|
|
1513
1646
|
dashboard = startDashboard({
|
|
1514
1647
|
url,
|
|
1515
1648
|
target,
|
|
@@ -1709,20 +1842,26 @@ async function setupCommand(flags) {
|
|
|
1709
1842
|
);
|
|
1710
1843
|
return;
|
|
1711
1844
|
}
|
|
1712
|
-
await runLogin();
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
|
|
1845
|
+
const fresh = await runLogin();
|
|
1846
|
+
let zone;
|
|
1847
|
+
try {
|
|
1848
|
+
if (!certHasZone(fresh)) {
|
|
1849
|
+
printZeroZoneGuidance();
|
|
1850
|
+
if (await askConfirm("Start a temporary quick tunnel now instead?")) {
|
|
1851
|
+
await quickCommand(flags);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
throw new CfldError(
|
|
1855
|
+
"Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
|
|
1856
|
+
"Add a domain (see above), then run `cfld setup` again."
|
|
1857
|
+
);
|
|
1718
1858
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
);
|
|
1859
|
+
const detected = await certZoneName(fresh);
|
|
1860
|
+
zone = await saveCert(detected ?? await askAuthorizedZone(knownZones), fresh);
|
|
1861
|
+
} finally {
|
|
1862
|
+
discardLoginCert(fresh);
|
|
1723
1863
|
}
|
|
1724
|
-
const
|
|
1725
|
-
const certPath = importCert(zone);
|
|
1864
|
+
const certPath = certPathForZone(zone);
|
|
1726
1865
|
info(`Saved cert for ${zone}.`);
|
|
1727
1866
|
await configureProject(flags, { zone, certPath, cwd });
|
|
1728
1867
|
await offerGoLive(flags, zone);
|
|
@@ -1953,9 +2092,9 @@ function resolveCurrentEntry2() {
|
|
|
1953
2092
|
import pc5 from "picocolors";
|
|
1954
2093
|
|
|
1955
2094
|
// src/core/service.ts
|
|
1956
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
2095
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1957
2096
|
import { homedir as homedir2, platform } from "os";
|
|
1958
|
-
import { join as
|
|
2097
|
+
import { join as join7 } from "path";
|
|
1959
2098
|
function serviceSlug(name) {
|
|
1960
2099
|
return name.replace(/^cfld-/, "");
|
|
1961
2100
|
}
|
|
@@ -1963,16 +2102,16 @@ function launchAgentLabel(name) {
|
|
|
1963
2102
|
return `com.cfld.${serviceSlug(name)}`;
|
|
1964
2103
|
}
|
|
1965
2104
|
function launchAgentPath(name) {
|
|
1966
|
-
return
|
|
2105
|
+
return join7(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
|
|
1967
2106
|
}
|
|
1968
2107
|
function systemdUnitName(name) {
|
|
1969
2108
|
return `cfld-${serviceSlug(name)}.service`;
|
|
1970
2109
|
}
|
|
1971
2110
|
function systemdUnitPath(name) {
|
|
1972
|
-
return
|
|
2111
|
+
return join7(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
|
|
1973
2112
|
}
|
|
1974
2113
|
function logDir() {
|
|
1975
|
-
return
|
|
2114
|
+
return join7(cfldDir(), "logs");
|
|
1976
2115
|
}
|
|
1977
2116
|
function renderLaunchAgent(spec) {
|
|
1978
2117
|
const args = [
|
|
@@ -1986,8 +2125,8 @@ function renderLaunchAgent(spec) {
|
|
|
1986
2125
|
spec.name
|
|
1987
2126
|
];
|
|
1988
2127
|
const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
1989
|
-
const out =
|
|
1990
|
-
const err =
|
|
2128
|
+
const out = join7(logDir(), `${serviceSlug(spec.name)}.out.log`);
|
|
2129
|
+
const err = join7(logDir(), `${serviceSlug(spec.name)}.err.log`);
|
|
1991
2130
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1992
2131
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1993
2132
|
<plist version="1.0">
|
|
@@ -2052,10 +2191,10 @@ function assertSupported() {
|
|
|
2052
2191
|
}
|
|
2053
2192
|
async function installService(spec) {
|
|
2054
2193
|
const p = assertSupported();
|
|
2055
|
-
|
|
2194
|
+
mkdirSync4(logDir(), { recursive: true });
|
|
2056
2195
|
if (p === "darwin") {
|
|
2057
2196
|
const path = launchAgentPath(spec.name);
|
|
2058
|
-
|
|
2197
|
+
mkdirSync4(join7(homedir2(), "Library", "LaunchAgents"), { recursive: true });
|
|
2059
2198
|
writeFileSync6(path, renderLaunchAgent(spec), "utf8");
|
|
2060
2199
|
await exec("launchctl", ["unload", path]);
|
|
2061
2200
|
const res = await exec("launchctl", ["load", "-w", path]);
|
|
@@ -2067,7 +2206,7 @@ async function installService(spec) {
|
|
|
2067
2206
|
}
|
|
2068
2207
|
} else {
|
|
2069
2208
|
const path = systemdUnitPath(spec.name);
|
|
2070
|
-
|
|
2209
|
+
mkdirSync4(join7(homedir2(), ".config", "systemd", "user"), { recursive: true });
|
|
2071
2210
|
writeFileSync6(path, renderSystemdUnit(spec), "utf8");
|
|
2072
2211
|
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
2073
2212
|
const res = await exec("systemctl", [
|
|
@@ -2090,13 +2229,13 @@ async function uninstallService(name) {
|
|
|
2090
2229
|
const path = launchAgentPath(name);
|
|
2091
2230
|
if (existsSync10(path)) {
|
|
2092
2231
|
await exec("launchctl", ["unload", "-w", path]);
|
|
2093
|
-
|
|
2232
|
+
rmSync2(path, { force: true });
|
|
2094
2233
|
}
|
|
2095
2234
|
} else {
|
|
2096
2235
|
const unit = systemdUnitName(name);
|
|
2097
2236
|
await exec("systemctl", ["--user", "disable", "--now", unit]);
|
|
2098
2237
|
const path = systemdUnitPath(name);
|
|
2099
|
-
if (existsSync10(path))
|
|
2238
|
+
if (existsSync10(path)) rmSync2(path, { force: true });
|
|
2100
2239
|
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
2101
2240
|
}
|
|
2102
2241
|
}
|
|
@@ -2216,6 +2355,14 @@ async function doctorCommand() {
|
|
|
2216
2355
|
const zones = listStoredZones();
|
|
2217
2356
|
if (zones.length) out(` ${ok} cert store: ${zones.join(", ")}`);
|
|
2218
2357
|
else out(` ${pc6.dim("\xB7")} cert store empty`);
|
|
2358
|
+
for (const zone of zones) {
|
|
2359
|
+
const actual = await certZoneName(certPathForZone(zone));
|
|
2360
|
+
if (actual && actual !== zone) {
|
|
2361
|
+
out(
|
|
2362
|
+
` ${bad} ${zone}.pem is really a cert for ${actual} \u2014 fix with \`cfld login --reauth --zone ${zone}\``
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2219
2366
|
const entries = listEntries();
|
|
2220
2367
|
out(` ${ok} registry: ${entries.length} tunnel(s)`);
|
|
2221
2368
|
out("");
|
|
@@ -2225,7 +2372,7 @@ async function doctorCommand() {
|
|
|
2225
2372
|
function readVersion() {
|
|
2226
2373
|
try {
|
|
2227
2374
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2228
|
-
const pkg = JSON.parse(readFileSync7(
|
|
2375
|
+
const pkg = JSON.parse(readFileSync7(join8(here, "../package.json"), "utf8"));
|
|
2229
2376
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
2230
2377
|
} catch {
|
|
2231
2378
|
return "0.0.0";
|
|
@@ -0,0 +1,277 @@
|
|
|
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 }) : (
|
|
98
|
+
// Wrap rather than truncate so long lines (e.g. magic-link / OAuth
|
|
99
|
+
// URLs) stay fully visible and selectable instead of ending in "…".
|
|
100
|
+
// The bottom-pinned box clips older wrapped lines off the top.
|
|
101
|
+
lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: line }, i))
|
|
102
|
+
) })
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function Dashboard(state) {
|
|
108
|
+
const termRows = process.stdout.rows || 24;
|
|
109
|
+
const view = state.split ? state.view ?? "split" : "tunnel";
|
|
110
|
+
const titleRows = state.split ? 1 : 0;
|
|
111
|
+
const logRows = Math.max(3, termRows - HEADER_ROWS - titleRows - 1);
|
|
112
|
+
const devColumn = (divider) => /* @__PURE__ */ jsx(
|
|
113
|
+
LogColumn,
|
|
114
|
+
{
|
|
115
|
+
title: state.devLabel ?? "dev server",
|
|
116
|
+
titleColor: "cyan",
|
|
117
|
+
lines: state.devLogs,
|
|
118
|
+
rows: logRows,
|
|
119
|
+
placeholder: "waiting for dev-server output\u2026",
|
|
120
|
+
divider
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
const tunnelColumn = (divider) => /* @__PURE__ */ jsx(
|
|
124
|
+
LogColumn,
|
|
125
|
+
{
|
|
126
|
+
title: state.split ? "tunnel" : void 0,
|
|
127
|
+
titleColor: "blue",
|
|
128
|
+
lines: state.tunnelLogs,
|
|
129
|
+
rows: logRows,
|
|
130
|
+
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)",
|
|
131
|
+
divider
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
let body;
|
|
135
|
+
if (view === "split") {
|
|
136
|
+
body = /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "row", paddingX: 1, paddingTop: 1, children: [
|
|
137
|
+
devColumn(),
|
|
138
|
+
tunnelColumn(true)
|
|
139
|
+
] });
|
|
140
|
+
} else {
|
|
141
|
+
body = /* @__PURE__ */ jsx(Box, { flexGrow: 1, paddingX: 1, paddingTop: 1, children: view === "dev" ? devColumn() : tunnelColumn() });
|
|
142
|
+
}
|
|
143
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [
|
|
144
|
+
/* @__PURE__ */ jsx(Header, { ...state }),
|
|
145
|
+
body
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
function App({
|
|
149
|
+
state,
|
|
150
|
+
actions,
|
|
151
|
+
onCycleView,
|
|
152
|
+
onOpen
|
|
153
|
+
}) {
|
|
154
|
+
const { exit } = useApp();
|
|
155
|
+
useInput((input, key) => {
|
|
156
|
+
if (key.leftArrow || input === "<" || input === ",") return onCycleView(-1);
|
|
157
|
+
if (key.rightArrow || input === ">" || input === ".") return onCycleView(1);
|
|
158
|
+
switch (input.toLowerCase()) {
|
|
159
|
+
case "r":
|
|
160
|
+
return actions.restartAll?.();
|
|
161
|
+
case "d":
|
|
162
|
+
return actions.restartDev?.();
|
|
163
|
+
case "t":
|
|
164
|
+
return actions.restartTunnel?.();
|
|
165
|
+
case "o":
|
|
166
|
+
return onOpen();
|
|
167
|
+
case "q":
|
|
168
|
+
return exit();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return /* @__PURE__ */ jsx(Dashboard, { ...state });
|
|
172
|
+
}
|
|
173
|
+
function openInBrowser(url) {
|
|
174
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
175
|
+
try {
|
|
176
|
+
spawn(cmd, [url], {
|
|
177
|
+
stdio: "ignore",
|
|
178
|
+
detached: true,
|
|
179
|
+
shell: process.platform === "win32"
|
|
180
|
+
}).unref();
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
185
|
+
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
186
|
+
function startDashboard(initial, actions = {}) {
|
|
187
|
+
const state = {
|
|
188
|
+
...initial,
|
|
189
|
+
view: initial.view ?? (initial.split ? "split" : "tunnel"),
|
|
190
|
+
canRestart: Boolean(actions.restartAll || actions.restartTunnel),
|
|
191
|
+
tunnelLogs: [...initial.tunnelLogs],
|
|
192
|
+
devLogs: [...initial.devLogs]
|
|
193
|
+
};
|
|
194
|
+
const cycleView = (delta) => {
|
|
195
|
+
if (!state.split) return;
|
|
196
|
+
const i = VIEW_ORDER.indexOf(state.view ?? "split");
|
|
197
|
+
const next = VIEW_ORDER[Math.min(VIEW_ORDER.length - 1, Math.max(0, i + delta))];
|
|
198
|
+
if (next !== state.view) {
|
|
199
|
+
state.view = next;
|
|
200
|
+
rerender();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const element = () => /* @__PURE__ */ jsx(
|
|
204
|
+
App,
|
|
205
|
+
{
|
|
206
|
+
state,
|
|
207
|
+
actions,
|
|
208
|
+
onCycleView: cycleView,
|
|
209
|
+
onOpen: () => openInBrowser(state.url)
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
const mount = () => render(element(), {
|
|
213
|
+
stdout: process.stdout,
|
|
214
|
+
// We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
|
|
215
|
+
// waitUntilExit, and the caller then stops the runner (preserving state).
|
|
216
|
+
exitOnCtrlC: true
|
|
217
|
+
});
|
|
218
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
219
|
+
let instance = mount();
|
|
220
|
+
let paused = false;
|
|
221
|
+
const rerender = () => {
|
|
222
|
+
if (!paused) instance.rerender(element());
|
|
223
|
+
};
|
|
224
|
+
let altActive = true;
|
|
225
|
+
const leaveAltScreen = () => {
|
|
226
|
+
if (!altActive) return;
|
|
227
|
+
altActive = false;
|
|
228
|
+
process.stdout.write(LEAVE_ALT_SCREEN);
|
|
229
|
+
};
|
|
230
|
+
const onResize = () => {
|
|
231
|
+
if (paused) return;
|
|
232
|
+
instance.clear();
|
|
233
|
+
rerender();
|
|
234
|
+
};
|
|
235
|
+
process.stdout.on("resize", onResize);
|
|
236
|
+
process.once("exit", leaveAltScreen);
|
|
237
|
+
return {
|
|
238
|
+
update(partial) {
|
|
239
|
+
Object.assign(state, partial);
|
|
240
|
+
rerender();
|
|
241
|
+
},
|
|
242
|
+
log(line, source = "tunnel") {
|
|
243
|
+
if (source === "dev") {
|
|
244
|
+
state.devLogs = [...state.devLogs, line].slice(-MAX_BUFFER);
|
|
245
|
+
} else {
|
|
246
|
+
state.tunnelLogs = [...state.tunnelLogs, line].slice(-MAX_BUFFER);
|
|
247
|
+
}
|
|
248
|
+
rerender();
|
|
249
|
+
},
|
|
250
|
+
async suspend(fn) {
|
|
251
|
+
paused = true;
|
|
252
|
+
instance.unmount();
|
|
253
|
+
leaveAltScreen();
|
|
254
|
+
try {
|
|
255
|
+
return await fn();
|
|
256
|
+
} finally {
|
|
257
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
258
|
+
altActive = true;
|
|
259
|
+
instance = mount();
|
|
260
|
+
paused = false;
|
|
261
|
+
rerender();
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
waitUntilExit() {
|
|
265
|
+
return instance.waitUntilExit();
|
|
266
|
+
},
|
|
267
|
+
stop() {
|
|
268
|
+
process.stdout.off("resize", onResize);
|
|
269
|
+
process.removeListener("exit", leaveAltScreen);
|
|
270
|
+
instance.unmount();
|
|
271
|
+
leaveAltScreen();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
startDashboard
|
|
277
|
+
};
|
package/package.json
CHANGED
|
@@ -1,194 +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
|
-
const mount = () => render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
|
|
130
|
-
stdout: process.stdout,
|
|
131
|
-
// We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
|
|
132
|
-
// waitUntilExit, and the caller then stops the runner (preserving state).
|
|
133
|
-
exitOnCtrlC: true
|
|
134
|
-
});
|
|
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
|
-
};
|
|
141
|
-
let altActive = true;
|
|
142
|
-
const leaveAltScreen = () => {
|
|
143
|
-
if (!altActive) return;
|
|
144
|
-
altActive = false;
|
|
145
|
-
process.stdout.write(LEAVE_ALT_SCREEN);
|
|
146
|
-
};
|
|
147
|
-
const onResize = () => {
|
|
148
|
-
if (paused) return;
|
|
149
|
-
instance.clear();
|
|
150
|
-
rerender();
|
|
151
|
-
};
|
|
152
|
-
process.stdout.on("resize", onResize);
|
|
153
|
-
process.once("exit", leaveAltScreen);
|
|
154
|
-
return {
|
|
155
|
-
update(partial) {
|
|
156
|
-
Object.assign(state, partial);
|
|
157
|
-
rerender();
|
|
158
|
-
},
|
|
159
|
-
log(line, source = "tunnel") {
|
|
160
|
-
if (source === "dev") {
|
|
161
|
-
state.devLogs = [...state.devLogs, line].slice(-MAX_BUFFER);
|
|
162
|
-
} else {
|
|
163
|
-
state.tunnelLogs = [...state.tunnelLogs, line].slice(-MAX_BUFFER);
|
|
164
|
-
}
|
|
165
|
-
rerender();
|
|
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
|
-
},
|
|
181
|
-
waitUntilExit() {
|
|
182
|
-
return instance.waitUntilExit();
|
|
183
|
-
},
|
|
184
|
-
stop() {
|
|
185
|
-
process.stdout.off("resize", onResize);
|
|
186
|
-
process.removeListener("exit", leaveAltScreen);
|
|
187
|
-
instance.unmount();
|
|
188
|
-
leaveAltScreen();
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
export {
|
|
193
|
-
startDashboard
|
|
194
|
-
};
|