@cliftonc/cfld 0.2.4 → 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
@@ -1206,23 +1206,15 @@ async function launch(ctx) {
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-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
- }
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();
1226
1218
  const setupStep = (s) => dashboard ? dashboard.log(s, "tunnel") : step(s);
1227
1219
  const setupDone = (s) => dashboard ? dashboard.log(s, "tunnel") : done(s);
1228
1220
  const emitLine = (line) => {
@@ -1236,11 +1228,6 @@ async function launch(ctx) {
1236
1228
  if (dashboard) dashboard.log(line, "dev");
1237
1229
  else process.stderr.write(line + "\n");
1238
1230
  };
1239
- let runner;
1240
- let child;
1241
- let poll;
1242
- let shuttingDown = false;
1243
- const childAbort = new AbortController();
1244
1231
  const shutdown = (opts = {}) => {
1245
1232
  if (shuttingDown) return;
1246
1233
  shuttingDown = true;
@@ -1253,6 +1240,65 @@ ${opts.reason}`);
1253
1240
  if (child) reapChild(child).then(finish, finish);
1254
1241
  else finish();
1255
1242
  };
1243
+ const startTunnel = (port) => startRunner({
1244
+ configPath: result.configPath,
1245
+ tunnelName: result.tunnelName,
1246
+ certPath: result.certPath,
1247
+ metricsPort: port,
1248
+ onLine: (line) => emitLine(line),
1249
+ onStatus: (status) => {
1250
+ if (status === "reconnecting") dashboard?.update({ status: "reconnecting" });
1251
+ else if (status === "connecting" && !dashboard) note("Reconnecting\u2026");
1252
+ },
1253
+ onFatal: (message) => {
1254
+ warn(message);
1255
+ shutdown({ code: 1 });
1256
+ }
1257
+ });
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;
1294
+ });
1295
+ };
1296
+ const restartDev = guard(doRestartDev);
1297
+ const restartTunnel = guard(doRestartTunnel);
1298
+ const restartAll = guard(async () => {
1299
+ await doRestartTunnel();
1300
+ await doRestartDev();
1301
+ });
1256
1302
  process.once(
1257
1303
  "SIGINT",
1258
1304
  () => shutdown({ reason: "Stopping \u2014 your URL is preserved. Re-run `cfld`." })
@@ -1262,24 +1308,35 @@ ${opts.reason}`);
1262
1308
  () => shutdown({ reason: "Stopping \u2014 your URL is preserved." })
1263
1309
  );
1264
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
+ }
1265
1335
  if (ctx.exec) {
1266
1336
  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
- });
1337
+ child = startDevServer();
1281
1338
  }
1282
- const result = await ctx.runReconcile({
1339
+ result = await ctx.runReconcile({
1283
1340
  onStep: (s) => setupStep(s),
1284
1341
  confirmOverwrite: interactive ? (h, t) => {
1285
1342
  const ask = () => askConfirm(`${h} already points to ${t}. Repoint it to this tunnel?`);
@@ -1299,22 +1356,8 @@ ${opts.reason}`);
1299
1356
  result,
1300
1357
  (msg) => dashboard ? dashboard.log(msg, "tunnel") : info(msg)
1301
1358
  );
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
- });
1359
+ metricsPort = await findFreePort();
1360
+ runner = await startTunnel(metricsPort);
1318
1361
  const ready = await waitForReady(metricsPort).catch((err) => {
1319
1362
  child?.stop();
1320
1363
  runner?.stop();
@@ -1324,6 +1367,7 @@ ${opts.reason}`);
1324
1367
  err instanceof Error ? err.message : void 0
1325
1368
  );
1326
1369
  });
1370
+ started = true;
1327
1371
  if (child && !shuttingDown) {
1328
1372
  if (dashboard) dashboard.update({ status: "waiting", connections: ready.connections });
1329
1373
  else step(`Waiting for your dev server on :${ctx.port}\u2026`);
@@ -1509,7 +1553,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1509
1553
  });
1510
1554
  }
1511
1555
  if (interactive) {
1512
- const { startDashboard } = await import("./dashboard-GA37Z4H4.js");
1556
+ const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
1513
1557
  dashboard = startDashboard({
1514
1558
  url,
1515
1559
  target,
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/ui/dashboard.tsx
4
+ import { spawn } from "child_process";
4
5
  import "react";
5
- import { Box, render, Text } from "ink";
6
+ import { Box, render, Text, useApp, useInput } from "ink";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
+ var VIEW_ORDER = ["dev", "split", "tunnel"];
7
9
  var MAX_BUFFER = 1e3;
8
10
  var HEADER_ROWS = 5;
9
11
  function statusDot(status) {
@@ -18,36 +20,53 @@ function statusDot(status) {
18
20
  return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CC CONNECTING" });
19
21
  }
20
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
+ }
21
30
  function Header(state) {
22
31
  const latency = state.avgLatencyMs !== void 0 ? ` \u26A1 ${state.avgLatencyMs}ms avg` : "";
32
+ const [h1, h2, h3] = hotkeyHints(state);
23
33
  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 })
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
28
44
  ] }),
29
- /* @__PURE__ */ jsxs(Box, { children: [
45
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
30
46
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
31
47
  "\u2192 ",
32
- state.target
33
- ] }),
34
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
48
+ state.target,
35
49
  " ",
36
50
  state.connections,
37
- " edge conns"
38
- ] })
51
+ " edge conns",
52
+ state.ephemeral ? " \xB7 ephemeral" : ""
53
+ ] }),
54
+ h2 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
55
+ " ",
56
+ h2
57
+ ] }) : null
39
58
  ] }),
40
- /* @__PURE__ */ jsxs(Box, { children: [
59
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
41
60
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
42
61
  "\u2191 ",
43
62
  state.requests,
44
63
  " reqs",
45
64
  latency
46
65
  ] }),
47
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
48
- " ",
49
- state.ephemeral ? "ephemeral \xB7 Ctrl-C stops" : "Ctrl-C stops \xB7 URL preserved"
50
- ] })
66
+ h3 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
67
+ " ",
68
+ h3
69
+ ] }) : null
51
70
  ] })
52
71
  ] });
53
72
  }
@@ -82,51 +101,110 @@ function LogColumn({
82
101
  }
83
102
  function Dashboard(state) {
84
103
  const termRows = process.stdout.rows || 24;
104
+ const view = state.split ? state.view ?? "split" : "tunnel";
85
105
  const titleRows = state.split ? 1 : 0;
86
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
+ }
87
138
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [
88
139
  /* @__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
- ) })
140
+ body
119
141
  ] });
120
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
+ }
121
179
  var ENTER_ALT_SCREEN = "\x1B[?1049h";
122
180
  var LEAVE_ALT_SCREEN = "\x1B[?1049l";
123
- function startDashboard(initial) {
181
+ function startDashboard(initial, actions = {}) {
124
182
  const state = {
125
183
  ...initial,
184
+ view: initial.view ?? (initial.split ? "split" : "tunnel"),
185
+ canRestart: Boolean(actions.restartAll || actions.restartTunnel),
126
186
  tunnelLogs: [...initial.tunnelLogs],
127
187
  devLogs: [...initial.devLogs]
128
188
  };
129
- const mount = () => render(/* @__PURE__ */ jsx(Dashboard, { ...state }), {
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(), {
130
208
  stdout: process.stdout,
131
209
  // We drive graceful shutdown ourselves; ink's Ctrl-C simply resolves
132
210
  // waitUntilExit, and the caller then stops the runner (preserving state).
@@ -136,7 +214,7 @@ function startDashboard(initial) {
136
214
  let instance = mount();
137
215
  let paused = false;
138
216
  const rerender = () => {
139
- if (!paused) instance.rerender(/* @__PURE__ */ jsx(Dashboard, { ...state }));
217
+ if (!paused) instance.rerender(element());
140
218
  };
141
219
  let altActive = true;
142
220
  const leaveAltScreen = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.4",
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": {