pinggy 0.4.6 → 0.4.8

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
@@ -367,9 +367,10 @@ var init_TunnelManager = __esm({
367
367
  * status information, and statistics
368
368
  */
369
369
  async createTunnel(config) {
370
- const { configId, tunnelid: requestedTunnelId, tunnelName, name, serve } = config;
370
+ const { configId, tunnelid: requestedTunnelId, tunnelName, name } = config;
371
371
  const tunnelid = requestedTunnelId || getRandomId();
372
372
  const autoReconnect = config.autoReconnect || false;
373
+ const serve = this.resolveServePath(config);
373
374
  if (!configId || typeof configId !== "string" || configId.trim() === "") {
374
375
  throw new Error("configId is required and must be a non-empty string");
375
376
  }
@@ -456,8 +457,11 @@ var init_TunnelManager = __esm({
456
457
  throw error;
457
458
  }
458
459
  logger.info("Tunnel started", { tunnelId, urls });
460
+ logger.info("Checking serve config for tunnel", { tunnelId, serve: managed.serve });
459
461
  if (managed.serve) {
460
462
  this.startStaticFileServer(managed);
463
+ } else {
464
+ logger.debug("No serve path configured, skipping static file server", { tunnelId });
461
465
  }
462
466
  try {
463
467
  const startListeners = this.tunnelStartListeners.get(tunnelId);
@@ -789,6 +793,7 @@ var init_TunnelManager = __esm({
789
793
  const currentTunnelName = existingTunnel.tunnelName;
790
794
  const currentServe = existingTunnel.serve;
791
795
  const currentAutoReconnect = existingTunnel.autoReconnect || false;
796
+ const requestedServe = this.resolveServePath(newConfig);
792
797
  try {
793
798
  if (!isStopped) {
794
799
  existingTunnel.instance.stop();
@@ -799,9 +804,9 @@ var init_TunnelManager = __esm({
799
804
  ...newConfig,
800
805
  configId,
801
806
  tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
802
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
807
+ serve: requestedServe !== void 0 ? requestedServe : currentServe
803
808
  };
804
- const effectiveServe = newConfig.serve !== void 0 ? newConfig.serve : currentServe;
809
+ const effectiveServe = requestedServe !== void 0 ? requestedServe : currentServe;
805
810
  const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
806
811
  let configWithForwarding;
807
812
  const newTunnel = await this._createTunnelWithProcessedConfig({
@@ -1560,21 +1565,35 @@ var init_TunnelManager = __esm({
1560
1565
  const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1561
1566
  return isNaN(parsed) ? 0 : parsed;
1562
1567
  }
1568
+ /**
1569
+ * Read serve path only from config.optional.serve.
1570
+ */
1571
+ resolveServePath(config) {
1572
+ const optional = config.optional;
1573
+ const servePath = optional?.serve;
1574
+ logger.debug("resolveServePath", { servePath, hasOptional: !!optional, optionalKeys: optional ? Object.keys(optional) : [] });
1575
+ return servePath;
1576
+ }
1563
1577
  startStaticFileServer(managed) {
1564
1578
  try {
1565
1579
  const __filename4 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1566
1580
  const __dirname3 = import_node_path.default.dirname(__filename4);
1567
1581
  const fileServerWorkerPath = import_node_path.default.join(__dirname3, "workers", "file_serve_worker.cjs");
1582
+ logger.info("Starting static file server worker", {
1583
+ dir: managed.serve,
1584
+ forwarding: JSON.stringify(managed.tunnelConfig?.forwarding),
1585
+ workerPath: fileServerWorkerPath
1586
+ });
1568
1587
  const staticServerWorker = new import_node_worker_threads.Worker(fileServerWorkerPath, {
1569
1588
  workerData: {
1570
1589
  dir: managed.serve,
1571
- port: managed.tunnelConfig?.forwarding
1590
+ forwarding: managed.tunnelConfig?.forwarding
1572
1591
  }
1573
1592
  });
1574
1593
  staticServerWorker.on("message", (msg) => {
1575
1594
  switch (msg.type) {
1576
1595
  case "started":
1577
- logger.info("Static file server started", { dir: managed.serve });
1596
+ logger.info("Static file server started", { dir: managed.serve, port: msg.portNum });
1578
1597
  break;
1579
1598
  case "warning":
1580
1599
  if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
@@ -3084,10 +3103,14 @@ var init_options = __esm({
3084
3103
  v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
3085
3104
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
3086
3105
  vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
3087
- autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
3088
- // Save and load config
3106
+ "no-autoreconnect": { type: "boolean", short: "a", description: "Disable auto reconnection on failure (enabled by default)." },
3107
+ // Save and load config (legacy file-based)
3089
3108
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
3090
3109
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
3110
+ // Used by `pinggy config save` and `buildAndStartTunnel` save flow
3111
+ save: { type: "boolean", short: "s", description: "Save the tunnel config (use with config save or -l)", hidden: true },
3112
+ name: { type: "string", description: "Name for the tunnel config", hidden: true },
3113
+ auto: { type: "boolean", description: "Mark tunnel config for auto-start", hidden: true },
3091
3114
  // File server
3092
3115
  serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
3093
3116
  // Remote Control
@@ -3137,7 +3160,21 @@ function printHelpMessage() {
3137
3160
  console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
3138
3161
  console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
3139
3162
  console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
3140
- console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
3163
+ console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region");
3164
+ console.log("\nConfig Management:");
3165
+ console.log(" pinggy config list # List saved configs");
3166
+ console.log(" pinggy config show my-tunnel # Show config details");
3167
+ console.log(" pinggy config save my-tunnel -l 3000 token@pro.pinggy.io # Save config");
3168
+ console.log(" pinggy config save my-tunnel --auto -l 3000 # Save with auto-start");
3169
+ console.log(" pinggy config update my-tunnel -l 4000 # Update saved config");
3170
+ console.log(" pinggy config delete my-tunnel # Delete saved config");
3171
+ console.log(" pinggy config auto my-tunnel # Enable auto-start");
3172
+ console.log(" pinggy config noauto my-tunnel # Disable auto-start");
3173
+ console.log("\nStart Saved Tunnels:");
3174
+ console.log(" pinggy start my-tunnel # Start saved tunnel");
3175
+ console.log(" pinggy start my-tunnel -l 4000 # Start with runtime overrides");
3176
+ console.log(" pinggy start tunnela tunnelb # Start multiple tunnels");
3177
+ console.log(" pinggy start --all # Start all auto-start tunnels\n");
3141
3178
  }
3142
3179
  var init_help = __esm({
3143
3180
  "src/cli/help.ts"() {
@@ -3147,6 +3184,74 @@ var init_help = __esm({
3147
3184
  }
3148
3185
  });
3149
3186
 
3187
+ // src/utils/parseArgs.ts
3188
+ function isAttachedReverseOrLocalFlag(arg) {
3189
+ return /^-[RL].+/.test(arg);
3190
+ }
3191
+ function shouldMergeReverseOrLocalFragment(current, next) {
3192
+ if (next.startsWith("-")) {
3193
+ return false;
3194
+ }
3195
+ if (next.startsWith(".")) {
3196
+ return true;
3197
+ }
3198
+ const body = current.slice(2);
3199
+ if (body.endsWith(":")) {
3200
+ return true;
3201
+ }
3202
+ if (body.includes("//") && !body.includes(":")) {
3203
+ return true;
3204
+ }
3205
+ return false;
3206
+ }
3207
+ function preprocessWindowsArgs(args) {
3208
+ if (os2.platform() !== "win32") {
3209
+ return args;
3210
+ }
3211
+ ;
3212
+ const out = [];
3213
+ let i = 0;
3214
+ while (i < args.length) {
3215
+ const arg = args[i];
3216
+ if (isAttachedReverseOrLocalFlag(arg)) {
3217
+ let merged = arg;
3218
+ while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
3219
+ merged += args[i + 1];
3220
+ i++;
3221
+ }
3222
+ out.push(merged);
3223
+ i++;
3224
+ continue;
3225
+ }
3226
+ out.push(arg);
3227
+ i++;
3228
+ }
3229
+ return out;
3230
+ }
3231
+ function parseCliArgs(options, overrideArgs) {
3232
+ const rawArgs = overrideArgs ?? process.argv.slice(2);
3233
+ const processedArgs = preprocessWindowsArgs(rawArgs);
3234
+ const parsed = (0, import_util5.parseArgs)({
3235
+ args: processedArgs,
3236
+ options,
3237
+ allowPositionals: true
3238
+ });
3239
+ const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
3240
+ return {
3241
+ ...parsed,
3242
+ hasAnyArgs
3243
+ };
3244
+ }
3245
+ var import_util5, os2;
3246
+ var init_parseArgs = __esm({
3247
+ "src/utils/parseArgs.ts"() {
3248
+ "use strict";
3249
+ init_cjs_shims();
3250
+ import_util5 = require("util");
3251
+ os2 = __toESM(require("os"), 1);
3252
+ }
3253
+ });
3254
+
3150
3255
  // src/cli/defaults.ts
3151
3256
  var defaultOptions;
3152
3257
  var init_defaults = __esm({
@@ -3154,6 +3259,7 @@ var init_defaults = __esm({
3154
3259
  "use strict";
3155
3260
  init_cjs_shims();
3156
3261
  defaultOptions = {
3262
+ version: "1.0",
3157
3263
  token: void 0,
3158
3264
  // No default token
3159
3265
  serverAddress: "a.pinggy.io",
@@ -3656,7 +3762,7 @@ function parseToken(finalConfig, explicitToken) {
3656
3762
  finalConfig.token = explicitToken;
3657
3763
  }
3658
3764
  }
3659
- function parseArgs(finalConfig, remainingPositionals) {
3765
+ function parseArgs2(finalConfig, remainingPositionals) {
3660
3766
  let localserverTls = "";
3661
3767
  localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
3662
3768
  if (localserverTls.length > 0 && finalConfig.forwarding) {
@@ -3667,10 +3773,10 @@ function parseArgs(finalConfig, remainingPositionals) {
3667
3773
  }
3668
3774
  function storeJson(config, saveconf) {
3669
3775
  if (saveconf) {
3670
- const path5 = saveconf;
3776
+ const path7 = saveconf;
3671
3777
  try {
3672
- import_fs4.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
3673
- logger.info(`Configuration saved to ${path5}`);
3778
+ import_fs4.default.writeFileSync(path7, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
3779
+ logger.info(`Configuration saved to ${path7}`);
3674
3780
  } catch (err) {
3675
3781
  const msg = err instanceof Error ? err.message : String(err);
3676
3782
  logger.error("Error loading configuration:", msg);
@@ -3707,20 +3813,12 @@ function parseServe(finalConfig, values) {
3707
3813
  return null;
3708
3814
  }
3709
3815
  function parseAutoReconnect(finalConfig, values) {
3710
- const autoReconnectValue = values.autoreconnect;
3711
- if (typeof autoReconnectValue === "string") {
3712
- const trimmed = autoReconnectValue.trim().toLowerCase();
3713
- if (trimmed === "true" || trimmed === "") {
3714
- finalConfig.autoReconnect = true;
3715
- } else if (trimmed === "false") {
3716
- finalConfig.autoReconnect = false;
3717
- } else {
3718
- return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
3719
- }
3816
+ if (values["no-autoreconnect"]) {
3817
+ finalConfig.autoReconnect = false;
3720
3818
  }
3721
3819
  return null;
3722
3820
  }
3723
- async function buildFinalConfig(values, positionals) {
3821
+ async function buildFinalConfig(values, positionals, baseConfig) {
3724
3822
  let token;
3725
3823
  let server;
3726
3824
  let type;
@@ -3728,7 +3826,7 @@ async function buildFinalConfig(values, positionals) {
3728
3826
  let qrCode = false;
3729
3827
  let finalConfig = new Object();
3730
3828
  let saveconf = isSaveConfOption(values);
3731
- const configFromFile = loadJsonConfig(values);
3829
+ const configFromFile = baseConfig || loadJsonConfig(values);
3732
3830
  const userParse = parseUsers(positionals, values.token);
3733
3831
  token = userParse.token;
3734
3832
  server = userParse.server;
@@ -3780,7 +3878,7 @@ async function buildFinalConfig(values, positionals) {
3780
3878
  if (forceFlag || values.force) {
3781
3879
  finalConfig.force = true;
3782
3880
  }
3783
- parseArgs(finalConfig, remainingPositionals);
3881
+ parseArgs2(finalConfig, remainingPositionals);
3784
3882
  storeJson(finalConfig, saveconf);
3785
3883
  return finalConfig;
3786
3884
  }
@@ -3811,74 +3909,6 @@ var init_buildConfig = __esm({
3811
3909
  }
3812
3910
  });
3813
3911
 
3814
- // src/utils/parseArgs.ts
3815
- function isAttachedReverseOrLocalFlag(arg) {
3816
- return /^-[RL].+/.test(arg);
3817
- }
3818
- function shouldMergeReverseOrLocalFragment(current, next) {
3819
- if (next.startsWith("-")) {
3820
- return false;
3821
- }
3822
- if (next.startsWith(".")) {
3823
- return true;
3824
- }
3825
- const body = current.slice(2);
3826
- if (body.endsWith(":")) {
3827
- return true;
3828
- }
3829
- if (body.includes("//") && !body.includes(":")) {
3830
- return true;
3831
- }
3832
- return false;
3833
- }
3834
- function preprocessWindowsArgs(args) {
3835
- if (os2.platform() !== "win32") {
3836
- return args;
3837
- }
3838
- ;
3839
- const out = [];
3840
- let i = 0;
3841
- while (i < args.length) {
3842
- const arg = args[i];
3843
- if (isAttachedReverseOrLocalFlag(arg)) {
3844
- let merged = arg;
3845
- while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
3846
- merged += args[i + 1];
3847
- i++;
3848
- }
3849
- out.push(merged);
3850
- i++;
3851
- continue;
3852
- }
3853
- out.push(arg);
3854
- i++;
3855
- }
3856
- return out;
3857
- }
3858
- function parseCliArgs(options) {
3859
- const rawArgs = process.argv.slice(2);
3860
- const processedArgs = preprocessWindowsArgs(rawArgs);
3861
- const parsed = (0, import_util6.parseArgs)({
3862
- args: processedArgs,
3863
- options,
3864
- allowPositionals: true
3865
- });
3866
- const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
3867
- return {
3868
- ...parsed,
3869
- hasAnyArgs
3870
- };
3871
- }
3872
- var import_util6, os2;
3873
- var init_parseArgs = __esm({
3874
- "src/utils/parseArgs.ts"() {
3875
- "use strict";
3876
- init_cjs_shims();
3877
- import_util6 = require("util");
3878
- os2 = __toESM(require("os"), 1);
3879
- }
3880
- });
3881
-
3882
3912
  // src/utils/getFreePort.ts
3883
3913
  function getFreePort(webDebugger) {
3884
3914
  return new Promise((resolve, reject) => {
@@ -5492,6 +5522,600 @@ var init_starCli = __esm({
5492
5522
  }
5493
5523
  });
5494
5524
 
5525
+ // src/utils/configDir.ts
5526
+ function getPinggyConfigDir() {
5527
+ const platform2 = import_os2.default.platform();
5528
+ let baseDir;
5529
+ if (platform2 === "win32") {
5530
+ baseDir = process.env.APPDATA || import_path5.default.join(import_os2.default.homedir(), "AppData", "Roaming");
5531
+ } else {
5532
+ baseDir = process.env.XDG_CONFIG_HOME || import_path5.default.join(import_os2.default.homedir(), ".config");
5533
+ }
5534
+ return import_path5.default.join(baseDir, "pinggy");
5535
+ }
5536
+ function getTunnelConfigDir() {
5537
+ return import_path5.default.join(getPinggyConfigDir(), "tunnels");
5538
+ }
5539
+ function ensureTunnelConfigDir() {
5540
+ const dir = getTunnelConfigDir();
5541
+ import_fs5.default.mkdirSync(dir, { recursive: true });
5542
+ return dir;
5543
+ }
5544
+ var import_os2, import_path5, import_fs5;
5545
+ var init_configDir = __esm({
5546
+ "src/utils/configDir.ts"() {
5547
+ "use strict";
5548
+ init_cjs_shims();
5549
+ import_os2 = __toESM(require("os"), 1);
5550
+ import_path5 = __toESM(require("path"), 1);
5551
+ import_fs5 = __toESM(require("fs"), 1);
5552
+ }
5553
+ });
5554
+
5555
+ // src/cli/configStore.ts
5556
+ function buildFilename(name, configId) {
5557
+ return `${name}_${configId}.json`;
5558
+ }
5559
+ function sanitizeName(name) {
5560
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
5561
+ }
5562
+ function validateName(name) {
5563
+ if (!name || name.trim().length === 0) {
5564
+ return new Error("Tunnel name cannot be empty.");
5565
+ }
5566
+ if (name.length > 128) {
5567
+ return new Error("Tunnel name cannot exceed 128 characters.");
5568
+ }
5569
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
5570
+ return new Error("Tunnel name can only contain alphanumeric characters, hyphens, and underscores.");
5571
+ }
5572
+ return null;
5573
+ }
5574
+ function readConfigFile(filePath) {
5575
+ try {
5576
+ const data = import_fs6.default.readFileSync(filePath, { encoding: "utf-8" });
5577
+ return JSON.parse(data);
5578
+ } catch (err) {
5579
+ logger.warn(`Failed to read config file ${filePath}:`, err);
5580
+ return null;
5581
+ }
5582
+ }
5583
+ function writeConfigFile(filePath, config) {
5584
+ import_fs6.default.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
5585
+ }
5586
+ function listSavedConfigs() {
5587
+ const dir = getTunnelConfigDir();
5588
+ if (!import_fs6.default.existsSync(dir)) {
5589
+ return [];
5590
+ }
5591
+ const files = import_fs6.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
5592
+ const configs = [];
5593
+ for (const file of files) {
5594
+ const config = readConfigFile(import_path6.default.join(dir, file));
5595
+ if (config && config.name && config.configId) {
5596
+ configs.push(config);
5597
+ }
5598
+ }
5599
+ return configs;
5600
+ }
5601
+ function findConfigFile(nameOrId) {
5602
+ const dir = getTunnelConfigDir();
5603
+ if (!import_fs6.default.existsSync(dir)) return null;
5604
+ const files = import_fs6.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
5605
+ const sanitized = sanitizeName(nameOrId);
5606
+ const nameMatch = files.find((f) => f.startsWith(sanitized + "_"));
5607
+ if (nameMatch) {
5608
+ const filePath = import_path6.default.join(dir, nameMatch);
5609
+ const config = readConfigFile(filePath);
5610
+ if (config && config.name === nameOrId) return { filePath, config };
5611
+ }
5612
+ const idCandidates = files.filter((f) => {
5613
+ const withoutExt = f.replace(/\.json$/, "");
5614
+ const lastUnderscore = withoutExt.indexOf("_");
5615
+ if (lastUnderscore === -1) return false;
5616
+ const idPart = withoutExt.slice(lastUnderscore + 1);
5617
+ return idPart.startsWith(nameOrId);
5618
+ });
5619
+ if (idCandidates.length === 1) {
5620
+ const filePath = import_path6.default.join(dir, idCandidates[0]);
5621
+ const config = readConfigFile(filePath);
5622
+ if (config) return { filePath, config };
5623
+ }
5624
+ return null;
5625
+ }
5626
+ function findConfigByName(name) {
5627
+ const resolved = findConfigFile(name);
5628
+ return resolved?.config.name === name ? resolved.config : null;
5629
+ }
5630
+ function findConfig(nameOrId) {
5631
+ return findConfigFile(nameOrId)?.config ?? null;
5632
+ }
5633
+ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
5634
+ const nameErr = validateName(name);
5635
+ if (nameErr) {
5636
+ throw nameErr;
5637
+ }
5638
+ const existing = findConfigByName(name);
5639
+ if (existing) {
5640
+ throw new Error(
5641
+ `A tunnel config with the name "${name}" already exists (configId: ${existing.configId}). Please use a different name.`
5642
+ );
5643
+ }
5644
+ const dir = ensureTunnelConfigDir();
5645
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5646
+ const saved = {
5647
+ name,
5648
+ configId,
5649
+ autoStart,
5650
+ createdAt: now,
5651
+ updatedAt: now,
5652
+ tunnelConfig
5653
+ };
5654
+ const filename = buildFilename(sanitizeName(name), configId);
5655
+ const filePath = import_path6.default.join(dir, filename);
5656
+ import_fs6.default.writeFileSync(filePath, JSON.stringify(saved, null, 2), { encoding: "utf-8" });
5657
+ logger.info(`Config "${name}" saved to ${filePath}`);
5658
+ return saved;
5659
+ }
5660
+ function deleteConfig(nameOrId) {
5661
+ const resolved = findConfigFile(nameOrId);
5662
+ if (!resolved) return null;
5663
+ import_fs6.default.unlinkSync(resolved.filePath);
5664
+ logger.info(`Config "${resolved.config.name}" deleted.`);
5665
+ return resolved.config.name;
5666
+ }
5667
+ function updateConfigAutoStart(nameOrId, autoStart) {
5668
+ const resolved = findConfigFile(nameOrId);
5669
+ if (!resolved) return null;
5670
+ resolved.config.autoStart = autoStart;
5671
+ resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5672
+ writeConfigFile(resolved.filePath, resolved.config);
5673
+ logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
5674
+ return resolved.config;
5675
+ }
5676
+ function updateTunnelConfig(nameOrId, tunnelConfig) {
5677
+ const resolved = findConfigFile(nameOrId);
5678
+ if (!resolved) return null;
5679
+ resolved.config.tunnelConfig = tunnelConfig;
5680
+ resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5681
+ writeConfigFile(resolved.filePath, resolved.config);
5682
+ logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
5683
+ return resolved.config;
5684
+ }
5685
+ function getAutoStartConfigs() {
5686
+ return listSavedConfigs().filter((c) => c.autoStart);
5687
+ }
5688
+ function printConfigList() {
5689
+ const configs = listSavedConfigs();
5690
+ if (configs.length === 0) {
5691
+ console.log(import_picocolors5.default.yellow("No saved tunnel configs found."));
5692
+ console.log(import_picocolors5.default.gray(`Config directory: ${getTunnelConfigDir()}`));
5693
+ return;
5694
+ }
5695
+ const nameW = 20;
5696
+ const idW = 12;
5697
+ const typeW = 8;
5698
+ const fwdW = 25;
5699
+ const serverW = 22;
5700
+ const autoW = 10;
5701
+ const header = import_picocolors5.default.bold("Name".padEnd(nameW)) + import_picocolors5.default.bold("Config ID".padEnd(idW)) + import_picocolors5.default.bold("Type".padEnd(typeW)) + import_picocolors5.default.bold("Forwarding".padEnd(fwdW)) + import_picocolors5.default.bold("Server".padEnd(serverW)) + import_picocolors5.default.bold("Auto-start".padEnd(autoW));
5702
+ console.log("\n" + header);
5703
+ console.log(import_picocolors5.default.gray("\u2500".repeat(nameW + idW + typeW + fwdW + serverW + autoW)));
5704
+ for (const c of configs) {
5705
+ const tc = c.tunnelConfig;
5706
+ const forwarding = Array.isArray(tc.forwarding) ? tc.forwarding[0]?.address : String(tc.forwarding || "");
5707
+ const type = (Array.isArray(tc.forwarding) ? tc.forwarding[0]?.type : void 0) || "http";
5708
+ const server = tc.serverAddress || "a.pinggy.io";
5709
+ const line = import_picocolors5.default.cyanBright(c.name.padEnd(nameW)) + import_picocolors5.default.gray(c.configId.slice(0, 8).padEnd(idW)) + type.padEnd(typeW) + forwarding.slice(0, fwdW - 2).padEnd(fwdW) + server.slice(0, serverW - 2).padEnd(serverW) + (c.autoStart ? import_picocolors5.default.green("yes") : import_picocolors5.default.gray("no")).padEnd(autoW);
5710
+ console.log(line);
5711
+ }
5712
+ console.log();
5713
+ }
5714
+ function printConfigDetail(config) {
5715
+ console.log(import_picocolors5.default.bold(`
5716
+ Tunnel Config: ${import_picocolors5.default.cyanBright(config.name)}`));
5717
+ console.log(import_picocolors5.default.gray("\u2500".repeat(40)));
5718
+ console.log(` Config ID: ${config.configId}`);
5719
+ console.log(` Auto-start: ${config.autoStart ? import_picocolors5.default.green("yes") : import_picocolors5.default.gray("no")}`);
5720
+ console.log(` Created: ${config.createdAt}`);
5721
+ console.log(` Updated: ${config.updatedAt}`);
5722
+ console.log(import_picocolors5.default.gray("\u2500".repeat(40)));
5723
+ console.log(` Server: ${config.tunnelConfig.serverAddress || "a.pinggy.io"}`);
5724
+ console.log(` Token: ${config.tunnelConfig.token ? "***" + config.tunnelConfig.token.slice(-4) : "(none)"}`);
5725
+ const fwd = config.tunnelConfig.forwarding;
5726
+ if (Array.isArray(fwd)) {
5727
+ const defaultFwds = [];
5728
+ const customFwds = [];
5729
+ for (const f of fwd) {
5730
+ if (typeof f === "string") {
5731
+ defaultFwds.push(f);
5732
+ } else if (f.listenAddress) {
5733
+ customFwds.push(f);
5734
+ } else {
5735
+ defaultFwds.push(f);
5736
+ }
5737
+ }
5738
+ for (const f of defaultFwds) {
5739
+ const addr = typeof f === "string" ? f : `${f.address} (${f.type || "http"})`;
5740
+ console.log(` Forwarding: ${addr}`);
5741
+ if (config.tunnelConfig.webDebugger) {
5742
+ console.log(` Debugger: ${config.tunnelConfig.webDebugger}`);
5743
+ }
5744
+ }
5745
+ if (customFwds.length > 0) {
5746
+ console.log(import_picocolors5.default.gray("\u2500".repeat(40)));
5747
+ console.log(import_picocolors5.default.bold(" Domain Mappings:"));
5748
+ for (const f of customFwds) {
5749
+ if (typeof f === "string") continue;
5750
+ const domain = f.listenAddress;
5751
+ const target = f.address;
5752
+ const type = f.type || "http";
5753
+ console.log(` ${import_picocolors5.default.cyanBright(domain)} \u2192 ${target} (${type})`);
5754
+ }
5755
+ }
5756
+ } else if (fwd) {
5757
+ console.log(` Forwarding: ${fwd}`);
5758
+ }
5759
+ console.log();
5760
+ }
5761
+ var import_fs6, import_path6, import_picocolors5;
5762
+ var init_configStore = __esm({
5763
+ "src/cli/configStore.ts"() {
5764
+ "use strict";
5765
+ init_cjs_shims();
5766
+ import_fs6 = __toESM(require("fs"), 1);
5767
+ import_path6 = __toESM(require("path"), 1);
5768
+ init_configDir();
5769
+ init_logger();
5770
+ import_picocolors5 = __toESM(require("picocolors"), 1);
5771
+ }
5772
+ });
5773
+
5774
+ // src/cli/buildAndStartTunnel.ts
5775
+ async function buildAndStartTunnel(values, positionals, manager) {
5776
+ await initRemoteManagement(values);
5777
+ logger.debug("Building final config from CLI values and positionals", { values, positionals });
5778
+ const finalConfig = await buildFinalConfig(values, positionals);
5779
+ logger.debug("Final configuration built", finalConfig);
5780
+ if (values.save) {
5781
+ const name = values.name;
5782
+ if (!name) {
5783
+ printer_default.error("--save requires --name to specify a name for the tunnel config.");
5784
+ process.exit(1);
5785
+ }
5786
+ const nameErr = validateName(name);
5787
+ if (nameErr) {
5788
+ printer_default.error(nameErr.message);
5789
+ process.exit(1);
5790
+ }
5791
+ const autoStart = !!values.auto;
5792
+ saveConfig(name, finalConfig.configId, finalConfig, autoStart);
5793
+ printer_default.success(`Config "${name}" saved.`);
5794
+ }
5795
+ await startCli(finalConfig, manager);
5796
+ }
5797
+ async function initRemoteManagement(values) {
5798
+ const parseResult = await parseRemoteManagement(values);
5799
+ if (parseResult?.ok === false) {
5800
+ logger.error("Failed to initiate remote management:", parseResult.error);
5801
+ printer_default.fatal(parseResult.error);
5802
+ }
5803
+ }
5804
+ var init_buildAndStartTunnel = __esm({
5805
+ "src/cli/buildAndStartTunnel.ts"() {
5806
+ "use strict";
5807
+ init_cjs_shims();
5808
+ init_logger();
5809
+ init_remoteManagement();
5810
+ init_buildConfig();
5811
+ init_starCli();
5812
+ init_printer();
5813
+ init_configStore();
5814
+ }
5815
+ });
5816
+
5817
+ // src/cli/subcommands.ts
5818
+ function isSubcommand(rawArgs) {
5819
+ return rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0]);
5820
+ }
5821
+ async function handleSubcommand(rawArgs, manager) {
5822
+ const sub = rawArgs[0];
5823
+ const rest = rawArgs.slice(1);
5824
+ switch (sub) {
5825
+ case "config":
5826
+ await handleConfig(rest);
5827
+ return;
5828
+ case "start":
5829
+ await handleStart(rest, manager);
5830
+ return;
5831
+ }
5832
+ }
5833
+ async function handleConfig(args) {
5834
+ if (args.length === 0) {
5835
+ printConfigHelp();
5836
+ return;
5837
+ }
5838
+ const verb = args[0];
5839
+ const rest = args.slice(1);
5840
+ switch (verb) {
5841
+ case "list":
5842
+ case "ls":
5843
+ printConfigList();
5844
+ return;
5845
+ case "show": {
5846
+ const names = requireNames(rest, "config show");
5847
+ for (const name of names) {
5848
+ const saved2 = resolveConfig(name);
5849
+ if (saved2) printConfigDetail(saved2);
5850
+ }
5851
+ return;
5852
+ }
5853
+ case "save": {
5854
+ const name = requireName(rest, "config save");
5855
+ await handleConfigSave(name, rest.slice(1));
5856
+ return;
5857
+ }
5858
+ case "delete": {
5859
+ const names = requireNames(rest, "config delete");
5860
+ for (const name of names) {
5861
+ const deletedName = deleteConfig(name);
5862
+ if (deletedName) {
5863
+ printer_default.success(`Config "${deletedName}" deleted.`);
5864
+ } else {
5865
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
5866
+ }
5867
+ }
5868
+ return;
5869
+ }
5870
+ case "update": {
5871
+ const name = requireName(rest, "config update");
5872
+ await handleConfigUpdate(name, rest.slice(1));
5873
+ return;
5874
+ }
5875
+ case "auto": {
5876
+ const names = requireNames(rest, "config auto");
5877
+ for (const name of names) {
5878
+ const updated = updateConfigAutoStart(name, true);
5879
+ if (updated) {
5880
+ printer_default.success(`Config "${updated.name}" auto-start set to on.`);
5881
+ } else {
5882
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
5883
+ }
5884
+ }
5885
+ return;
5886
+ }
5887
+ case "noauto": {
5888
+ const names = requireNames(rest, "config noauto");
5889
+ for (const name of names) {
5890
+ const updated = updateConfigAutoStart(name, false);
5891
+ if (updated) {
5892
+ printer_default.success(`Config "${updated.name}" auto-start set to off.`);
5893
+ } else {
5894
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
5895
+ }
5896
+ }
5897
+ return;
5898
+ }
5899
+ default:
5900
+ const saved = resolveConfig(verb);
5901
+ if (saved) printConfigDetail(saved);
5902
+ return;
5903
+ }
5904
+ }
5905
+ async function handleConfigSave(name, remainingArgs) {
5906
+ const nameErr = validateName(name);
5907
+ if (nameErr) {
5908
+ printer_default.error(nameErr.message);
5909
+ process.exit(1);
5910
+ }
5911
+ const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
5912
+ const autoStart = !!values.auto;
5913
+ logger.debug("Building config for save", { name, values, positionals });
5914
+ const finalConfig = await buildFinalConfig(values, positionals);
5915
+ saveConfig(name, finalConfig.configId, finalConfig, autoStart);
5916
+ printer_default.success(`Config "${name}" saved.`);
5917
+ }
5918
+ async function handleConfigUpdate(nameOrId, remainingArgs) {
5919
+ const saved = resolveConfig(nameOrId);
5920
+ if (!saved) return;
5921
+ const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
5922
+ logger.debug("Building updated config", { nameOrId, values, positionals });
5923
+ const updatedConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
5924
+ const result = updateTunnelConfig(nameOrId, updatedConfig);
5925
+ if (result) {
5926
+ printer_default.success(`Config "${result.name}" updated.`);
5927
+ printConfigDetail(result);
5928
+ } else {
5929
+ printer_default.error(`Failed to update config "${nameOrId}".`);
5930
+ }
5931
+ }
5932
+ async function handleStart(args, manager) {
5933
+ const startAll = args.includes("--all");
5934
+ const argsWithoutAll = args.filter((a) => a !== "--all");
5935
+ const names = [];
5936
+ let i = 0;
5937
+ while (i < argsWithoutAll.length && !argsWithoutAll[i].startsWith("-")) {
5938
+ names.push(argsWithoutAll[i]);
5939
+ i++;
5940
+ }
5941
+ const flagArgs = argsWithoutAll.slice(i);
5942
+ const { values, positionals } = parseCliArgs(cliOptions, flagArgs);
5943
+ configureLogger(values);
5944
+ if (startAll) {
5945
+ await initRemoteManagementBackground(values);
5946
+ await startAutoStartTunnels(manager);
5947
+ return;
5948
+ }
5949
+ if (names.length === 0) {
5950
+ printStartHelp();
5951
+ return;
5952
+ }
5953
+ const resolved = [];
5954
+ for (const name of names) {
5955
+ const saved = resolveConfig(name);
5956
+ if (!saved) return;
5957
+ resolved.push(saved);
5958
+ }
5959
+ if (resolved.length > 1 && flagArgs.length > 0) {
5960
+ printer_default.error("Runtime overrides (-l, --type, etc.) can only be used when starting a single tunnel.");
5961
+ printer_default.print(" Start one tunnel: pinggy start my-tunnel -l 4000");
5962
+ printer_default.print(" Or update first: pinggy config update my-tunnel -l 4000");
5963
+ return;
5964
+ }
5965
+ await initRemoteManagementBackground(values);
5966
+ if (resolved.length === 1) {
5967
+ const saved = resolved[0];
5968
+ logger.debug("Building config with overrides", { name: saved.name });
5969
+ const finalConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
5970
+ finalConfig.configId = saved.configId;
5971
+ await startCli(finalConfig, manager);
5972
+ } else {
5973
+ await startNamedTunnels(resolved, manager);
5974
+ }
5975
+ }
5976
+ async function startAutoStartTunnels(manager) {
5977
+ const configs = getAutoStartConfigs();
5978
+ if (configs.length === 0) {
5979
+ printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
5980
+ return;
5981
+ }
5982
+ printer_default.print(import_picocolors6.default.cyanBright(`Starting ${configs.length} auto-start tunnel(s)...`));
5983
+ for (const saved of configs) {
5984
+ await startSavedTunnel(saved, manager);
5985
+ }
5986
+ printer_default.print(import_picocolors6.default.gray("\nAll auto-start tunnels launched. Press Ctrl+C to stop.\n"));
5987
+ await new Promise(() => {
5988
+ });
5989
+ }
5990
+ async function startNamedTunnels(configs, manager) {
5991
+ printer_default.print(import_picocolors6.default.cyanBright(`Starting ${configs.length} tunnel(s)...`));
5992
+ for (const saved of configs) {
5993
+ await startSavedTunnel(saved, manager);
5994
+ }
5995
+ printer_default.print(import_picocolors6.default.gray("\nAll tunnels launched. Press Ctrl+C to stop.\n"));
5996
+ await new Promise(() => {
5997
+ });
5998
+ }
5999
+ async function startSavedTunnel(saved, manager) {
6000
+ const config = {
6001
+ ...saved.tunnelConfig,
6002
+ configId: saved.configId,
6003
+ name: saved.name,
6004
+ optional: {
6005
+ ...saved.tunnelConfig.optional,
6006
+ noTui: true
6007
+ }
6008
+ };
6009
+ try {
6010
+ const tunnel = await manager.createTunnel(config);
6011
+ await manager.startTunnel(tunnel.tunnelid);
6012
+ const urls = await manager.getTunnelUrls(tunnel.tunnelid);
6013
+ printer_default.success(`"${saved.name}" started`);
6014
+ (urls ?? []).forEach(
6015
+ (url) => printer_default.print(" " + import_picocolors6.default.magentaBright(url))
6016
+ );
6017
+ manager.registerWorkerErrorListner(tunnel.tunnelid, (_id, error) => {
6018
+ printer_default.error(`[${saved.name}] Fatal: ${error.message}`);
6019
+ });
6020
+ manager.registerDisconnectListener(tunnel.tunnelid, async (_id, error, messages) => {
6021
+ if (error) printer_default.warn(`[${saved.name}] Disconnected: ${error}`);
6022
+ messages?.forEach((m) => printer_default.warn(`[${saved.name}] ${m}`));
6023
+ });
6024
+ manager.registerReconnectingListener(tunnel.tunnelid, (_id, retryCnt) => {
6025
+ printer_default.print(import_picocolors6.default.gray(`[${saved.name}] Reconnecting (attempt #${retryCnt})...`));
6026
+ });
6027
+ manager.registerReconnectionCompletedListener(tunnel.tunnelid, async (_id, urls2) => {
6028
+ printer_default.success(`[${saved.name}] Reconnected`);
6029
+ (urls2 ?? []).forEach(
6030
+ (url) => printer_default.print(" " + import_picocolors6.default.magentaBright(url))
6031
+ );
6032
+ });
6033
+ manager.registerReconnectionFailedListener(tunnel.tunnelid, (_id, retryCnt) => {
6034
+ printer_default.error(`[${saved.name}] Reconnection failed after ${retryCnt} attempts`);
6035
+ });
6036
+ } catch (err) {
6037
+ printer_default.error(`[${saved.name}] Failed to start: ${err.message || err}`);
6038
+ }
6039
+ }
6040
+ function resolveConfig(nameOrId) {
6041
+ const saved = findConfig(nameOrId);
6042
+ if (!saved) {
6043
+ printer_default.error(`No config found matching "${nameOrId}". Use: pinggy config list`);
6044
+ return null;
6045
+ }
6046
+ return saved;
6047
+ }
6048
+ function requireName(args, command) {
6049
+ if (args.length === 0 || args[0].startsWith("-")) {
6050
+ printer_default.error(`Tunnel name is required. Usage: pinggy ${command} <name>`);
6051
+ process.exit(1);
6052
+ }
6053
+ return args[0];
6054
+ }
6055
+ function requireNames(args, command) {
6056
+ const names = [];
6057
+ for (const arg of args) {
6058
+ if (arg.startsWith("-")) break;
6059
+ names.push(arg);
6060
+ }
6061
+ if (names.length === 0) {
6062
+ printer_default.error(`At least one tunnel name is required. Usage: pinggy ${command} <name> [name2 ...]`);
6063
+ process.exit(1);
6064
+ }
6065
+ return names;
6066
+ }
6067
+ async function initRemoteManagementBackground(values) {
6068
+ const rmToken = values["remote-management"];
6069
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
6070
+ const manageHost = values["manage"];
6071
+ try {
6072
+ await startRemoteManagement({
6073
+ apiKey: rmToken,
6074
+ serverUrl: buildRemoteManagementWsUrl(manageHost)
6075
+ });
6076
+ } catch (e) {
6077
+ logger.error("Failed to initiate remote management:", e);
6078
+ printer_default.fatal(e);
6079
+ }
6080
+ }
6081
+ }
6082
+ function printConfigHelp() {
6083
+ console.log("\nUsage: pinggy config <command> [name] [options]\n");
6084
+ console.log("Commands:");
6085
+ console.log(" list List all saved configs");
6086
+ console.log(" show <name> Show config details");
6087
+ console.log(" save <name> [tunnel flags] Save a tunnel config");
6088
+ console.log(" update <name> [tunnel flags] Update a saved config");
6089
+ console.log(" delete <name> Delete a saved config");
6090
+ console.log(" auto <name> Enable auto-start");
6091
+ console.log(" noauto <name> Disable auto-start\n");
6092
+ }
6093
+ function printStartHelp() {
6094
+ console.log("\nUsage: pinggy start <name> [options]\n");
6095
+ console.log("Examples:");
6096
+ console.log(" pinggy start my-tunnel Start a saved tunnel");
6097
+ console.log(" pinggy start my-tunnel -l 4000 Start with override");
6098
+ console.log(" pinggy start tunnela tunnelb Start multiple tunnels");
6099
+ console.log(" pinggy start --all Start all auto-start tunnels\n");
6100
+ }
6101
+ var import_picocolors6, SUBCOMMANDS;
6102
+ var init_subcommands = __esm({
6103
+ "src/cli/subcommands.ts"() {
6104
+ "use strict";
6105
+ init_cjs_shims();
6106
+ init_options();
6107
+ init_parseArgs();
6108
+ init_buildConfig();
6109
+ init_starCli();
6110
+ init_printer();
6111
+ init_logger();
6112
+ import_picocolors6 = __toESM(require("picocolors"), 1);
6113
+ init_configStore();
6114
+ init_remoteManagement();
6115
+ SUBCOMMANDS = /* @__PURE__ */ new Set(["config", "start"]);
6116
+ }
6117
+ });
6118
+
5495
6119
  // src/main.ts
5496
6120
  var main_exports = {};
5497
6121
  __export(main_exports, {
@@ -5505,16 +6129,23 @@ __export(main_exports, {
5505
6129
  });
5506
6130
  async function main() {
5507
6131
  try {
5508
- const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
5509
- configureLogger(values);
6132
+ const rawArgs = process.argv.slice(2);
5510
6133
  const manager = TunnelManager.getInstance();
5511
- process.on("SIGINT", () => {
5512
- logger.info("SIGINT received: stopping tunnels and exiting");
6134
+ const gracefulShutdown = (signal) => {
6135
+ logger.info(`${signal} received: stopping tunnels and exiting`);
5513
6136
  console.log("\nStopping all tunnels...");
5514
6137
  manager.stopAllTunnels();
5515
6138
  console.log("Tunnels stopped. Exiting.");
5516
6139
  process.exit(0);
5517
- });
6140
+ };
6141
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
6142
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
6143
+ if (isSubcommand(rawArgs)) {
6144
+ await handleSubcommand(rawArgs, manager);
6145
+ return;
6146
+ }
6147
+ const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
6148
+ configureLogger(values);
5518
6149
  if (!hasAnyArgs || values.help) {
5519
6150
  printHelpMessage();
5520
6151
  return;
@@ -5523,21 +6154,13 @@ async function main() {
5523
6154
  printer_default.print(`Pinggy CLI version: ${getVersion()}`);
5524
6155
  return;
5525
6156
  }
5526
- const parseResult = await parseRemoteManagement(values);
5527
- if (parseResult?.ok === false) {
5528
- logger.error("Failed to initiate remote management:", parseResult.error);
5529
- printer_default.fatal(parseResult.error);
5530
- }
5531
- logger.debug("Building final config from CLI values and positionals", { values, positionals });
5532
- const finalConfig = await buildFinalConfig(values, positionals);
5533
- logger.debug("Final configuration built", finalConfig);
5534
- await startCli(finalConfig, manager);
6157
+ await buildAndStartTunnel(values, positionals, manager);
5535
6158
  } catch (error) {
5536
6159
  logger.error("Unhandled error in CLI:", error);
5537
6160
  printer_default.fatal(error);
5538
6161
  }
5539
6162
  }
5540
- var import_url2, import_process, import_fs5, currentFile, entryFile;
6163
+ var import_url2, import_process, import_fs7, currentFile, entryFile;
5541
6164
  var init_main = __esm({
5542
6165
  "src/main.ts"() {
5543
6166
  "use strict";
@@ -5545,23 +6168,22 @@ var init_main = __esm({
5545
6168
  init_TunnelManager();
5546
6169
  init_help();
5547
6170
  init_options();
5548
- init_buildConfig();
5549
6171
  init_logger();
5550
- init_remoteManagement();
5551
6172
  init_parseArgs();
5552
6173
  init_printer();
5553
- init_starCli();
5554
6174
  init_util();
5555
6175
  init_handler();
5556
6176
  import_url2 = require("url");
5557
6177
  import_process = require("process");
5558
- import_fs5 = require("fs");
6178
+ import_fs7 = require("fs");
5559
6179
  init_logger();
5560
6180
  init_remoteManagement();
6181
+ init_buildAndStartTunnel();
6182
+ init_subcommands();
5561
6183
  currentFile = (0, import_url2.fileURLToPath)(importMetaUrl);
5562
6184
  entryFile = null;
5563
6185
  try {
5564
- entryFile = import_process.argv[1] ? (0, import_fs5.realpathSync)(import_process.argv[1]) : null;
6186
+ entryFile = import_process.argv[1] ? (0, import_fs7.realpathSync)(import_process.argv[1]) : null;
5565
6187
  } catch (e) {
5566
6188
  entryFile = null;
5567
6189
  }