@dutodev/cli 1.1.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.
@@ -0,0 +1,571 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../shared/src/names.ts
4
+ var EMAIL_SENDING_SUBDOMAINS = ["send"];
5
+ var RESERVED_NAMES = [
6
+ "www",
7
+ "api",
8
+ "app",
9
+ "dash",
10
+ "admin",
11
+ "docs",
12
+ "status",
13
+ "mail",
14
+ "custom",
15
+ "cname",
16
+ "duto",
17
+ "auth",
18
+ "login",
19
+ "signup",
20
+ "invite",
21
+ "static",
22
+ "assets",
23
+ "cdn",
24
+ "help",
25
+ "support",
26
+ "blog",
27
+ ...EMAIL_SENDING_SUBDOMAINS
28
+ ];
29
+ var MAX_NAME = 40;
30
+ var FORMAT = /^[a-z0-9]+$/;
31
+ function normalizeName(input) {
32
+ return input.trim().toLowerCase();
33
+ }
34
+ function validateName(name, max = MAX_NAME) {
35
+ if (name.length === 0 || name.length > max) return { ok: false, reason: "length" };
36
+ if (!FORMAT.test(name)) return { ok: false, reason: "format" };
37
+ if (RESERVED_NAMES.includes(name)) return { ok: false, reason: "reserved" };
38
+ return { ok: true };
39
+ }
40
+
41
+ // ../shared/src/places.ts
42
+ var PLACES = {
43
+ AAE: ["Annaba", "Algeria"],
44
+ ABJ: ["Abidjan", "C\xF4te d\u2019Ivoire"],
45
+ ABQ: ["Albuquerque", "United States"],
46
+ ACC: ["Accra", "Ghana"],
47
+ ACX: ["Xingyi", "China"],
48
+ ADB: ["Izmir", "Turkey"],
49
+ ADD: ["Addis Ababa", "Ethiopia"],
50
+ ADL: ["Adelaide", "Australia"],
51
+ AGR: ["Agra", "India"],
52
+ AIP: ["Jalandhar", "India"],
53
+ AKL: ["Auckland", "New Zealand"],
54
+ AKX: ["Aktobe", "Kazakhstan"],
55
+ ALA: ["Almaty", "Kazakhstan"],
56
+ ALG: ["Algiers", "Algeria"],
57
+ AMD: ["Ahmedabad", "India"],
58
+ AMM: ["Amman", "Jordan"],
59
+ AMS: ["Amsterdam", "Netherlands"],
60
+ ANC: ["Anchorage", "United States"],
61
+ ARI: ["Arica", "Chile"],
62
+ ARN: ["Stockholm", "Sweden"],
63
+ ARU: ["Aracatuba", "Brazil"],
64
+ ASK: ["Yamoussoukro", "C\xF4te d\u2019Ivoire"],
65
+ ASU: ["Asunci\xF3n", "Paraguay"],
66
+ ATH: ["Athens", "Greece"],
67
+ ATL: ["Atlanta", "United States"],
68
+ AUS: ["Austin", "United States"],
69
+ AVA: ["Anshun", "China"],
70
+ BAH: ["Manama", "Bahrain"],
71
+ BAQ: ["Barranquilla", "Colombia"],
72
+ BBI: ["Bhubaneswar", "India"],
73
+ BCN: ["Barcelona", "Spain"],
74
+ BDQ: ["Jamnagar", "India"],
75
+ BEG: ["Belgrade", "Serbia"],
76
+ BEL: ["Bel\xE9m", "Brazil"],
77
+ BEY: ["Beirut", "Lebanon"],
78
+ BGI: ["Bridgetown", "Barbados"],
79
+ BGR: ["Bangor", "United States"],
80
+ BGW: ["Baghdad", "Iraq"],
81
+ BKK: ["Bangkok", "Thailand"],
82
+ BLR: ["Bangalore", "India"],
83
+ BNA: ["Nashville", "United States"],
84
+ BNE: ["Brisbane", "Australia"],
85
+ BOD: ["Bordeaux", "France"],
86
+ BOG: ["Bogot\xE1", "Colombia"],
87
+ BOM: ["Mumbai", "India"],
88
+ BOS: ["Boston", "United States"],
89
+ BRU: ["Brussels", "Belgium"],
90
+ BSB: ["Brasilia", "Brazil"],
91
+ BSR: ["Basra", "Iraq"],
92
+ BTS: ["Bratislava", "Slovakia"],
93
+ BUD: ["Budapest", "Hungary"],
94
+ BWN: ["Bandar Seri Begawan", "Brunei"],
95
+ CAI: ["Cairo", "Egypt"],
96
+ CAN: ["Guangzhou", "China"],
97
+ CAW: ["Campos dos Goytacazes", "Brazil"],
98
+ CBR: ["Canberra", "Australia"],
99
+ CCU: ["Kolkata", "India"],
100
+ CDG: ["Paris", "France"],
101
+ CEB: ["Cebu", "Philippines"],
102
+ CFC: ["Cacador", "Brazil"],
103
+ CGB: ["Cuiaba", "Brazil"],
104
+ CGD: ["Changde", "China"],
105
+ CGK: ["Jakarta", "Indonesia"],
106
+ CGO: ["Zhengzhou", "China"],
107
+ CGP: ["Chittagong", "Bangladesh"],
108
+ CGY: ["Cagayan de Oro", "Philippines"],
109
+ CHC: ["Christchurch", "New Zealand"],
110
+ CJB: ["Coimbatore", "India"],
111
+ CKG: ["Chongqing", "China"],
112
+ CLE: ["Cleveland", "United States"],
113
+ CLO: ["Cali", "Colombia"],
114
+ CLT: ["Charlotte", "United States"],
115
+ CMB: ["Colombo", "Sri Lanka"],
116
+ CMH: ["Columbus", "United States"],
117
+ CNF: ["Belo Horizonte", "Brazil"],
118
+ CNN: ["Kannur", "India"],
119
+ CNX: ["Chiang Mai", "Thailand"],
120
+ COK: ["Kochi", "India"],
121
+ COR: ["C\xF3rdoba", "Argentina"],
122
+ CPH: ["Copenhagen", "Denmark"],
123
+ CPT: ["Cape Town", "South Africa"],
124
+ CRK: ["Tarlac City", "Philippines"],
125
+ CSX: ["Changsha", "China"],
126
+ CTU: ["Chengdu", "China"],
127
+ CVG: ["Cincinnati", "United States"],
128
+ CWB: ["Curitiba", "Brazil"],
129
+ CZL: ["Constantine", "Algeria"],
130
+ CZX: ["Changzhou", "China"],
131
+ DAC: ["Dhaka", "Bangladesh"],
132
+ DAD: ["Da Nang", "Vietnam"],
133
+ DAR: ["Dar es Salaam", "Tanzania"],
134
+ DEL: ["New Delhi", "India"],
135
+ DEN: ["Denver", "United States"],
136
+ DFW: ["Dallas", "United States"],
137
+ DKR: ["Dakar", "Senegal"],
138
+ DLA: ["Douala", "Cameroon"],
139
+ DLC: ["Dalian", "China"],
140
+ DME: ["Moscow", "Russia"],
141
+ DMM: ["Dammam", "Saudi Arabia"],
142
+ DOH: ["Doha", "Qatar"],
143
+ DPS: ["Denpasar", "Indonesia"],
144
+ DTW: ["Detroit", "United States"],
145
+ DUB: ["Dublin", "Ireland"],
146
+ DUR: ["Durban", "South Africa"],
147
+ DUS: ["D\xFCsseldorf", "Germany"],
148
+ DXB: ["Dubai", "United Arab Emirates"],
149
+ DYU: ["Dushanbe", "Tajikistan"],
150
+ EBB: ["Kampala", "Uganda"],
151
+ EBL: ["Erbil", "Iraq"],
152
+ EVN: ["Yerevan", "Armenia"],
153
+ EWR: ["Newark", "United States"],
154
+ EZE: ["Buenos Aires", "Argentina"],
155
+ FCO: ["Rome", "Italy"],
156
+ FLN: ["Florianopolis", "Brazil"],
157
+ FOC: ["Fuzhou", "China"],
158
+ FOR: ["Fortaleza", "Brazil"],
159
+ FRA: ["Frankfurt", "Germany"],
160
+ FRU: ["Bishkek", "Kyrgyzstan"],
161
+ FSD: ["Sioux Falls", "United States"],
162
+ FUK: ["Fukuoka", "Japan"],
163
+ FUO: ["Foshan", "China"],
164
+ GBE: ["Gaborone", "Botswana"],
165
+ GDL: ["Guadalajara", "Mexico"],
166
+ GEO: ["Georgetown", "Guyana"],
167
+ GIG: ["Rio de Janeiro", "Brazil"],
168
+ GND: ["St. George's", "Grenada"],
169
+ GOT: ["Gothenburg", "Sweden"],
170
+ GRU: ["S\xE3o Paulo", "Brazil"],
171
+ GUA: ["Guatemala City", "Guatemala"],
172
+ GUM: ["Hagatna", "Guam"],
173
+ GVA: ["Geneva", "Switzerland"],
174
+ GYD: ["Baku", "Azerbaijan"],
175
+ GYE: ["Guayaquil", "Ecuador"],
176
+ GYN: ["Goiania", "Brazil"],
177
+ HAK: ["Chengmai", "China"],
178
+ HAM: ["Hamburg", "Germany"],
179
+ HAN: ["Hanoi", "Vietnam"],
180
+ HBA: ["Hobart", "Australia"],
181
+ HEL: ["Helsinki", "Finland"],
182
+ HFA: ["Haifa", "Israel"],
183
+ HGH: ["Shaoxing", "China"],
184
+ HKG: ["Hong Kong", null],
185
+ HNL: ["Honolulu", "United States"],
186
+ HRE: ["Harare", "Zimbabwe"],
187
+ HYD: ["Hyderabad", "India"],
188
+ HYN: ["Taizhou", "China"],
189
+ IAD: ["Ashburn", "United States"],
190
+ IAH: ["Houston", "United States"],
191
+ ICN: ["Seoul", "South Korea"],
192
+ IND: ["Indianapolis", "United States"],
193
+ ISB: ["Islamabad", "Pakistan"],
194
+ IST: ["Istanbul", "Turkey"],
195
+ ISU: ["Sulaymaniyah", "Iraq"],
196
+ IXC: ["Chandigarh", "India"],
197
+ JAI: ["\u201CJaipur\u201D", "India"],
198
+ JAX: ["Jacksonville", "United States"],
199
+ JDO: ["Juazeiro do Norte", "Brazil"],
200
+ JED: ["Jeddah", "Saudi Arabia"],
201
+ JHB: ["Johor Bahru", "Malaysia"],
202
+ JIB: ["Djibouti", "Djibouti"],
203
+ JNB: ["Johannesburg", "South Africa"],
204
+ JOG: ["Yogyakarta", "Indonesia"],
205
+ JOI: ["Joinville", "Brazil"],
206
+ JRG: ["Sambalpur", "India"],
207
+ JXG: ["Jiaxing", "China"],
208
+ KBP: ["Kyiv", "Ukraine"],
209
+ KCH: ["Kuching", "Malaysia"],
210
+ KEF: ["Reykjav\xEDk", "Iceland"],
211
+ KGL: ["Kigali", "Rwanda"],
212
+ KHH: ["Kaohsiung City", null],
213
+ KHI: ["Karachi", "Pakistan"],
214
+ KHN: ["Nanchang", "China"],
215
+ KIN: ["Kingston", "Jamaica"],
216
+ KIV: ["Chi\u0219in\u0103u", "Moldova"],
217
+ KIX: ["Osaka", "Japan"],
218
+ KMG: ["Kunming", "China"],
219
+ KNU: ["Kanpur", "India"],
220
+ KTM: ["Kathmandu", "Nepal"],
221
+ KUL: ["Kuala Lumpur", "Malaysia"],
222
+ KWE: ["Guiyang", "China"],
223
+ KWI: ["Kuwait City", "Kuwait"],
224
+ LAD: ["Luanda", "Angola"],
225
+ LAS: ["Las Vegas", "United States"],
226
+ LAX: ["Los Angeles", "United States"],
227
+ LCA: ["Nicosia", "Cyprus"],
228
+ LED: ["Saint Petersburg", "Russia"],
229
+ LHE: ["Lahore", "Pakistan"],
230
+ LHR: ["London", "United Kingdom"],
231
+ LHW: ["Lanzhou", "China"],
232
+ LIM: ["Lima", "Peru"],
233
+ LIS: ["Lisbon", "Portugal"],
234
+ LJU: ["Ljubljana", "Slovenia"],
235
+ LLK: ["Astara", "Azerbaijan"],
236
+ LLW: ["Lilongwe", "Malawi"],
237
+ LOS: ["Lagos", "Nigeria"],
238
+ LPB: ["La Paz", "Bolivia"],
239
+ LUH: ["Ludhiana", "India"],
240
+ LUN: ["Lusaka", "Zambia"],
241
+ LUX: ["Luxembourg City", "Luxembourg"],
242
+ LYA: ["Luoyang", "China"],
243
+ LYS: ["Lyon", "France"],
244
+ MAA: ["Chennai", "India"],
245
+ MAD: ["Madrid", "Spain"],
246
+ MAN: ["Manchester", "United Kingdom"],
247
+ MAO: ["Manaus", "Brazil"],
248
+ MBA: ["Mombasa", "Kenya"],
249
+ MCI: ["Kansas City", "United States"],
250
+ MCT: ["Muscat", "Oman"],
251
+ MDE: ["Medell\xEDn", "Colombia"],
252
+ MEL: ["Melbourne", "Australia"],
253
+ MEM: ["Memphis", "United States"],
254
+ MEX: ["Mexico City", "Mexico"],
255
+ MFM: ["Macau", null],
256
+ MIA: ["Miami", "United States"],
257
+ MLA: ["Valletta", "Malta"],
258
+ MLE: ["Male", "Maldives"],
259
+ MLG: ["Malang", "Indonesia"],
260
+ MNL: ["Manila", "Philippines"],
261
+ MPM: ["Maputo", "Mozambique"],
262
+ MRS: ["Marseille", "France"],
263
+ MRU: ["Port Louis", "Mauritius"],
264
+ MSP: ["Minneapolis", "United States"],
265
+ MSQ: ["Minsk", "Belarus"],
266
+ MUC: ["Munich", "Germany"],
267
+ MXP: ["Milan", "Italy"],
268
+ NAG: ["Nagpur", "India"],
269
+ NBO: ["Nairobi", "Kenya"],
270
+ NJF: ["Najaf", "Iraq"],
271
+ NOU: ["Noumea", "New Caledonia"],
272
+ NQN: ["Neuquen", "Argentina"],
273
+ NQZ: ["Astana", "Kazakhstan"],
274
+ NRT: ["Tokyo", "Japan"],
275
+ NVT: ["Timbo", "Brazil"],
276
+ OKA: ["Naha", "Japan"],
277
+ OKC: ["Oklahoma City", "United States"],
278
+ OMA: ["Omaha", "United States"],
279
+ ORD: ["Chicago", "United States"],
280
+ ORF: ["Norfolk", "United States"],
281
+ ORN: ["Oran", "Algeria"],
282
+ OSL: ["Oslo", "Norway"],
283
+ OTP: ["Bucharest", "Romania"],
284
+ OUA: ["Ouagadougou", "Burkina Faso"],
285
+ PAT: ["Patna", "India"],
286
+ PBH: ["Thimphu", "Bhutan"],
287
+ PBM: ["Paramaribo", "Suriname"],
288
+ PDX: ["Portland", "United States"],
289
+ PER: ["Perth", "Australia"],
290
+ PHL: ["Philadelphia", "United States"],
291
+ PHX: ["Phoenix", "United States"],
292
+ PIT: ["Pittsburgh", "United States"],
293
+ PKX: ["Beijing", "China"],
294
+ PMO: ["Palermo", "Italy"],
295
+ PMW: ["Palmas", "Brazil"],
296
+ PNH: ["Phnom Penh", "Cambodia"],
297
+ PNQ: ["Pune", "India"],
298
+ POA: ["Porto Alegre", "Brazil"],
299
+ POS: ["Port of Spain", "Trinidad & Tobago"],
300
+ PPT: ["Tahiti", "French Polynesia"],
301
+ PRG: ["Prague", "Czechia"],
302
+ PRN: ["Prishtine", "Kosovo"],
303
+ PTY: ["Panama City", "Panama"],
304
+ QRO: ["Queretaro", "Mexico"],
305
+ QWJ: ["Americana", "Brazil"],
306
+ RAO: ["Ribeirao Preto", "Brazil"],
307
+ RDU: ["Durham", "United States"],
308
+ REC: ["Recife", "Brazil"],
309
+ RIC: ["Richmond", "United States"],
310
+ RIX: ["Riga", "Latvia"],
311
+ RUH: ["Riyadh", "Saudi Arabia"],
312
+ RUN: ["Saint-Denis", "R\xE9union"],
313
+ SAN: ["San Diego", "United States"],
314
+ SAP: ["San Pedro Sula", "Honduras"],
315
+ SAT: ["San Antonio", "United States"],
316
+ SCL: ["Santiago", "Chile"],
317
+ SDQ: ["Santo Domingo", "Dominican Republic"],
318
+ SEA: ["Seattle", "United States"],
319
+ SFO: ["San Francisco", "United States"],
320
+ SGN: ["Ho Chi Minh City", "Vietnam"],
321
+ SHA: ["Shanghai", "China"],
322
+ SIN: ["Singapore", "Singapore"],
323
+ SJC: ["San Jose", "United States"],
324
+ SJK: ["S\xE3o Jos\xE9 dos Campos", "Brazil"],
325
+ SJO: ["San Jos\xE9", "Costa Rica"],
326
+ SJP: ["S\xE3o Jos\xE9 do Rio Preto", "Brazil"],
327
+ SJU: ["San Juan", "Puerto Rico"],
328
+ SJW: ["Hengshui", "China"],
329
+ SKG: ["Thessaloniki", "Greece"],
330
+ SKP: ["Skopje", "Macedonia"],
331
+ SLC: ["Salt Lake City", "United States"],
332
+ SMF: ["Sacramento", "United States"],
333
+ SOD: ["Sorocaba", "Brazil"],
334
+ SOF: ["Sofia", "Bulgaria"],
335
+ SSA: ["Salvador", "Brazil"],
336
+ STI: ["Santiago de los Caballeros", "Dominican Republic"],
337
+ STL: ["St. Louis", "United States"],
338
+ STR: ["Stuttgart", "Germany"],
339
+ SUV: ["Suva", "Fiji"],
340
+ SYD: ["Sydney", "Australia"],
341
+ SZX: ["Shenzhen", "China"],
342
+ TAO: ["Qingdao", "China"],
343
+ TBS: ["Tbilisi", "Georgia"],
344
+ TEN: ["Tongren", "China"],
345
+ TGU: ["Tegucigalpa", "Honduras"],
346
+ TIA: ["Tirana", "Albania"],
347
+ TLH: ["Tallahassee", "United States"],
348
+ TLL: ["Tallinn", "Estonia"],
349
+ TLV: ["Tel Aviv", "Israel"],
350
+ TNA: ["Jinan", "China"],
351
+ TNR: ["Antananarivo", "Madagascar"],
352
+ TPA: ["Tampa", "United States"],
353
+ TPE: ["Taipei", null],
354
+ TUN: ["Tunis", "Tunisia"],
355
+ TXL: ["Berlin", "Germany"],
356
+ TYN: ["Yangquan", "China"],
357
+ UDI: ["Uberlandia", "Brazil"],
358
+ UDR: ["Udaipur", "India"],
359
+ UIO: ["Quito", "Ecuador"],
360
+ ULN: ["Ulaanbaatar", "Mongolia"],
361
+ URT: ["Surat Thani", "Thailand"],
362
+ VCP: ["Campinas", "Brazil"],
363
+ VIE: ["Vienna", "Austria"],
364
+ VIX: ["Vitoria", "Brazil"],
365
+ VNO: ["Vilnius", "Lithuania"],
366
+ VTE: ["Vientiane", "Laos"],
367
+ WAW: ["Warsaw", "Poland"],
368
+ WDH: ["Windhoek", "Namibia"],
369
+ WLG: ["Wellington", "New Zealand"],
370
+ WRO: ["Wroclaw", "Poland"],
371
+ XAP: ["Chapeco", "Brazil"],
372
+ XFN: ["Xiangyang", "China"],
373
+ XIY: ["Baoji", "China"],
374
+ XNH: ["Nasiriyah", "Iraq"],
375
+ YHZ: ["Halifax", "Canada"],
376
+ YUL: ["Montr\xE9al", "Canada"],
377
+ YVR: ["Vancouver", "Canada"],
378
+ YWG: ["Winnipeg", "Canada"],
379
+ YXE: ["Saskatoon", "Canada"],
380
+ YYC: ["Calgary", "Canada"],
381
+ YYZ: ["Toronto", "Canada"],
382
+ ZAG: ["Zagreb", "Croatia"],
383
+ ZDM: ["Ramallah", null],
384
+ ZRH: ["Zurich", "Switzerland"]
385
+ };
386
+ function placeOf(colo) {
387
+ const entry = colo ? PLACES[colo] : void 0;
388
+ return entry ? { city: entry[0], country: entry[1] } : null;
389
+ }
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
+
492
+ // src/ui/theme.ts
493
+ var C = {
494
+ signal: "#2dd4bf",
495
+ muted: "#8a8f8b",
496
+ strong: "#f3f4f1",
497
+ ok: "#4ade80",
498
+ warn: "#facc15",
499
+ err: "#f87171"
500
+ };
501
+ var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
502
+ function tunnelColor(index) {
503
+ return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
504
+ }
505
+ function fit(text, width) {
506
+ if (width <= 0) return "";
507
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
508
+ }
509
+
510
+ // src/session/bodyNote.ts
511
+ var TRUNCATED_NOTE = "Body cut at 64 KB.";
512
+ function bodyNote(r, side) {
513
+ if (side === "req") {
514
+ if (r.reqBodyBytes === 0) return "No body.";
515
+ if (r.reqBody === null && !r.reqTruncated) return "Binary body; not stored.";
516
+ return null;
517
+ }
518
+ if (r.bytes === 0) return "No body.";
519
+ if (r.resBody === null) return "Binary body; not stored.";
520
+ return null;
521
+ }
522
+
523
+ // src/ui/format.ts
524
+ function statusColor(status) {
525
+ if (status === null) return "muted";
526
+ if (status === 101) return "signal";
527
+ if (status >= 500) return "err";
528
+ if (status >= 400) return "warn";
529
+ if (status >= 300) return "muted";
530
+ return "ok";
531
+ }
532
+ function formatDuration(ms) {
533
+ return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
534
+ }
535
+ function formatBytes(n) {
536
+ if (n < 1024) return `${n} B`;
537
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
538
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
539
+ }
540
+ function formatTime(ms) {
541
+ const d = new Date(ms);
542
+ return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
543
+ }
544
+ function localLabel(t) {
545
+ return t.host ? `localhost:${t.port} (Host: ${t.host})` : `localhost:${t.port}`;
546
+ }
547
+
548
+ export {
549
+ MAX_NAME,
550
+ normalizeName,
551
+ validateName,
552
+ placeOf,
553
+ CLI_VERSION_HEADER,
554
+ CLI_USER_AGENT,
555
+ dutoHome,
556
+ apiUrl,
557
+ saveConfig,
558
+ loadConfig,
559
+ C,
560
+ tunnelColor,
561
+ fit,
562
+ TRUNCATED_NOTE,
563
+ bodyNote,
564
+ statusColor,
565
+ formatDuration,
566
+ formatBytes,
567
+ formatTime,
568
+ localLabel,
569
+ checkForUpdate,
570
+ updateLine
571
+ };
@@ -12,8 +12,9 @@ import {
12
12
  normalizeName,
13
13
  statusColor,
14
14
  tunnelColor,
15
+ updateLine,
15
16
  validateName
16
- } from "./chunk-76YCOMXH.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,17 +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,
18
+ placeOf,
19
+ saveConfig,
12
20
  statusColor,
21
+ updateLine,
13
22
  validateName
14
- } from "./chunk-76YCOMXH.js";
23
+ } from "./chunk-4SIBC52A.js";
15
24
 
16
25
  // src/ui/brand.ts
17
26
  function colorOn(stream = process.stdout) {
@@ -37,9 +46,12 @@ var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
37
46
 
38
47
  // src/index.ts
39
48
  import { hostname as hostname2 } from "os";
40
- import { join as join6, relative } from "path";
49
+ import { join as join5, relative } from "path";
41
50
  import cac from "cac";
42
51
 
52
+ // src/version.ts
53
+ var CLI_VERSION = "1.3.0";
54
+
43
55
  // src/api.ts
44
56
  var DutoApiError = class extends Error {
45
57
  constructor(message2, status, code) {
@@ -51,6 +63,7 @@ var DutoApiError = class extends Error {
51
63
  status;
52
64
  code;
53
65
  };
66
+ var BOOKMARK_HEADER = "x-d1-bookmark";
54
67
  var MESSAGES = {
55
68
  unauthorized: "This machine is not logged in. Run: duto login",
56
69
  suspended: "Your account is suspended.",
@@ -59,6 +72,7 @@ var MESSAGES = {
59
72
  };
60
73
  function createApiClient(o) {
61
74
  const doFetch = o.fetchImpl ?? fetch;
75
+ let bookmark = null;
62
76
  async function call(method, path, body) {
63
77
  let res;
64
78
  try {
@@ -67,13 +81,16 @@ function createApiClient(o) {
67
81
  headers: {
68
82
  ...o.token === void 0 ? {} : { authorization: `Bearer ${o.token}` },
69
83
  ...body === void 0 ? {} : { "content-type": "application/json" },
70
- "user-agent": "duto-cli"
84
+ ...bookmark === null ? {} : { [BOOKMARK_HEADER]: bookmark },
85
+ "user-agent": CLI_USER_AGENT,
86
+ [CLI_VERSION_HEADER]: CLI_VERSION
71
87
  },
72
88
  body: body === void 0 ? void 0 : JSON.stringify(body)
73
89
  });
74
90
  } catch {
75
91
  throw new DutoApiError(MESSAGES.network, 0, "network");
76
92
  }
93
+ bookmark = res.headers.get(BOOKMARK_HEADER) ?? bookmark;
77
94
  return res;
78
95
  }
79
96
  async function fail3(res) {
@@ -113,8 +130,8 @@ function createApiClient(o) {
113
130
 
114
131
  // src/cloudflared.ts
115
132
  import { createHash } from "crypto";
116
- import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
117
- import { join as join2 } from "path";
133
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "fs";
134
+ import { join } from "path";
118
135
  import { extract } from "tar";
119
136
 
120
137
  // src/checksums.ts
@@ -127,40 +144,6 @@ var CHECKSUMS = {
127
144
  "cloudflared-windows-amd64.exe": "2493dbc168923bb3e59e5e7716f71deced92992eea9acd6e9d74f84ac7751343"
128
145
  };
129
146
 
130
- // src/config.ts
131
- import { randomUUID } from "crypto";
132
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
133
- import { homedir } from "os";
134
- import { join } from "path";
135
- function dutoHome() {
136
- return process.env.DUTO_HOME || join(homedir(), ".duto");
137
- }
138
- function configPath() {
139
- return join(dutoHome(), "config.json");
140
- }
141
- function apiUrl() {
142
- return (process.env.DUTO_API_URL || "https://duto.dev").replace(/\/+$/, "");
143
- }
144
- function saveConfig(c) {
145
- mkdirSync(dutoHome(), { recursive: true, mode: 448 });
146
- const path = configPath();
147
- writeFileSync(path, JSON.stringify(c, null, 2) + "\n", { mode: 384 });
148
- chmodSync(path, 384);
149
- }
150
- function loadConfig() {
151
- const path = configPath();
152
- if (existsSync(path)) {
153
- const parsed = JSON.parse(readFileSync(path, "utf8"));
154
- if (parsed.deviceId) return parsed;
155
- const fixed = { ...parsed, deviceId: randomUUID() };
156
- saveConfig(fixed);
157
- return fixed;
158
- }
159
- const fresh = { deviceId: randomUUID() };
160
- saveConfig(fresh);
161
- return fresh;
162
- }
163
-
164
147
  // src/cloudflared.ts
165
148
  function assetFor(platform, arch) {
166
149
  const key = `${platform}/${arch}`;
@@ -185,11 +168,11 @@ function downloadUrl(asset, version = CLOUDFLARED_VERSION) {
185
168
  async function ensureCloudflared(opts = {}) {
186
169
  const platform = opts.platform ?? process.platform;
187
170
  const { asset, kind } = assetFor(platform, opts.arch ?? process.arch);
188
- const binDir = join2(dutoHome(), "bin");
189
- const target = join2(binDir, `cloudflared-${CLOUDFLARED_VERSION}${platform === "win32" ? ".exe" : ""}`);
190
- 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;
191
174
  opts.onDownload?.();
192
- mkdirSync2(binDir, { recursive: true });
175
+ mkdirSync(binDir, { recursive: true });
193
176
  const res = await (opts.fetchImpl ?? fetch)(downloadUrl(asset));
194
177
  if (!res.ok) throw new Error(`Could not download cloudflared (HTTP ${res.status}).`);
195
178
  const bytes = Buffer.from(await res.arrayBuffer());
@@ -199,22 +182,22 @@ async function ensureCloudflared(opts = {}) {
199
182
  rmSync(target, { force: true });
200
183
  throw new Error("cloudflared checksum mismatch. Try again; if it keeps failing, update duto.");
201
184
  }
202
- const tmp = mkdtempSync(join2(binDir, ".download-"));
185
+ const tmp = mkdtempSync(join(binDir, ".download-"));
203
186
  try {
204
187
  if (kind === "tgz") {
205
- const tgz = join2(tmp, asset);
206
- writeFileSync2(tgz, bytes);
188
+ const tgz = join(tmp, asset);
189
+ writeFileSync(tgz, bytes);
207
190
  await extract({ file: tgz, cwd: tmp });
208
- renameSync(join2(tmp, "cloudflared"), target);
191
+ renameSync(join(tmp, "cloudflared"), target);
209
192
  } else {
210
- const file = join2(tmp, "cloudflared");
211
- writeFileSync2(file, bytes);
193
+ const file = join(tmp, "cloudflared");
194
+ writeFileSync(file, bytes);
212
195
  renameSync(file, target);
213
196
  }
214
197
  } finally {
215
198
  rmSync(tmp, { recursive: true, force: true });
216
199
  }
217
- if (platform !== "win32") chmodSync2(target, 493);
200
+ if (platform !== "win32") chmodSync(target, 493);
218
201
  return target;
219
202
  }
220
203
  async function ensureCloudflaredWithProgress(write, opts = {}) {
@@ -236,9 +219,9 @@ async function ensureCloudflaredWithProgress(write, opts = {}) {
236
219
  }
237
220
 
238
221
  // src/session/allowlist.ts
239
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
222
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
240
223
  import { BlockList, isIP } from "net";
241
- import { dirname, join as join3 } from "path";
224
+ import { dirname, join as join2 } from "path";
242
225
 
243
226
  // src/session/allowlist-snapshot.ts
244
227
  var ALLOW_SNAPSHOT = {
@@ -307,7 +290,7 @@ function blockListOf(cidrs) {
307
290
  }
308
291
  function readCache(file) {
309
292
  try {
310
- const c = JSON.parse(readFileSync2(file, "utf8"));
293
+ const c = JSON.parse(readFileSync(file, "utf8"));
311
294
  if (typeof c?.fetchedAt === "number" && c.lists && typeof c.lists === "object") return c;
312
295
  } catch {
313
296
  }
@@ -324,7 +307,7 @@ async function fetchRanges(name, f) {
324
307
  return out.map((x) => x.includes("/") ? x : `${x}/${splitCidr(x).prefix}`);
325
308
  }
326
309
  async function resolveAllow(entries, o = {}) {
327
- const cacheFile = o.cacheFile ?? join3(dutoHome(), "allowlist.json");
310
+ const cacheFile = o.cacheFile ?? join2(dutoHome(), "allowlist.json");
328
311
  const f = o.fetch ?? fetch;
329
312
  const now = (o.now ?? Date.now)();
330
313
  const parsed = entries.map((raw) => ({ raw, e: parseAllowEntry(raw) }));
@@ -347,8 +330,8 @@ async function resolveAllow(entries, o = {}) {
347
330
  const everyListFromNetwork = names.length > 0 && Object.keys(net).length === names.length;
348
331
  if (everyListFromNetwork) {
349
332
  try {
350
- mkdirSync3(dirname(cacheFile), { recursive: true });
351
- writeFileSync3(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
333
+ mkdirSync2(dirname(cacheFile), { recursive: true });
334
+ writeFileSync2(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
352
335
  } catch {
353
336
  }
354
337
  }
@@ -370,8 +353,8 @@ async function resolveAllow(entries, o = {}) {
370
353
  }
371
354
 
372
355
  // src/session/config.ts
373
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
374
- 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";
375
358
  import { isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
376
359
  var ConfigError = class extends Error {
377
360
  constructor(message2, line, file = CONFIG_FILES[0]) {
@@ -517,13 +500,13 @@ function parseConfig(text, file = CONFIG_FILES[0], env = process.env) {
517
500
  function findConfig(cwd) {
518
501
  let dir = cwd;
519
502
  for (; ; ) {
520
- const present = CONFIG_FILES.filter((f) => existsSync3(join4(dir, f)));
503
+ const present = CONFIG_FILES.filter((f) => existsSync2(join3(dir, f)));
521
504
  if (present.length > 1) throw new ConfigError(`${CONFIG_FILES.join(" and ")} in the same folder. Delete one of them.`, void 0, "");
522
505
  if (present.length === 1) {
523
- const path = join4(dir, present[0]);
524
- 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]) };
525
508
  }
526
- if (existsSync3(join4(dir, ".git"))) return null;
509
+ if (existsSync2(join3(dir, ".git"))) return null;
527
510
  const up = dirname2(dir);
528
511
  if (up === dir) return null;
529
512
  dir = up;
@@ -540,7 +523,7 @@ async function browserLogin(o) {
540
523
  const start = await api.startLogin({
541
524
  deviceId: o.deviceId,
542
525
  deviceName: hostname(),
543
- cliVersion: "1.1.0",
526
+ cliVersion: "1.3.0",
544
527
  nodeVersion: process.version,
545
528
  os: process.platform,
546
529
  arch: process.arch
@@ -616,6 +599,22 @@ function startSpinner(label, stream = process.stderr) {
616
599
  };
617
600
  }
618
601
 
602
+ // src/inspector/edge.ts
603
+ var RAY_COLO = /-([A-Za-z]{3})$/;
604
+ function edgeOf(headers) {
605
+ const name = Object.keys(headers).find((k) => k.toLowerCase() === "cf-ray");
606
+ const code = name ? RAY_COLO.exec(headers[name] ?? "")?.[1] : void 0;
607
+ if (!code) return null;
608
+ const colo = code.toUpperCase();
609
+ const place = placeOf(colo);
610
+ return { colo, city: place?.city ?? null, country: place?.country ?? null };
611
+ }
612
+ function edgeLabel(edge) {
613
+ if (!edge) return "";
614
+ const place = [edge.city, edge.country].filter(Boolean).join(", ");
615
+ return place ? "via " + edge.colo + " (" + place + ")" : "via " + edge.colo;
616
+ }
617
+
619
618
  // src/inspector/jsonView.ts
620
619
  function countLabel(n, one, many) {
621
620
  return n + " " + (n === 1 ? one : many);
@@ -796,6 +795,7 @@ th { color: ${JSON_KEY}; font-weight: 400; white-space: nowrap; }
796
795
  `;
797
796
  var SCRIPT = String.raw`"use strict";
798
797
  var countLabel = (${countLabel.toString()});
798
+ var edgeLabel = (${edgeLabel.toString()});
799
799
  var jsonSyntaxError = (${jsonSyntaxError.toString()});
800
800
  var jsonTokens = (${jsonTokens.toString()});
801
801
  var jsonIndent = (${jsonIndent.toString()});
@@ -969,6 +969,7 @@ function renderDetail() {
969
969
  if (duration(d)) parts.push(duration(d));
970
970
  parts.push(size(d.bytes));
971
971
  parts.push(d.tunnel);
972
+ if (d.edge) parts.push(edgeLabel(d.edge));
972
973
  if (d.replayOf !== undefined) parts.push("↻ replay of #" + d.replayOf);
973
974
  head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
974
975
  if (d.error) head.appendChild(el("div", "msg err", d.error));
@@ -1362,6 +1363,7 @@ function detail(r, session) {
1362
1363
  }
1363
1364
  return {
1364
1365
  ...listItem(r),
1366
+ edge: edgeOf(r.reqHeaders),
1365
1367
  reqHeaders: r.reqHeaders,
1366
1368
  resHeaders: r.resHeaders,
1367
1369
  reqBody: r.reqBody,
@@ -2225,13 +2227,13 @@ async function startProxy(o) {
2225
2227
 
2226
2228
  // src/session/orphans.ts
2227
2229
  import { spawnSync } from "child_process";
2228
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
2229
- import { join as join5 } from "path";
2230
- 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`);
2231
2233
  function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
2232
2234
  try {
2233
- mkdirSync4(dir, { recursive: true });
2234
- 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() }));
2235
2237
  } catch {
2236
2238
  }
2237
2239
  }
@@ -2249,7 +2251,7 @@ var realProc = {
2249
2251
  },
2250
2252
  isCloudflared(pid) {
2251
2253
  try {
2252
- 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");
2253
2255
  if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
2254
2256
  } catch {
2255
2257
  return false;
@@ -2264,14 +2266,14 @@ var realProc = {
2264
2266
  }
2265
2267
  };
2266
2268
  function reapOrphans(dir, proc = realProc) {
2267
- if (!existsSync4(dir)) return 0;
2269
+ if (!existsSync3(dir)) return 0;
2268
2270
  let killed = 0;
2269
2271
  for (const name of readdirSync(dir)) {
2270
2272
  if (!/^cloudflared-\d+\.json$/.test(name)) continue;
2271
- const path = join5(dir, name);
2273
+ const path = join4(dir, name);
2272
2274
  let rec;
2273
2275
  try {
2274
- rec = JSON.parse(readFileSync4(path, "utf8"));
2276
+ rec = JSON.parse(readFileSync3(path, "utf8"));
2275
2277
  } catch {
2276
2278
  rmSync2(path, { force: true });
2277
2279
  continue;
@@ -2662,13 +2664,14 @@ async function runSession(specs, source, flags) {
2662
2664
  const config = loadConfig();
2663
2665
  if (!config.authtoken) fail2(LOGIN_HINT);
2664
2666
  const api = createApiClient({ baseUrl: apiUrl(), token: config.authtoken });
2667
+ const update = checkForUpdate(CLI_VERSION);
2665
2668
  let cloudflaredPath;
2666
2669
  try {
2667
2670
  cloudflaredPath = process.env.DUTO_CLOUDFLARED || await ensureCloudflaredWithProgress((s) => process.stdout.write(s));
2668
2671
  } catch (err) {
2669
2672
  fail2(err instanceof Error ? err.message : String(err));
2670
2673
  }
2671
- 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") });
2672
2675
  let inspector = null;
2673
2676
  let inspectorError;
2674
2677
  if (flags.inspect !== false) {
@@ -2679,14 +2682,15 @@ async function runSession(specs, source, flags) {
2679
2682
  }
2680
2683
  }
2681
2684
  if (wantsUi(flags)) {
2682
- const { runFullscreen } = await import("./fullscreen-ECEH3R2V.js");
2683
- const code = await runFullscreen(session, specs, "1.1.0", inspector, inspectorError);
2685
+ const { runFullscreen } = await import("./fullscreen-AI6PC7RL.js");
2686
+ const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update);
2684
2687
  await inspector?.close().catch(() => void 0);
2685
2688
  process.exit(code);
2686
2689
  }
2687
- console.log(`${mark()} ${paint("muted", `${"1.1.0"} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2690
+ console.log(`${mark()} ${paint("muted", `${CLI_VERSION} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2688
2691
  if (inspector) console.log(paint("muted", `inspector: ${inspector.openUrl}`));
2689
2692
  if (inspectorError) console.log(warn(`inspector failed to start: ${inspectorError}`));
2693
+ void update.then((latest) => latest && console.log(warn(updateLine(latest))));
2690
2694
  const stopLog = runPlain(session, (l) => console.log(l));
2691
2695
  const stopped = new Promise((resolve) => {
2692
2696
  const onSignal = () => {
@@ -2812,7 +2816,7 @@ cli.command("status", "Show your account and live tunnels").action(async () => {
2812
2816
  }
2813
2817
  });
2814
2818
  cli.help();
2815
- cli.version("1.1.0");
2819
+ cli.version(CLI_VERSION);
2816
2820
  try {
2817
2821
  cli.parse(process.argv, { run: false });
2818
2822
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "1.1.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",
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // ../shared/src/names.ts
4
- var EMAIL_SENDING_SUBDOMAINS = ["send"];
5
- var RESERVED_NAMES = [
6
- "www",
7
- "api",
8
- "app",
9
- "dash",
10
- "admin",
11
- "docs",
12
- "status",
13
- "mail",
14
- "custom",
15
- "cname",
16
- "duto",
17
- "auth",
18
- "login",
19
- "signup",
20
- "invite",
21
- "static",
22
- "assets",
23
- "cdn",
24
- "help",
25
- "support",
26
- "blog",
27
- ...EMAIL_SENDING_SUBDOMAINS
28
- ];
29
- var MAX_NAME = 40;
30
- var FORMAT = /^[a-z0-9]+$/;
31
- function normalizeName(input) {
32
- return input.trim().toLowerCase();
33
- }
34
- function validateName(name, max = MAX_NAME) {
35
- if (name.length === 0 || name.length > max) return { ok: false, reason: "length" };
36
- if (!FORMAT.test(name)) return { ok: false, reason: "format" };
37
- if (RESERVED_NAMES.includes(name)) return { ok: false, reason: "reserved" };
38
- return { ok: true };
39
- }
40
-
41
- // src/ui/theme.ts
42
- var C = {
43
- signal: "#2dd4bf",
44
- muted: "#8a8f8b",
45
- strong: "#f3f4f1",
46
- ok: "#4ade80",
47
- warn: "#facc15",
48
- err: "#f87171"
49
- };
50
- var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
51
- function tunnelColor(index) {
52
- return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
53
- }
54
- function fit(text, width) {
55
- if (width <= 0) return "";
56
- return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
57
- }
58
-
59
- // src/session/bodyNote.ts
60
- var TRUNCATED_NOTE = "Body cut at 64 KB.";
61
- function bodyNote(r, side) {
62
- if (side === "req") {
63
- if (r.reqBodyBytes === 0) return "No body.";
64
- if (r.reqBody === null && !r.reqTruncated) return "Binary body; not stored.";
65
- return null;
66
- }
67
- if (r.bytes === 0) return "No body.";
68
- if (r.resBody === null) return "Binary body; not stored.";
69
- return null;
70
- }
71
-
72
- // src/ui/format.ts
73
- function statusColor(status) {
74
- if (status === null) return "muted";
75
- if (status === 101) return "signal";
76
- if (status >= 500) return "err";
77
- if (status >= 400) return "warn";
78
- if (status >= 300) return "muted";
79
- return "ok";
80
- }
81
- function formatDuration(ms) {
82
- return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
83
- }
84
- function formatBytes(n) {
85
- if (n < 1024) return `${n} B`;
86
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
87
- return `${(n / 1024 / 1024).toFixed(1)} MB`;
88
- }
89
- function formatTime(ms) {
90
- const d = new Date(ms);
91
- return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
92
- }
93
- function localLabel(t) {
94
- return t.host ? `localhost:${t.port} (Host: ${t.host})` : `localhost:${t.port}`;
95
- }
96
-
97
- export {
98
- MAX_NAME,
99
- normalizeName,
100
- validateName,
101
- C,
102
- tunnelColor,
103
- fit,
104
- TRUNCATED_NOTE,
105
- bodyNote,
106
- statusColor,
107
- formatDuration,
108
- formatBytes,
109
- formatTime,
110
- localLabel
111
- };