@qpjoy/tunnel-cli 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -45,7 +45,9 @@ The command:
45
45
  - downloads the HDO manifest from `electron-server`
46
46
  - writes a local WireGuard config
47
47
  - stores local HDO state and refresh credentials
48
- - starts a system-level tunnel at boot
48
+ - starts a system-level tunnel at boot. On Linux, if `wg-quick@.service` is not
49
+ installed by the OS, the CLI writes a compatible systemd unit that uses the
50
+ bundled WireGuard tools from the npm package.
49
51
 
50
52
  Useful follow-up commands:
51
53
 
@@ -55,6 +57,13 @@ qp-tunnel-cli hdo refresh
55
57
  qp-tunnel-cli hdo down
56
58
  ```
57
59
 
60
+ If a tunnel was created by the Electron HDO plugin, its default interface is
61
+ `hdo-client`, so stop it with:
62
+
63
+ ```bash
64
+ qp-tunnel-cli hdo down --interface hdo-client
65
+ ```
66
+
58
67
  Platform behavior:
59
68
 
60
69
  - Linux: writes `/etc/wireguard/hdo-internal.conf` and enables `wg-quick@hdo-internal`
package/README.setup.md CHANGED
@@ -13,4 +13,7 @@ sudo qp-tunnel-cli server-on
13
13
  sudo qp-tunnel-cli status
14
14
 
15
15
  qp-tunnel-cli curl google.com
16
+
17
+ # 删除mac的HDO进程
18
+ qp-tunnel-cli hdo down --interface hdo-client
16
19
  ```
package/dist/hdo.js CHANGED
@@ -23,10 +23,10 @@ async function runHdoCli(args, ctx) {
23
23
  await enrollCommand(rest, command === 'refresh');
24
24
  return;
25
25
  case 'status':
26
- statusCommand(parseStateFileArg(rest));
26
+ statusCommand(parseCommandOptions(rest));
27
27
  return;
28
28
  case 'down':
29
- await downCommand(parseStateFileArg(rest));
29
+ await downCommand(parseCommandOptions(rest));
30
30
  return;
31
31
  default:
32
32
  process.stderr.write(`Unknown hdo command: ${command}\n\n`);
@@ -41,8 +41,8 @@ Usage:
41
41
  qp-tunnel-cli hdo enroll --server-url URL --username USER [options]
42
42
  qp-tunnel-cli hdo enroll --server-url URL --token TOKEN [options]
43
43
  qp-tunnel-cli hdo refresh [--server-url URL] [--username USER]
44
- qp-tunnel-cli hdo status
45
- qp-tunnel-cli hdo down
44
+ qp-tunnel-cli hdo status [--interface NAME]
45
+ qp-tunnel-cli hdo down [--interface NAME]
46
46
 
47
47
  Enroll options:
48
48
  --server-url URL HDO/electron-server base URL
@@ -78,8 +78,14 @@ Examples:
78
78
 
79
79
  Notes:
80
80
  Linux writes /etc/wireguard and enables wg-quick@<interface>.
81
+ If Linux does not provide wg-quick@.service, this CLI installs a compatible
82
+ systemd unit that uses the bundled WireGuard tools from npm.
81
83
  macOS installs a LaunchDaemon and may prompt for an administrator password.
82
84
  Windows installs a WireGuard tunnel service and may show a UAC prompt.
85
+
86
+ Tip:
87
+ If the Electron HDO plugin created the tunnel, stop it with:
88
+ qp-tunnel-cli hdo down --interface hdo-client
83
89
  `);
84
90
  }
85
91
  async function enrollCommand(args, refreshOnly) {
@@ -172,51 +178,72 @@ async function enrollCommand(args, refreshOnly) {
172
178
  '',
173
179
  ].join('\n'));
174
180
  }
175
- function statusCommand(stateFileInput) {
176
- const stateFile = resolveStateFile(stateFileInput);
181
+ function statusCommand(input) {
182
+ const stateFile = resolveStateFile(input.stateFile);
177
183
  const state = readState(stateFile);
178
- const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
179
- const configPath = resolveConfigPath(state.configPath, interfaceName);
180
- const installDir = resolveInstallDir(state.installDir);
184
+ const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
185
+ const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
186
+ const installDir = resolveInstallDir(input.installDir || state.installDir);
187
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
188
+ installDir,
189
+ allowSystemFallback: true,
190
+ });
181
191
  process.stdout.write(`State file: ${stateFile}\n`);
182
192
  process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
183
193
  process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
184
194
  process.stdout.write(`Overlay IP: ${state.overlayIp || 'unset'}\n`);
185
195
  process.stdout.write(`WireGuard config: ${configPath}\n\n`);
186
196
  if (process.platform === 'linux') {
187
- inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
188
- process.stdout.write('\n');
189
- inherit('wg', ['show', interfaceName]);
197
+ if (commandAvailable('systemctl')) {
198
+ inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
199
+ process.stdout.write('\n');
200
+ }
201
+ else {
202
+ process.stdout.write('systemctl unavailable; showing WireGuard runtime status only.\n\n');
203
+ }
204
+ printWireGuardRuntimeStatus(runtime, configPath);
190
205
  return;
191
206
  }
192
- const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
193
- installDir,
194
- allowSystemFallback: true,
195
- });
196
207
  if (process.platform === 'darwin') {
197
208
  const daemon = (0, electron_core_wireguard_1.getDarwinWireGuardLaunchDaemonStatus)({ runtime, configPath });
198
209
  process.stdout.write(`LaunchDaemon: ${daemon.loaded ? 'loaded' : 'not loaded'}; running=${daemon.running}\n`);
199
210
  if (daemon.plistPath)
200
211
  process.stdout.write(`plist: ${daemon.plistPath}\n`);
201
212
  }
202
- const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
203
- process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
204
- process.stdout.write(`Runtime: ${runtime.method}\n`);
205
- if (tunnel.realInterfaceName)
206
- process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
207
- if (tunnel.peers.length)
208
- process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
209
- if (tunnel.error)
210
- process.stdout.write(`Status detail: ${tunnel.error}\n`);
211
- }
212
- async function downCommand(stateFileInput) {
213
- const stateFile = resolveStateFile(stateFileInput);
213
+ try {
214
+ const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
215
+ process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
216
+ process.stdout.write(`Runtime: ${runtime.method}\n`);
217
+ if (tunnel.realInterfaceName)
218
+ process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
219
+ if (tunnel.peers.length)
220
+ process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
221
+ if (tunnel.error)
222
+ process.stdout.write(`Status detail: ${tunnel.error}\n`);
223
+ }
224
+ catch (err) {
225
+ process.stdout.write(`WireGuard status detail: ${errorMessage(err)}\n`);
226
+ }
227
+ }
228
+ async function downCommand(input) {
229
+ const stateFile = resolveStateFile(input.stateFile);
214
230
  const state = readState(stateFile);
215
- const interfaceName = sanitizeInterfaceName(state.interfaceName || defaultInterfaceName);
216
- const configPath = resolveConfigPath(state.configPath, interfaceName);
217
- const installDir = resolveInstallDir(state.installDir);
231
+ const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
232
+ const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
233
+ const installDir = resolveInstallDir(input.installDir || state.installDir);
218
234
  if (process.platform === 'linux') {
219
- inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
235
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
236
+ installDir,
237
+ allowSystemFallback: true,
238
+ });
239
+ if (commandAvailable('systemctl') && systemdUnitExists('wg-quick@.service')) {
240
+ inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
241
+ return;
242
+ }
243
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'down' });
244
+ if (!result.ok)
245
+ throw new Error(result.message);
246
+ process.stdout.write(`${result.message}\n`);
220
247
  return;
221
248
  }
222
249
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
@@ -224,6 +251,10 @@ async function downCommand(stateFileInput) {
224
251
  allowSystemFallback: true,
225
252
  });
226
253
  if (process.platform === 'darwin') {
254
+ if (!canReadFile(configPath)) {
255
+ uninstallDarwinLaunchDaemonByInterface(interfaceName);
256
+ return;
257
+ }
227
258
  const result = await (0, electron_core_wireguard_1.uninstallDarwinWireGuardLaunchDaemon)({ runtime, configPath });
228
259
  if (!result.ok)
229
260
  throw new Error(result.message);
@@ -304,22 +335,35 @@ function parseEnrollOptions(args) {
304
335
  }
305
336
  return options;
306
337
  }
307
- function parseStateFileArg(args) {
308
- let stateFile;
338
+ function parseCommandOptions(args) {
339
+ const options = {};
309
340
  for (let index = 0; index < args.length; index += 1) {
310
341
  const arg = args[index];
311
- if (arg === '--state-file') {
342
+ const readValue = () => {
312
343
  const value = args[index + 1];
313
344
  if (!value)
314
- throw new Error('Missing value for --state-file');
315
- stateFile = value;
345
+ throw new Error(`Missing value for ${arg}`);
316
346
  index += 1;
317
- }
318
- else {
319
- throw new Error(`Unknown hdo option: ${arg}`);
347
+ return value;
348
+ };
349
+ switch (arg) {
350
+ case '--state-file':
351
+ options.stateFile = readValue();
352
+ break;
353
+ case '--interface':
354
+ options.interfaceName = readValue();
355
+ break;
356
+ case '--config-path':
357
+ options.configPath = readValue();
358
+ break;
359
+ case '--install-dir':
360
+ options.installDir = readValue();
361
+ break;
362
+ default:
363
+ throw new Error(`Unknown hdo option: ${arg}`);
320
364
  }
321
365
  }
322
- return stateFile;
366
+ return options;
323
367
  }
324
368
  async function resolveAuth(serverUrl, options, previous, username) {
325
369
  const token = resolveExplicitToken(options);
@@ -425,6 +469,15 @@ function resolveKeypair(options, previous, installDir) {
425
469
  }
426
470
  async function startSystemTunnel(interfaceName, configPath, installDir) {
427
471
  if (process.platform === 'linux') {
472
+ const runtime = await ensureLinuxWireGuardRuntime(installDir);
473
+ if (!commandAvailable('systemctl')) {
474
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'restart' });
475
+ if (!result.ok)
476
+ throw new Error(result.message);
477
+ return `${result.message} systemctl is unavailable, so this tunnel is not installed as a boot service.`;
478
+ }
479
+ ensureLinuxWgQuickSystemdUnit(runtime);
480
+ inheritRequired('systemctl', ['daemon-reload']);
428
481
  inheritRequired('systemctl', ['enable', `wg-quick@${interfaceName}`]);
429
482
  inheritRequired('systemctl', ['restart', `wg-quick@${interfaceName}`]);
430
483
  return `Enabled and restarted wg-quick@${interfaceName}.`;
@@ -524,7 +577,12 @@ function resolveStateFile(input) {
524
577
  return (0, node_path_1.resolve)(input || defaultStateFile());
525
578
  }
526
579
  function resolveConfigPath(input, interfaceName) {
527
- return (0, node_path_1.resolve)(input || defaultConfigPath(interfaceName));
580
+ if (input)
581
+ return (0, node_path_1.resolve)(input);
582
+ const launchDaemonConfig = darwinLaunchDaemonConfigPath(interfaceName);
583
+ if (launchDaemonConfig && (0, node_fs_1.existsSync)(launchDaemonConfig))
584
+ return launchDaemonConfig;
585
+ return (0, node_path_1.resolve)(defaultConfigPath(interfaceName));
528
586
  }
529
587
  function resolveInstallDir(input) {
530
588
  return (0, node_path_1.resolve)(input || defaultInstallDir());
@@ -543,6 +601,11 @@ function defaultConfigPath(interfaceName) {
543
601
  return (0, node_path_1.join)(windowsUserDataDir(), `${interfaceName}.conf`);
544
602
  return (0, node_path_1.join)((0, node_os_1.homedir)(), '.qpjoy', 'hdo', `${interfaceName}.conf`);
545
603
  }
604
+ function darwinLaunchDaemonConfigPath(interfaceName) {
605
+ if (process.platform !== 'darwin')
606
+ return null;
607
+ return `/Library/Application Support/QPJoy/HDO/${interfaceName}/${interfaceName}.conf`;
608
+ }
546
609
  function defaultInstallDir() {
547
610
  if (process.platform === 'linux')
548
611
  return '/usr/local/lib/qpjoy/hdo/bin';
@@ -560,6 +623,164 @@ function inheritRequired(command, args) {
560
623
  const result = (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
561
624
  assertSpawnOk(command, args, result);
562
625
  }
626
+ function printWireGuardRuntimeStatus(runtime, configPath) {
627
+ process.stdout.write(`Runtime: ${runtime.method}\n`);
628
+ if (runtime.warnings.length) {
629
+ process.stdout.write(`Runtime warnings: ${runtime.warnings.join('; ')}\n`);
630
+ }
631
+ try {
632
+ const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
633
+ process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
634
+ if (tunnel.realInterfaceName)
635
+ process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
636
+ if (tunnel.peers.length)
637
+ process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
638
+ if (tunnel.error)
639
+ process.stdout.write(`Status detail: ${tunnel.error}\n`);
640
+ }
641
+ catch (err) {
642
+ process.stdout.write(`WireGuard status detail: ${errorMessage(err)}\n`);
643
+ }
644
+ }
645
+ async function ensureLinuxWireGuardRuntime(installDir) {
646
+ let runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
647
+ installDir,
648
+ allowSystemFallback: true,
649
+ });
650
+ if (runtime.available)
651
+ return runtime;
652
+ const installed = installLinuxWireGuardTools();
653
+ runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
654
+ installDir,
655
+ allowSystemFallback: true,
656
+ });
657
+ if (runtime.available)
658
+ return runtime;
659
+ throw new Error(installed
660
+ ? runtime.error ?? 'WireGuard runtime unavailable after installing wireguard-tools.'
661
+ : `${runtime.error ?? 'WireGuard runtime unavailable'}. Install wireguard-tools or use an npm package with the matching @qpjoy/electron-core-wireguard-engine package.`);
662
+ }
663
+ function installLinuxWireGuardTools() {
664
+ const installers = [
665
+ { probe: 'apt-get', label: 'apt-get', commands: [['apt-get', 'update'], ['apt-get', 'install', '-y', 'wireguard-tools']] },
666
+ { probe: 'dnf', label: 'dnf', commands: [['dnf', 'install', '-y', 'wireguard-tools']] },
667
+ { probe: 'yum', label: 'yum', commands: [['yum', 'install', '-y', 'epel-release'], ['yum', 'install', '-y', 'wireguard-tools']] },
668
+ { probe: 'apk', label: 'apk', commands: [['apk', 'add', '--no-cache', 'wireguard-tools']] },
669
+ { probe: 'zypper', label: 'zypper', commands: [['zypper', '--non-interactive', 'install', 'wireguard-tools']] },
670
+ { probe: 'pacman', label: 'pacman', commands: [['pacman', '-Sy', '--noconfirm', 'wireguard-tools']] },
671
+ ];
672
+ for (const installer of installers) {
673
+ if (!commandAvailable(installer.probe))
674
+ continue;
675
+ process.stdout.write(`WireGuard tools are missing; installing wireguard-tools with ${installer.label}.\n`);
676
+ for (const command of installer.commands) {
677
+ const [name, ...args] = command;
678
+ const result = (0, node_child_process_1.spawnSync)(name, args, { stdio: 'inherit' });
679
+ if (result.status !== 0) {
680
+ process.stdout.write(`wireguard-tools install step failed: ${command.join(' ')}\n`);
681
+ return false;
682
+ }
683
+ }
684
+ return true;
685
+ }
686
+ return false;
687
+ }
688
+ function ensureLinuxWgQuickSystemdUnit(runtime) {
689
+ if (systemdUnitUsable())
690
+ return;
691
+ const wgQuick = runtime.wgQuick?.command;
692
+ if (!wgQuick) {
693
+ throw new Error(runtime.error ?? 'wg-quick unavailable; cannot install systemd boot service.');
694
+ }
695
+ const unitPath = '/etc/systemd/system/wg-quick@.service';
696
+ if ((0, node_fs_1.existsSync)(unitPath)) {
697
+ throw new Error(`${unitPath} exists but wg/wg-quick is not available in PATH. Install wireguard-tools or fix the existing unit.`);
698
+ }
699
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(unitPath), { recursive: true });
700
+ (0, node_fs_1.writeFileSync)(unitPath, renderLinuxWgQuickSystemdUnit(runtime, wgQuick), { mode: 0o644 });
701
+ chmodSyncSafe(unitPath, 0o644);
702
+ process.stdout.write(`Installed ${unitPath} using bundled WireGuard tools.\n`);
703
+ }
704
+ function renderLinuxWgQuickSystemdUnit(runtime, wgQuick) {
705
+ const pathDirs = uniqueStrings([
706
+ runtime.wg.command ? (0, node_path_1.dirname)(runtime.wg.command) : '',
707
+ runtime.wgQuick?.command ? (0, node_path_1.dirname)(runtime.wgQuick.command) : '',
708
+ runtime.wireGuardGo?.command ? (0, node_path_1.dirname)(runtime.wireGuardGo.command) : '',
709
+ '/usr/local/sbin',
710
+ '/usr/local/bin',
711
+ '/usr/sbin',
712
+ '/usr/bin',
713
+ '/sbin',
714
+ '/bin',
715
+ ].filter(Boolean));
716
+ return `[Unit]
717
+ Description=WireGuard via wg-quick(8) for %I
718
+ Documentation=man:wg-quick(8) man:wg(8)
719
+ Wants=network-online.target
720
+ After=network-online.target nss-lookup.target
721
+ ConditionPathExists=/etc/wireguard/%i.conf
722
+
723
+ [Service]
724
+ Type=oneshot
725
+ RemainAfterExit=yes
726
+ Environment=${systemdQuote(`PATH=${pathDirs.join(':')}`)}
727
+ Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity
728
+ ExecStart=${systemdQuote(wgQuick)} up %i
729
+ ExecStop=${systemdQuote(wgQuick)} down %i
730
+
731
+ [Install]
732
+ WantedBy=multi-user.target
733
+ `;
734
+ }
735
+ function systemdUnitUsable() {
736
+ return systemdUnitExists('wg-quick@.service') && commandAvailable('wg') && commandAvailable('wg-quick');
737
+ }
738
+ function systemdUnitExists(unitName) {
739
+ const paths = [
740
+ `/etc/systemd/system/${unitName}`,
741
+ `/run/systemd/system/${unitName}`,
742
+ `/lib/systemd/system/${unitName}`,
743
+ `/usr/lib/systemd/system/${unitName}`,
744
+ ];
745
+ if (paths.some((path) => (0, node_fs_1.existsSync)(path)))
746
+ return true;
747
+ if (!commandAvailable('systemctl'))
748
+ return false;
749
+ const result = (0, node_child_process_1.spawnSync)('systemctl', ['cat', unitName], { stdio: 'ignore' });
750
+ return result.status === 0;
751
+ }
752
+ function commandAvailable(command) {
753
+ const result = (0, node_child_process_1.spawnSync)('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command], {
754
+ stdio: 'ignore',
755
+ });
756
+ return result.status === 0;
757
+ }
758
+ function uninstallDarwinLaunchDaemonByInterface(interfaceName) {
759
+ const component = sanitizeLaunchDaemonComponent(interfaceName);
760
+ const label = `com.qpjoy.hdo.wireguard.${component}`;
761
+ const plist = `/Library/LaunchDaemons/${label}.plist`;
762
+ const supportDir = `/Library/Application Support/QPJoy/HDO/${component}`;
763
+ const script = [
764
+ 'set -e',
765
+ `LABEL=${shellQuote(label)}`,
766
+ `PLIST=${shellQuote(plist)}`,
767
+ `SUPPORT_DIR=${shellQuote(supportDir)}`,
768
+ `PID_FILE=${shellQuote(`/var/run/wireguard/${interfaceName}.pid`)}`,
769
+ `NAME_FILE=${shellQuote(`/var/run/wireguard/${interfaceName}.name`)}`,
770
+ `WIREGUARD_GO=${shellQuote(`${supportDir}/bin/wireguard-go`)}`,
771
+ 'launchctl bootout "system/$LABEL" >/dev/null 2>&1 || launchctl bootout system "$PLIST" >/dev/null 2>&1 || true',
772
+ 'if [ -s "$PID_FILE" ]; then WG_PID="$(cat "$PID_FILE" 2>/dev/null || true)"; if [ -n "$WG_PID" ]; then kill "$WG_PID" >/dev/null 2>&1 || true; sleep 0.2; kill -9 "$WG_PID" >/dev/null 2>&1 || true; fi; fi',
773
+ 'if command -v pgrep >/dev/null 2>&1; then for stale_pid in $(pgrep -x wireguard-go 2>/dev/null || true); do stale_command="$(ps -p "$stale_pid" -o command= 2>/dev/null || true)"; printf "%s\\n" "$stale_command" | grep -F "$WIREGUARD_GO" >/dev/null 2>&1 && kill "$stale_pid" >/dev/null 2>&1 || true; done; fi',
774
+ 'rm -f "$PLIST" "$PID_FILE" "$NAME_FILE"',
775
+ 'rm -rf "$SUPPORT_DIR"'
776
+ ].join('\n');
777
+ const appleScript = `do shell script ${appleScriptString(script)} with administrator privileges`;
778
+ const result = (0, node_child_process_1.spawnSync)('osascript', ['-e', appleScript], {
779
+ encoding: 'utf8'
780
+ });
781
+ assertSpawnOk('osascript', ['-e', '<uninstall-hdo-launchdaemon>'], result);
782
+ process.stdout.write(`Stopped and removed ${label}.\n`);
783
+ }
563
784
  function assertSpawnOk(command, args, result) {
564
785
  if (result.error) {
565
786
  throw new Error(`${command} failed: ${result.error.message}`);
@@ -594,11 +815,35 @@ function tokenExpired(value) {
594
815
  const time = Date.parse(value);
595
816
  return Number.isFinite(time) && Date.now() > time - 60_000;
596
817
  }
818
+ function canReadFile(path) {
819
+ try {
820
+ (0, node_fs_1.readFileSync)(path, 'utf8');
821
+ return true;
822
+ }
823
+ catch {
824
+ return false;
825
+ }
826
+ }
597
827
  function chmodSyncSafe(path, mode) {
598
828
  if (process.platform === 'win32')
599
829
  return;
600
830
  (0, node_fs_1.chmodSync)(path, mode);
601
831
  }
832
+ function sanitizeLaunchDaemonComponent(value) {
833
+ return value.replace(/[^A-Za-z0-9.-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'hdo-client';
834
+ }
835
+ function shellQuote(value) {
836
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
837
+ }
838
+ function systemdQuote(value) {
839
+ return `"${String(value).replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
840
+ }
841
+ function appleScriptString(value) {
842
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
843
+ }
844
+ function errorMessage(err) {
845
+ return err instanceof Error ? err.message : String(err);
846
+ }
602
847
  function requireRecord(value, label) {
603
848
  if (!isRecord(value))
604
849
  throw new Error(`${label} is not an object.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
5
5
  "private": false,
6
6
  "type": "commonjs",