pinggy 0.5.1 → 0.5.3

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);
@@ -2703,7 +2717,7 @@ var init_handler = __esm({
2703
2717
  });
2704
2718
 
2705
2719
  // src/daemon/ipc/ipcRoutes.ts
2706
- var Route, SessionMode;
2720
+ var Route, DaemonHost, ShutdownStatus, SessionMode;
2707
2721
  var init_ipcRoutes = __esm({
2708
2722
  "src/daemon/ipc/ipcRoutes.ts"() {
2709
2723
  "use strict";
@@ -2727,6 +2741,14 @@ var init_ipcRoutes = __esm({
2727
2741
  SetTunnelLogging: "POST /config/tunnel-logging",
2728
2742
  GetLogPaths: "GET /logs/paths"
2729
2743
  };
2744
+ DaemonHost = {
2745
+ CLI: "cli",
2746
+ APP: "app"
2747
+ };
2748
+ ShutdownStatus = {
2749
+ ShuttingDown: "shutting_down",
2750
+ RefusedInApp: "refused_in_app"
2751
+ };
2730
2752
  SessionMode = {
2731
2753
  Foreground: "foreground",
2732
2754
  Detached: "detached"
@@ -4350,6 +4372,13 @@ function parseToken(finalConfig, explicitToken) {
4350
4372
  finalConfig.token = explicitToken;
4351
4373
  }
4352
4374
  }
4375
+ function normalizeForwardingToArray(finalConfig, type) {
4376
+ const fwd = finalConfig.forwarding;
4377
+ if (typeof fwd !== "string") {
4378
+ return;
4379
+ }
4380
+ finalConfig.forwarding = fwd.trim() === "" ? [] : [{ type: type || import_pinggy4.TunnelType.Http, address: fwd.trim() }];
4381
+ }
4353
4382
  function parseArgs2(finalConfig, remainingPositionals) {
4354
4383
  let localserverTls = "";
4355
4384
  localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
@@ -4470,6 +4499,7 @@ function buildFinalConfig(values, positionals, baseConfig) {
4470
4499
  if (forceFlag || values.force) {
4471
4500
  finalConfig.force = true;
4472
4501
  }
4502
+ normalizeForwardingToArray(finalConfig, type);
4473
4503
  parseArgs2(finalConfig, remainingPositionals);
4474
4504
  storeJson(finalConfig, saveconf);
4475
4505
  return finalConfig;
@@ -4509,6 +4539,8 @@ __export(configStore_exports, {
4509
4539
  RESERVED_NAMES: () => RESERVED_NAMES,
4510
4540
  SUBCOMMANDS: () => SUBCOMMANDS,
4511
4541
  Subcommand: () => Subcommand,
4542
+ bulkReplace: () => bulkReplace,
4543
+ configureStorageLogger: () => configureStorageLogger,
4512
4544
  deleteConfig: () => deleteConfig,
4513
4545
  findConfig: () => findConfig,
4514
4546
  findConfigByName: () => findConfigByName,
@@ -4521,15 +4553,21 @@ __export(configStore_exports, {
4521
4553
  saveConfig: () => saveConfig,
4522
4554
  updateConfigAutoStart: () => updateConfigAutoStart,
4523
4555
  updateTunnelConfig: () => updateTunnelConfig,
4524
- validateName: () => validateName
4556
+ upsertConfig: () => upsertConfig,
4557
+ validateName: () => validateName,
4558
+ validateNameForStorage: () => validateNameForStorage,
4559
+ validateNameStrict: () => validateNameStrict
4525
4560
  });
4561
+ function configureStorageLogger(l) {
4562
+ storageLogger = l;
4563
+ }
4526
4564
  function buildFilename(name, configId) {
4527
4565
  return `${name}_${configId}.json`;
4528
4566
  }
4529
4567
  function sanitizeName(name) {
4530
4568
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
4531
4569
  }
4532
- function validateName(name) {
4570
+ function validateNameStrict(name) {
4533
4571
  if (!name || name.trim().length === 0) {
4534
4572
  return new Error("Tunnel name cannot be empty.");
4535
4573
  }
@@ -4544,17 +4582,38 @@ function validateName(name) {
4544
4582
  }
4545
4583
  return null;
4546
4584
  }
4585
+ function validateNameForStorage(name) {
4586
+ if (!name || name.trim().length === 0) {
4587
+ return new Error("Tunnel name cannot be empty.");
4588
+ }
4589
+ if (/[\u0000-\u001F\/\\]/.test(name)) {
4590
+ return new Error("Tunnel name cannot contain path separators or control characters.");
4591
+ }
4592
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
4593
+ return new Error(`"${name}" is a reserved subcommand name. Use a different name.`);
4594
+ }
4595
+ return null;
4596
+ }
4547
4597
  function readConfigFile(filePath) {
4548
4598
  try {
4549
4599
  const data = import_fs7.default.readFileSync(filePath, { encoding: "utf-8" });
4550
4600
  return JSON.parse(data);
4551
4601
  } catch (err) {
4552
- logger.warn(`Failed to read config file ${filePath}:`, err);
4602
+ storageLogger.warn(`Failed to read config file ${filePath}: ${String(err)}`);
4553
4603
  return null;
4554
4604
  }
4555
4605
  }
4556
4606
  function writeConfigFile(filePath, config) {
4557
- import_fs7.default.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
4607
+ const tmp = `${filePath}.tmp.${process.pid}`;
4608
+ import_fs7.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { encoding: "utf-8" });
4609
+ import_fs7.default.renameSync(tmp, filePath);
4610
+ }
4611
+ function nextTimestamp(prev) {
4612
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
4613
+ if (prev && ts <= prev) {
4614
+ return new Date(new Date(prev).getTime() + 1).toISOString();
4615
+ }
4616
+ return ts;
4558
4617
  }
4559
4618
  function listSavedConfigs() {
4560
4619
  const dir = getTunnelConfigDir();
@@ -4576,15 +4635,15 @@ function findConfigFile(nameOrId) {
4576
4635
  if (!import_fs7.default.existsSync(dir)) return null;
4577
4636
  const files = import_fs7.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
4578
4637
  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);
4638
+ const nameCandidates = files.filter((f) => f.startsWith(sanitized + "_"));
4639
+ for (const f of nameCandidates) {
4640
+ const filePath = import_path7.default.join(dir, f);
4582
4641
  const config = readConfigFile(filePath);
4583
4642
  if (config && config.name === nameOrId) return { filePath, config };
4584
4643
  }
4585
4644
  const idCandidates = files.filter((f) => {
4586
4645
  const withoutExt = f.replace(/\.json$/, "");
4587
- const lastUnderscore = withoutExt.indexOf("_");
4646
+ const lastUnderscore = withoutExt.lastIndexOf("_");
4588
4647
  if (lastUnderscore === -1) return false;
4589
4648
  const idPart = withoutExt.slice(lastUnderscore + 1);
4590
4649
  return idPart.startsWith(nameOrId);
@@ -4596,15 +4655,33 @@ function findConfigFile(nameOrId) {
4596
4655
  }
4597
4656
  return null;
4598
4657
  }
4658
+ function findConfigFileByConfigId(configId) {
4659
+ const dir = getTunnelConfigDir();
4660
+ if (!import_fs7.default.existsSync(dir)) return null;
4661
+ const files = import_fs7.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
4662
+ for (const file of files) {
4663
+ const filePath = import_path7.default.join(dir, file);
4664
+ const config = readConfigFile(filePath);
4665
+ if (config && config.configId === configId) {
4666
+ return { filePath, config };
4667
+ }
4668
+ }
4669
+ return null;
4670
+ }
4599
4671
  function findConfigByName(name) {
4600
- const resolved = findConfigFile(name);
4601
- return resolved?.config.name === name ? resolved.config : null;
4672
+ const dir = getTunnelConfigDir();
4673
+ if (!import_fs7.default.existsSync(dir)) return null;
4674
+ const prefix = `${sanitizeName(name)}_`;
4675
+ 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);
4676
+ if (matches.length === 0) return null;
4677
+ return matches.sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""))[0];
4602
4678
  }
4603
4679
  function findConfig(nameOrId) {
4604
4680
  return findConfigFile(nameOrId)?.config ?? null;
4605
4681
  }
4606
- function saveConfig(name, configId, tunnelConfig, autoStart = false) {
4607
- const nameErr = validateName(name);
4682
+ function saveConfig(name, configId, tunnelConfig, autoStart = false, options = {}) {
4683
+ const { strict = true } = options;
4684
+ const nameErr = strict ? validateNameStrict(name) : validateNameForStorage(name);
4608
4685
  if (nameErr) {
4609
4686
  throw nameErr;
4610
4687
  }
@@ -4624,12 +4701,58 @@ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
4624
4701
  updatedAt: now,
4625
4702
  tunnelConfig
4626
4703
  };
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}`);
4704
+ const filePath = import_path7.default.join(dir, buildFilename(sanitizeName(name), configId));
4705
+ writeConfigFile(filePath, saved);
4706
+ storageLogger.info(`Config "${name}" saved to ${filePath}`);
4707
+ return saved;
4708
+ }
4709
+ function upsertConfig(saved, options = {}) {
4710
+ const { strict = true } = options;
4711
+ const dir = ensureTunnelConfigDir();
4712
+ const targetPath = import_path7.default.join(dir, buildFilename(sanitizeName(saved.name), saved.configId));
4713
+ const existing = findConfigFileByConfigId(saved.configId);
4714
+ if (existing) {
4715
+ if (import_path7.default.resolve(existing.filePath) !== import_path7.default.resolve(targetPath)) {
4716
+ try {
4717
+ import_fs7.default.unlinkSync(existing.filePath);
4718
+ } catch (err) {
4719
+ storageLogger.warn(`upsertConfig: failed to remove old file ${existing.filePath}: ${String(err)}`);
4720
+ }
4721
+ }
4722
+ writeConfigFile(targetPath, saved);
4723
+ storageLogger.info(`Config "${saved.name}" (${saved.configId}) updated at ${targetPath}`);
4724
+ return saved;
4725
+ }
4726
+ const nameErr = strict ? validateNameStrict(saved.name) : validateNameForStorage(saved.name);
4727
+ if (nameErr) {
4728
+ throw nameErr;
4729
+ }
4730
+ writeConfigFile(targetPath, saved);
4731
+ storageLogger.info(`Config "${saved.name}" (${saved.configId}) saved to ${targetPath}`);
4631
4732
  return saved;
4632
4733
  }
4734
+ function bulkReplace(configs, knownConfigIds, options = {}) {
4735
+ const written = [];
4736
+ const deleted = [];
4737
+ const incomingIds = /* @__PURE__ */ new Set();
4738
+ for (const cfg of configs) {
4739
+ upsertConfig(cfg, options);
4740
+ written.push(cfg.configId);
4741
+ incomingIds.add(cfg.configId);
4742
+ }
4743
+ for (const id of knownConfigIds) {
4744
+ if (incomingIds.has(id)) continue;
4745
+ const existing = findConfigFileByConfigId(id);
4746
+ if (!existing) continue;
4747
+ try {
4748
+ import_fs7.default.unlinkSync(existing.filePath);
4749
+ deleted.push(id);
4750
+ } catch (err) {
4751
+ storageLogger.warn(`bulkReplace: failed to delete ${existing.filePath}: ${String(err)}`);
4752
+ }
4753
+ }
4754
+ return { written, deleted };
4755
+ }
4633
4756
  function loadConfigByName(name) {
4634
4757
  return findConfigByName(name);
4635
4758
  }
@@ -4637,25 +4760,25 @@ function deleteConfig(nameOrId) {
4637
4760
  const resolved = findConfigFile(nameOrId);
4638
4761
  if (!resolved) return null;
4639
4762
  import_fs7.default.unlinkSync(resolved.filePath);
4640
- logger.info(`Config "${resolved.config.name}" deleted.`);
4763
+ storageLogger.info(`Config "${resolved.config.name}" deleted.`);
4641
4764
  return resolved.config.name;
4642
4765
  }
4643
4766
  function updateConfigAutoStart(nameOrId, autoStart) {
4644
4767
  const resolved = findConfigFile(nameOrId);
4645
4768
  if (!resolved) return null;
4646
4769
  resolved.config.autoStart = autoStart;
4647
- resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4770
+ resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
4648
4771
  writeConfigFile(resolved.filePath, resolved.config);
4649
- logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
4772
+ storageLogger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
4650
4773
  return resolved.config;
4651
4774
  }
4652
4775
  function updateTunnelConfig(nameOrId, tunnelConfig) {
4653
4776
  const resolved = findConfigFile(nameOrId);
4654
4777
  if (!resolved) return null;
4655
4778
  resolved.config.tunnelConfig = tunnelConfig;
4656
- resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4779
+ resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
4657
4780
  writeConfigFile(resolved.filePath, resolved.config);
4658
- logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
4781
+ storageLogger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
4659
4782
  return resolved.config;
4660
4783
  }
4661
4784
  function getAutoStartConfigs() {
@@ -4734,7 +4857,7 @@ Tunnel Config: ${import_picocolors4.default.cyanBright(config.name)}`));
4734
4857
  }
4735
4858
  console.log();
4736
4859
  }
4737
- var import_fs7, import_path7, import_picocolors4, Subcommand, ConfigVerb, SUBCOMMANDS, CONFIG_VERBS, RESERVED_NAMES;
4860
+ var import_fs7, import_path7, import_picocolors4, storageLogger, Subcommand, ConfigVerb, SUBCOMMANDS, CONFIG_VERBS, RESERVED_NAMES, validateName;
4738
4861
  var init_configStore = __esm({
4739
4862
  "src/cli/configStore.ts"() {
4740
4863
  "use strict";
@@ -4744,6 +4867,11 @@ var init_configStore = __esm({
4744
4867
  init_configDir();
4745
4868
  init_logger();
4746
4869
  import_picocolors4 = __toESM(require("picocolors"), 1);
4870
+ storageLogger = {
4871
+ info: (message, ...meta) => logger.info(message, ...meta),
4872
+ warn: (message, ...meta) => logger.warn(message, ...meta),
4873
+ error: (message, ...meta) => logger.error(message, ...meta)
4874
+ };
4747
4875
  Subcommand = {
4748
4876
  Config: "config",
4749
4877
  Start: "start",
@@ -4769,6 +4897,7 @@ var init_configStore = __esm({
4769
4897
  SUBCOMMANDS = Object.values(Subcommand);
4770
4898
  CONFIG_VERBS = Object.values(ConfigVerb);
4771
4899
  RESERVED_NAMES = /* @__PURE__ */ new Set([...SUBCOMMANDS, ...CONFIG_VERBS]);
4900
+ validateName = validateNameStrict;
4772
4901
  }
4773
4902
  });
4774
4903
 
@@ -7122,6 +7251,8 @@ async function handleDaemonStop() {
7122
7251
  const result = await stopDaemon();
7123
7252
  if (result.ok) {
7124
7253
  printer_default.success("Daemon stopped.");
7254
+ } else if (result.reason === DaemonHost.APP) {
7255
+ printer_default.print(import_picocolors6.default.yellow("The Pinggy daemon is running inside the Pinggy desktop app. Please quit the Pinggy app to stop it."));
7125
7256
  } else {
7126
7257
  printer_default.error(result.error);
7127
7258
  }
@@ -7142,6 +7273,7 @@ function handleDaemonStatus() {
7142
7273
  printer_default.print(` Port: ${info.port}`);
7143
7274
  printer_default.print(` Started: ${info.startedAt}`);
7144
7275
  printer_default.print(` Uptime: ${uptimeStr}`);
7276
+ printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
7145
7277
  }
7146
7278
  var import_picocolors6;
7147
7279
  var init_daemonCommandsHandler = __esm({
@@ -7152,6 +7284,7 @@ var init_daemonCommandsHandler = __esm({
7152
7284
  import_picocolors6 = __toESM(require("picocolors"), 1);
7153
7285
  init_configStore();
7154
7286
  init_daemonManager();
7287
+ init_ipcRoutes();
7155
7288
  init_serviceInstaller();
7156
7289
  init_helpMessages();
7157
7290
  init_util();
@@ -7651,10 +7784,10 @@ function handleConfig(args) {
7651
7784
  printConfigList();
7652
7785
  return;
7653
7786
  case ConfigVerb.Show: {
7654
- const names = requireNames(rest, "config show");
7787
+ const names = clubSpacedName(requireNames(rest, "config show"));
7655
7788
  for (const name of names) {
7656
- const saved2 = resolveConfig(name);
7657
- if (saved2) printConfigDetail(saved2);
7789
+ const saved = resolveConfig(name, "config show");
7790
+ if (saved) printConfigDetail(saved);
7658
7791
  }
7659
7792
  return;
7660
7793
  }
@@ -7664,7 +7797,7 @@ function handleConfig(args) {
7664
7797
  return;
7665
7798
  }
7666
7799
  case ConfigVerb.Delete: {
7667
- const names = requireNames(rest, "config delete");
7800
+ const names = clubSpacedName(requireNames(rest, "config delete"));
7668
7801
  for (const name of names) {
7669
7802
  const deletedName = deleteConfig(name);
7670
7803
  if (deletedName) {
@@ -7681,7 +7814,7 @@ function handleConfig(args) {
7681
7814
  return;
7682
7815
  }
7683
7816
  case ConfigVerb.Auto: {
7684
- const names = requireNames(rest, "config auto");
7817
+ const names = clubSpacedName(requireNames(rest, "config auto"));
7685
7818
  for (const name of names) {
7686
7819
  const updated = updateConfigAutoStart(name, true);
7687
7820
  if (updated) {
@@ -7693,7 +7826,7 @@ function handleConfig(args) {
7693
7826
  return;
7694
7827
  }
7695
7828
  case ConfigVerb.Noauto: {
7696
- const names = requireNames(rest, "config noauto");
7829
+ const names = clubSpacedName(requireNames(rest, "config noauto"));
7697
7830
  for (const name of names) {
7698
7831
  const updated = updateConfigAutoStart(name, false);
7699
7832
  if (updated) {
@@ -7704,10 +7837,14 @@ function handleConfig(args) {
7704
7837
  }
7705
7838
  return;
7706
7839
  }
7707
- default:
7708
- const saved = resolveConfig(verb);
7709
- if (saved) printConfigDetail(saved);
7840
+ default: {
7841
+ const names = clubSpacedName([verb, ...rest]);
7842
+ for (const name of names) {
7843
+ const saved = resolveConfig(name, "config");
7844
+ if (saved) printConfigDetail(saved);
7845
+ }
7710
7846
  return;
7847
+ }
7711
7848
  }
7712
7849
  }
7713
7850
  function handleConfigSave(name, remainingArgs) {
@@ -7725,7 +7862,7 @@ function handleConfigSave(name, remainingArgs) {
7725
7862
  printer_default.success(`Config "${name}" saved.`);
7726
7863
  }
7727
7864
  function handleConfigUpdate(nameOrId, remainingArgs) {
7728
- const saved = resolveConfig(nameOrId);
7865
+ const saved = resolveConfig(nameOrId, "config update");
7729
7866
  if (!saved) return;
7730
7867
  const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
7731
7868
  logger.debug("Building updated config", { nameOrId, values, positionals });
@@ -7759,7 +7896,7 @@ async function handleStart(args) {
7759
7896
  return;
7760
7897
  }
7761
7898
  const resolved = [];
7762
- for (const name of names) {
7899
+ for (const name of clubSpacedName(names)) {
7763
7900
  const saved = resolveConfig(name);
7764
7901
  if (!saved) return;
7765
7902
  resolved.push(saved);
@@ -7784,13 +7921,34 @@ async function handleStart(args) {
7784
7921
  await startMultipleForegroundViaDaemon(resolved, values, positionals);
7785
7922
  }
7786
7923
  }
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;
7924
+ function clubSpacedName(names) {
7925
+ if (names.length > 1) {
7926
+ const joined = names.join(" ");
7927
+ if (findConfig(joined) ?? findConfigByName(joined)) {
7928
+ return [joined];
7929
+ }
7792
7930
  }
7793
- return saved;
7931
+ return names;
7932
+ }
7933
+ function resolveConfig(nameOrId, command = "start") {
7934
+ const saved = findConfig(nameOrId) ?? findConfigByName(nameOrId);
7935
+ if (saved) return saved;
7936
+ const sanitizedQuery = sanitizeName(nameOrId);
7937
+ const suggestions = listSavedConfigs().filter(
7938
+ (c) => c.name !== nameOrId && sanitizeName(c.name) === sanitizedQuery
7939
+ );
7940
+ printer_default.error(`No config found matching "${nameOrId}".`);
7941
+ if (suggestions.length > 0) {
7942
+ for (const c of suggestions.slice(0, 3)) {
7943
+ printer_default.info(` Did you mean "${c.name}"? \u2192 pinggy ${command} "${c.name}"`);
7944
+ }
7945
+ if (suggestions.length > 3) {
7946
+ printer_default.info(` \u2026and ${suggestions.length - 3} more. Use: pinggy config list`);
7947
+ }
7948
+ } else {
7949
+ printer_default.info(" Use: pinggy config list");
7950
+ }
7951
+ return null;
7794
7952
  }
7795
7953
  function requireName(args, command) {
7796
7954
  if (args.length === 0 || args[0].startsWith("-")) {
@@ -7861,6 +8019,7 @@ var init_subcommands = __esm({
7861
8019
  // src/main.ts
7862
8020
  var main_exports = {};
7863
8021
  __export(main_exports, {
8022
+ DaemonHost: () => DaemonHost,
7864
8023
  RemoteManagementUnauthorizedError: () => RemoteManagementUnauthorizedError,
7865
8024
  TunnelManager: () => TunnelManager,
7866
8025
  TunnelOperations: () => TunnelOperations,
@@ -7923,6 +8082,7 @@ var init_main = __esm({
7923
8082
  init_buildAndStartTunnel();
7924
8083
  init_subcommands();
7925
8084
  init_daemonChild();
8085
+ init_ipcRoutes();
7926
8086
  init_daemonManager();
7927
8087
  currentFile = (0, import_url2.fileURLToPath)(importMetaUrl);
7928
8088
  entryFile = null;
@@ -8229,6 +8389,10 @@ var init_ipcServer = __esm({
8229
8389
  },
8230
8390
  [Route.Shutdown]: () => {
8231
8391
  logger.info("Daemon shutdown requested via IPC");
8392
+ if (getDaemonHost() === DaemonHost.APP) {
8393
+ logger.info("Shutdown refused: daemon is hosted in-process by the Pinggy app");
8394
+ return { status: ShutdownStatus.RefusedInApp, errors: [] };
8395
+ }
8232
8396
  const errors = [];
8233
8397
  const step = (label, fn) => {
8234
8398
  try {
@@ -8243,7 +8407,7 @@ var init_ipcServer = __esm({
8243
8407
  step("clearDaemonState", clearDaemonState);
8244
8408
  step("stopAllTunnels", () => TunnelManager.getInstance().stopAllTunnels());
8245
8409
  setTimeout(() => process.exit(0), 200);
8246
- return { status: "shutting_down", errors };
8410
+ return { status: ShutdownStatus.ShuttingDown, errors };
8247
8411
  }
8248
8412
  };
8249
8413
  }
@@ -8701,6 +8865,8 @@ var init_sessionTracker = __esm({
8701
8865
  // src/daemon/lifecycle/daemonChild.ts
8702
8866
  var daemonChild_exports = {};
8703
8867
  __export(daemonChild_exports, {
8868
+ DaemonHost: () => DaemonHost,
8869
+ getDaemonHost: () => getDaemonHost,
8704
8870
  removeDaemonInfo: () => removeDaemonInfo,
8705
8871
  runDaemonChild: () => runDaemonChild,
8706
8872
  setDaemonSessionTracker: () => setDaemonSessionTracker,
@@ -8708,6 +8874,9 @@ __export(daemonChild_exports, {
8708
8874
  trackTunnelStart: () => trackTunnelStart,
8709
8875
  trackTunnelStop: () => trackTunnelStop
8710
8876
  });
8877
+ function getDaemonHost() {
8878
+ return daemonHost;
8879
+ }
8711
8880
  function setDaemonSessionTracker(st) {
8712
8881
  sessionTrackerRef = st;
8713
8882
  }
@@ -8836,6 +9005,7 @@ async function restoreCrashedTunnels(manager) {
8836
9005
  async function runDaemonChild(opts = {}) {
8837
9006
  const installSignalHandlers = opts.installSignalHandlers ?? true;
8838
9007
  const exitOnFailure = opts.exitOnFailure ?? true;
9008
+ daemonHost = opts.host ?? DaemonHost.CLI;
8839
9009
  ensurePinggyConfigDir();
8840
9010
  const persistedLevel = readDaemonConfig()?.logLevel;
8841
9011
  const envLevel = process.env.PINGGY_LOG_LEVEL;
@@ -8911,7 +9081,8 @@ async function runDaemonChild(opts = {}) {
8911
9081
  const info = {
8912
9082
  pid: process.pid,
8913
9083
  port,
8914
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
9084
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9085
+ host: daemonHost
8915
9086
  };
8916
9087
  writeDaemonInfo(info);
8917
9088
  logger.info("Daemon info written", info);
@@ -8944,7 +9115,7 @@ async function runDaemonChild(opts = {}) {
8944
9115
  throw err;
8945
9116
  }
8946
9117
  }
8947
- var import_node_fs7, daemonState, sessionTrackerRef;
9118
+ var import_node_fs7, daemonState, daemonHost, sessionTrackerRef;
8948
9119
  var init_daemonChild = __esm({
8949
9120
  "src/daemon/lifecycle/daemonChild.ts"() {
8950
9121
  "use strict";
@@ -8962,6 +9133,7 @@ var init_daemonChild = __esm({
8962
9133
  init_util();
8963
9134
  init_ipcRoutes();
8964
9135
  daemonState = { tunnels: [], lastUpdated: "" };
9136
+ daemonHost = DaemonHost.CLI;
8965
9137
  sessionTrackerRef = null;
8966
9138
  }
8967
9139
  });
@@ -9048,13 +9220,15 @@ async function ensureDaemonRunning() {
9048
9220
  const { runDaemonChild: runDaemonChild2 } = await Promise.resolve().then(() => (init_daemonChild(), daemonChild_exports));
9049
9221
  const handle = await runDaemonChild2({
9050
9222
  installSignalHandlers: false,
9051
- exitOnFailure: false
9223
+ exitOnFailure: false,
9224
+ host: DaemonHost.APP
9052
9225
  });
9053
9226
  inProcessHandle = handle;
9054
9227
  return getDaemonInfo() ?? {
9055
9228
  pid: handle.pid,
9056
9229
  port: handle.port,
9057
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
9230
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9231
+ host: DaemonHost.APP
9058
9232
  };
9059
9233
  }
9060
9234
  return startDaemon();
@@ -9116,6 +9290,13 @@ async function stopDaemon() {
9116
9290
  const client = new IPCClient2(info.port);
9117
9291
  const result = await client.shutdown();
9118
9292
  logger.debug("Sent shutdown command to daemon", { result });
9293
+ if (result?.status === ShutdownStatus.RefusedInApp) {
9294
+ return {
9295
+ ok: false,
9296
+ reason: DaemonHost.APP,
9297
+ error: "The daemon is running inside the Pinggy app."
9298
+ };
9299
+ }
9119
9300
  if (Array.isArray(result?.errors)) daemonErrors = result.errors;
9120
9301
  } catch (e) {
9121
9302
  return { ok: false, error: `Failed to reach daemon: ${errorMessage(e)}` };
@@ -9147,6 +9328,7 @@ var init_daemonManager = __esm({
9147
9328
  import_node_fs8 = __toESM(require("fs"), 1);
9148
9329
  import_node_child_process2 = require("child_process");
9149
9330
  init_configDir();
9331
+ init_ipcRoutes();
9150
9332
  init_logger();
9151
9333
  init_types();
9152
9334
  init_util();
@@ -9437,6 +9619,13 @@ var init_daemonHealth = __esm({
9437
9619
  bindPid(pid) {
9438
9620
  this.originalPid = pid;
9439
9621
  }
9622
+ /**
9623
+ * Forget a previous loss verdict. Called when the client re-binds to a
9624
+ * live daemon via ensureDaemon():
9625
+ */
9626
+ reset() {
9627
+ this.lost = false;
9628
+ }
9440
9629
  isLost() {
9441
9630
  return this.lost;
9442
9631
  }
@@ -9563,6 +9752,7 @@ var init_tunnelClient = __esm({
9563
9752
  const info = await ensureDaemonRunning();
9564
9753
  this.ipc = new IPCClient(info.port, this.origin);
9565
9754
  this.health.bindPid(info.pid);
9755
+ this.health.reset();
9566
9756
  }
9567
9757
  static async forRemoteManagement() {
9568
9758
  const client = new _TunnelClient({ origin: "remote" });
@@ -9636,6 +9826,21 @@ var init_tunnelClient = __esm({
9636
9826
  await this.ipc.shutdown();
9637
9827
  this.close();
9638
9828
  }
9829
+ /**
9830
+ * Tear down the daemon hosted in THIS process, if any. Stops all tunnels
9831
+ * and removes daemon.json + daemon-state.json so the next CLI run doesn't
9832
+ * "crash-recover" tunnels that died with the host application.
9833
+ */
9834
+ shutdownInProcessDaemon() {
9835
+ const handle = getInProcessDaemonHandle();
9836
+ if (!handle) return false;
9837
+ try {
9838
+ this.close();
9839
+ } catch {
9840
+ }
9841
+ handle.shutdown();
9842
+ return true;
9843
+ }
9639
9844
  async getLogLevel() {
9640
9845
  this.assertClient();
9641
9846
  const res = await this.ipc.getLogLevel();
@@ -9751,17 +9956,41 @@ var init_tunnelClient = __esm({
9751
9956
  var index_exports = {};
9752
9957
  __export(index_exports, {
9753
9958
  RemoteManagementUnauthorizedError: () => RemoteManagementUnauthorizedError,
9959
+ TUNNEL_CONFIG_V1_KEYS: () => TUNNEL_CONFIG_V1_KEYS,
9754
9960
  TunnelClient: () => TunnelClient,
9961
+ TunnelConfigV1Schema: () => TunnelConfigV1Schema,
9755
9962
  TunnelErrorCodeType: () => TunnelErrorCodeType,
9756
9963
  TunnelManager: () => TunnelManager,
9757
9964
  TunnelOperations: () => TunnelOperations,
9758
9965
  TunnelStateType: () => TunnelStateType,
9759
9966
  TunnelWarningCode: () => TunnelWarningCode,
9967
+ bulkReplace: () => bulkReplace,
9760
9968
  closeRemoteManagement: () => closeRemoteManagement,
9969
+ configureStorageLogger: () => configureStorageLogger,
9970
+ deleteConfig: () => deleteConfig,
9761
9971
  enablePackageLogging: () => enablePackageLogging,
9972
+ ensureTunnelConfigDir: () => ensureTunnelConfigDir,
9973
+ findConfig: () => findConfig,
9974
+ findConfigByName: () => findConfigByName,
9975
+ getAutoStartConfigs: () => getAutoStartConfigs,
9976
+ getDaemonInfo: () => getDaemonInfo,
9977
+ getDaemonInfoPath: () => getDaemonInfoPath,
9978
+ getPinggyConfigDir: () => getPinggyConfigDir,
9762
9979
  getRemoteManagementState: () => getRemoteManagementState,
9980
+ getTunnelConfigDir: () => getTunnelConfigDir,
9763
9981
  initiateRemoteManagement: () => initiateRemoteManagement,
9764
- startRemoteManagement: () => startRemoteManagement
9982
+ isDaemonRunning: () => isDaemonRunning,
9983
+ listSavedConfigs: () => listSavedConfigs,
9984
+ loadConfigByName: () => loadConfigByName,
9985
+ sanitizeName: () => sanitizeName,
9986
+ saveConfig: () => saveConfig,
9987
+ startRemoteManagement: () => startRemoteManagement,
9988
+ updateConfigAutoStart: () => updateConfigAutoStart,
9989
+ updateTunnelConfig: () => updateTunnelConfig,
9990
+ upsertConfig: () => upsertConfig,
9991
+ validateName: () => validateName,
9992
+ validateNameForStorage: () => validateNameForStorage,
9993
+ validateNameStrict: () => validateNameStrict
9765
9994
  });
9766
9995
  module.exports = __toCommonJS(index_exports);
9767
9996
  init_cjs_shims();
@@ -9851,9 +10080,13 @@ init_printer();
9851
10080
  init_TunnelManager();
9852
10081
  init_handler();
9853
10082
  init_tunnelClient();
10083
+ init_daemonManager();
9854
10084
  init_logger();
9855
10085
  init_remoteManagement();
9856
10086
  init_types();
10087
+ init_configStore();
10088
+ init_configDir();
10089
+ init_remote_schema();
9857
10090
  async function verifyAndLoad() {
9858
10091
  if (process.platform === "win32") {
9859
10092
  const vcRedist = checkVCRedist();
@@ -9873,15 +10106,39 @@ verifyAndLoad().catch((err) => {
9873
10106
  // Annotate the CommonJS export names for ESM import in node:
9874
10107
  0 && (module.exports = {
9875
10108
  RemoteManagementUnauthorizedError,
10109
+ TUNNEL_CONFIG_V1_KEYS,
9876
10110
  TunnelClient,
10111
+ TunnelConfigV1Schema,
9877
10112
  TunnelErrorCodeType,
9878
10113
  TunnelManager,
9879
10114
  TunnelOperations,
9880
10115
  TunnelStateType,
9881
10116
  TunnelWarningCode,
10117
+ bulkReplace,
9882
10118
  closeRemoteManagement,
10119
+ configureStorageLogger,
10120
+ deleteConfig,
9883
10121
  enablePackageLogging,
10122
+ ensureTunnelConfigDir,
10123
+ findConfig,
10124
+ findConfigByName,
10125
+ getAutoStartConfigs,
10126
+ getDaemonInfo,
10127
+ getDaemonInfoPath,
10128
+ getPinggyConfigDir,
9884
10129
  getRemoteManagementState,
10130
+ getTunnelConfigDir,
9885
10131
  initiateRemoteManagement,
9886
- startRemoteManagement
10132
+ isDaemonRunning,
10133
+ listSavedConfigs,
10134
+ loadConfigByName,
10135
+ sanitizeName,
10136
+ saveConfig,
10137
+ startRemoteManagement,
10138
+ updateConfigAutoStart,
10139
+ updateTunnelConfig,
10140
+ upsertConfig,
10141
+ validateName,
10142
+ validateNameForStorage,
10143
+ validateNameStrict
9887
10144
  });