@dutodev/cli 1.2.0 → 1.3.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";
@@ -284,7 +285,7 @@ var KEYS = [
284
285
  ["tab", "focus"],
285
286
  ["q", "quit"]
286
287
  ];
287
- function App({ session, version, system, onExit, inspector = null, inspectorError }) {
288
+ function App({ session, version, system, onExit, inspector = null, inspectorError, update = null }) {
288
289
  const subState = useCallback((fn) => session.subscribe(fn), [session]);
289
290
  const subRecords = useCallback((fn) => session.records.subscribe(fn), [session]);
290
291
  const state = useSyncExternalStore(subState, () => session.getState());
@@ -435,7 +436,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
435
436
  });
436
437
  const live = state.tunnels.filter((t) => t.status === "live").length;
437
438
  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));
439
+ const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9) - (update ? 1 : 0));
439
440
  const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
440
441
  const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
441
442
  return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width, children: [
@@ -460,6 +461,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
460
461
  ] }),
461
462
  /* @__PURE__ */ jsx6(Text6, { color: conn.color, children: conn.text })
462
463
  ] }),
464
+ update && /* @__PURE__ */ jsx6(Text6, { color: C.warn, children: updateLine(update) }),
463
465
  /* @__PURE__ */ jsx6(Text6, { children: " " }),
464
466
  /* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
465
467
  inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `inspector ${inspector.url}` }),
@@ -548,7 +550,7 @@ var realSystem = {
548
550
 
549
551
  // src/ui/fullscreen.tsx
550
552
  import { jsx as jsx7 } from "react/jsx-runtime";
551
- function runFullscreen(session, specs, version, inspector, inspectorError) {
553
+ function runFullscreen(session, specs, version, inspector, inspectorError, update = Promise.resolve(null)) {
552
554
  return withFullscreen(
553
555
  process.stdout,
554
556
  (signal) => new Promise((resolve) => {
@@ -559,7 +561,11 @@ function runFullscreen(session, specs, version, inspector, inspectorError) {
559
561
  app.unmount();
560
562
  resolve(code);
561
563
  };
562
- const app = render(/* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, onExit: () => finish(0) }), { exitOnCtrlC: false });
564
+ const view = (latest) => /* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, update: latest, onExit: () => finish(0) });
565
+ const app = render(view(null), { exitOnCtrlC: false });
566
+ void update.then((latest) => {
567
+ if (latest && !done) app.rerender(view(latest));
568
+ });
563
569
  signal.addEventListener("abort", () => void session.stop().then(() => finish(0)), { once: true });
564
570
  session.start(specs).catch(() => void 0);
565
571
  })
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.3.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.3.0",
549
527
  nodeVersion: process.version,
550
528
  os: process.platform,
551
529
  arch: process.arch
@@ -2249,13 +2227,13 @@ async function startProxy(o) {
2249
2227
 
2250
2228
  // src/session/orphans.ts
2251
2229
  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`);
2230
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2231
+ import { join as join4 } from "path";
2232
+ var fileOf = (dir, cfPid) => join4(dir, `cloudflared-${cfPid}.json`);
2255
2233
  function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
2256
2234
  try {
2257
- mkdirSync4(dir, { recursive: true });
2258
- writeFileSync4(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
2235
+ mkdirSync3(dir, { recursive: true });
2236
+ writeFileSync3(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
2259
2237
  } catch {
2260
2238
  }
2261
2239
  }
@@ -2273,7 +2251,7 @@ var realProc = {
2273
2251
  },
2274
2252
  isCloudflared(pid) {
2275
2253
  try {
2276
- if (process.platform === "linux") return readFileSync4(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
2254
+ if (process.platform === "linux") return readFileSync3(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
2277
2255
  if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
2278
2256
  } catch {
2279
2257
  return false;
@@ -2288,14 +2266,14 @@ var realProc = {
2288
2266
  }
2289
2267
  };
2290
2268
  function reapOrphans(dir, proc = realProc) {
2291
- if (!existsSync4(dir)) return 0;
2269
+ if (!existsSync3(dir)) return 0;
2292
2270
  let killed = 0;
2293
2271
  for (const name of readdirSync(dir)) {
2294
2272
  if (!/^cloudflared-\d+\.json$/.test(name)) continue;
2295
- const path = join5(dir, name);
2273
+ const path = join4(dir, name);
2296
2274
  let rec;
2297
2275
  try {
2298
- rec = JSON.parse(readFileSync4(path, "utf8"));
2276
+ rec = JSON.parse(readFileSync3(path, "utf8"));
2299
2277
  } catch {
2300
2278
  rmSync2(path, { force: true });
2301
2279
  continue;
@@ -2686,13 +2664,14 @@ async function runSession(specs, source, flags) {
2686
2664
  const config = loadConfig();
2687
2665
  if (!config.authtoken) fail2(LOGIN_HINT);
2688
2666
  const api = createApiClient({ baseUrl: apiUrl(), token: config.authtoken });
2667
+ const update = checkForUpdate(CLI_VERSION);
2689
2668
  let cloudflaredPath;
2690
2669
  try {
2691
2670
  cloudflaredPath = process.env.DUTO_CLOUDFLARED || await ensureCloudflaredWithProgress((s) => process.stdout.write(s));
2692
2671
  } catch (err) {
2693
2672
  fail2(err instanceof Error ? err.message : String(err));
2694
2673
  }
2695
- const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
2674
+ const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join5(dutoHome(), "run") });
2696
2675
  let inspector = null;
2697
2676
  let inspectorError;
2698
2677
  if (flags.inspect !== false) {
@@ -2703,14 +2682,15 @@ async function runSession(specs, source, flags) {
2703
2682
  }
2704
2683
  }
2705
2684
  if (wantsUi(flags)) {
2706
- const { runFullscreen } = await import("./fullscreen-2I3U6JL7.js");
2707
- const code = await runFullscreen(session, specs, "1.2.0", inspector, inspectorError);
2685
+ const { runFullscreen } = await import("./fullscreen-AI6PC7RL.js");
2686
+ const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update);
2708
2687
  await inspector?.close().catch(() => void 0);
2709
2688
  process.exit(code);
2710
2689
  }
2711
- console.log(`${mark()} ${paint("muted", `${"1.2.0"} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2690
+ console.log(`${mark()} ${paint("muted", `${CLI_VERSION} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2712
2691
  if (inspector) console.log(paint("muted", `inspector: ${inspector.openUrl}`));
2713
2692
  if (inspectorError) console.log(warn(`inspector failed to start: ${inspectorError}`));
2693
+ void update.then((latest) => latest && console.log(warn(updateLine(latest))));
2714
2694
  const stopLog = runPlain(session, (l) => console.log(l));
2715
2695
  const stopped = new Promise((resolve) => {
2716
2696
  const onSignal = () => {
@@ -2836,7 +2816,7 @@ cli.command("status", "Show your account and live tunnels").action(async () => {
2836
2816
  }
2837
2817
  });
2838
2818
  cli.help();
2839
- cli.version("1.2.0");
2819
+ cli.version(CLI_VERSION);
2840
2820
  try {
2841
2821
  cli.parse(process.argv, { run: false });
2842
2822
  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.3.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",