pinggy 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1281,7 +1281,7 @@ var init_options = __esm({
1281
1281
  v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1282
1282
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1283
1283
  vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1284
- autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
1284
+ autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure. Use -a (defaults to true), -a true, or -a false." },
1285
1285
  // Save and load config
1286
1286
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1287
1287
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
@@ -1365,7 +1365,7 @@ var init_defaults = __esm({
1365
1365
  originalRequestUrl: false,
1366
1366
  allowPreflight: false,
1367
1367
  reverseProxy: false,
1368
- autoReconnect: false
1368
+ autoReconnect: true
1369
1369
  };
1370
1370
  }
1371
1371
  });
@@ -1506,33 +1506,58 @@ var init_extendedOptions = __esm({
1506
1506
  });
1507
1507
 
1508
1508
  // src/cli/buildConfig.ts
1509
+ function isKeyword(str) {
1510
+ return KEYWORDS.has(str.toLowerCase());
1511
+ }
1509
1512
  function parseUserAndDomain(str) {
1510
1513
  let token;
1511
1514
  let type;
1512
1515
  let server;
1513
1516
  let qrCode;
1514
- if (!str) return { token, type, server, qrCode };
1517
+ let forceFlag;
1518
+ if (!str) return { token, type, server, qrCode, forceFlag };
1515
1519
  if (str.includes("@")) {
1516
1520
  const [user, domain] = str.split("@", 2);
1517
1521
  if (domainRegex.test(domain)) {
1522
+ let processKeyword2 = function(keyword) {
1523
+ if ([import_pinggy3.TunnelType.Http, import_pinggy3.TunnelType.Tcp, import_pinggy3.TunnelType.Tls, import_pinggy3.TunnelType.Udp, import_pinggy3.TunnelType.TlsTcp].includes(keyword)) {
1524
+ type = keyword;
1525
+ } else if (keyword === "force") {
1526
+ forceFlag = true;
1527
+ } else if (keyword === "qr") {
1528
+ qrCode = true;
1529
+ }
1530
+ };
1531
+ var processKeyword = processKeyword2;
1518
1532
  server = domain;
1519
1533
  const parts = user.split("+");
1520
- for (const part of parts) {
1521
- if ([import_pinggy3.TunnelType.Http, import_pinggy3.TunnelType.Tcp, import_pinggy3.TunnelType.Tls, import_pinggy3.TunnelType.Udp, import_pinggy3.TunnelType.TlsTcp].includes(part.toLowerCase())) {
1522
- type = part;
1523
- } else if (part === "force") {
1524
- token = (token ? token + "+" : "") + part;
1525
- } else if (part === "qr") {
1526
- qrCode = true;
1527
- } else {
1528
- token = (token ? token + "+" : "") + part;
1534
+ if (parts.length === 0) {
1535
+ return { token, type, server, qrCode, forceFlag };
1536
+ }
1537
+ const firstPart = parts[0];
1538
+ if (!isKeyword(firstPart)) {
1539
+ token = firstPart;
1540
+ for (let i = 1; i < parts.length; i++) {
1541
+ const part = parts[i].toLowerCase();
1542
+ if (!isKeyword(part)) {
1543
+ throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
1544
+ }
1545
+ processKeyword2(part);
1546
+ }
1547
+ } else {
1548
+ for (const part of parts) {
1549
+ const lowerPart = part.toLowerCase();
1550
+ if (!isKeyword(lowerPart)) {
1551
+ throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
1552
+ }
1553
+ processKeyword2(lowerPart);
1529
1554
  }
1530
1555
  }
1531
1556
  }
1532
1557
  } else if (domainRegex.test(str)) {
1533
1558
  server = str;
1534
1559
  }
1535
- return { token, type, server, qrCode };
1560
+ return { token, type, server, qrCode, forceFlag };
1536
1561
  }
1537
1562
  function parseUsers(positionalArgs, explicitToken) {
1538
1563
  let token;
@@ -1546,6 +1571,8 @@ function parseUsers(positionalArgs, explicitToken) {
1546
1571
  if (parsed.server) server = parsed.server;
1547
1572
  if (parsed.type) type = parsed.type;
1548
1573
  if (parsed.token) token = parsed.token;
1574
+ if (parsed.forceFlag) forceFlag = true;
1575
+ if (parsed.qrCode) qrCode = true;
1549
1576
  }
1550
1577
  if (remaining.length > 0) {
1551
1578
  const first = remaining[0];
@@ -1553,19 +1580,9 @@ function parseUsers(positionalArgs, explicitToken) {
1553
1580
  if (parsed.server) {
1554
1581
  server = parsed.server;
1555
1582
  if (parsed.type) type = parsed.type;
1556
- if (parsed.token) {
1557
- if (parsed.token.includes("+")) {
1558
- const parts = parsed.token.split("+");
1559
- const tOnly = parts.filter((p) => p !== "force").join("+");
1560
- if (tOnly) token = tOnly;
1561
- if (parts.includes("force")) forceFlag = true;
1562
- } else {
1563
- token = parsed.token;
1564
- }
1565
- }
1566
- if (parsed.qrCode) {
1567
- qrCode = true;
1568
- }
1583
+ if (parsed.token) token = parsed.token;
1584
+ if (parsed.forceFlag) forceFlag = true;
1585
+ if (parsed.qrCode) qrCode = true;
1569
1586
  remaining = remaining.slice(1);
1570
1587
  }
1571
1588
  }
@@ -1747,7 +1764,7 @@ function parseLocalTunnelAddr(finalConfig, values) {
1747
1764
  const firstL = values.L[0];
1748
1765
  const parts = firstL.split(":");
1749
1766
  if (parts.length === 3) {
1750
- const lp = parseInt(parts[2], 10);
1767
+ const lp = parseInt(parts[0], 10);
1751
1768
  if (!Number.isNaN(lp) && isValidPort(lp)) {
1752
1769
  finalConfig.webDebugger = `localhost:${lp}`;
1753
1770
  } else {
@@ -1816,6 +1833,20 @@ function parseServe(finalConfig, values) {
1816
1833
  finalConfig.serve = sv;
1817
1834
  return null;
1818
1835
  }
1836
+ function parseAutoReconnect(finalConfig, values) {
1837
+ const autoReconnectValue = values.autoreconnect;
1838
+ if (typeof autoReconnectValue === "string") {
1839
+ const trimmed = autoReconnectValue.trim().toLowerCase();
1840
+ if (trimmed === "true" || trimmed === "") {
1841
+ finalConfig.autoReconnect = true;
1842
+ } else if (trimmed === "false") {
1843
+ finalConfig.autoReconnect = false;
1844
+ } else {
1845
+ return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
1846
+ }
1847
+ }
1848
+ return null;
1849
+ }
1819
1850
  async function buildFinalConfig(values, positionals) {
1820
1851
  let token;
1821
1852
  let server;
@@ -1843,7 +1874,7 @@ async function buildFinalConfig(values, positionals) {
1843
1874
  tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [import_pinggy3.TunnelType.Http],
1844
1875
  NoTUI: values.notui || (configFromFile?.NoTUI || false),
1845
1876
  qrCode: qrCode || (configFromFile?.qrCode || false),
1846
- autoReconnect: values.autoreconnect || (configFromFile?.autoReconnect || false)
1877
+ autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
1847
1878
  };
1848
1879
  parseType(finalConfig, values, type);
1849
1880
  parseToken(finalConfig, token || values.token);
@@ -1857,12 +1888,14 @@ async function buildFinalConfig(values, positionals) {
1857
1888
  if (lErr instanceof Error) throw lErr;
1858
1889
  const serveErr = parseServe(finalConfig, values);
1859
1890
  if (serveErr instanceof Error) throw serveErr;
1891
+ const autoReconnectErr = parseAutoReconnect(finalConfig, values);
1892
+ if (autoReconnectErr instanceof Error) throw autoReconnectErr;
1860
1893
  if (forceFlag) finalConfig.force = true;
1861
1894
  parseArgs(finalConfig, remainingPositionals);
1862
1895
  storeJson(finalConfig, saveconf);
1863
1896
  return finalConfig;
1864
1897
  }
1865
- var import_pinggy3, import_fs3, import_path3, domainRegex, VALID_PROTOCOLS;
1898
+ var import_pinggy3, import_fs3, import_path3, domainRegex, KEYWORDS, VALID_PROTOCOLS;
1866
1899
  var init_buildConfig = __esm({
1867
1900
  "src/cli/buildConfig.ts"() {
1868
1901
  "use strict";
@@ -1875,6 +1908,15 @@ var init_buildConfig = __esm({
1875
1908
  import_fs3 = __toESM(require("fs"), 1);
1876
1909
  import_path3 = __toESM(require("path"), 1);
1877
1910
  domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1911
+ KEYWORDS = /* @__PURE__ */ new Set([
1912
+ import_pinggy3.TunnelType.Http,
1913
+ import_pinggy3.TunnelType.Tcp,
1914
+ import_pinggy3.TunnelType.Tls,
1915
+ import_pinggy3.TunnelType.Udp,
1916
+ import_pinggy3.TunnelType.TlsTcp,
1917
+ "force",
1918
+ "qr"
1919
+ ]);
1878
1920
  VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1879
1921
  }
1880
1922
  });
@@ -4275,52 +4317,21 @@ function checkRegistry() {
4275
4317
  }
4276
4318
  return false;
4277
4319
  }
4278
- function getVCRedistVersion() {
4279
- if (import_os.default.platform() !== "win32") return null;
4280
- try {
4281
- for (const key of REGISTRY_KEYS) {
4282
- const cmd = `reg query "${key}" /v Version 2>nul`;
4283
- const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8" });
4284
- const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
4285
- if (match) {
4286
- return match[1];
4287
- }
4288
- }
4289
- } catch {
4290
- }
4291
- return null;
4292
- }
4293
- function hasVCRedist() {
4294
- if (import_os.default.platform() !== "win32") {
4295
- return true;
4296
- }
4297
- if (checkRegistry()) {
4298
- return true;
4299
- }
4300
- if (checkDLLs()) {
4301
- return true;
4302
- }
4303
- return false;
4304
- }
4305
- function getVCRedistStatus() {
4320
+ function checkVCRedist() {
4306
4321
  if (import_os.default.platform() !== "win32") {
4307
4322
  return {
4308
4323
  required: false,
4309
4324
  installed: true,
4310
- version: null,
4311
- method: "non-windows"
4325
+ message: null
4312
4326
  };
4313
4327
  }
4314
4328
  const registryInstalled = checkRegistry();
4315
4329
  const dllsPresent = checkDLLs();
4316
- const version = getVCRedistVersion();
4330
+ const installed = registryInstalled || dllsPresent;
4317
4331
  return {
4318
4332
  required: true,
4319
- installed: registryInstalled || dllsPresent,
4320
- version,
4321
- registryCheck: registryInstalled,
4322
- dllCheck: dllsPresent,
4323
- method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none"
4333
+ installed,
4334
+ message: installed ? null : "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\n"
4324
4335
  };
4325
4336
  }
4326
4337
  async function openDownloadPage() {
@@ -4343,27 +4354,19 @@ async function openDownloadPage() {
4343
4354
  printer_default.info(url + "\n");
4344
4355
  }
4345
4356
  }
4346
- function getVCRedistMessage() {
4347
- const status = getVCRedistStatus();
4348
- if (!status.required || status.installed) {
4349
- return null;
4350
- }
4351
- return {
4352
- error: true,
4353
- message: "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\nPlease download and install it using the link below, then restart this application.\n"
4354
- };
4355
- }
4356
4357
 
4357
4358
  // src/index.ts
4358
4359
  init_printer();
4359
4360
  async function verifyAndLoad() {
4360
- if (process.platform === "win32" && !hasVCRedist()) {
4361
- const msg = getVCRedistMessage();
4362
- printer_default.warn(
4363
- msg?.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
4364
- );
4365
- await openDownloadPage();
4366
- process.exit(1);
4361
+ if (process.platform === "win32") {
4362
+ const vcRedist = checkVCRedist();
4363
+ if (!vcRedist.installed) {
4364
+ printer_default.warn(
4365
+ vcRedist.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
4366
+ );
4367
+ await openDownloadPage();
4368
+ process.exit(1);
4369
+ }
4367
4370
  }
4368
4371
  await Promise.resolve().then(() => (init_main(), main_exports));
4369
4372
  }
package/dist/index.js CHANGED
@@ -43,52 +43,21 @@ function checkRegistry() {
43
43
  }
44
44
  return false;
45
45
  }
46
- function getVCRedistVersion() {
47
- if (os.platform() !== "win32") return null;
48
- try {
49
- for (const key of REGISTRY_KEYS) {
50
- const cmd = `reg query "${key}" /v Version 2>nul`;
51
- const result = execSync(cmd, { encoding: "utf8" });
52
- const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
53
- if (match) {
54
- return match[1];
55
- }
56
- }
57
- } catch {
58
- }
59
- return null;
60
- }
61
- function hasVCRedist() {
62
- if (os.platform() !== "win32") {
63
- return true;
64
- }
65
- if (checkRegistry()) {
66
- return true;
67
- }
68
- if (checkDLLs()) {
69
- return true;
70
- }
71
- return false;
72
- }
73
- function getVCRedistStatus() {
46
+ function checkVCRedist() {
74
47
  if (os.platform() !== "win32") {
75
48
  return {
76
49
  required: false,
77
50
  installed: true,
78
- version: null,
79
- method: "non-windows"
51
+ message: null
80
52
  };
81
53
  }
82
54
  const registryInstalled = checkRegistry();
83
55
  const dllsPresent = checkDLLs();
84
- const version = getVCRedistVersion();
56
+ const installed = registryInstalled || dllsPresent;
85
57
  return {
86
58
  required: true,
87
- installed: registryInstalled || dllsPresent,
88
- version,
89
- registryCheck: registryInstalled,
90
- dllCheck: dllsPresent,
91
- method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none"
59
+ installed,
60
+ message: installed ? null : "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\n"
92
61
  };
93
62
  }
94
63
  async function openDownloadPage() {
@@ -111,28 +80,20 @@ async function openDownloadPage() {
111
80
  printer_default.info(url + "\n");
112
81
  }
113
82
  }
114
- function getVCRedistMessage() {
115
- const status = getVCRedistStatus();
116
- if (!status.required || status.installed) {
117
- return null;
118
- }
119
- return {
120
- error: true,
121
- message: "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\nPlease download and install it using the link below, then restart this application.\n"
122
- };
123
- }
124
83
 
125
84
  // src/index.ts
126
85
  async function verifyAndLoad() {
127
- if (process.platform === "win32" && !hasVCRedist()) {
128
- const msg = getVCRedistMessage();
129
- printer_default.warn(
130
- msg?.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
131
- );
132
- await openDownloadPage();
133
- process.exit(1);
86
+ if (process.platform === "win32") {
87
+ const vcRedist = checkVCRedist();
88
+ if (!vcRedist.installed) {
89
+ printer_default.warn(
90
+ vcRedist.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
91
+ );
92
+ await openDownloadPage();
93
+ process.exit(1);
94
+ }
134
95
  }
135
- await import("./main-CZY6GID4.js");
96
+ await import("./main-K44C44NW.js");
136
97
  }
137
98
  verifyAndLoad().catch((err) => {
138
99
  printer_default.error(`Failed to start CLI:, ${err}`);
@@ -995,7 +995,7 @@ var cliOptions = {
995
995
  v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
996
996
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
997
997
  vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
998
- autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
998
+ autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure. Use -a (defaults to true), -a true, or -a false." },
999
999
  // Save and load config
1000
1000
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1001
1001
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
@@ -1065,7 +1065,7 @@ var defaultOptions = {
1065
1065
  originalRequestUrl: false,
1066
1066
  allowPreflight: false,
1067
1067
  reverseProxy: false,
1068
- autoReconnect: false
1068
+ autoReconnect: true
1069
1069
  };
1070
1070
 
1071
1071
  // src/cli/extendedOptions.ts
@@ -1199,33 +1199,67 @@ import { TunnelType as TunnelType2 } from "@pinggy/pinggy";
1199
1199
  import fs from "fs";
1200
1200
  import path2 from "path";
1201
1201
  var domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1202
+ var KEYWORDS = /* @__PURE__ */ new Set([
1203
+ TunnelType2.Http,
1204
+ TunnelType2.Tcp,
1205
+ TunnelType2.Tls,
1206
+ TunnelType2.Udp,
1207
+ TunnelType2.TlsTcp,
1208
+ "force",
1209
+ "qr"
1210
+ ]);
1211
+ function isKeyword(str) {
1212
+ return KEYWORDS.has(str.toLowerCase());
1213
+ }
1202
1214
  function parseUserAndDomain(str) {
1203
1215
  let token;
1204
1216
  let type;
1205
1217
  let server;
1206
1218
  let qrCode;
1207
- if (!str) return { token, type, server, qrCode };
1219
+ let forceFlag;
1220
+ if (!str) return { token, type, server, qrCode, forceFlag };
1208
1221
  if (str.includes("@")) {
1209
1222
  const [user, domain] = str.split("@", 2);
1210
1223
  if (domainRegex.test(domain)) {
1224
+ let processKeyword2 = function(keyword) {
1225
+ if ([TunnelType2.Http, TunnelType2.Tcp, TunnelType2.Tls, TunnelType2.Udp, TunnelType2.TlsTcp].includes(keyword)) {
1226
+ type = keyword;
1227
+ } else if (keyword === "force") {
1228
+ forceFlag = true;
1229
+ } else if (keyword === "qr") {
1230
+ qrCode = true;
1231
+ }
1232
+ };
1233
+ var processKeyword = processKeyword2;
1211
1234
  server = domain;
1212
1235
  const parts = user.split("+");
1213
- for (const part of parts) {
1214
- if ([TunnelType2.Http, TunnelType2.Tcp, TunnelType2.Tls, TunnelType2.Udp, TunnelType2.TlsTcp].includes(part.toLowerCase())) {
1215
- type = part;
1216
- } else if (part === "force") {
1217
- token = (token ? token + "+" : "") + part;
1218
- } else if (part === "qr") {
1219
- qrCode = true;
1220
- } else {
1221
- token = (token ? token + "+" : "") + part;
1236
+ if (parts.length === 0) {
1237
+ return { token, type, server, qrCode, forceFlag };
1238
+ }
1239
+ const firstPart = parts[0];
1240
+ if (!isKeyword(firstPart)) {
1241
+ token = firstPart;
1242
+ for (let i = 1; i < parts.length; i++) {
1243
+ const part = parts[i].toLowerCase();
1244
+ if (!isKeyword(part)) {
1245
+ throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
1246
+ }
1247
+ processKeyword2(part);
1248
+ }
1249
+ } else {
1250
+ for (const part of parts) {
1251
+ const lowerPart = part.toLowerCase();
1252
+ if (!isKeyword(lowerPart)) {
1253
+ throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
1254
+ }
1255
+ processKeyword2(lowerPart);
1222
1256
  }
1223
1257
  }
1224
1258
  }
1225
1259
  } else if (domainRegex.test(str)) {
1226
1260
  server = str;
1227
1261
  }
1228
- return { token, type, server, qrCode };
1262
+ return { token, type, server, qrCode, forceFlag };
1229
1263
  }
1230
1264
  function parseUsers(positionalArgs, explicitToken) {
1231
1265
  let token;
@@ -1239,6 +1273,8 @@ function parseUsers(positionalArgs, explicitToken) {
1239
1273
  if (parsed.server) server = parsed.server;
1240
1274
  if (parsed.type) type = parsed.type;
1241
1275
  if (parsed.token) token = parsed.token;
1276
+ if (parsed.forceFlag) forceFlag = true;
1277
+ if (parsed.qrCode) qrCode = true;
1242
1278
  }
1243
1279
  if (remaining.length > 0) {
1244
1280
  const first = remaining[0];
@@ -1246,19 +1282,9 @@ function parseUsers(positionalArgs, explicitToken) {
1246
1282
  if (parsed.server) {
1247
1283
  server = parsed.server;
1248
1284
  if (parsed.type) type = parsed.type;
1249
- if (parsed.token) {
1250
- if (parsed.token.includes("+")) {
1251
- const parts = parsed.token.split("+");
1252
- const tOnly = parts.filter((p) => p !== "force").join("+");
1253
- if (tOnly) token = tOnly;
1254
- if (parts.includes("force")) forceFlag = true;
1255
- } else {
1256
- token = parsed.token;
1257
- }
1258
- }
1259
- if (parsed.qrCode) {
1260
- qrCode = true;
1261
- }
1285
+ if (parsed.token) token = parsed.token;
1286
+ if (parsed.forceFlag) forceFlag = true;
1287
+ if (parsed.qrCode) qrCode = true;
1262
1288
  remaining = remaining.slice(1);
1263
1289
  }
1264
1290
  }
@@ -1441,7 +1467,7 @@ function parseLocalTunnelAddr(finalConfig, values) {
1441
1467
  const firstL = values.L[0];
1442
1468
  const parts = firstL.split(":");
1443
1469
  if (parts.length === 3) {
1444
- const lp = parseInt(parts[2], 10);
1470
+ const lp = parseInt(parts[0], 10);
1445
1471
  if (!Number.isNaN(lp) && isValidPort(lp)) {
1446
1472
  finalConfig.webDebugger = `localhost:${lp}`;
1447
1473
  } else {
@@ -1510,6 +1536,20 @@ function parseServe(finalConfig, values) {
1510
1536
  finalConfig.serve = sv;
1511
1537
  return null;
1512
1538
  }
1539
+ function parseAutoReconnect(finalConfig, values) {
1540
+ const autoReconnectValue = values.autoreconnect;
1541
+ if (typeof autoReconnectValue === "string") {
1542
+ const trimmed = autoReconnectValue.trim().toLowerCase();
1543
+ if (trimmed === "true" || trimmed === "") {
1544
+ finalConfig.autoReconnect = true;
1545
+ } else if (trimmed === "false") {
1546
+ finalConfig.autoReconnect = false;
1547
+ } else {
1548
+ return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
1549
+ }
1550
+ }
1551
+ return null;
1552
+ }
1513
1553
  async function buildFinalConfig(values, positionals) {
1514
1554
  let token;
1515
1555
  let server;
@@ -1537,7 +1577,7 @@ async function buildFinalConfig(values, positionals) {
1537
1577
  tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [TunnelType2.Http],
1538
1578
  NoTUI: values.notui || (configFromFile?.NoTUI || false),
1539
1579
  qrCode: qrCode || (configFromFile?.qrCode || false),
1540
- autoReconnect: values.autoreconnect || (configFromFile?.autoReconnect || false)
1580
+ autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
1541
1581
  };
1542
1582
  parseType(finalConfig, values, type);
1543
1583
  parseToken(finalConfig, token || values.token);
@@ -1551,6 +1591,8 @@ async function buildFinalConfig(values, positionals) {
1551
1591
  if (lErr instanceof Error) throw lErr;
1552
1592
  const serveErr = parseServe(finalConfig, values);
1553
1593
  if (serveErr instanceof Error) throw serveErr;
1594
+ const autoReconnectErr = parseAutoReconnect(finalConfig, values);
1595
+ if (autoReconnectErr instanceof Error) throw autoReconnectErr;
1554
1596
  if (forceFlag) finalConfig.force = true;
1555
1597
  parseArgs(finalConfig, remainingPositionals);
1556
1598
  storeJson(finalConfig, saveconf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinggy",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Create secure, shareable tunnels to your localhost and manage them from the command line. ",
@@ -50,7 +50,7 @@
50
50
  ]
51
51
  },
52
52
  "dependencies": {
53
- "@pinggy/pinggy": "^0.3.2",
53
+ "@pinggy/pinggy": "^0.3.3",
54
54
  "blessed": "^0.1.81",
55
55
  "clipboardy": "^5.0.0",
56
56
  "mime": "^4.1.0",
@@ -11,36 +11,85 @@ import path from "path";
11
11
 
12
12
  const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
13
13
 
14
+ // Define the set of valid keywords
15
+ const KEYWORDS = new Set([
16
+ TunnelType.Http,
17
+ TunnelType.Tcp,
18
+ TunnelType.Tls,
19
+ TunnelType.Udp,
20
+ TunnelType.TlsTcp,
21
+ 'force',
22
+ 'qr',
23
+ ] as const);
24
+
25
+ type Keyword = typeof KEYWORDS extends Set<infer T> ? T : never;
26
+
27
+ function isKeyword(str: string): boolean {
28
+ return KEYWORDS.has(str.toLowerCase() as Keyword);
29
+ }
30
+
14
31
  function parseUserAndDomain(str: string) {
15
32
  let token: string | undefined;
16
33
  let type: string | undefined;
17
34
  let server: string | undefined;
18
35
  let qrCode: boolean | undefined;
36
+ let forceFlag: boolean | undefined;
19
37
 
20
- if (!str) return { token, type, server, qrCode } as const;
38
+ if (!str) return { token, type, server, qrCode, forceFlag } as const;
21
39
 
22
40
  if (str.includes('@')) {
23
41
  const [user, domain] = str.split('@', 2);
24
42
  if (domainRegex.test(domain)) {
25
43
  server = domain;
44
+
26
45
  // parse user modifiers like token+type or just type
27
46
  const parts = user.split('+');
28
- for (const part of parts) {
29
- if ([TunnelType.Http, TunnelType.Tcp, TunnelType.Tls, TunnelType.Udp, TunnelType.TlsTcp].includes(part.toLowerCase() as typeof TunnelType[keyof typeof TunnelType])) {
30
- type = part;
31
- } else if (part === 'force') {
32
- token = (token ? token + '+' : '') + part;
33
- } else if (part === 'qr') {
47
+
48
+ if (parts.length === 0) {
49
+ return { token, type, server, qrCode, forceFlag } as const;
50
+ }
51
+
52
+ const firstPart = parts[0];
53
+
54
+ // If the first part is NOT a keyword, it's a token
55
+ // All subsequent parts must be keywords
56
+ if (!isKeyword(firstPart)) {
57
+ token = firstPart;
58
+ // Process remaining parts as keywords
59
+ for (let i = 1; i < parts.length; i++) {
60
+ const part = parts[i].toLowerCase();
61
+ if (!isKeyword(part)) {
62
+ throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
63
+ }
64
+ processKeyword(part);
65
+ }
66
+ } else {
67
+ // First part is a keyword, all parts must be keywords
68
+ for (const part of parts) {
69
+ const lowerPart = part.toLowerCase();
70
+ if (!isKeyword(lowerPart)) {
71
+ // Invalid: non-keyword when keywords are expected
72
+ throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
73
+ }
74
+ processKeyword(lowerPart);
75
+ }
76
+ }
77
+
78
+ function processKeyword(keyword: string) {
79
+ if ([TunnelType.Http, TunnelType.Tcp, TunnelType.Tls, TunnelType.Udp, TunnelType.TlsTcp].includes(keyword as TunnelType)) {
80
+ type = keyword;
81
+ } else if (keyword === 'force') {
82
+ forceFlag = true;
83
+ } else if (keyword === 'qr') {
34
84
  qrCode = true;
35
- } else {
36
- token = (token ? token + '+' : '') + part;
37
85
  }
38
86
  }
39
87
  }
40
88
  } else if (domainRegex.test(str)) {
41
89
  server = str;
42
90
  }
43
- return { token, type, server, qrCode } as const;
91
+
92
+ return { token, type, server, qrCode, forceFlag } as const;
44
93
  }
45
94
 
46
95
  function parseUsers(positionalArgs: string[], explicitToken?: string) {
@@ -57,6 +106,8 @@ function parseUsers(positionalArgs: string[], explicitToken?: string) {
57
106
  if (parsed.server) server = parsed.server;
58
107
  if (parsed.type) type = parsed.type;
59
108
  if (parsed.token) token = parsed.token;
109
+ if (parsed.forceFlag) forceFlag = true;
110
+ if (parsed.qrCode) qrCode = true;
60
111
  }
61
112
 
62
113
  if (remaining.length > 0) {
@@ -65,19 +116,9 @@ function parseUsers(positionalArgs: string[], explicitToken?: string) {
65
116
  if (parsed.server) {
66
117
  server = parsed.server;
67
118
  if (parsed.type) type = parsed.type;
68
- if (parsed.token) {
69
- if (parsed.token.includes('+')) {
70
- const parts = parsed.token.split('+');
71
- const tOnly = parts.filter((p) => p !== 'force').join('+');
72
- if (tOnly) token = tOnly;
73
- if (parts.includes('force')) forceFlag = true;
74
- } else {
75
- token = parsed.token;
76
- }
77
- } if (parsed.qrCode) {
78
- // QR code request detected
79
- qrCode = true;
80
- }
119
+ if (parsed.token) token = parsed.token;
120
+ if (parsed.forceFlag) forceFlag = true;
121
+ if (parsed.qrCode) qrCode = true;
81
122
  remaining = remaining.slice(1);
82
123
  }
83
124
  }
@@ -327,7 +368,7 @@ function parseLocalTunnelAddr(finalConfig: FinalConfig, values: ParsedValues<typ
327
368
  const firstL = values.L[0] as string;
328
369
  const parts = firstL.split(':');
329
370
  if (parts.length === 3) {
330
- const lp = parseInt(parts[2], 10);
371
+ const lp = parseInt(parts[0], 10);
331
372
  if (!Number.isNaN(lp) && isValidPort(lp)) {
332
373
  finalConfig.webDebugger = `localhost:${lp}`;
333
374
  } else {
@@ -410,6 +451,22 @@ function parseServe(finalConfig: FinalConfig, values: ParsedValues<typeof cliOpt
410
451
  return null;
411
452
  }
412
453
 
454
+ function parseAutoReconnect(finalConfig: FinalConfig, values: ParsedValues<typeof cliOptions>): Error | null {
455
+ const autoReconnectValue = values.autoreconnect;
456
+ if (typeof autoReconnectValue === 'string') {
457
+ const trimmed = autoReconnectValue.trim().toLowerCase();
458
+ if (trimmed === 'true' || trimmed === '') {
459
+ finalConfig.autoReconnect = true;
460
+ } else if (trimmed === 'false') {
461
+ finalConfig.autoReconnect = false;
462
+ } else {
463
+ return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
464
+ }
465
+ }
466
+
467
+ return null;
468
+ }
469
+
413
470
  export async function buildFinalConfig(values: ParsedValues<typeof cliOptions>, positionals: string[]): Promise<FinalConfig> {
414
471
  let token: string | undefined;
415
472
  let server: string | undefined;
@@ -439,7 +496,7 @@ export async function buildFinalConfig(values: ParsedValues<typeof cliOptions>,
439
496
  tunnelType: initialTunnel ? [initialTunnel] : (configFromFile?.tunnelType || [TunnelType.Http]),
440
497
  NoTUI: values.notui || (configFromFile?.NoTUI || false),
441
498
  qrCode: qrCode || (configFromFile?.qrCode || false),
442
- autoReconnect: values.autoreconnect || (configFromFile?.autoReconnect || false),
499
+ autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
443
500
  };
444
501
 
445
502
 
@@ -463,6 +520,9 @@ export async function buildFinalConfig(values: ParsedValues<typeof cliOptions>,
463
520
  const serveErr = parseServe(finalConfig, values);
464
521
  if (serveErr instanceof Error) throw serveErr;
465
522
 
523
+ const autoReconnectErr = parseAutoReconnect(finalConfig, values);
524
+ if (autoReconnectErr instanceof Error) throw autoReconnectErr;
525
+
466
526
  // Apply force flag if indicated via user
467
527
  if (forceFlag) finalConfig.force = true;
468
528
 
@@ -16,5 +16,5 @@ export const defaultOptions: Omit<PinggyOptions, 'token'> & { token: string | un
16
16
  originalRequestUrl: false,
17
17
  allowPreflight: false,
18
18
  reverseProxy: false,
19
- autoReconnect: false,
19
+ autoReconnect: true,
20
20
  };
@@ -27,7 +27,7 @@ export const cliOptions = {
27
27
  vv: { type: 'boolean' as const, description: 'Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs.' },
28
28
  vvv: { type: 'boolean' as const, description: 'Enable all logs from Cli, SDK and internal components.' },
29
29
 
30
- autoreconnect: { type: 'boolean' as const, short :'a', description: 'Automatically reconnect tunnel on failure.' },
30
+ autoreconnect: { type: 'string' as const, short :'a', description: 'Automatically reconnect tunnel on failure. Use -a (defaults to true), -a true, or -a false.' },
31
31
 
32
32
  // Save and load config
33
33
  saveconf: { type: 'string' as const, description: 'Create the configuration file based on the options provided here' },
package/src/index.ts CHANGED
@@ -1,30 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import {
4
- hasVCRedist,
5
- getVCRedistMessage,
6
- openDownloadPage,
4
+ checkVCRedist,
5
+ openDownloadPage,
7
6
  } from "./utils/detect_vc_redist_on_windows.js";
8
7
  import CLIPrinter from "./utils/printer.js";
9
8
 
10
9
  async function verifyAndLoad() {
11
- if (process.platform === "win32" && !hasVCRedist()) {
12
- const msg = getVCRedistMessage();
13
- CLIPrinter.warn(
14
- msg?.message ??
15
- "This application requires the Microsoft Visual C++ Runtime on Windows."
16
- );
10
+ if (process.platform === "win32") {
11
+ const vcRedist = checkVCRedist();
12
+ if ( !vcRedist.installed ) {
13
+ CLIPrinter.warn(
14
+ vcRedist.message ??
15
+ "This application requires the Microsoft Visual C++ Runtime on Windows.",
16
+ );
17
17
 
18
- // open browser
19
- await openDownloadPage();
18
+ // open browser
19
+ await openDownloadPage();
20
20
 
21
- process.exit(1);
21
+ process.exit(1);
22
22
  }
23
+ }
23
24
 
24
- await import("./main.js");
25
+ await import("./main.js");
25
26
  }
26
27
 
27
28
  verifyAndLoad().catch((err) => {
28
- CLIPrinter.error(`Failed to start CLI:, ${err}`);
29
- process.exit(1);
29
+ CLIPrinter.error(`Failed to start CLI:, ${err}`);
30
+ process.exit(1);
30
31
  });
@@ -56,66 +56,29 @@ function checkRegistry() {
56
56
  return false;
57
57
  }
58
58
 
59
- function getVCRedistVersion() {
60
- if (os.platform() !== "win32") return null;
61
-
62
- try {
63
- for (const key of REGISTRY_KEYS) {
64
- const cmd = `reg query "${key}" /v Version 2>nul`;
65
- const result = execSync(cmd, { encoding: "utf8" });
66
-
67
- const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
68
- if (match) {
69
- return match[1];
70
- }
71
- }
72
- } catch {
73
- // Ignore errors
74
- }
75
-
76
- return null;
77
- }
78
59
 
79
60
  /**
80
- * Main detection function
61
+ * Main detection function - returns status and message for VC++ Redistributable
81
62
  */
82
- export function hasVCRedist() {
83
- if (os.platform() !== "win32") {
84
- return true; // Not Windows, assume OK
85
- }
86
-
87
- if (checkRegistry()) {
88
- return true;
89
- }
90
-
91
- if (checkDLLs()) {
92
- return true;
93
- }
94
-
95
- return false;
96
- }
97
-
98
- export function getVCRedistStatus() {
63
+ export function checkVCRedist() {
99
64
  if (os.platform() !== "win32") {
100
65
  return {
101
66
  required: false,
102
67
  installed: true,
103
- version: null,
104
- method: "non-windows",
68
+ message: null,
105
69
  };
106
70
  }
107
71
 
108
72
  const registryInstalled = checkRegistry();
109
73
  const dllsPresent = checkDLLs();
110
- const version = getVCRedistVersion();
74
+ const installed = registryInstalled || dllsPresent;
111
75
 
112
76
  return {
113
77
  required: true,
114
- installed: registryInstalled || dllsPresent,
115
- version,
116
- registryCheck: registryInstalled,
117
- dllCheck: dllsPresent,
118
- method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none",
78
+ installed,
79
+ message: installed
80
+ ? null
81
+ : "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\n"
119
82
  };
120
83
  }
121
84
 
@@ -128,10 +91,10 @@ export async function openDownloadPage() {
128
91
  }
129
92
  const url =
130
93
  "https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170";
131
-
94
+
132
95
  // Use cmd.exe explicitly for better compatibility
133
96
  const command = `cmd.exe /c start "" "${url}"`;
134
-
97
+
135
98
  try {
136
99
  await execAsync(command);
137
100
  CLIPrinter.info("\nOpening Microsoft download page in your browser...");
@@ -146,22 +109,3 @@ export async function openDownloadPage() {
146
109
  CLIPrinter.info(url + "\n");
147
110
  }
148
111
  }
149
-
150
-
151
- /**
152
- * Get error message
153
- */
154
- export function getVCRedistMessage() {
155
- const status = getVCRedistStatus();
156
-
157
- if (!status.required || status.installed) {
158
- return null;
159
- }
160
-
161
- return {
162
- error: true,
163
- message:
164
- "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\n" +
165
- "Please download and install it using the link below, then restart this application.\n",
166
- };
167
- }