@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 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 join7 } from "path";
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 join5 } from "path";
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
- return storedCertForZone(zone) ?? (certExists(defaultCertPath()) ? defaultCertPath() : void 0);
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
- if (!certHasZone(defaultCertPath())) {
1042
- printZeroZoneGuidance();
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
- "Logged in, but your account has no domain to authorize.",
1045
- "Add a domain (see above), then run `cfld login --reauth`."
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 = flags.zone ? normalizeZone(flags.zone) : void 0;
1128
+ let zone = expected ?? actual;
1049
1129
  if (!zone) {
1050
- if (!interactive) {
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
- importCert(zone);
1059
- done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
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
- "Login finished but no certificate was written.",
1076
- "Re-run `cfld login` and complete the browser step."
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 existing = resolveCert(zone);
1082
- if (existing) return existing;
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
- "Run `cfld login` first (needs a browser), or set TUNNEL_ORIGIN_CERT."
1154
+ `Run \`cfld login --reauth --zone ${zone}\` first (needs a browser), or set TUNNEL_ORIGIN_CERT.`
1087
1155
  );
1088
1156
  }
1089
- await runLogin();
1090
- if (!certHasZone(defaultCertPath())) {
1091
- printZeroZoneGuidance();
1092
- throw new CfldError(
1093
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1094
- "Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
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 stored;
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 join4 } from "path";
1215
+ import { join as join5 } from "path";
1127
1216
  function ensureGitignored(cwd, pattern) {
1128
- const gitignorePath = join4(cwd, ".gitignore");
1217
+ const gitignorePath = join5(cwd, ".gitignore");
1129
1218
  const hasGitignore = existsSync9(gitignorePath);
1130
- const inRepo = hasGitignore || existsSync9(join4(cwd, ".git"));
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
- if (interactive) {
1210
- const { startDashboard } = await import("./dashboard-GA37Z4H4.js");
1211
- dashboard = startDashboard({
1212
- url,
1213
- target,
1214
- tunnelName: ctx.tunnelName,
1215
- status: "connecting",
1216
- connections: 0,
1217
- requests: 0,
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 = startChild({
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
- const result = await ctx.runReconcile({
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
- const metricsPort = await findFreePort();
1303
- runner = await startRunner({
1304
- configPath: result.configPath,
1305
- tunnelName: result.tunnelName,
1306
- certPath: result.certPath,
1307
- metricsPort,
1308
- onLine: (line) => emitLine(line),
1309
- onStatus: (status) => {
1310
- if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
1311
- else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
1312
- },
1313
- onFatal: (message) => {
1314
- warn(message);
1315
- shutdown({ code: 1 });
1316
- }
1317
- });
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(join5(ctx.cwd, ".env"), ctx.envKey, url);
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-GA37Z4H4.js");
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
- if (!certHasZone(defaultCertPath())) {
1714
- printZeroZoneGuidance();
1715
- if (await askConfirm("Start a temporary quick tunnel now instead?")) {
1716
- await quickCommand(flags);
1717
- return;
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
- throw new CfldError(
1720
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1721
- "Add a domain (see above), then run `cfld setup` again."
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 zone = await askAuthorizedZone(knownZones);
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 mkdirSync3, rmSync, writeFileSync as writeFileSync6 } from "fs";
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 join6 } from "path";
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 join6(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
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 join6(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
2111
+ return join7(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
1973
2112
  }
1974
2113
  function logDir() {
1975
- return join6(cfldDir(), "logs");
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 = join6(logDir(), `${serviceSlug(spec.name)}.out.log`);
1990
- const err = join6(logDir(), `${serviceSlug(spec.name)}.err.log`);
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
- mkdirSync3(logDir(), { recursive: true });
2194
+ mkdirSync4(logDir(), { recursive: true });
2056
2195
  if (p === "darwin") {
2057
2196
  const path = launchAgentPath(spec.name);
2058
- mkdirSync3(join6(homedir2(), "Library", "LaunchAgents"), { recursive: true });
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
- mkdirSync3(join6(homedir2(), ".config", "systemd", "user"), { recursive: true });
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
- rmSync(path, { force: true });
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)) rmSync(path, { force: true });
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(join7(here, "../package.json"), "utf8"));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Zero-token, persistent Cloudflare tunnel CLI for local dev — same public HTTPS URL every run.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- };