pinggy 0.5.0 → 0.5.2

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
@@ -2255,7 +2255,7 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
2255
2255
  serve: serve || ""
2256
2256
  };
2257
2257
  }
2258
- var import_pinggy3, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema, ForwardingEntryV2Schema, TunnelConfigV1Schema, StartV2Schema, UpdateConfigV2Schema;
2258
+ var import_pinggy3, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema, ForwardingEntryV2Schema, OptionalSchema, TunnelConfigV1Schema, TUNNEL_CONFIG_V1_KEYS, StartV2Schema, UpdateConfigV2Schema;
2259
2259
  var init_remote_schema = __esm({
2260
2260
  "src/remote_management/remote_schema.ts"() {
2261
2261
  "use strict";
@@ -2343,11 +2343,23 @@ var init_remote_schema = __esm({
2343
2343
  address: import_zod.z.string(),
2344
2344
  type: import_zod.z.enum([import_pinggy3.TunnelType.Http, import_pinggy3.TunnelType.Tcp, import_pinggy3.TunnelType.Udp, import_pinggy3.TunnelType.Tls, import_pinggy3.TunnelType.TlsTcp])
2345
2345
  });
2346
+ OptionalSchema = import_zod.z.object({
2347
+ sniServerName: import_zod.z.string().optional(),
2348
+ ssl: import_zod.z.boolean().optional(),
2349
+ additionalArguments: import_zod.z.string().optional(),
2350
+ serve: import_zod.z.string().optional(),
2351
+ manage: import_zod.z.string().optional(),
2352
+ remoteManagement: import_zod.z.object({ serverUrl: import_zod.z.string(), apiKey: import_zod.z.string() }).optional(),
2353
+ noTui: import_zod.z.boolean().optional()
2354
+ }).catchall(import_zod.z.unknown());
2346
2355
  TunnelConfigV1Schema = import_zod.z.object({
2347
2356
  // Meta Info
2348
2357
  version: import_zod.z.string(),
2349
2358
  name: import_zod.z.string(),
2350
2359
  configId: import_zod.z.string(),
2360
+ hostKeyCheck: import_zod.z.boolean().optional(),
2361
+ platformValue: import_zod.z.string().optional(),
2362
+ isQRCode: import_zod.z.boolean().optional(),
2351
2363
  // General tunnel configurations
2352
2364
  serverAddress: import_zod.z.string().optional(),
2353
2365
  token: import_zod.z.string().optional(),
@@ -2373,9 +2385,11 @@ var init_remote_schema = __esm({
2373
2385
  httpsOnly: import_zod.z.boolean().optional(),
2374
2386
  originalRequestUrl: import_zod.z.boolean().optional(),
2375
2387
  allowPreflight: import_zod.z.boolean().optional(),
2376
- serve: import_zod.z.string().optional(),
2377
- optional: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
2388
+ optional: OptionalSchema.optional()
2378
2389
  });
2390
+ TUNNEL_CONFIG_V1_KEYS = Object.keys(
2391
+ TunnelConfigV1Schema.shape
2392
+ );
2379
2393
  StartV2Schema = import_zod.z.object({
2380
2394
  tunnelID: import_zod.z.string().nullable().optional(),
2381
2395
  tunnelConfig: TunnelConfigV1Schema
@@ -2511,11 +2525,11 @@ var init_handler = __esm({
2511
2525
  startPromise.catch((err) => {
2512
2526
  logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
2513
2527
  });
2514
- return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
2528
+ return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.optional?.serve);
2515
2529
  }
2516
2530
  await startPromise;
2517
2531
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2518
- return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
2532
+ return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.optional?.serve);
2519
2533
  } catch (err) {
2520
2534
  if (err instanceof TunnelAlreadyRunningError) {
2521
2535
  return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
@@ -3872,7 +3886,7 @@ var init_defaults = __esm({
3872
3886
  function parseExtendedOptions(options, config, localServerTls) {
3873
3887
  if (!options) return localServerTls;
3874
3888
  for (const opt of options) {
3875
- const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
3889
+ const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.*)/).filter(Boolean);
3876
3890
  switch (key) {
3877
3891
  case "x":
3878
3892
  switch (value) {
@@ -3933,7 +3947,7 @@ function parseExtendedOptions(options, config, localServerTls) {
3933
3947
  break;
3934
3948
  case "b":
3935
3949
  if (value && value.includes(":")) {
3936
- const [username, password] = value.split(/:(.+)/);
3950
+ const [username, password] = value.split(/:(.*)/);
3937
3951
  if (!config.basicAuth) config.basicAuth = [];
3938
3952
  config.basicAuth.push({ username, password });
3939
3953
  } else {
@@ -3943,7 +3957,7 @@ function parseExtendedOptions(options, config, localServerTls) {
3943
3957
  break;
3944
3958
  case "a":
3945
3959
  if (value && value.includes(":")) {
3946
- const [key2, val] = value.split(/:(.+)/);
3960
+ const [key2, val] = value.split(/:(.*)/);
3947
3961
  if (!config.headerModification) config.headerModification = [];
3948
3962
  config.headerModification.push({ type: "add", key: key2, value: [val] });
3949
3963
  } else {
@@ -3953,7 +3967,7 @@ function parseExtendedOptions(options, config, localServerTls) {
3953
3967
  break;
3954
3968
  case "u":
3955
3969
  if (value && value.includes(":")) {
3956
- const [key2, val] = value.split(/:(.+)/);
3970
+ const [key2, val] = value.split(/:(.*)/);
3957
3971
  if (!config.headerModification) config.headerModification = [];
3958
3972
  config.headerModification.push({ type: "update", key: key2, value: [val] });
3959
3973
  } else {
@@ -4350,6 +4364,13 @@ function parseToken(finalConfig, explicitToken) {
4350
4364
  finalConfig.token = explicitToken;
4351
4365
  }
4352
4366
  }
4367
+ function normalizeForwardingToArray(finalConfig, type) {
4368
+ const fwd = finalConfig.forwarding;
4369
+ if (typeof fwd !== "string") {
4370
+ return;
4371
+ }
4372
+ finalConfig.forwarding = fwd.trim() === "" ? [] : [{ type: type || import_pinggy4.TunnelType.Http, address: fwd.trim() }];
4373
+ }
4353
4374
  function parseArgs2(finalConfig, remainingPositionals) {
4354
4375
  let localserverTls = "";
4355
4376
  localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
@@ -4470,6 +4491,7 @@ function buildFinalConfig(values, positionals, baseConfig) {
4470
4491
  if (forceFlag || values.force) {
4471
4492
  finalConfig.force = true;
4472
4493
  }
4494
+ normalizeForwardingToArray(finalConfig, type);
4473
4495
  parseArgs2(finalConfig, remainingPositionals);
4474
4496
  storeJson(finalConfig, saveconf);
4475
4497
  return finalConfig;
@@ -4509,6 +4531,8 @@ __export(configStore_exports, {
4509
4531
  RESERVED_NAMES: () => RESERVED_NAMES,
4510
4532
  SUBCOMMANDS: () => SUBCOMMANDS,
4511
4533
  Subcommand: () => Subcommand,
4534
+ bulkReplace: () => bulkReplace,
4535
+ configureStorageLogger: () => configureStorageLogger,
4512
4536
  deleteConfig: () => deleteConfig,
4513
4537
  findConfig: () => findConfig,
4514
4538
  findConfigByName: () => findConfigByName,
@@ -4521,15 +4545,21 @@ __export(configStore_exports, {
4521
4545
  saveConfig: () => saveConfig,
4522
4546
  updateConfigAutoStart: () => updateConfigAutoStart,
4523
4547
  updateTunnelConfig: () => updateTunnelConfig,
4524
- validateName: () => validateName
4548
+ upsertConfig: () => upsertConfig,
4549
+ validateName: () => validateName,
4550
+ validateNameForStorage: () => validateNameForStorage,
4551
+ validateNameStrict: () => validateNameStrict
4525
4552
  });
4553
+ function configureStorageLogger(l) {
4554
+ storageLogger = l;
4555
+ }
4526
4556
  function buildFilename(name, configId) {
4527
4557
  return `${name}_${configId}.json`;
4528
4558
  }
4529
4559
  function sanitizeName(name) {
4530
4560
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
4531
4561
  }
4532
- function validateName(name) {
4562
+ function validateNameStrict(name) {
4533
4563
  if (!name || name.trim().length === 0) {
4534
4564
  return new Error("Tunnel name cannot be empty.");
4535
4565
  }
@@ -4544,17 +4574,38 @@ function validateName(name) {
4544
4574
  }
4545
4575
  return null;
4546
4576
  }
4577
+ function validateNameForStorage(name) {
4578
+ if (!name || name.trim().length === 0) {
4579
+ return new Error("Tunnel name cannot be empty.");
4580
+ }
4581
+ if (/[\u0000-\u001F\/\\]/.test(name)) {
4582
+ return new Error("Tunnel name cannot contain path separators or control characters.");
4583
+ }
4584
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
4585
+ return new Error(`"${name}" is a reserved subcommand name. Use a different name.`);
4586
+ }
4587
+ return null;
4588
+ }
4547
4589
  function readConfigFile(filePath) {
4548
4590
  try {
4549
4591
  const data = import_fs7.default.readFileSync(filePath, { encoding: "utf-8" });
4550
4592
  return JSON.parse(data);
4551
4593
  } catch (err) {
4552
- logger.warn(`Failed to read config file ${filePath}:`, err);
4594
+ storageLogger.warn(`Failed to read config file ${filePath}: ${String(err)}`);
4553
4595
  return null;
4554
4596
  }
4555
4597
  }
4556
4598
  function writeConfigFile(filePath, config) {
4557
- import_fs7.default.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
4599
+ const tmp = `${filePath}.tmp.${process.pid}`;
4600
+ import_fs7.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { encoding: "utf-8" });
4601
+ import_fs7.default.renameSync(tmp, filePath);
4602
+ }
4603
+ function nextTimestamp(prev) {
4604
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
4605
+ if (prev && ts <= prev) {
4606
+ return new Date(new Date(prev).getTime() + 1).toISOString();
4607
+ }
4608
+ return ts;
4558
4609
  }
4559
4610
  function listSavedConfigs() {
4560
4611
  const dir = getTunnelConfigDir();
@@ -4576,15 +4627,15 @@ function findConfigFile(nameOrId) {
4576
4627
  if (!import_fs7.default.existsSync(dir)) return null;
4577
4628
  const files = import_fs7.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
4578
4629
  const sanitized = sanitizeName(nameOrId);
4579
- const nameMatch = files.find((f) => f.startsWith(sanitized + "_"));
4580
- if (nameMatch) {
4581
- const filePath = import_path7.default.join(dir, nameMatch);
4630
+ const nameCandidates = files.filter((f) => f.startsWith(sanitized + "_"));
4631
+ for (const f of nameCandidates) {
4632
+ const filePath = import_path7.default.join(dir, f);
4582
4633
  const config = readConfigFile(filePath);
4583
4634
  if (config && config.name === nameOrId) return { filePath, config };
4584
4635
  }
4585
4636
  const idCandidates = files.filter((f) => {
4586
4637
  const withoutExt = f.replace(/\.json$/, "");
4587
- const lastUnderscore = withoutExt.indexOf("_");
4638
+ const lastUnderscore = withoutExt.lastIndexOf("_");
4588
4639
  if (lastUnderscore === -1) return false;
4589
4640
  const idPart = withoutExt.slice(lastUnderscore + 1);
4590
4641
  return idPart.startsWith(nameOrId);
@@ -4596,15 +4647,33 @@ function findConfigFile(nameOrId) {
4596
4647
  }
4597
4648
  return null;
4598
4649
  }
4650
+ function findConfigFileByConfigId(configId) {
4651
+ const dir = getTunnelConfigDir();
4652
+ if (!import_fs7.default.existsSync(dir)) return null;
4653
+ const files = import_fs7.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
4654
+ for (const file of files) {
4655
+ const filePath = import_path7.default.join(dir, file);
4656
+ const config = readConfigFile(filePath);
4657
+ if (config && config.configId === configId) {
4658
+ return { filePath, config };
4659
+ }
4660
+ }
4661
+ return null;
4662
+ }
4599
4663
  function findConfigByName(name) {
4600
- const resolved = findConfigFile(name);
4601
- return resolved?.config.name === name ? resolved.config : null;
4664
+ const dir = getTunnelConfigDir();
4665
+ if (!import_fs7.default.existsSync(dir)) return null;
4666
+ const prefix = `${sanitizeName(name)}_`;
4667
+ const matches = import_fs7.default.readdirSync(dir).filter((f) => f.endsWith(".json") && f.startsWith(prefix)).map((f) => readConfigFile(import_path7.default.join(dir, f))).filter((c) => c !== null && c.name === name);
4668
+ if (matches.length === 0) return null;
4669
+ return matches.sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""))[0];
4602
4670
  }
4603
4671
  function findConfig(nameOrId) {
4604
4672
  return findConfigFile(nameOrId)?.config ?? null;
4605
4673
  }
4606
- function saveConfig(name, configId, tunnelConfig, autoStart = false) {
4607
- const nameErr = validateName(name);
4674
+ function saveConfig(name, configId, tunnelConfig, autoStart = false, options = {}) {
4675
+ const { strict = true } = options;
4676
+ const nameErr = strict ? validateNameStrict(name) : validateNameForStorage(name);
4608
4677
  if (nameErr) {
4609
4678
  throw nameErr;
4610
4679
  }
@@ -4624,12 +4693,58 @@ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
4624
4693
  updatedAt: now,
4625
4694
  tunnelConfig
4626
4695
  };
4627
- const filename = buildFilename(sanitizeName(name), configId);
4628
- const filePath = import_path7.default.join(dir, filename);
4629
- import_fs7.default.writeFileSync(filePath, JSON.stringify(saved, null, 2), { encoding: "utf-8" });
4630
- logger.info(`Config "${name}" saved to ${filePath}`);
4696
+ const filePath = import_path7.default.join(dir, buildFilename(sanitizeName(name), configId));
4697
+ writeConfigFile(filePath, saved);
4698
+ storageLogger.info(`Config "${name}" saved to ${filePath}`);
4631
4699
  return saved;
4632
4700
  }
4701
+ function upsertConfig(saved, options = {}) {
4702
+ const { strict = true } = options;
4703
+ const dir = ensureTunnelConfigDir();
4704
+ const targetPath = import_path7.default.join(dir, buildFilename(sanitizeName(saved.name), saved.configId));
4705
+ const existing = findConfigFileByConfigId(saved.configId);
4706
+ if (existing) {
4707
+ if (import_path7.default.resolve(existing.filePath) !== import_path7.default.resolve(targetPath)) {
4708
+ try {
4709
+ import_fs7.default.unlinkSync(existing.filePath);
4710
+ } catch (err) {
4711
+ storageLogger.warn(`upsertConfig: failed to remove old file ${existing.filePath}: ${String(err)}`);
4712
+ }
4713
+ }
4714
+ writeConfigFile(targetPath, saved);
4715
+ storageLogger.info(`Config "${saved.name}" (${saved.configId}) updated at ${targetPath}`);
4716
+ return saved;
4717
+ }
4718
+ const nameErr = strict ? validateNameStrict(saved.name) : validateNameForStorage(saved.name);
4719
+ if (nameErr) {
4720
+ throw nameErr;
4721
+ }
4722
+ writeConfigFile(targetPath, saved);
4723
+ storageLogger.info(`Config "${saved.name}" (${saved.configId}) saved to ${targetPath}`);
4724
+ return saved;
4725
+ }
4726
+ function bulkReplace(configs, knownConfigIds, options = {}) {
4727
+ const written = [];
4728
+ const deleted = [];
4729
+ const incomingIds = /* @__PURE__ */ new Set();
4730
+ for (const cfg of configs) {
4731
+ upsertConfig(cfg, options);
4732
+ written.push(cfg.configId);
4733
+ incomingIds.add(cfg.configId);
4734
+ }
4735
+ for (const id of knownConfigIds) {
4736
+ if (incomingIds.has(id)) continue;
4737
+ const existing = findConfigFileByConfigId(id);
4738
+ if (!existing) continue;
4739
+ try {
4740
+ import_fs7.default.unlinkSync(existing.filePath);
4741
+ deleted.push(id);
4742
+ } catch (err) {
4743
+ storageLogger.warn(`bulkReplace: failed to delete ${existing.filePath}: ${String(err)}`);
4744
+ }
4745
+ }
4746
+ return { written, deleted };
4747
+ }
4633
4748
  function loadConfigByName(name) {
4634
4749
  return findConfigByName(name);
4635
4750
  }
@@ -4637,25 +4752,25 @@ function deleteConfig(nameOrId) {
4637
4752
  const resolved = findConfigFile(nameOrId);
4638
4753
  if (!resolved) return null;
4639
4754
  import_fs7.default.unlinkSync(resolved.filePath);
4640
- logger.info(`Config "${resolved.config.name}" deleted.`);
4755
+ storageLogger.info(`Config "${resolved.config.name}" deleted.`);
4641
4756
  return resolved.config.name;
4642
4757
  }
4643
4758
  function updateConfigAutoStart(nameOrId, autoStart) {
4644
4759
  const resolved = findConfigFile(nameOrId);
4645
4760
  if (!resolved) return null;
4646
4761
  resolved.config.autoStart = autoStart;
4647
- resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4762
+ resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
4648
4763
  writeConfigFile(resolved.filePath, resolved.config);
4649
- logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
4764
+ storageLogger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
4650
4765
  return resolved.config;
4651
4766
  }
4652
4767
  function updateTunnelConfig(nameOrId, tunnelConfig) {
4653
4768
  const resolved = findConfigFile(nameOrId);
4654
4769
  if (!resolved) return null;
4655
4770
  resolved.config.tunnelConfig = tunnelConfig;
4656
- resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4771
+ resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
4657
4772
  writeConfigFile(resolved.filePath, resolved.config);
4658
- logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
4773
+ storageLogger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
4659
4774
  return resolved.config;
4660
4775
  }
4661
4776
  function getAutoStartConfigs() {
@@ -4734,7 +4849,7 @@ Tunnel Config: ${import_picocolors4.default.cyanBright(config.name)}`));
4734
4849
  }
4735
4850
  console.log();
4736
4851
  }
4737
- var import_fs7, import_path7, import_picocolors4, Subcommand, ConfigVerb, SUBCOMMANDS, CONFIG_VERBS, RESERVED_NAMES;
4852
+ var import_fs7, import_path7, import_picocolors4, storageLogger, Subcommand, ConfigVerb, SUBCOMMANDS, CONFIG_VERBS, RESERVED_NAMES, validateName;
4738
4853
  var init_configStore = __esm({
4739
4854
  "src/cli/configStore.ts"() {
4740
4855
  "use strict";
@@ -4744,6 +4859,11 @@ var init_configStore = __esm({
4744
4859
  init_configDir();
4745
4860
  init_logger();
4746
4861
  import_picocolors4 = __toESM(require("picocolors"), 1);
4862
+ storageLogger = {
4863
+ info: (message, ...meta) => logger.info(message, ...meta),
4864
+ warn: (message, ...meta) => logger.warn(message, ...meta),
4865
+ error: (message, ...meta) => logger.error(message, ...meta)
4866
+ };
4747
4867
  Subcommand = {
4748
4868
  Config: "config",
4749
4869
  Start: "start",
@@ -4769,6 +4889,7 @@ var init_configStore = __esm({
4769
4889
  SUBCOMMANDS = Object.values(Subcommand);
4770
4890
  CONFIG_VERBS = Object.values(ConfigVerb);
4771
4891
  RESERVED_NAMES = /* @__PURE__ */ new Set([...SUBCOMMANDS, ...CONFIG_VERBS]);
4892
+ validateName = validateNameStrict;
4772
4893
  }
4773
4894
  });
4774
4895
 
@@ -7651,10 +7772,10 @@ function handleConfig(args) {
7651
7772
  printConfigList();
7652
7773
  return;
7653
7774
  case ConfigVerb.Show: {
7654
- const names = requireNames(rest, "config show");
7775
+ const names = clubSpacedName(requireNames(rest, "config show"));
7655
7776
  for (const name of names) {
7656
- const saved2 = resolveConfig(name);
7657
- if (saved2) printConfigDetail(saved2);
7777
+ const saved = resolveConfig(name, "config show");
7778
+ if (saved) printConfigDetail(saved);
7658
7779
  }
7659
7780
  return;
7660
7781
  }
@@ -7664,7 +7785,7 @@ function handleConfig(args) {
7664
7785
  return;
7665
7786
  }
7666
7787
  case ConfigVerb.Delete: {
7667
- const names = requireNames(rest, "config delete");
7788
+ const names = clubSpacedName(requireNames(rest, "config delete"));
7668
7789
  for (const name of names) {
7669
7790
  const deletedName = deleteConfig(name);
7670
7791
  if (deletedName) {
@@ -7681,7 +7802,7 @@ function handleConfig(args) {
7681
7802
  return;
7682
7803
  }
7683
7804
  case ConfigVerb.Auto: {
7684
- const names = requireNames(rest, "config auto");
7805
+ const names = clubSpacedName(requireNames(rest, "config auto"));
7685
7806
  for (const name of names) {
7686
7807
  const updated = updateConfigAutoStart(name, true);
7687
7808
  if (updated) {
@@ -7693,7 +7814,7 @@ function handleConfig(args) {
7693
7814
  return;
7694
7815
  }
7695
7816
  case ConfigVerb.Noauto: {
7696
- const names = requireNames(rest, "config noauto");
7817
+ const names = clubSpacedName(requireNames(rest, "config noauto"));
7697
7818
  for (const name of names) {
7698
7819
  const updated = updateConfigAutoStart(name, false);
7699
7820
  if (updated) {
@@ -7704,10 +7825,14 @@ function handleConfig(args) {
7704
7825
  }
7705
7826
  return;
7706
7827
  }
7707
- default:
7708
- const saved = resolveConfig(verb);
7709
- if (saved) printConfigDetail(saved);
7828
+ default: {
7829
+ const names = clubSpacedName([verb, ...rest]);
7830
+ for (const name of names) {
7831
+ const saved = resolveConfig(name, "config");
7832
+ if (saved) printConfigDetail(saved);
7833
+ }
7710
7834
  return;
7835
+ }
7711
7836
  }
7712
7837
  }
7713
7838
  function handleConfigSave(name, remainingArgs) {
@@ -7725,7 +7850,7 @@ function handleConfigSave(name, remainingArgs) {
7725
7850
  printer_default.success(`Config "${name}" saved.`);
7726
7851
  }
7727
7852
  function handleConfigUpdate(nameOrId, remainingArgs) {
7728
- const saved = resolveConfig(nameOrId);
7853
+ const saved = resolveConfig(nameOrId, "config update");
7729
7854
  if (!saved) return;
7730
7855
  const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
7731
7856
  logger.debug("Building updated config", { nameOrId, values, positionals });
@@ -7759,7 +7884,7 @@ async function handleStart(args) {
7759
7884
  return;
7760
7885
  }
7761
7886
  const resolved = [];
7762
- for (const name of names) {
7887
+ for (const name of clubSpacedName(names)) {
7763
7888
  const saved = resolveConfig(name);
7764
7889
  if (!saved) return;
7765
7890
  resolved.push(saved);
@@ -7784,13 +7909,34 @@ async function handleStart(args) {
7784
7909
  await startMultipleForegroundViaDaemon(resolved, values, positionals);
7785
7910
  }
7786
7911
  }
7787
- function resolveConfig(nameOrId) {
7788
- const saved = findConfig(nameOrId);
7789
- if (!saved) {
7790
- printer_default.error(`No config found matching "${nameOrId}". Use: pinggy config list`);
7791
- return null;
7912
+ function clubSpacedName(names) {
7913
+ if (names.length > 1) {
7914
+ const joined = names.join(" ");
7915
+ if (findConfig(joined) ?? findConfigByName(joined)) {
7916
+ return [joined];
7917
+ }
7792
7918
  }
7793
- return saved;
7919
+ return names;
7920
+ }
7921
+ function resolveConfig(nameOrId, command = "start") {
7922
+ const saved = findConfig(nameOrId) ?? findConfigByName(nameOrId);
7923
+ if (saved) return saved;
7924
+ const sanitizedQuery = sanitizeName(nameOrId);
7925
+ const suggestions = listSavedConfigs().filter(
7926
+ (c) => c.name !== nameOrId && sanitizeName(c.name) === sanitizedQuery
7927
+ );
7928
+ printer_default.error(`No config found matching "${nameOrId}".`);
7929
+ if (suggestions.length > 0) {
7930
+ for (const c of suggestions.slice(0, 3)) {
7931
+ printer_default.info(` Did you mean "${c.name}"? \u2192 pinggy ${command} "${c.name}"`);
7932
+ }
7933
+ if (suggestions.length > 3) {
7934
+ printer_default.info(` \u2026and ${suggestions.length - 3} more. Use: pinggy config list`);
7935
+ }
7936
+ } else {
7937
+ printer_default.info(" Use: pinggy config list");
7938
+ }
7939
+ return null;
7794
7940
  }
7795
7941
  function requireName(args, command) {
7796
7942
  if (args.length === 0 || args[0].startsWith("-")) {
@@ -9751,17 +9897,39 @@ var init_tunnelClient = __esm({
9751
9897
  var index_exports = {};
9752
9898
  __export(index_exports, {
9753
9899
  RemoteManagementUnauthorizedError: () => RemoteManagementUnauthorizedError,
9900
+ TUNNEL_CONFIG_V1_KEYS: () => TUNNEL_CONFIG_V1_KEYS,
9754
9901
  TunnelClient: () => TunnelClient,
9902
+ TunnelConfigV1Schema: () => TunnelConfigV1Schema,
9755
9903
  TunnelErrorCodeType: () => TunnelErrorCodeType,
9756
9904
  TunnelManager: () => TunnelManager,
9757
9905
  TunnelOperations: () => TunnelOperations,
9758
9906
  TunnelStateType: () => TunnelStateType,
9759
9907
  TunnelWarningCode: () => TunnelWarningCode,
9908
+ bulkReplace: () => bulkReplace,
9760
9909
  closeRemoteManagement: () => closeRemoteManagement,
9910
+ configureStorageLogger: () => configureStorageLogger,
9911
+ deleteConfig: () => deleteConfig,
9761
9912
  enablePackageLogging: () => enablePackageLogging,
9913
+ ensureTunnelConfigDir: () => ensureTunnelConfigDir,
9914
+ findConfig: () => findConfig,
9915
+ findConfigByName: () => findConfigByName,
9916
+ getAutoStartConfigs: () => getAutoStartConfigs,
9917
+ getDaemonInfoPath: () => getDaemonInfoPath,
9918
+ getPinggyConfigDir: () => getPinggyConfigDir,
9762
9919
  getRemoteManagementState: () => getRemoteManagementState,
9920
+ getTunnelConfigDir: () => getTunnelConfigDir,
9763
9921
  initiateRemoteManagement: () => initiateRemoteManagement,
9764
- startRemoteManagement: () => startRemoteManagement
9922
+ listSavedConfigs: () => listSavedConfigs,
9923
+ loadConfigByName: () => loadConfigByName,
9924
+ sanitizeName: () => sanitizeName,
9925
+ saveConfig: () => saveConfig,
9926
+ startRemoteManagement: () => startRemoteManagement,
9927
+ updateConfigAutoStart: () => updateConfigAutoStart,
9928
+ updateTunnelConfig: () => updateTunnelConfig,
9929
+ upsertConfig: () => upsertConfig,
9930
+ validateName: () => validateName,
9931
+ validateNameForStorage: () => validateNameForStorage,
9932
+ validateNameStrict: () => validateNameStrict
9765
9933
  });
9766
9934
  module.exports = __toCommonJS(index_exports);
9767
9935
  init_cjs_shims();
@@ -9854,6 +10022,9 @@ init_tunnelClient();
9854
10022
  init_logger();
9855
10023
  init_remoteManagement();
9856
10024
  init_types();
10025
+ init_configStore();
10026
+ init_configDir();
10027
+ init_remote_schema();
9857
10028
  async function verifyAndLoad() {
9858
10029
  if (process.platform === "win32") {
9859
10030
  const vcRedist = checkVCRedist();
@@ -9873,15 +10044,37 @@ verifyAndLoad().catch((err) => {
9873
10044
  // Annotate the CommonJS export names for ESM import in node:
9874
10045
  0 && (module.exports = {
9875
10046
  RemoteManagementUnauthorizedError,
10047
+ TUNNEL_CONFIG_V1_KEYS,
9876
10048
  TunnelClient,
10049
+ TunnelConfigV1Schema,
9877
10050
  TunnelErrorCodeType,
9878
10051
  TunnelManager,
9879
10052
  TunnelOperations,
9880
10053
  TunnelStateType,
9881
10054
  TunnelWarningCode,
10055
+ bulkReplace,
9882
10056
  closeRemoteManagement,
10057
+ configureStorageLogger,
10058
+ deleteConfig,
9883
10059
  enablePackageLogging,
10060
+ ensureTunnelConfigDir,
10061
+ findConfig,
10062
+ findConfigByName,
10063
+ getAutoStartConfigs,
10064
+ getDaemonInfoPath,
10065
+ getPinggyConfigDir,
9884
10066
  getRemoteManagementState,
10067
+ getTunnelConfigDir,
9885
10068
  initiateRemoteManagement,
9886
- startRemoteManagement
10069
+ listSavedConfigs,
10070
+ loadConfigByName,
10071
+ sanitizeName,
10072
+ saveConfig,
10073
+ startRemoteManagement,
10074
+ updateConfigAutoStart,
10075
+ updateTunnelConfig,
10076
+ upsertConfig,
10077
+ validateName,
10078
+ validateNameForStorage,
10079
+ validateNameStrict
9887
10080
  });