@dutodev/cli 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -203,3 +203,4 @@ The page has the list on the left and the details on the right, with the tabs **
203
203
 
204
204
  - Config lives in `~/.duto/config.json` (mode 600). `DUTO_HOME` changes that folder.
205
205
  - `DUTO_API_URL` points the CLI at another server (default: `https://duto.dev`).
206
+ - Once a day, duto asks npm for the latest version and shows a line when there is a newer one. `DUTO_NO_UPDATE_CHECK=1` turns that off.
@@ -388,6 +388,107 @@ function placeOf(colo) {
388
388
  return entry ? { city: entry[0], country: entry[1] } : null;
389
389
  }
390
390
 
391
+ // ../shared/src/cliVersion.ts
392
+ var CLI_VERSION_HEADER = "x-duto-cli-version";
393
+ var CLI_USER_AGENT = "duto-cli";
394
+ var CLI_UPDATE_COMMAND = "npm i -g @dutodev/cli";
395
+ var VERSION = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
396
+ function parts(version) {
397
+ const m = VERSION.exec(version.trim());
398
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
399
+ }
400
+ function compareVersions(a, b) {
401
+ const x = parts(a);
402
+ const y = parts(b);
403
+ if (!x || !y) return null;
404
+ for (let i = 0; i < 3; i++) {
405
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
406
+ if (d !== 0) return d;
407
+ }
408
+ return 0;
409
+ }
410
+
411
+ // src/config.ts
412
+ import { randomUUID } from "crypto";
413
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
414
+ import { homedir } from "os";
415
+ import { join } from "path";
416
+ function dutoHome() {
417
+ return process.env.DUTO_HOME || join(homedir(), ".duto");
418
+ }
419
+ function configPath() {
420
+ return join(dutoHome(), "config.json");
421
+ }
422
+ function apiUrl() {
423
+ return (process.env.DUTO_API_URL || "https://duto.dev").replace(/\/+$/, "");
424
+ }
425
+ function saveConfig(c) {
426
+ mkdirSync(dutoHome(), { recursive: true, mode: 448 });
427
+ const path = configPath();
428
+ writeFileSync(path, JSON.stringify(c, null, 2) + "\n", { mode: 384 });
429
+ chmodSync(path, 384);
430
+ }
431
+ function loadConfig() {
432
+ const path = configPath();
433
+ if (existsSync(path)) {
434
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
435
+ if (parsed.deviceId) return parsed;
436
+ const fixed = { ...parsed, deviceId: randomUUID() };
437
+ saveConfig(fixed);
438
+ return fixed;
439
+ }
440
+ const fresh = { deviceId: randomUUID() };
441
+ saveConfig(fresh);
442
+ return fresh;
443
+ }
444
+
445
+ // src/updates.ts
446
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
447
+ import { join as join2 } from "path";
448
+ var LATEST_URL = "https://registry.npmjs.org/@dutodev/cli/latest";
449
+ var CHECK_EVERY_MS = 24 * 60 * 60 * 1e3;
450
+ var TIMEOUT_MS = 3e3;
451
+ async function fetchLatest(fetchImpl) {
452
+ const res = await fetchImpl(LATEST_URL, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
453
+ if (!res.ok) return null;
454
+ const { version } = await res.json();
455
+ return typeof version === "string" && compareVersions(version, version) === 0 ? version : null;
456
+ }
457
+ async function availableUpdate(o) {
458
+ const cached = o.read();
459
+ let latest = cached?.latest ?? null;
460
+ if (!cached || o.now - cached.checkedAt >= CHECK_EVERY_MS) {
461
+ latest = await fetchLatest(o.fetchImpl ?? fetch).catch(() => null) ?? latest;
462
+ o.write({ checkedAt: o.now, latest });
463
+ }
464
+ const diff = latest ? compareVersions(latest, o.current) : null;
465
+ return diff !== null && diff > 0 ? latest : null;
466
+ }
467
+ var cachePath = () => join2(dutoHome(), "update.json");
468
+ function readUpdateCache() {
469
+ try {
470
+ const c = JSON.parse(readFileSync2(cachePath(), "utf8"));
471
+ return typeof c.checkedAt === "number" ? { checkedAt: c.checkedAt, latest: typeof c.latest === "string" ? c.latest : null } : null;
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+ function writeUpdateCache(cache) {
477
+ try {
478
+ mkdirSync2(dutoHome(), { recursive: true, mode: 448 });
479
+ writeFileSync2(cachePath(), JSON.stringify(cache) + "\n", { mode: 384 });
480
+ } catch {
481
+ return;
482
+ }
483
+ }
484
+ function checkForUpdate(current) {
485
+ if (process.env.DUTO_NO_UPDATE_CHECK) return Promise.resolve(null);
486
+ return availableUpdate({ current, now: Date.now(), read: readUpdateCache, write: writeUpdateCache }).catch(() => null);
487
+ }
488
+ function updateLine(latest) {
489
+ return `duto ${latest} is available. Update: ${CLI_UPDATE_COMMAND}`;
490
+ }
491
+
391
492
  // src/ui/theme.ts
392
493
  var C = {
393
494
  signal: "#2dd4bf",
@@ -449,6 +550,12 @@ export {
449
550
  normalizeName,
450
551
  validateName,
451
552
  placeOf,
553
+ CLI_VERSION_HEADER,
554
+ CLI_USER_AGENT,
555
+ dutoHome,
556
+ apiUrl,
557
+ saveConfig,
558
+ loadConfig,
452
559
  C,
453
560
  tunnelColor,
454
561
  fit,
@@ -458,5 +565,7 @@ export {
458
565
  formatDuration,
459
566
  formatBytes,
460
567
  formatTime,
461
- localLabel
568
+ localLabel,
569
+ checkForUpdate,
570
+ updateLine
462
571
  };
@@ -12,8 +12,9 @@ import {
12
12
  normalizeName,
13
13
  statusColor,
14
14
  tunnelColor,
15
+ updateLine,
15
16
  validateName
16
- } from "./chunk-KBH2C2UC.js";
17
+ } from "./chunk-4SIBC52A.js";
17
18
 
18
19
  // src/ui/fullscreen.tsx
19
20
  import { render } from "ink";
@@ -75,6 +76,17 @@ function Detail({ record }) {
75
76
  record.replayOf
76
77
  ] }),
77
78
  record.error && /* @__PURE__ */ jsx(Text, { color: C.err, children: record.error }),
79
+ record.redirect && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
80
+ /* @__PURE__ */ jsx(Text, { color: C.signal, children: "\u21AA Location rewritten" }),
81
+ /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
82
+ " ",
83
+ record.redirect.from
84
+ ] }),
85
+ /* @__PURE__ */ jsxs(Text, { children: [
86
+ " \u2192 ",
87
+ record.redirect.to
88
+ ] })
89
+ ] }),
78
90
  /* @__PURE__ */ jsx(Section, { title: "Request", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated, note: bodyNote(record, "req") }),
79
91
  /* @__PURE__ */ jsx(Section, { title: "Response", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated, note: bodyNote(record, "res") }),
80
92
  /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
@@ -168,6 +180,7 @@ function QrView({ tunnel, link, now }) {
168
180
  // src/ui/RequestList.tsx
169
181
  import { Box as Box3, Text as Text4 } from "ink";
170
182
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
183
+ var REWRITTEN = " \u21AA rewritten";
171
184
  function accessLabel(a) {
172
185
  if (!a) return null;
173
186
  if (a.kind === "user") return { text: a.user, color: C.muted };
@@ -179,7 +192,8 @@ function RequestList(props) {
179
192
  const start = Math.max(0, Math.min(selected - Math.floor(height / 2), records.length - height));
180
193
  const visible = records.slice(start, start + height);
181
194
  const accessW = Math.max(0, ...visible.map((r) => (accessLabel(r.access)?.text.length ?? -1) + 1));
182
- const pathW = Math.max(10, width - 54 - accessW);
195
+ const redirectW = visible.some((r) => r.redirect) ? REWRITTEN.length : 0;
196
+ const pathW = Math.max(10, width - 54 - accessW - redirectW);
183
197
  const title = props.filter ? `REQUESTS \xB7 filter: ${props.filter} ` : "REQUESTS ";
184
198
  return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
185
199
  /* @__PURE__ */ jsx4(Text4, { color: C.muted, children: title + "\u2500".repeat(Math.max(0, width - title.length)) }),
@@ -203,7 +217,8 @@ function RequestList(props) {
203
217
  /* @__PURE__ */ jsx4(Text4, { color: C.muted, children: tail.padStart(7) }),
204
218
  " ",
205
219
  /* @__PURE__ */ jsx4(Text4, { color: C.muted, children: r.websocket ? "" : formatBytes(r.bytes).padStart(8) }),
206
- acc ? /* @__PURE__ */ jsx4(Text4, { color: acc.color, children: ` ${acc.text}` }) : null
220
+ acc ? /* @__PURE__ */ jsx4(Text4, { color: acc.color, children: ` ${acc.text}` }) : null,
221
+ r.redirect ? /* @__PURE__ */ jsx4(Text4, { color: C.signal, children: REWRITTEN }) : null
207
222
  ] }, r.id);
208
223
  })
209
224
  ] });
@@ -284,7 +299,7 @@ var KEYS = [
284
299
  ["tab", "focus"],
285
300
  ["q", "quit"]
286
301
  ];
287
- function App({ session, version, system, onExit, inspector = null, inspectorError }) {
302
+ function App({ session, version, system, onExit, inspector = null, inspectorError, update = null }) {
288
303
  const subState = useCallback((fn) => session.subscribe(fn), [session]);
289
304
  const subRecords = useCallback((fn) => session.records.subscribe(fn), [session]);
290
305
  const state = useSyncExternalStore(subState, () => session.getState());
@@ -435,7 +450,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
435
450
  });
436
451
  const live = state.tunnels.filter((t) => t.status === "live").length;
437
452
  const conn = state.connection === "online" ? { text: "connected \u25CF", color: C.signal } : state.connection === "connecting" ? { text: "connecting\u2026", color: C.muted } : { text: "reconnecting\u2026", color: C.warn };
438
- const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9));
453
+ const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9) - (update ? 1 : 0));
439
454
  const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
440
455
  const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
441
456
  return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width, children: [
@@ -460,6 +475,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
460
475
  ] }),
461
476
  /* @__PURE__ */ jsx6(Text6, { color: conn.color, children: conn.text })
462
477
  ] }),
478
+ update && /* @__PURE__ */ jsx6(Text6, { color: C.warn, children: updateLine(update) }),
463
479
  /* @__PURE__ */ jsx6(Text6, { children: " " }),
464
480
  /* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
465
481
  inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `inspector ${inspector.url}` }),
@@ -548,7 +564,7 @@ var realSystem = {
548
564
 
549
565
  // src/ui/fullscreen.tsx
550
566
  import { jsx as jsx7 } from "react/jsx-runtime";
551
- function runFullscreen(session, specs, version, inspector, inspectorError) {
567
+ function runFullscreen(session, specs, version, inspector, inspectorError, update = Promise.resolve(null)) {
552
568
  return withFullscreen(
553
569
  process.stdout,
554
570
  (signal) => new Promise((resolve) => {
@@ -559,7 +575,11 @@ function runFullscreen(session, specs, version, inspector, inspectorError) {
559
575
  app.unmount();
560
576
  resolve(code);
561
577
  };
562
- const app = render(/* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, onExit: () => finish(0) }), { exitOnCtrlC: false });
578
+ const view = (latest) => /* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, update: latest, onExit: () => finish(0) });
579
+ const app = render(view(null), { exitOnCtrlC: false });
580
+ void update.then((latest) => {
581
+ if (latest && !done) app.rerender(view(latest));
582
+ });
563
583
  signal.addEventListener("abort", () => void session.stop().then(() => finish(0)), { once: true });
564
584
  session.start(specs).catch(() => void 0);
565
585
  })
package/dist/index.js CHANGED
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  C,
4
+ CLI_USER_AGENT,
5
+ CLI_VERSION_HEADER,
4
6
  MAX_NAME,
5
7
  TRUNCATED_NOTE,
8
+ apiUrl,
6
9
  bodyNote,
10
+ checkForUpdate,
11
+ dutoHome,
7
12
  formatBytes,
8
13
  formatDuration,
9
14
  formatTime,
15
+ loadConfig,
10
16
  localLabel,
11
17
  normalizeName,
12
18
  placeOf,
19
+ saveConfig,
13
20
  statusColor,
21
+ updateLine,
14
22
  validateName
15
- } from "./chunk-KBH2C2UC.js";
23
+ } from "./chunk-4SIBC52A.js";
16
24
 
17
25
  // src/ui/brand.ts
18
26
  function colorOn(stream = process.stdout) {
@@ -38,9 +46,12 @@ var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
38
46
 
39
47
  // src/index.ts
40
48
  import { hostname as hostname2 } from "os";
41
- import { join as join6, relative } from "path";
49
+ import { join as join5, relative } from "path";
42
50
  import cac from "cac";
43
51
 
52
+ // src/version.ts
53
+ var CLI_VERSION = "1.4.0";
54
+
44
55
  // src/api.ts
45
56
  var DutoApiError = class extends Error {
46
57
  constructor(message2, status, code) {
@@ -71,7 +82,8 @@ function createApiClient(o) {
71
82
  ...o.token === void 0 ? {} : { authorization: `Bearer ${o.token}` },
72
83
  ...body === void 0 ? {} : { "content-type": "application/json" },
73
84
  ...bookmark === null ? {} : { [BOOKMARK_HEADER]: bookmark },
74
- "user-agent": "duto-cli"
85
+ "user-agent": CLI_USER_AGENT,
86
+ [CLI_VERSION_HEADER]: CLI_VERSION
75
87
  },
76
88
  body: body === void 0 ? void 0 : JSON.stringify(body)
77
89
  });
@@ -118,8 +130,8 @@ function createApiClient(o) {
118
130
 
119
131
  // src/cloudflared.ts
120
132
  import { createHash } from "crypto";
121
- import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
122
- import { join as join2 } from "path";
133
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "fs";
134
+ import { join } from "path";
123
135
  import { extract } from "tar";
124
136
 
125
137
  // src/checksums.ts
@@ -132,40 +144,6 @@ var CHECKSUMS = {
132
144
  "cloudflared-windows-amd64.exe": "2493dbc168923bb3e59e5e7716f71deced92992eea9acd6e9d74f84ac7751343"
133
145
  };
134
146
 
135
- // src/config.ts
136
- import { randomUUID } from "crypto";
137
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
138
- import { homedir } from "os";
139
- import { join } from "path";
140
- function dutoHome() {
141
- return process.env.DUTO_HOME || join(homedir(), ".duto");
142
- }
143
- function configPath() {
144
- return join(dutoHome(), "config.json");
145
- }
146
- function apiUrl() {
147
- return (process.env.DUTO_API_URL || "https://duto.dev").replace(/\/+$/, "");
148
- }
149
- function saveConfig(c) {
150
- mkdirSync(dutoHome(), { recursive: true, mode: 448 });
151
- const path = configPath();
152
- writeFileSync(path, JSON.stringify(c, null, 2) + "\n", { mode: 384 });
153
- chmodSync(path, 384);
154
- }
155
- function loadConfig() {
156
- const path = configPath();
157
- if (existsSync(path)) {
158
- const parsed = JSON.parse(readFileSync(path, "utf8"));
159
- if (parsed.deviceId) return parsed;
160
- const fixed = { ...parsed, deviceId: randomUUID() };
161
- saveConfig(fixed);
162
- return fixed;
163
- }
164
- const fresh = { deviceId: randomUUID() };
165
- saveConfig(fresh);
166
- return fresh;
167
- }
168
-
169
147
  // src/cloudflared.ts
170
148
  function assetFor(platform, arch) {
171
149
  const key = `${platform}/${arch}`;
@@ -190,11 +168,11 @@ function downloadUrl(asset, version = CLOUDFLARED_VERSION) {
190
168
  async function ensureCloudflared(opts = {}) {
191
169
  const platform = opts.platform ?? process.platform;
192
170
  const { asset, kind } = assetFor(platform, opts.arch ?? process.arch);
193
- const binDir = join2(dutoHome(), "bin");
194
- const target = join2(binDir, `cloudflared-${CLOUDFLARED_VERSION}${platform === "win32" ? ".exe" : ""}`);
195
- if (existsSync2(target)) return target;
171
+ const binDir = join(dutoHome(), "bin");
172
+ const target = join(binDir, `cloudflared-${CLOUDFLARED_VERSION}${platform === "win32" ? ".exe" : ""}`);
173
+ if (existsSync(target)) return target;
196
174
  opts.onDownload?.();
197
- mkdirSync2(binDir, { recursive: true });
175
+ mkdirSync(binDir, { recursive: true });
198
176
  const res = await (opts.fetchImpl ?? fetch)(downloadUrl(asset));
199
177
  if (!res.ok) throw new Error(`Could not download cloudflared (HTTP ${res.status}).`);
200
178
  const bytes = Buffer.from(await res.arrayBuffer());
@@ -204,22 +182,22 @@ async function ensureCloudflared(opts = {}) {
204
182
  rmSync(target, { force: true });
205
183
  throw new Error("cloudflared checksum mismatch. Try again; if it keeps failing, update duto.");
206
184
  }
207
- const tmp = mkdtempSync(join2(binDir, ".download-"));
185
+ const tmp = mkdtempSync(join(binDir, ".download-"));
208
186
  try {
209
187
  if (kind === "tgz") {
210
- const tgz = join2(tmp, asset);
211
- writeFileSync2(tgz, bytes);
188
+ const tgz = join(tmp, asset);
189
+ writeFileSync(tgz, bytes);
212
190
  await extract({ file: tgz, cwd: tmp });
213
- renameSync(join2(tmp, "cloudflared"), target);
191
+ renameSync(join(tmp, "cloudflared"), target);
214
192
  } else {
215
- const file = join2(tmp, "cloudflared");
216
- writeFileSync2(file, bytes);
193
+ const file = join(tmp, "cloudflared");
194
+ writeFileSync(file, bytes);
217
195
  renameSync(file, target);
218
196
  }
219
197
  } finally {
220
198
  rmSync(tmp, { recursive: true, force: true });
221
199
  }
222
- if (platform !== "win32") chmodSync2(target, 493);
200
+ if (platform !== "win32") chmodSync(target, 493);
223
201
  return target;
224
202
  }
225
203
  async function ensureCloudflaredWithProgress(write, opts = {}) {
@@ -241,9 +219,9 @@ async function ensureCloudflaredWithProgress(write, opts = {}) {
241
219
  }
242
220
 
243
221
  // src/session/allowlist.ts
244
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
222
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
245
223
  import { BlockList, isIP } from "net";
246
- import { dirname, join as join3 } from "path";
224
+ import { dirname, join as join2 } from "path";
247
225
 
248
226
  // src/session/allowlist-snapshot.ts
249
227
  var ALLOW_SNAPSHOT = {
@@ -312,7 +290,7 @@ function blockListOf(cidrs) {
312
290
  }
313
291
  function readCache(file) {
314
292
  try {
315
- const c = JSON.parse(readFileSync2(file, "utf8"));
293
+ const c = JSON.parse(readFileSync(file, "utf8"));
316
294
  if (typeof c?.fetchedAt === "number" && c.lists && typeof c.lists === "object") return c;
317
295
  } catch {
318
296
  }
@@ -329,7 +307,7 @@ async function fetchRanges(name, f) {
329
307
  return out.map((x) => x.includes("/") ? x : `${x}/${splitCidr(x).prefix}`);
330
308
  }
331
309
  async function resolveAllow(entries, o = {}) {
332
- const cacheFile = o.cacheFile ?? join3(dutoHome(), "allowlist.json");
310
+ const cacheFile = o.cacheFile ?? join2(dutoHome(), "allowlist.json");
333
311
  const f = o.fetch ?? fetch;
334
312
  const now = (o.now ?? Date.now)();
335
313
  const parsed = entries.map((raw) => ({ raw, e: parseAllowEntry(raw) }));
@@ -352,8 +330,8 @@ async function resolveAllow(entries, o = {}) {
352
330
  const everyListFromNetwork = names.length > 0 && Object.keys(net).length === names.length;
353
331
  if (everyListFromNetwork) {
354
332
  try {
355
- mkdirSync3(dirname(cacheFile), { recursive: true });
356
- writeFileSync3(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
333
+ mkdirSync2(dirname(cacheFile), { recursive: true });
334
+ writeFileSync2(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
357
335
  } catch {
358
336
  }
359
337
  }
@@ -375,8 +353,8 @@ async function resolveAllow(entries, o = {}) {
375
353
  }
376
354
 
377
355
  // src/session/config.ts
378
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
379
- import { dirname as dirname2, join as join4 } from "path";
356
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
357
+ import { dirname as dirname2, join as join3 } from "path";
380
358
  import { isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
381
359
  var ConfigError = class extends Error {
382
360
  constructor(message2, line, file = CONFIG_FILES[0]) {
@@ -522,13 +500,13 @@ function parseConfig(text, file = CONFIG_FILES[0], env = process.env) {
522
500
  function findConfig(cwd) {
523
501
  let dir = cwd;
524
502
  for (; ; ) {
525
- const present = CONFIG_FILES.filter((f) => existsSync3(join4(dir, f)));
503
+ const present = CONFIG_FILES.filter((f) => existsSync2(join3(dir, f)));
526
504
  if (present.length > 1) throw new ConfigError(`${CONFIG_FILES.join(" and ")} in the same folder. Delete one of them.`, void 0, "");
527
505
  if (present.length === 1) {
528
- const path = join4(dir, present[0]);
529
- return { path, tunnels: parseConfig(readFileSync3(path, "utf8"), present[0]) };
506
+ const path = join3(dir, present[0]);
507
+ return { path, tunnels: parseConfig(readFileSync2(path, "utf8"), present[0]) };
530
508
  }
531
- if (existsSync3(join4(dir, ".git"))) return null;
509
+ if (existsSync2(join3(dir, ".git"))) return null;
532
510
  const up = dirname2(dir);
533
511
  if (up === dir) return null;
534
512
  dir = up;
@@ -545,7 +523,7 @@ async function browserLogin(o) {
545
523
  const start = await api.startLogin({
546
524
  deviceId: o.deviceId,
547
525
  deviceName: hostname(),
548
- cliVersion: "1.2.0",
526
+ cliVersion: "1.4.0",
549
527
  nodeVersion: process.version,
550
528
  os: process.platform,
551
529
  arch: process.arch
@@ -777,6 +755,8 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
777
755
  .row .dur { text-align: right; }
778
756
  .m { font-weight: 700; }
779
757
  .mut { color: ${C.muted}; }
758
+ .sig { color: ${C.signal}; }
759
+ .redirect { margin-top: 6px; display: grid; gap: 2px; }
780
760
  .empty { padding: 12px; color: ${C.muted}; }
781
761
  .detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
782
762
  .pane { flex: 1; }
@@ -913,7 +893,12 @@ function renderList() {
913
893
  row.setAttribute("role", "option");
914
894
  row.setAttribute("aria-selected", it.id === state.selected ? "true" : "false");
915
895
  row.dataset.id = String(it.id);
916
- row.appendChild(el("span", "mut", it.replayOf !== undefined ? "↻" : ""));
896
+ if (it.replayOf !== undefined) row.appendChild(el("span", "mut", "↻"));
897
+ else if (it.rewritten) {
898
+ var mark = el("span", "sig", "↪");
899
+ mark.title = "Location rewritten";
900
+ row.appendChild(mark);
901
+ } else row.appendChild(el("span", "mut", ""));
917
902
  if (multi) row.appendChild(el("span", "mut", it.tunnel));
918
903
  row.appendChild(el("span", "m", methodOf(it)));
919
904
  var p = el("span", "", it.path);
@@ -995,6 +980,13 @@ function renderDetail() {
995
980
  if (d.replayOf !== undefined) parts.push("↻ replay of #" + d.replayOf);
996
981
  head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
997
982
  if (d.error) head.appendChild(el("div", "msg err", d.error));
983
+ if (d.redirect) {
984
+ var moved = el("div", "redirect");
985
+ moved.appendChild(el("div", "sig", "↪ Location rewritten"));
986
+ moved.appendChild(el("div", "mut", d.redirect.from));
987
+ moved.appendChild(el("div", "", "→ " + d.redirect.to));
988
+ head.appendChild(moved);
989
+ }
998
990
 
999
991
  var tabs = document.querySelectorAll(".tab");
1000
992
  for (var i = 0; i < tabs.length; i++) {
@@ -1370,7 +1362,8 @@ function listItem(r) {
1370
1362
  startedAt: r.startedAt,
1371
1363
  websocket: r.websocket,
1372
1364
  ...r.replayOf !== void 0 && { replayOf: r.replayOf },
1373
- ...r.error !== void 0 && { error: r.error }
1365
+ ...r.error !== void 0 && { error: r.error },
1366
+ ...r.redirect !== void 0 && { rewritten: true }
1374
1367
  };
1375
1368
  }
1376
1369
  function detail(r, session) {
@@ -1386,6 +1379,7 @@ function detail(r, session) {
1386
1379
  return {
1387
1380
  ...listItem(r),
1388
1381
  edge: edgeOf(r.reqHeaders),
1382
+ redirect: r.redirect ?? null,
1389
1383
  reqHeaders: r.reqHeaders,
1390
1384
  resHeaders: r.resHeaders,
1391
1385
  reqBody: r.reqBody,
@@ -2160,7 +2154,12 @@ async function startProxy(o) {
2160
2154
  return;
2161
2155
  }
2162
2156
  if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
2163
- up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
2157
+ const from = up.headers.location;
2158
+ const to = rewriteLocation(from, route.host, hostOf(req));
2159
+ if (to !== from) {
2160
+ up.headers.location = to;
2161
+ o.store.update(id, { redirect: { from, to } });
2162
+ }
2164
2163
  }
2165
2164
  res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
2166
2165
  trackResponse(id, started, up, reqCap);
@@ -2249,13 +2248,13 @@ async function startProxy(o) {
2249
2248
 
2250
2249
  // src/session/orphans.ts
2251
2250
  import { spawnSync } from "child_process";
2252
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
2253
- import { join as join5 } from "path";
2254
- var fileOf = (dir, cfPid) => join5(dir, `cloudflared-${cfPid}.json`);
2251
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2252
+ import { join as join4 } from "path";
2253
+ var fileOf = (dir, cfPid) => join4(dir, `cloudflared-${cfPid}.json`);
2255
2254
  function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
2256
2255
  try {
2257
- mkdirSync4(dir, { recursive: true });
2258
- writeFileSync4(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
2256
+ mkdirSync3(dir, { recursive: true });
2257
+ writeFileSync3(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
2259
2258
  } catch {
2260
2259
  }
2261
2260
  }
@@ -2273,7 +2272,7 @@ var realProc = {
2273
2272
  },
2274
2273
  isCloudflared(pid) {
2275
2274
  try {
2276
- if (process.platform === "linux") return readFileSync4(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
2275
+ if (process.platform === "linux") return readFileSync3(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
2277
2276
  if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
2278
2277
  } catch {
2279
2278
  return false;
@@ -2288,14 +2287,14 @@ var realProc = {
2288
2287
  }
2289
2288
  };
2290
2289
  function reapOrphans(dir, proc = realProc) {
2291
- if (!existsSync4(dir)) return 0;
2290
+ if (!existsSync3(dir)) return 0;
2292
2291
  let killed = 0;
2293
2292
  for (const name of readdirSync(dir)) {
2294
2293
  if (!/^cloudflared-\d+\.json$/.test(name)) continue;
2295
- const path = join5(dir, name);
2294
+ const path = join4(dir, name);
2296
2295
  let rec;
2297
2296
  try {
2298
- rec = JSON.parse(readFileSync4(path, "utf8"));
2297
+ rec = JSON.parse(readFileSync3(path, "utf8"));
2299
2298
  } catch {
2300
2299
  rmSync2(path, { force: true });
2301
2300
  continue;
@@ -2623,7 +2622,7 @@ function runPlain(session, out) {
2623
2622
  printed.add(r.id);
2624
2623
  const method = r.websocket ? "WS" : r.method;
2625
2624
  out(
2626
- `${formatTime(r.startedAt)} ${r.tunnel.padEnd(8)} ${method} ${r.path} ${paint(statusColor(r.status), String(r.status))} ${formatDuration(r.durationMs)} ${formatBytes(r.bytes)}`
2625
+ `${formatTime(r.startedAt)} ${r.tunnel.padEnd(8)} ${method} ${r.path} ${paint(statusColor(r.status), String(r.status))} ${formatDuration(r.durationMs)} ${formatBytes(r.bytes)}${r.redirect ? ` ${paint("signal", `\u21AA Location rewritten to ${r.redirect.to}`)}` : ""}`
2627
2626
  );
2628
2627
  }
2629
2628
  };
@@ -2686,13 +2685,14 @@ async function runSession(specs, source, flags) {
2686
2685
  const config = loadConfig();
2687
2686
  if (!config.authtoken) fail2(LOGIN_HINT);
2688
2687
  const api = createApiClient({ baseUrl: apiUrl(), token: config.authtoken });
2688
+ const update = checkForUpdate(CLI_VERSION);
2689
2689
  let cloudflaredPath;
2690
2690
  try {
2691
2691
  cloudflaredPath = process.env.DUTO_CLOUDFLARED || await ensureCloudflaredWithProgress((s) => process.stdout.write(s));
2692
2692
  } catch (err) {
2693
2693
  fail2(err instanceof Error ? err.message : String(err));
2694
2694
  }
2695
- const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
2695
+ const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join5(dutoHome(), "run") });
2696
2696
  let inspector = null;
2697
2697
  let inspectorError;
2698
2698
  if (flags.inspect !== false) {
@@ -2703,14 +2703,15 @@ async function runSession(specs, source, flags) {
2703
2703
  }
2704
2704
  }
2705
2705
  if (wantsUi(flags)) {
2706
- const { runFullscreen } = await import("./fullscreen-2I3U6JL7.js");
2707
- const code = await runFullscreen(session, specs, "1.2.0", inspector, inspectorError);
2706
+ const { runFullscreen } = await import("./fullscreen-MZMQ6ADN.js");
2707
+ const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update);
2708
2708
  await inspector?.close().catch(() => void 0);
2709
2709
  process.exit(code);
2710
2710
  }
2711
- console.log(`${mark()} ${paint("muted", `${"1.2.0"} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2711
+ console.log(`${mark()} ${paint("muted", `${CLI_VERSION} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2712
2712
  if (inspector) console.log(paint("muted", `inspector: ${inspector.openUrl}`));
2713
2713
  if (inspectorError) console.log(warn(`inspector failed to start: ${inspectorError}`));
2714
+ void update.then((latest) => latest && console.log(warn(updateLine(latest))));
2714
2715
  const stopLog = runPlain(session, (l) => console.log(l));
2715
2716
  const stopped = new Promise((resolve) => {
2716
2717
  const onSignal = () => {
@@ -2836,7 +2837,7 @@ cli.command("status", "Show your account and live tunnels").action(async () => {
2836
2837
  }
2837
2838
  });
2838
2839
  cli.help();
2839
- cli.version("1.2.0");
2840
+ cli.version(CLI_VERSION);
2840
2841
  try {
2841
2842
  cli.parse(process.argv, { run: false });
2842
2843
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Expose a local port at https://<name>-<user>.duto.dev with one command.",
5
5
  "license": "MIT",
6
6
  "type": "module",