pinggy 0.4.6 → 0.4.7

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.
@@ -3,6 +3,7 @@ import {
3
3
  RemoteManagementUnauthorizedError,
4
4
  TunnelManager,
5
5
  TunnelOperations,
6
+ buildRemoteManagementWsUrl,
6
7
  closeRemoteManagement,
7
8
  getRandomId,
8
9
  getRemoteManagementState,
@@ -10,8 +11,9 @@ import {
10
11
  initiateRemoteManagement,
11
12
  isValidPort,
12
13
  parseRemoteManagement,
13
- printer_default
14
- } from "./chunk-STEISST3.js";
14
+ printer_default,
15
+ startRemoteManagement
16
+ } from "./chunk-443UO6IY.js";
15
17
  import {
16
18
  configureLogger,
17
19
  enablePackageLogging,
@@ -45,9 +47,13 @@ var cliOptions = {
45
47
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
46
48
  vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
47
49
  autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
48
- // Save and load config
50
+ // Save and load config (legacy file-based)
49
51
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
50
52
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
53
+ // Used by `pinggy config save` and `buildAndStartTunnel` save flow
54
+ save: { type: "boolean", short: "s", description: "Save the tunnel config (use with config save or -l)", hidden: true },
55
+ name: { type: "string", description: "Name for the tunnel config", hidden: true },
56
+ auto: { type: "boolean", description: "Mark tunnel config for auto-start", hidden: true },
51
57
  // File server
52
58
  serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
53
59
  // Remote Control
@@ -95,11 +101,92 @@ function printHelpMessage() {
95
101
  console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
96
102
  console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
97
103
  console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
98
- console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
104
+ console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region");
105
+ console.log("\nConfig Management:");
106
+ console.log(" pinggy config list # List saved configs");
107
+ console.log(" pinggy config show my-tunnel # Show config details");
108
+ console.log(" pinggy config save my-tunnel -l 3000 token@pro.pinggy.io # Save config");
109
+ console.log(" pinggy config save my-tunnel --auto -l 3000 # Save with auto-start");
110
+ console.log(" pinggy config update my-tunnel -l 4000 # Update saved config");
111
+ console.log(" pinggy config delete my-tunnel # Delete saved config");
112
+ console.log(" pinggy config auto my-tunnel # Enable auto-start");
113
+ console.log(" pinggy config noauto my-tunnel # Disable auto-start");
114
+ console.log("\nStart Saved Tunnels:");
115
+ console.log(" pinggy start my-tunnel # Start saved tunnel");
116
+ console.log(" pinggy start my-tunnel -l 4000 # Start with runtime overrides");
117
+ console.log(" pinggy start tunnela tunnelb # Start multiple tunnels");
118
+ console.log(" pinggy start --all # Start all auto-start tunnels\n");
99
119
  }
100
120
 
121
+ // src/utils/parseArgs.ts
122
+ import { parseArgs } from "util";
123
+ import * as os from "os";
124
+ function isAttachedReverseOrLocalFlag(arg) {
125
+ return /^-[RL].+/.test(arg);
126
+ }
127
+ function shouldMergeReverseOrLocalFragment(current, next) {
128
+ if (next.startsWith("-")) {
129
+ return false;
130
+ }
131
+ if (next.startsWith(".")) {
132
+ return true;
133
+ }
134
+ const body = current.slice(2);
135
+ if (body.endsWith(":")) {
136
+ return true;
137
+ }
138
+ if (body.includes("//") && !body.includes(":")) {
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ function preprocessWindowsArgs(args) {
144
+ if (os.platform() !== "win32") {
145
+ return args;
146
+ }
147
+ ;
148
+ const out = [];
149
+ let i = 0;
150
+ while (i < args.length) {
151
+ const arg = args[i];
152
+ if (isAttachedReverseOrLocalFlag(arg)) {
153
+ let merged = arg;
154
+ while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
155
+ merged += args[i + 1];
156
+ i++;
157
+ }
158
+ out.push(merged);
159
+ i++;
160
+ continue;
161
+ }
162
+ out.push(arg);
163
+ i++;
164
+ }
165
+ return out;
166
+ }
167
+ function parseCliArgs(options, overrideArgs) {
168
+ const rawArgs = overrideArgs ?? process.argv.slice(2);
169
+ const processedArgs = preprocessWindowsArgs(rawArgs);
170
+ const parsed = parseArgs({
171
+ args: processedArgs,
172
+ options,
173
+ allowPositionals: true
174
+ });
175
+ const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
176
+ return {
177
+ ...parsed,
178
+ hasAnyArgs
179
+ };
180
+ }
181
+
182
+ // src/main.ts
183
+ import { fileURLToPath } from "url";
184
+ import { argv } from "process";
185
+ import { realpathSync } from "fs";
186
+
101
187
  // src/cli/defaults.ts
102
188
  var defaultOptions = {
189
+ version: "1.0",
103
190
  token: void 0,
104
191
  // No default token
105
192
  serverAddress: "a.pinggy.io",
@@ -606,7 +693,7 @@ function parseToken(finalConfig, explicitToken) {
606
693
  finalConfig.token = explicitToken;
607
694
  }
608
695
  }
609
- function parseArgs(finalConfig, remainingPositionals) {
696
+ function parseArgs2(finalConfig, remainingPositionals) {
610
697
  let localserverTls = "";
611
698
  localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
612
699
  if (localserverTls.length > 0 && finalConfig.forwarding) {
@@ -617,10 +704,10 @@ function parseArgs(finalConfig, remainingPositionals) {
617
704
  }
618
705
  function storeJson(config, saveconf) {
619
706
  if (saveconf) {
620
- const path2 = saveconf;
707
+ const path4 = saveconf;
621
708
  try {
622
- fs.writeFileSync(path2, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
623
- logger.info(`Configuration saved to ${path2}`);
709
+ fs.writeFileSync(path4, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
710
+ logger.info(`Configuration saved to ${path4}`);
624
711
  } catch (err) {
625
712
  const msg = err instanceof Error ? err.message : String(err);
626
713
  logger.error("Error loading configuration:", msg);
@@ -670,7 +757,7 @@ function parseAutoReconnect(finalConfig, values) {
670
757
  }
671
758
  return null;
672
759
  }
673
- async function buildFinalConfig(values, positionals) {
760
+ async function buildFinalConfig(values, positionals, baseConfig) {
674
761
  let token;
675
762
  let server;
676
763
  let type;
@@ -678,7 +765,7 @@ async function buildFinalConfig(values, positionals) {
678
765
  let qrCode = false;
679
766
  let finalConfig = new Object();
680
767
  let saveconf = isSaveConfOption(values);
681
- const configFromFile = loadJsonConfig(values);
768
+ const configFromFile = baseConfig || loadJsonConfig(values);
682
769
  const userParse = parseUsers(positionals, values.token);
683
770
  token = userParse.token;
684
771
  server = userParse.server;
@@ -730,72 +817,11 @@ async function buildFinalConfig(values, positionals) {
730
817
  if (forceFlag || values.force) {
731
818
  finalConfig.force = true;
732
819
  }
733
- parseArgs(finalConfig, remainingPositionals);
820
+ parseArgs2(finalConfig, remainingPositionals);
734
821
  storeJson(finalConfig, saveconf);
735
822
  return finalConfig;
736
823
  }
737
824
 
738
- // src/utils/parseArgs.ts
739
- import { parseArgs as parseArgs2 } from "util";
740
- import * as os from "os";
741
- function isAttachedReverseOrLocalFlag(arg) {
742
- return /^-[RL].+/.test(arg);
743
- }
744
- function shouldMergeReverseOrLocalFragment(current, next) {
745
- if (next.startsWith("-")) {
746
- return false;
747
- }
748
- if (next.startsWith(".")) {
749
- return true;
750
- }
751
- const body = current.slice(2);
752
- if (body.endsWith(":")) {
753
- return true;
754
- }
755
- if (body.includes("//") && !body.includes(":")) {
756
- return true;
757
- }
758
- return false;
759
- }
760
- function preprocessWindowsArgs(args) {
761
- if (os.platform() !== "win32") {
762
- return args;
763
- }
764
- ;
765
- const out = [];
766
- let i = 0;
767
- while (i < args.length) {
768
- const arg = args[i];
769
- if (isAttachedReverseOrLocalFlag(arg)) {
770
- let merged = arg;
771
- while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
772
- merged += args[i + 1];
773
- i++;
774
- }
775
- out.push(merged);
776
- i++;
777
- continue;
778
- }
779
- out.push(arg);
780
- i++;
781
- }
782
- return out;
783
- }
784
- function parseCliArgs(options) {
785
- const rawArgs = process.argv.slice(2);
786
- const processedArgs = preprocessWindowsArgs(rawArgs);
787
- const parsed = parseArgs2({
788
- args: processedArgs,
789
- options,
790
- allowPositionals: true
791
- });
792
- const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
793
- return {
794
- ...parsed,
795
- hasAnyArgs
796
- };
797
- }
798
-
799
825
  // src/utils/getFreePort.ts
800
826
  import net from "net";
801
827
  function getFreePort(webDebugger) {
@@ -2299,14 +2325,563 @@ async function startCli(finalConfig, manager) {
2299
2325
  }
2300
2326
  }
2301
2327
 
2328
+ // src/cli/configStore.ts
2329
+ import fs3 from "fs";
2330
+ import path3 from "path";
2331
+
2332
+ // src/utils/configDir.ts
2333
+ import os2 from "os";
2334
+ import path2 from "path";
2335
+ import fs2 from "fs";
2336
+ function getPinggyConfigDir() {
2337
+ const platform2 = os2.platform();
2338
+ let baseDir;
2339
+ if (platform2 === "win32") {
2340
+ baseDir = process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming");
2341
+ } else {
2342
+ baseDir = process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
2343
+ }
2344
+ return path2.join(baseDir, "pinggy");
2345
+ }
2346
+ function getTunnelConfigDir() {
2347
+ return path2.join(getPinggyConfigDir(), "tunnels");
2348
+ }
2349
+ function ensureTunnelConfigDir() {
2350
+ const dir = getTunnelConfigDir();
2351
+ fs2.mkdirSync(dir, { recursive: true });
2352
+ return dir;
2353
+ }
2354
+
2355
+ // src/cli/configStore.ts
2356
+ import pico2 from "picocolors";
2357
+ function buildFilename(name, configId) {
2358
+ return `${name}_${configId}.json`;
2359
+ }
2360
+ function sanitizeName(name) {
2361
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
2362
+ }
2363
+ function validateName(name) {
2364
+ if (!name || name.trim().length === 0) {
2365
+ return new Error("Tunnel name cannot be empty.");
2366
+ }
2367
+ if (name.length > 128) {
2368
+ return new Error("Tunnel name cannot exceed 128 characters.");
2369
+ }
2370
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
2371
+ return new Error("Tunnel name can only contain alphanumeric characters, hyphens, and underscores.");
2372
+ }
2373
+ return null;
2374
+ }
2375
+ function readConfigFile(filePath) {
2376
+ try {
2377
+ const data = fs3.readFileSync(filePath, { encoding: "utf-8" });
2378
+ return JSON.parse(data);
2379
+ } catch (err) {
2380
+ logger.warn(`Failed to read config file ${filePath}:`, err);
2381
+ return null;
2382
+ }
2383
+ }
2384
+ function writeConfigFile(filePath, config) {
2385
+ fs3.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
2386
+ }
2387
+ function listSavedConfigs() {
2388
+ const dir = getTunnelConfigDir();
2389
+ if (!fs3.existsSync(dir)) {
2390
+ return [];
2391
+ }
2392
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
2393
+ const configs = [];
2394
+ for (const file of files) {
2395
+ const config = readConfigFile(path3.join(dir, file));
2396
+ if (config && config.name && config.configId) {
2397
+ configs.push(config);
2398
+ }
2399
+ }
2400
+ return configs;
2401
+ }
2402
+ function findConfigFile(nameOrId) {
2403
+ const dir = getTunnelConfigDir();
2404
+ if (!fs3.existsSync(dir)) return null;
2405
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
2406
+ const sanitized = sanitizeName(nameOrId);
2407
+ const nameMatch = files.find((f) => f.startsWith(sanitized + "_"));
2408
+ if (nameMatch) {
2409
+ const filePath = path3.join(dir, nameMatch);
2410
+ const config = readConfigFile(filePath);
2411
+ if (config && config.name === nameOrId) return { filePath, config };
2412
+ }
2413
+ const idCandidates = files.filter((f) => {
2414
+ const withoutExt = f.replace(/\.json$/, "");
2415
+ const lastUnderscore = withoutExt.indexOf("_");
2416
+ if (lastUnderscore === -1) return false;
2417
+ const idPart = withoutExt.slice(lastUnderscore + 1);
2418
+ return idPart.startsWith(nameOrId);
2419
+ });
2420
+ if (idCandidates.length === 1) {
2421
+ const filePath = path3.join(dir, idCandidates[0]);
2422
+ const config = readConfigFile(filePath);
2423
+ if (config) return { filePath, config };
2424
+ }
2425
+ return null;
2426
+ }
2427
+ function findConfigByName(name) {
2428
+ const resolved = findConfigFile(name);
2429
+ return resolved?.config.name === name ? resolved.config : null;
2430
+ }
2431
+ function findConfig(nameOrId) {
2432
+ return findConfigFile(nameOrId)?.config ?? null;
2433
+ }
2434
+ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
2435
+ const nameErr = validateName(name);
2436
+ if (nameErr) {
2437
+ throw nameErr;
2438
+ }
2439
+ const existing = findConfigByName(name);
2440
+ if (existing) {
2441
+ throw new Error(
2442
+ `A tunnel config with the name "${name}" already exists (configId: ${existing.configId}). Please use a different name.`
2443
+ );
2444
+ }
2445
+ const dir = ensureTunnelConfigDir();
2446
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2447
+ const saved = {
2448
+ name,
2449
+ configId,
2450
+ autoStart,
2451
+ createdAt: now,
2452
+ updatedAt: now,
2453
+ tunnelConfig
2454
+ };
2455
+ const filename = buildFilename(sanitizeName(name), configId);
2456
+ const filePath = path3.join(dir, filename);
2457
+ fs3.writeFileSync(filePath, JSON.stringify(saved, null, 2), { encoding: "utf-8" });
2458
+ logger.info(`Config "${name}" saved to ${filePath}`);
2459
+ return saved;
2460
+ }
2461
+ function deleteConfig(nameOrId) {
2462
+ const resolved = findConfigFile(nameOrId);
2463
+ if (!resolved) return null;
2464
+ fs3.unlinkSync(resolved.filePath);
2465
+ logger.info(`Config "${resolved.config.name}" deleted.`);
2466
+ return resolved.config.name;
2467
+ }
2468
+ function updateConfigAutoStart(nameOrId, autoStart) {
2469
+ const resolved = findConfigFile(nameOrId);
2470
+ if (!resolved) return null;
2471
+ resolved.config.autoStart = autoStart;
2472
+ resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2473
+ writeConfigFile(resolved.filePath, resolved.config);
2474
+ logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
2475
+ return resolved.config;
2476
+ }
2477
+ function updateTunnelConfig(nameOrId, tunnelConfig) {
2478
+ const resolved = findConfigFile(nameOrId);
2479
+ if (!resolved) return null;
2480
+ resolved.config.tunnelConfig = tunnelConfig;
2481
+ resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2482
+ writeConfigFile(resolved.filePath, resolved.config);
2483
+ logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
2484
+ return resolved.config;
2485
+ }
2486
+ function getAutoStartConfigs() {
2487
+ return listSavedConfigs().filter((c) => c.autoStart);
2488
+ }
2489
+ function printConfigList() {
2490
+ const configs = listSavedConfigs();
2491
+ if (configs.length === 0) {
2492
+ console.log(pico2.yellow("No saved tunnel configs found."));
2493
+ console.log(pico2.gray(`Config directory: ${getTunnelConfigDir()}`));
2494
+ return;
2495
+ }
2496
+ const nameW = 20;
2497
+ const idW = 12;
2498
+ const typeW = 8;
2499
+ const fwdW = 25;
2500
+ const serverW = 22;
2501
+ const autoW = 10;
2502
+ const header = pico2.bold("Name".padEnd(nameW)) + pico2.bold("Config ID".padEnd(idW)) + pico2.bold("Type".padEnd(typeW)) + pico2.bold("Forwarding".padEnd(fwdW)) + pico2.bold("Server".padEnd(serverW)) + pico2.bold("Auto-start".padEnd(autoW));
2503
+ console.log("\n" + header);
2504
+ console.log(pico2.gray("\u2500".repeat(nameW + idW + typeW + fwdW + serverW + autoW)));
2505
+ for (const c of configs) {
2506
+ const tc = c.tunnelConfig;
2507
+ const forwarding = Array.isArray(tc.forwarding) ? tc.forwarding[0]?.address : String(tc.forwarding || "");
2508
+ const type = (Array.isArray(tc.forwarding) ? tc.forwarding[0]?.type : void 0) || "http";
2509
+ const server = tc.serverAddress || "a.pinggy.io";
2510
+ const line = pico2.cyanBright(c.name.padEnd(nameW)) + pico2.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 ? pico2.green("yes") : pico2.gray("no")).padEnd(autoW);
2511
+ console.log(line);
2512
+ }
2513
+ console.log();
2514
+ }
2515
+ function printConfigDetail(config) {
2516
+ console.log(pico2.bold(`
2517
+ Tunnel Config: ${pico2.cyanBright(config.name)}`));
2518
+ console.log(pico2.gray("\u2500".repeat(40)));
2519
+ console.log(` Config ID: ${config.configId}`);
2520
+ console.log(` Auto-start: ${config.autoStart ? pico2.green("yes") : pico2.gray("no")}`);
2521
+ console.log(` Created: ${config.createdAt}`);
2522
+ console.log(` Updated: ${config.updatedAt}`);
2523
+ console.log(pico2.gray("\u2500".repeat(40)));
2524
+ console.log(` Server: ${config.tunnelConfig.serverAddress || "a.pinggy.io"}`);
2525
+ console.log(` Token: ${config.tunnelConfig.token ? "***" + config.tunnelConfig.token.slice(-4) : "(none)"}`);
2526
+ const fwd = config.tunnelConfig.forwarding;
2527
+ if (Array.isArray(fwd)) {
2528
+ const defaultFwds = [];
2529
+ const customFwds = [];
2530
+ for (const f of fwd) {
2531
+ if (typeof f === "string") {
2532
+ defaultFwds.push(f);
2533
+ } else if (f.listenAddress) {
2534
+ customFwds.push(f);
2535
+ } else {
2536
+ defaultFwds.push(f);
2537
+ }
2538
+ }
2539
+ for (const f of defaultFwds) {
2540
+ const addr = typeof f === "string" ? f : `${f.address} (${f.type || "http"})`;
2541
+ console.log(` Forwarding: ${addr}`);
2542
+ if (config.tunnelConfig.webDebugger) {
2543
+ console.log(` Debugger: ${config.tunnelConfig.webDebugger}`);
2544
+ }
2545
+ }
2546
+ if (customFwds.length > 0) {
2547
+ console.log(pico2.gray("\u2500".repeat(40)));
2548
+ console.log(pico2.bold(" Domain Mappings:"));
2549
+ for (const f of customFwds) {
2550
+ if (typeof f === "string") continue;
2551
+ const domain = f.listenAddress;
2552
+ const target = f.address;
2553
+ const type = f.type || "http";
2554
+ console.log(` ${pico2.cyanBright(domain)} \u2192 ${target} (${type})`);
2555
+ }
2556
+ }
2557
+ } else if (fwd) {
2558
+ console.log(` Forwarding: ${fwd}`);
2559
+ }
2560
+ console.log();
2561
+ }
2562
+
2563
+ // src/cli/buildAndStartTunnel.ts
2564
+ async function buildAndStartTunnel(values, positionals, manager) {
2565
+ await initRemoteManagement(values);
2566
+ logger.debug("Building final config from CLI values and positionals", { values, positionals });
2567
+ const finalConfig = await buildFinalConfig(values, positionals);
2568
+ logger.debug("Final configuration built", finalConfig);
2569
+ if (values.save) {
2570
+ const name = values.name;
2571
+ if (!name) {
2572
+ printer_default.error("--save requires --name to specify a name for the tunnel config.");
2573
+ process.exit(1);
2574
+ }
2575
+ const nameErr = validateName(name);
2576
+ if (nameErr) {
2577
+ printer_default.error(nameErr.message);
2578
+ process.exit(1);
2579
+ }
2580
+ const autoStart = !!values.auto;
2581
+ saveConfig(name, finalConfig.configId, finalConfig, autoStart);
2582
+ printer_default.success(`Config "${name}" saved.`);
2583
+ }
2584
+ await startCli(finalConfig, manager);
2585
+ }
2586
+ async function initRemoteManagement(values) {
2587
+ const parseResult = await parseRemoteManagement(values);
2588
+ if (parseResult?.ok === false) {
2589
+ logger.error("Failed to initiate remote management:", parseResult.error);
2590
+ printer_default.fatal(parseResult.error);
2591
+ }
2592
+ }
2593
+
2594
+ // src/cli/subcommands.ts
2595
+ import pico3 from "picocolors";
2596
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["config", "start"]);
2597
+ function isSubcommand(rawArgs) {
2598
+ return rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0]);
2599
+ }
2600
+ async function handleSubcommand(rawArgs, manager) {
2601
+ const sub = rawArgs[0];
2602
+ const rest = rawArgs.slice(1);
2603
+ switch (sub) {
2604
+ case "config":
2605
+ await handleConfig(rest);
2606
+ return;
2607
+ case "start":
2608
+ await handleStart(rest, manager);
2609
+ return;
2610
+ }
2611
+ }
2612
+ async function handleConfig(args) {
2613
+ if (args.length === 0) {
2614
+ printConfigHelp();
2615
+ return;
2616
+ }
2617
+ const verb = args[0];
2618
+ const rest = args.slice(1);
2619
+ switch (verb) {
2620
+ case "list":
2621
+ case "ls":
2622
+ printConfigList();
2623
+ return;
2624
+ case "show": {
2625
+ const names = requireNames(rest, "config show");
2626
+ for (const name of names) {
2627
+ const saved2 = resolveConfig(name);
2628
+ if (saved2) printConfigDetail(saved2);
2629
+ }
2630
+ return;
2631
+ }
2632
+ case "save": {
2633
+ const name = requireName(rest, "config save");
2634
+ await handleConfigSave(name, rest.slice(1));
2635
+ return;
2636
+ }
2637
+ case "delete": {
2638
+ const names = requireNames(rest, "config delete");
2639
+ for (const name of names) {
2640
+ const deletedName = deleteConfig(name);
2641
+ if (deletedName) {
2642
+ printer_default.success(`Config "${deletedName}" deleted.`);
2643
+ } else {
2644
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
2645
+ }
2646
+ }
2647
+ return;
2648
+ }
2649
+ case "update": {
2650
+ const name = requireName(rest, "config update");
2651
+ await handleConfigUpdate(name, rest.slice(1));
2652
+ return;
2653
+ }
2654
+ case "auto": {
2655
+ const names = requireNames(rest, "config auto");
2656
+ for (const name of names) {
2657
+ const updated = updateConfigAutoStart(name, true);
2658
+ if (updated) {
2659
+ printer_default.success(`Config "${updated.name}" auto-start set to on.`);
2660
+ } else {
2661
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
2662
+ }
2663
+ }
2664
+ return;
2665
+ }
2666
+ case "noauto": {
2667
+ const names = requireNames(rest, "config noauto");
2668
+ for (const name of names) {
2669
+ const updated = updateConfigAutoStart(name, false);
2670
+ if (updated) {
2671
+ printer_default.success(`Config "${updated.name}" auto-start set to off.`);
2672
+ } else {
2673
+ printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
2674
+ }
2675
+ }
2676
+ return;
2677
+ }
2678
+ default:
2679
+ const saved = resolveConfig(verb);
2680
+ if (saved) printConfigDetail(saved);
2681
+ return;
2682
+ }
2683
+ }
2684
+ async function handleConfigSave(name, remainingArgs) {
2685
+ const nameErr = validateName(name);
2686
+ if (nameErr) {
2687
+ printer_default.error(nameErr.message);
2688
+ process.exit(1);
2689
+ }
2690
+ const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
2691
+ const autoStart = !!values.auto;
2692
+ logger.debug("Building config for save", { name, values, positionals });
2693
+ const finalConfig = await buildFinalConfig(values, positionals);
2694
+ saveConfig(name, finalConfig.configId, finalConfig, autoStart);
2695
+ printer_default.success(`Config "${name}" saved.`);
2696
+ }
2697
+ async function handleConfigUpdate(nameOrId, remainingArgs) {
2698
+ const saved = resolveConfig(nameOrId);
2699
+ if (!saved) return;
2700
+ const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
2701
+ logger.debug("Building updated config", { nameOrId, values, positionals });
2702
+ const updatedConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
2703
+ const result = updateTunnelConfig(nameOrId, updatedConfig);
2704
+ if (result) {
2705
+ printer_default.success(`Config "${result.name}" updated.`);
2706
+ printConfigDetail(result);
2707
+ } else {
2708
+ printer_default.error(`Failed to update config "${nameOrId}".`);
2709
+ }
2710
+ }
2711
+ async function handleStart(args, manager) {
2712
+ const startAll = args.includes("--all");
2713
+ const argsWithoutAll = args.filter((a) => a !== "--all");
2714
+ const names = [];
2715
+ let i = 0;
2716
+ while (i < argsWithoutAll.length && !argsWithoutAll[i].startsWith("-")) {
2717
+ names.push(argsWithoutAll[i]);
2718
+ i++;
2719
+ }
2720
+ const flagArgs = argsWithoutAll.slice(i);
2721
+ const { values, positionals } = parseCliArgs(cliOptions, flagArgs);
2722
+ configureLogger(values);
2723
+ if (startAll) {
2724
+ await initRemoteManagementBackground(values);
2725
+ await startAutoStartTunnels(manager);
2726
+ return;
2727
+ }
2728
+ if (names.length === 0) {
2729
+ printStartHelp();
2730
+ return;
2731
+ }
2732
+ const resolved = [];
2733
+ for (const name of names) {
2734
+ const saved = resolveConfig(name);
2735
+ if (!saved) return;
2736
+ resolved.push(saved);
2737
+ }
2738
+ if (resolved.length > 1 && flagArgs.length > 0) {
2739
+ printer_default.error("Runtime overrides (-l, --type, etc.) can only be used when starting a single tunnel.");
2740
+ printer_default.print(" Start one tunnel: pinggy start my-tunnel -l 4000");
2741
+ printer_default.print(" Or update first: pinggy config update my-tunnel -l 4000");
2742
+ return;
2743
+ }
2744
+ await initRemoteManagementBackground(values);
2745
+ if (resolved.length === 1) {
2746
+ const saved = resolved[0];
2747
+ logger.debug("Building config with overrides", { name: saved.name });
2748
+ const finalConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
2749
+ finalConfig.configId = saved.configId;
2750
+ await startCli(finalConfig, manager);
2751
+ } else {
2752
+ await startNamedTunnels(resolved, manager);
2753
+ }
2754
+ }
2755
+ async function startAutoStartTunnels(manager) {
2756
+ const configs = getAutoStartConfigs();
2757
+ if (configs.length === 0) {
2758
+ printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
2759
+ return;
2760
+ }
2761
+ printer_default.print(pico3.cyanBright(`Starting ${configs.length} auto-start tunnel(s)...`));
2762
+ for (const saved of configs) {
2763
+ await startSavedTunnel(saved, manager);
2764
+ }
2765
+ printer_default.print(pico3.gray("\nAll auto-start tunnels launched. Press Ctrl+C to stop.\n"));
2766
+ await new Promise(() => {
2767
+ });
2768
+ }
2769
+ async function startNamedTunnels(configs, manager) {
2770
+ printer_default.print(pico3.cyanBright(`Starting ${configs.length} tunnel(s)...`));
2771
+ for (const saved of configs) {
2772
+ await startSavedTunnel(saved, manager);
2773
+ }
2774
+ printer_default.print(pico3.gray("\nAll tunnels launched. Press Ctrl+C to stop.\n"));
2775
+ await new Promise(() => {
2776
+ });
2777
+ }
2778
+ async function startSavedTunnel(saved, manager) {
2779
+ const config = {
2780
+ ...saved.tunnelConfig,
2781
+ configId: saved.configId,
2782
+ name: saved.name,
2783
+ optional: {
2784
+ ...saved.tunnelConfig.optional,
2785
+ noTui: true
2786
+ }
2787
+ };
2788
+ try {
2789
+ const tunnel = await manager.createTunnel(config);
2790
+ await manager.startTunnel(tunnel.tunnelid);
2791
+ const urls = await manager.getTunnelUrls(tunnel.tunnelid);
2792
+ printer_default.success(`"${saved.name}" started`);
2793
+ (urls ?? []).forEach(
2794
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2795
+ );
2796
+ manager.registerWorkerErrorListner(tunnel.tunnelid, (_id, error) => {
2797
+ printer_default.error(`[${saved.name}] Fatal: ${error.message}`);
2798
+ });
2799
+ manager.registerDisconnectListener(tunnel.tunnelid, async (_id, error, messages) => {
2800
+ if (error) printer_default.warn(`[${saved.name}] Disconnected: ${error}`);
2801
+ messages?.forEach((m) => printer_default.warn(`[${saved.name}] ${m}`));
2802
+ });
2803
+ manager.registerReconnectingListener(tunnel.tunnelid, (_id, retryCnt) => {
2804
+ printer_default.print(pico3.gray(`[${saved.name}] Reconnecting (attempt #${retryCnt})...`));
2805
+ });
2806
+ manager.registerReconnectionCompletedListener(tunnel.tunnelid, async (_id, urls2) => {
2807
+ printer_default.success(`[${saved.name}] Reconnected`);
2808
+ (urls2 ?? []).forEach(
2809
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2810
+ );
2811
+ });
2812
+ manager.registerReconnectionFailedListener(tunnel.tunnelid, (_id, retryCnt) => {
2813
+ printer_default.error(`[${saved.name}] Reconnection failed after ${retryCnt} attempts`);
2814
+ });
2815
+ } catch (err) {
2816
+ printer_default.error(`[${saved.name}] Failed to start: ${err.message || err}`);
2817
+ }
2818
+ }
2819
+ function resolveConfig(nameOrId) {
2820
+ const saved = findConfig(nameOrId);
2821
+ if (!saved) {
2822
+ printer_default.error(`No config found matching "${nameOrId}". Use: pinggy config list`);
2823
+ return null;
2824
+ }
2825
+ return saved;
2826
+ }
2827
+ function requireName(args, command) {
2828
+ if (args.length === 0 || args[0].startsWith("-")) {
2829
+ printer_default.error(`Tunnel name is required. Usage: pinggy ${command} <name>`);
2830
+ process.exit(1);
2831
+ }
2832
+ return args[0];
2833
+ }
2834
+ function requireNames(args, command) {
2835
+ const names = [];
2836
+ for (const arg of args) {
2837
+ if (arg.startsWith("-")) break;
2838
+ names.push(arg);
2839
+ }
2840
+ if (names.length === 0) {
2841
+ printer_default.error(`At least one tunnel name is required. Usage: pinggy ${command} <name> [name2 ...]`);
2842
+ process.exit(1);
2843
+ }
2844
+ return names;
2845
+ }
2846
+ async function initRemoteManagementBackground(values) {
2847
+ const rmToken = values["remote-management"];
2848
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2849
+ const manageHost = values["manage"];
2850
+ try {
2851
+ await startRemoteManagement({
2852
+ apiKey: rmToken,
2853
+ serverUrl: buildRemoteManagementWsUrl(manageHost)
2854
+ });
2855
+ } catch (e) {
2856
+ logger.error("Failed to initiate remote management:", e);
2857
+ printer_default.fatal(e);
2858
+ }
2859
+ }
2860
+ }
2861
+ function printConfigHelp() {
2862
+ console.log("\nUsage: pinggy config <command> [name] [options]\n");
2863
+ console.log("Commands:");
2864
+ console.log(" list List all saved configs");
2865
+ console.log(" show <name> Show config details");
2866
+ console.log(" save <name> [tunnel flags] Save a tunnel config");
2867
+ console.log(" update <name> [tunnel flags] Update a saved config");
2868
+ console.log(" delete <name> Delete a saved config");
2869
+ console.log(" auto <name> Enable auto-start");
2870
+ console.log(" noauto <name> Disable auto-start\n");
2871
+ }
2872
+ function printStartHelp() {
2873
+ console.log("\nUsage: pinggy start <name> [options]\n");
2874
+ console.log("Examples:");
2875
+ console.log(" pinggy start my-tunnel Start a saved tunnel");
2876
+ console.log(" pinggy start my-tunnel -l 4000 Start with override");
2877
+ console.log(" pinggy start tunnela tunnelb Start multiple tunnels");
2878
+ console.log(" pinggy start --all Start all auto-start tunnels\n");
2879
+ }
2880
+
2302
2881
  // src/main.ts
2303
- import { fileURLToPath } from "url";
2304
- import { argv } from "process";
2305
- import { realpathSync } from "fs";
2306
2882
  async function main() {
2307
2883
  try {
2308
- const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
2309
- configureLogger(values);
2884
+ const rawArgs = process.argv.slice(2);
2310
2885
  const manager = TunnelManager.getInstance();
2311
2886
  process.on("SIGINT", () => {
2312
2887
  logger.info("SIGINT received: stopping tunnels and exiting");
@@ -2315,6 +2890,12 @@ async function main() {
2315
2890
  console.log("Tunnels stopped. Exiting.");
2316
2891
  process.exit(0);
2317
2892
  });
2893
+ if (isSubcommand(rawArgs)) {
2894
+ await handleSubcommand(rawArgs, manager);
2895
+ return;
2896
+ }
2897
+ const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
2898
+ configureLogger(values);
2318
2899
  if (!hasAnyArgs || values.help) {
2319
2900
  printHelpMessage();
2320
2901
  return;
@@ -2323,15 +2904,7 @@ async function main() {
2323
2904
  printer_default.print(`Pinggy CLI version: ${getVersion()}`);
2324
2905
  return;
2325
2906
  }
2326
- const parseResult = await parseRemoteManagement(values);
2327
- if (parseResult?.ok === false) {
2328
- logger.error("Failed to initiate remote management:", parseResult.error);
2329
- printer_default.fatal(parseResult.error);
2330
- }
2331
- logger.debug("Building final config from CLI values and positionals", { values, positionals });
2332
- const finalConfig = await buildFinalConfig(values, positionals);
2333
- logger.debug("Final configuration built", finalConfig);
2334
- await startCli(finalConfig, manager);
2907
+ await buildAndStartTunnel(values, positionals, manager);
2335
2908
  } catch (error) {
2336
2909
  logger.error("Unhandled error in CLI:", error);
2337
2910
  printer_default.fatal(error);