@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 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
- const result = await reconcile(
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
- exec: flags.exec
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(result, ctx) {
1204
- const metricsPort = await findFreePort();
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
- if (interactive) {
1210
- const { startDashboard } = await import("./dashboard-Q6WI32AK.js");
1211
- dashboard = startDashboard({
1212
- url,
1213
- target,
1214
- tunnelName: result.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("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
- runner = await startRunner({
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
- if (ctx.exec) {
1270
- if (!dashboard) step(`Starting \`${describeCommand(ctx.exec)}\`\u2026`);
1271
- child = startChild({
1272
- command: ctx.exec,
1273
- cwd: ctx.cwd,
1274
- env: ctx.noEnv ? void 0 : { [ctx.envKey]: url },
1275
- onLine: (line) => emitDevLine(line),
1276
- onExit: (code, signal) => {
1277
- childAbort.abort();
1278
- const how = code !== null ? `exited (code ${code})` : `was terminated${signal ? ` (${signal})` : ""}`;
1279
- shutdown({
1280
- reason: `Your dev command ${how} \u2014 stopping. Your tunnel URL is preserved.`,
1281
- code: code ?? 1
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-Q6WI32AK.js");
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
- const result = await reconcile(
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
- port: entry.port
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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,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
- };