neko-harness 0.4.0 → 0.4.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.
Files changed (68) hide show
  1. package/README.md +1 -0
  2. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/client/assets/index-C4UO2YJF.js +391 -0
  62. package/dist/client/assets/index-DPSmVlrG.css +2 -0
  63. package/dist/client/index.html +2 -2
  64. package/dist/gateway.js +668 -103
  65. package/dist/neko.js +833 -157
  66. package/package.json +1 -1
  67. package/dist/client/assets/index-0Q-trLXg.js +0 -135
  68. package/dist/client/assets/index-BYawicLw.css +0 -2
package/dist/neko.js CHANGED
@@ -80,7 +80,7 @@ function updateAction(input) {
80
80
  return "install";
81
81
  return "none";
82
82
  }
83
- var RUNNING_VERSION = "0.4.0", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
83
+ var RUNNING_VERSION = "0.4.2", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
84
84
  var init_application_version = __esm(() => {
85
85
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
86
86
  constructor() {
@@ -20276,7 +20276,9 @@ var init_admin = __esm(() => {
20276
20276
  Type6.Object({
20277
20277
  status: Type6.Literal("listening"),
20278
20278
  address: Type6.String(),
20279
- port: Type6.Integer({ minimum: 1, maximum: 65535 })
20279
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20280
+ bindAddress: Type6.Optional(Type6.String()),
20281
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
20280
20282
  }),
20281
20283
  Type6.Object({ status: Type6.Literal("error"), error: Type6.String() }),
20282
20284
  Type6.Object({ status: Type6.Literal("stopping") })
@@ -20305,7 +20307,11 @@ var init_admin = __esm(() => {
20305
20307
  Type6.Null()
20306
20308
  ]),
20307
20309
  devices: Type6.Array(adminDeviceSchema),
20308
- lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()])
20310
+ lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
20311
+ wsl: Type6.Optional(Type6.Union([
20312
+ Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
20313
+ Type6.Null()
20314
+ ]))
20309
20315
  })
20310
20316
  ],
20311
20317
  enable: [
@@ -20316,7 +20322,9 @@ var init_admin = __esm(() => {
20316
20322
  Type6.Object({
20317
20323
  status: Type6.Literal("listening"),
20318
20324
  address: Type6.String(),
20319
- port: Type6.Integer({ minimum: 1, maximum: 65535 })
20325
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20326
+ bindAddress: Type6.Optional(Type6.String()),
20327
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
20320
20328
  })
20321
20329
  ],
20322
20330
  disable: [Type6.Null(), listenerStateSchema],
@@ -38553,6 +38561,73 @@ var init_certificate = __esm(() => {
38553
38561
  CERT_VALIDITY_MS = 825 * 24 * 60 * 60 * 1000;
38554
38562
  });
38555
38563
 
38564
+ // packages/gateway/src/mobile/network.ts
38565
+ import { networkInterfaces } from "os";
38566
+ function isVirtualInterfaceName(name) {
38567
+ const lower = name.toLowerCase();
38568
+ if (VIRTUAL_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix)))
38569
+ return true;
38570
+ return VIRTUAL_INTERFACE_KEYWORDS.some((keyword) => lower.includes(keyword));
38571
+ }
38572
+ function isBenchmarkAddress(address) {
38573
+ return address.startsWith("198.18.") || address.startsWith("198.19.");
38574
+ }
38575
+ function collectListenCandidates(interfaces, skipVirtual) {
38576
+ const candidates = [];
38577
+ for (const [name, infos] of Object.entries(interfaces)) {
38578
+ if (infos === undefined)
38579
+ continue;
38580
+ if (skipVirtual && isVirtualInterfaceName(name))
38581
+ continue;
38582
+ for (const info of infos) {
38583
+ if (info.family !== "IPv4" || info.internal)
38584
+ continue;
38585
+ if (info.address.startsWith("169.254."))
38586
+ continue;
38587
+ if (skipVirtual && isBenchmarkAddress(info.address))
38588
+ continue;
38589
+ candidates.push(info.address);
38590
+ }
38591
+ }
38592
+ return candidates.sort();
38593
+ }
38594
+ function mobileListenCandidates(interfaces) {
38595
+ const source = interfaces ?? networkInterfaces();
38596
+ const filtered = collectListenCandidates(source, true);
38597
+ return filtered.length > 0 ? filtered : collectListenCandidates(source, false);
38598
+ }
38599
+ function prioritizeListenCandidate(candidates, lastConnectedAddress) {
38600
+ if (lastConnectedAddress === null || !candidates.includes(lastConnectedAddress))
38601
+ return [...candidates];
38602
+ return [lastConnectedAddress, ...candidates.filter((candidate) => candidate !== lastConnectedAddress)];
38603
+ }
38604
+ function isIpv4Literal(address) {
38605
+ const parts = address.split(".");
38606
+ return parts.length === 4 && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
38607
+ }
38608
+ var VIRTUAL_INTERFACE_PREFIXES, VIRTUAL_INTERFACE_KEYWORDS;
38609
+ var init_network = __esm(() => {
38610
+ VIRTUAL_INTERFACE_PREFIXES = [
38611
+ "anpi",
38612
+ "awdl",
38613
+ "bridge",
38614
+ "docker",
38615
+ "gif",
38616
+ "ipsec",
38617
+ "llw",
38618
+ "ppp",
38619
+ "stf",
38620
+ "tap",
38621
+ "tun",
38622
+ "utun",
38623
+ "vboxnet",
38624
+ "veth",
38625
+ "virbr",
38626
+ "vmnet"
38627
+ ];
38628
+ VIRTUAL_INTERFACE_KEYWORDS = ["hyper-v", "virtual", "virtualbox", "vmware", "vpn", "wsl"];
38629
+ });
38630
+
38556
38631
  // packages/gateway/src/mobile/pairing.ts
38557
38632
  import { createHash as createHash5, randomBytes as randomBytes3 } from "crypto";
38558
38633
  function sha256Hex2(value) {
@@ -38745,57 +38820,379 @@ var init_pairing = __esm(() => {
38745
38820
  BASE64URL_32_BYTES = /^[A-Za-z0-9_-]{43}$/;
38746
38821
  });
38747
38822
 
38748
- // packages/gateway/src/mobile/listener.ts
38749
- import { randomUUID as randomUUID2 } from "crypto";
38750
- import { createServer as createServer2 } from "https";
38751
- import { hostname, networkInterfaces } from "os";
38752
- import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
38753
- import { WebSocketServer as WebSocketServer2 } from "ws";
38754
- function defaultServerName() {
38755
- const name = hostname().trim();
38756
- return (name === "" ? "neko-computer" : name).slice(0, 64);
38823
+ // packages/gateway/src/mobile/relay.ts
38824
+ import { spawn as nodeSpawn } from "child_process";
38825
+ function assertPort2(port, label) {
38826
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
38827
+ throw new Error(`relay ${label} port is invalid`);
38757
38828
  }
38758
- function isVirtualInterfaceName(name) {
38759
- const lower = name.toLowerCase();
38760
- if (VIRTUAL_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix)))
38761
- return true;
38762
- return VIRTUAL_INTERFACE_KEYWORDS.some((keyword) => lower.includes(keyword));
38829
+ function buildRelayScript(endpoints, watchdogMs = RELAY_WATCHDOG_MS) {
38830
+ if (!isIpv4Literal(endpoints.listenAddress) || !isIpv4Literal(endpoints.targetAddress)) {
38831
+ throw new Error("relay addresses must be IPv4 literals");
38832
+ }
38833
+ assertPort2(endpoints.listenPort, "listen");
38834
+ assertPort2(endpoints.targetPort, "target");
38835
+ if (!Number.isInteger(watchdogMs) || watchdogMs < 1000)
38836
+ throw new Error("relay watchdog is invalid");
38837
+ return [
38838
+ "$ErrorActionPreference='Stop'",
38839
+ `$src=@'
38840
+ ${RELAY_SOURCE}
38841
+ '@`,
38842
+ "Add-Type -TypeDefinition $src -Language CSharp",
38843
+ `[NekoRelay]::Run('${endpoints.listenAddress}',${endpoints.listenPort},'${endpoints.targetAddress}',${endpoints.targetPort},${watchdogMs})`
38844
+ ].join(`
38845
+ `);
38763
38846
  }
38764
- function isBenchmarkAddress(address) {
38765
- return address.startsWith("198.18.") || address.startsWith("198.19.");
38847
+ function encodeRelayCommand(script) {
38848
+ return Buffer.from(script, "utf16le").toString("base64");
38766
38849
  }
38767
- function collectListenCandidates(interfaces, skipVirtual) {
38768
- const candidates = [];
38769
- for (const [name, infos] of Object.entries(interfaces)) {
38770
- if (infos === undefined)
38771
- continue;
38772
- if (skipVirtual && isVirtualInterfaceName(name))
38850
+ function describeExit(child) {
38851
+ return child.signalCode !== null ? `signal ${child.signalCode}` : `exit code ${child.exitCode}`;
38852
+ }
38853
+ async function startWindowsRelay(options) {
38854
+ const watchdogMs = options.watchdogMs ?? RELAY_WATCHDOG_MS;
38855
+ const script = buildRelayScript(options, watchdogMs);
38856
+ const spawn = options.spawn ?? nodeSpawn;
38857
+ const child = spawn(options.powershell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodeRelayCommand(script)], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
38858
+ let stderrTail = "";
38859
+ child.stderr?.on("data", (chunk) => {
38860
+ stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_TAIL_BYTES);
38861
+ });
38862
+ child.stdin?.on("error", () => {});
38863
+ const exited = new Promise((resolve) => {
38864
+ child.once("error", (error) => resolve(`spawn error: ${error.message}`));
38865
+ child.once("exit", () => resolve(describeExit(child)));
38866
+ });
38867
+ let heartbeat;
38868
+ let stopping;
38869
+ const stop = () => {
38870
+ stopping ??= (async () => {
38871
+ clearInterval(heartbeat);
38872
+ child.stdin?.end();
38873
+ const grace = options.stopGraceMs ?? STOP_GRACE_MS;
38874
+ const done = await Promise.race([exited.then(() => true), delay2(grace).then(() => false)]);
38875
+ if (!done) {
38876
+ child.kill("SIGKILL");
38877
+ await Promise.race([exited, delay2(grace)]);
38878
+ }
38879
+ })();
38880
+ return stopping;
38881
+ };
38882
+ const ready = new Promise((resolve, reject) => {
38883
+ let buffer = "";
38884
+ const timer = setTimeout(() => reject(new RelayError("timeout", "Windows relay did not become ready in time")), options.startTimeoutMs ?? START_TIMEOUT_MS);
38885
+ const finish = (action) => {
38886
+ clearTimeout(timer);
38887
+ action();
38888
+ };
38889
+ child.stdout?.on("data", (chunk) => {
38890
+ buffer += chunk.toString("utf8");
38891
+ for (;; ) {
38892
+ const newline = buffer.indexOf(`
38893
+ `);
38894
+ if (newline < 0)
38895
+ break;
38896
+ const line = buffer.slice(0, newline).trim();
38897
+ buffer = buffer.slice(newline + 1);
38898
+ if (line === "READY")
38899
+ finish(resolve);
38900
+ else if (line.startsWith("ERROR ")) {
38901
+ const [, code = "start_failed", ...rest] = line.split(" ");
38902
+ const known = ["port_in_use", "listen_failed", "target_unreachable"];
38903
+ const mapped = known.find((item) => item === code) ?? "start_failed";
38904
+ finish(() => reject(new RelayError(mapped, rest.join(" ") || code)));
38905
+ }
38906
+ }
38907
+ });
38908
+ exited.then((reason) => {
38909
+ const detail = stderrTail.trim();
38910
+ finish(() => reject(new RelayError("start_failed", `Windows relay exited before it was ready (${reason})${detail === "" ? "" : `: ${detail}`}`)));
38911
+ });
38912
+ });
38913
+ try {
38914
+ await ready;
38915
+ } catch (error) {
38916
+ await stop();
38917
+ throw error;
38918
+ }
38919
+ heartbeat = setInterval(() => {
38920
+ child.stdin?.write(`
38921
+ `);
38922
+ }, options.heartbeatMs ?? RELAY_HEARTBEAT_INTERVAL_MS);
38923
+ heartbeat.unref?.();
38924
+ return { exited, stop };
38925
+ }
38926
+ function delay2(ms) {
38927
+ return new Promise((resolve) => setTimeout(resolve, ms));
38928
+ }
38929
+ var RelayError, RELAY_HEARTBEAT_INTERVAL_MS = 2000, RELAY_WATCHDOG_MS = 8000, START_TIMEOUT_MS = 20000, STOP_GRACE_MS = 1000, STDERR_TAIL_BYTES = 2000, RELAY_SOURCE = `using System;
38930
+ using System.Diagnostics;
38931
+ using System.Net;
38932
+ using System.Net.Sockets;
38933
+ using System.Threading;
38934
+
38935
+ public static class NekoRelay
38936
+ {
38937
+ static readonly Stopwatch Clock = Stopwatch.StartNew();
38938
+ static long lastBeatMs = 0;
38939
+
38940
+ static void Touch() { Interlocked.Exchange(ref lastBeatMs, Clock.ElapsedMilliseconds); }
38941
+ static string Flat(string text) { return text.Replace("\\r", " ").Replace("\\n", " "); }
38942
+ static void Close(TcpClient client) { if (client != null) { try { client.Close(); } catch (Exception) { } } }
38943
+
38944
+ static void Copy(TcpClient from, TcpClient to)
38945
+ {
38946
+ byte[] buffer = new byte[16384];
38947
+ try
38948
+ {
38949
+ System.IO.Stream source = from.GetStream();
38950
+ System.IO.Stream sink = to.GetStream();
38951
+ int count;
38952
+ while ((count = source.Read(buffer, 0, buffer.Length)) > 0) { sink.Write(buffer, 0, count); }
38953
+ }
38954
+ catch (Exception) { }
38955
+ try { to.Client.Shutdown(SocketShutdown.Send); } catch (Exception) { }
38956
+ }
38957
+
38958
+ static void Pump(object state)
38959
+ {
38960
+ object[] args = (object[])state;
38961
+ TcpClient client = (TcpClient)args[0];
38962
+ TcpClient target = null;
38963
+ try
38964
+ {
38965
+ target = new TcpClient();
38966
+ target.NoDelay = true;
38967
+ client.NoDelay = true;
38968
+ target.Connect((string)args[1], (int)args[2]);
38969
+ Thread upstream = new Thread(delegate() { Copy(client, target); });
38970
+ upstream.IsBackground = true;
38971
+ upstream.Start();
38972
+ Copy(target, client);
38973
+ }
38974
+ catch (Exception) { }
38975
+ finally { Close(client); Close(target); }
38976
+ }
38977
+
38978
+ public static void Run(string listenIp, int listenPort, string targetIp, int targetPort, int watchdogMs)
38979
+ {
38980
+ TcpListener listener = new TcpListener(IPAddress.Parse(listenIp), listenPort);
38981
+ listener.ExclusiveAddressUse = true;
38982
+ try { listener.Start(128); }
38983
+ catch (SocketException e)
38984
+ {
38985
+ string code = (e.SocketErrorCode == SocketError.AddressAlreadyInUse || e.SocketErrorCode == SocketError.AccessDenied) ? "port_in_use" : "listen_failed";
38986
+ Console.Out.WriteLine("ERROR " + code + " " + Flat(e.Message));
38987
+ Console.Out.Flush();
38988
+ Environment.Exit(2);
38989
+ }
38990
+ try
38991
+ {
38992
+ TcpClient probe = new TcpClient();
38993
+ IAsyncResult pending = probe.BeginConnect(targetIp, targetPort, null, null);
38994
+ if (!pending.AsyncWaitHandle.WaitOne(3000)) { throw new TimeoutException("connect timed out"); }
38995
+ probe.EndConnect(pending);
38996
+ probe.Close();
38997
+ }
38998
+ catch (Exception e)
38999
+ {
39000
+ Console.Out.WriteLine("ERROR target_unreachable " + Flat(e.Message));
39001
+ Console.Out.Flush();
39002
+ Environment.Exit(3);
39003
+ }
39004
+ Touch();
39005
+ Thread input = new Thread(delegate()
39006
+ {
39007
+ try { while (Console.In.ReadLine() != null) { Touch(); } } catch (Exception) { }
39008
+ Environment.Exit(0);
39009
+ });
39010
+ input.IsBackground = true;
39011
+ input.Start();
39012
+ Thread watchdog = new Thread(delegate()
39013
+ {
39014
+ while (true)
39015
+ {
39016
+ Thread.Sleep(1000);
39017
+ if (Clock.ElapsedMilliseconds - Interlocked.Read(ref lastBeatMs) > watchdogMs) { Environment.Exit(0); }
39018
+ }
39019
+ });
39020
+ watchdog.IsBackground = true;
39021
+ watchdog.Start();
39022
+ Console.Out.WriteLine("READY");
39023
+ Console.Out.Flush();
39024
+ while (true)
39025
+ {
39026
+ TcpClient accepted;
39027
+ try { accepted = listener.AcceptTcpClient(); }
39028
+ catch (Exception) { Environment.Exit(4); return; }
39029
+ ThreadPool.QueueUserWorkItem(Pump, new object[] { accepted, targetIp, targetPort });
39030
+ }
39031
+ }
39032
+ }`;
39033
+ var init_relay = __esm(() => {
39034
+ init_network();
39035
+ RelayError = class RelayError extends Error {
39036
+ code;
39037
+ constructor(code, message) {
39038
+ super(message);
39039
+ this.name = "RelayError";
39040
+ this.code = code;
39041
+ }
39042
+ };
39043
+ });
39044
+
39045
+ // packages/gateway/src/mobile/wsl.ts
39046
+ import { execFile as execFile2 } from "child_process";
39047
+ import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync2 } from "fs";
39048
+ import { networkInterfaces as networkInterfaces2 } from "os";
39049
+ import { delimiter, join as join10 } from "path";
39050
+ function detectWsl(overrides = {}) {
39051
+ const deps = { ...defaultProbeDeps, ...overrides };
39052
+ if (deps.platform !== "linux")
39053
+ return;
39054
+ const { env } = deps;
39055
+ const version = deps.readProcVersion() ?? "";
39056
+ const inWsl = (env.WSL_DISTRO_NAME ?? "") !== "" || (env.WSL_INTEROP ?? "") !== "" || /microsoft|wsl/i.test(version);
39057
+ if (!inWsl)
39058
+ return;
39059
+ const candidates = [
39060
+ ...(env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join10(dir, "powershell.exe")),
39061
+ POWERSHELL_FALLBACK
39062
+ ];
39063
+ return { powershell: candidates.find((candidate) => deps.isExecutable(candidate)) };
39064
+ }
39065
+ function parseWindowsInterfaces(output) {
39066
+ const text = output.replace(/^\uFEFF/, "").trim();
39067
+ if (text === "")
39068
+ return [];
39069
+ const parsed = JSON.parse(text);
39070
+ const items = Array.isArray(parsed) ? parsed : [parsed];
39071
+ const result = [];
39072
+ for (const item of items) {
39073
+ if (typeof item !== "object" || item === null)
38773
39074
  continue;
38774
- for (const info of infos) {
38775
- if (info.family !== "IPv4" || info.internal)
38776
- continue;
38777
- if (info.address.startsWith("169.254."))
38778
- continue;
38779
- if (skipVirtual && isBenchmarkAddress(info.address))
38780
- continue;
38781
- candidates.push(info.address);
39075
+ const { name, address } = item;
39076
+ if (typeof name === "string" && typeof address === "string" && isIpv4Literal(address)) {
39077
+ result.push({ name, address });
38782
39078
  }
38783
39079
  }
38784
- return candidates.sort();
39080
+ return result;
38785
39081
  }
38786
- function mobileListenCandidates(interfaces) {
38787
- const source = interfaces ?? networkInterfaces();
38788
- const filtered = collectListenCandidates(source, true);
38789
- return filtered.length > 0 ? filtered : collectListenCandidates(source, false);
39082
+ function toInterfaceDict(interfaces) {
39083
+ const dict = {};
39084
+ for (const { name, address } of interfaces) {
39085
+ const list = dict[name] ?? [];
39086
+ dict[name] = list;
39087
+ list.push({
39088
+ address,
39089
+ netmask: "255.255.255.0",
39090
+ family: "IPv4",
39091
+ mac: "00:00:00:00:00:00",
39092
+ internal: false,
39093
+ cidr: `${address}/24`
39094
+ });
39095
+ }
39096
+ return dict;
38790
39097
  }
38791
- function prioritizeListenCandidate(candidates, lastConnectedAddress) {
38792
- if (lastConnectedAddress === null || !candidates.includes(lastConnectedAddress))
38793
- return [...candidates];
38794
- return [lastConnectedAddress, ...candidates.filter((candidate) => candidate !== lastConnectedAddress)];
39098
+
39099
+ class WindowsAddressDiscovery {
39100
+ powershell;
39101
+ run;
39102
+ cacheMs;
39103
+ timeoutMs;
39104
+ now;
39105
+ cached;
39106
+ pending;
39107
+ constructor(options) {
39108
+ this.powershell = options.powershell;
39109
+ this.run = options.run ?? runPowerShell;
39110
+ this.cacheMs = options.cacheMs ?? 15000;
39111
+ this.timeoutMs = options.timeoutMs ?? 1e4;
39112
+ this.now = options.now ?? Date.now;
39113
+ }
39114
+ invalidate() {
39115
+ this.cached = undefined;
39116
+ }
39117
+ candidates() {
39118
+ if (this.cached !== undefined && this.now() - this.cached.at < this.cacheMs) {
39119
+ return Promise.resolve([...this.cached.addresses]);
39120
+ }
39121
+ this.pending ??= this.read().finally(() => {
39122
+ this.pending = undefined;
39123
+ });
39124
+ return this.pending.then((addresses) => [...addresses]);
39125
+ }
39126
+ async read() {
39127
+ const output = await this.run(this.powershell, WINDOWS_ADDRESSES_SCRIPT, this.timeoutMs);
39128
+ const usable = parseWindowsInterfaces(output).filter((item) => !WSL_ADAPTER_PATTERN.test(item.name));
39129
+ const addresses = mobileListenCandidates(toInterfaceDict(usable));
39130
+ this.cached = { at: this.now(), addresses };
39131
+ return addresses;
39132
+ }
38795
39133
  }
38796
- function isIpv4Literal(address) {
38797
- const parts = address.split(".");
38798
- return parts.length === 4 && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
39134
+ function wslOwnAddresses(interfaces = networkInterfaces2()) {
39135
+ const result = [];
39136
+ for (const infos of Object.values(interfaces)) {
39137
+ for (const info of infos ?? []) {
39138
+ if (info.family === "IPv4" && !info.internal)
39139
+ result.push(info.address);
39140
+ }
39141
+ }
39142
+ return result;
39143
+ }
39144
+ function primaryWslAddress(interfaces = networkInterfaces2()) {
39145
+ const eth0 = (interfaces.eth0 ?? []).find((info) => info.family === "IPv4" && !info.internal);
39146
+ return eth0?.address ?? wslOwnAddresses(interfaces)[0];
39147
+ }
39148
+ var POWERSHELL_FALLBACK = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", defaultProbeDeps, WINDOWS_ADDRESSES_SCRIPT, WSL_ADAPTER_PATTERN, runPowerShell = (powershell, script, timeoutMs) => new Promise((resolve, reject) => {
39149
+ execFile2(powershell, ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024, encoding: "utf8" }, (error, stdout) => error ? reject(error) : resolve(stdout));
39150
+ });
39151
+ var init_wsl = __esm(() => {
39152
+ init_network();
39153
+ defaultProbeDeps = {
39154
+ platform: process.platform,
39155
+ env: process.env,
39156
+ readProcVersion() {
39157
+ try {
39158
+ return readFileSync2("/proc/version", "utf8");
39159
+ } catch {
39160
+ return;
39161
+ }
39162
+ },
39163
+ isExecutable(path) {
39164
+ try {
39165
+ accessSync2(path, constants2.X_OK);
39166
+ return true;
39167
+ } catch {
39168
+ return false;
39169
+ }
39170
+ }
39171
+ };
39172
+ WINDOWS_ADDRESSES_SCRIPT = [
39173
+ "$ErrorActionPreference='Stop'",
39174
+ "$r=@()",
39175
+ "foreach($n in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()){",
39176
+ "if($n.OperationalStatus -ne 'Up'){continue}",
39177
+ "if($n.NetworkInterfaceType -eq 'Loopback' -or $n.NetworkInterfaceType -eq 'Tunnel'){continue}",
39178
+ "foreach($a in $n.GetIPProperties().UnicastAddresses){",
39179
+ "if($a.Address.AddressFamily -eq 'InterNetwork'){$r+=[pscustomobject]@{name=$n.Name;address=$a.Address.IPAddressToString}}",
39180
+ "}}",
39181
+ "ConvertTo-Json -InputObject @($r) -Compress"
39182
+ ].join(`
39183
+ `);
39184
+ WSL_ADAPTER_PATTERN = /wsl|hyper-v|vethernet/i;
39185
+ });
39186
+
39187
+ // packages/gateway/src/mobile/listener.ts
39188
+ import { randomUUID as randomUUID2 } from "crypto";
39189
+ import { createServer as createServer2 } from "https";
39190
+ import { hostname } from "os";
39191
+ import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
39192
+ import { WebSocketServer as WebSocketServer2 } from "ws";
39193
+ function defaultServerName() {
39194
+ const name = hostname().trim();
39195
+ return (name === "" ? "neko-computer" : name).slice(0, 64);
38799
39196
  }
38800
39197
  function assertListenAddress(address) {
38801
39198
  if (!isIpv4Literal(address))
@@ -38807,6 +39204,22 @@ function assertListenAddress(address) {
38807
39204
  function toError(error) {
38808
39205
  return error instanceof Error ? error : new Error(String(error));
38809
39206
  }
39207
+ function describeRelayError(error, address, port, bindAddress) {
39208
+ if (!(error instanceof RelayError))
39209
+ return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${toMessage(error)}`;
39210
+ switch (error.code) {
39211
+ case "port_in_use":
39212
+ return `Windows \u4E0A ${address}:${port} \u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u66F4\u6362\u7AEF\u53E3\u6216\u91CA\u653E\u8BE5\u7AEF\u53E3\u540E\u91CD\u8BD5\u3002`;
39213
+ case "target_unreachable":
39214
+ return `Windows \u65E0\u6CD5\u8FDE\u63A5\u5230 WSL \u5185\u7684\u76D1\u542C\u5730\u5740 ${bindAddress}:${port}\uFF08${error.message}\uFF09\u3002`;
39215
+ case "timeout":
39216
+ return "Windows \u4E2D\u7EE7\u542F\u52A8\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
39217
+ case "listen_failed":
39218
+ return `Windows \u4E2D\u7EE7\u65E0\u6CD5\u5728 ${address}:${port} \u4E0A\u76D1\u542C\uFF1A${error.message}`;
39219
+ default:
39220
+ return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
39221
+ }
39222
+ }
38810
39223
  function isMobilePrincipal2(principal) {
38811
39224
  return typeof principal === "object" && principal !== null && principal.kind === "mobile";
38812
39225
  }
@@ -38836,7 +39249,26 @@ class MobileGateway {
38836
39249
  stateValue;
38837
39250
  queue = Promise.resolve();
38838
39251
  closing;
38839
- constructor(serverId, serverName, onError, chord, store, storeFailure, initial) {
39252
+ relay;
39253
+ wslEnvironment;
39254
+ windowsAddresses;
39255
+ startRelay;
39256
+ localInterfaces;
39257
+ relayPortRetryMs;
39258
+ relayPortRetryIntervalMs;
39259
+ constructor(serverId, serverName, onError, chord, store, storeFailure, initial, network = {}) {
39260
+ this.wslEnvironment = network.wsl === null ? undefined : network.wsl ?? detectWsl();
39261
+ const powershell = this.wslEnvironment?.powershell;
39262
+ if (network.windowsCandidates !== undefined)
39263
+ this.windowsAddresses = network.windowsCandidates;
39264
+ else if (powershell !== undefined) {
39265
+ const discovery = new WindowsAddressDiscovery({ powershell });
39266
+ this.windowsAddresses = () => discovery.candidates();
39267
+ }
39268
+ this.startRelay = network.startRelay ?? startWindowsRelay;
39269
+ this.localInterfaces = network.localInterfaces;
39270
+ this.relayPortRetryMs = network.relayPortRetryMs ?? RELAY_PORT_RETRY_MS;
39271
+ this.relayPortRetryIntervalMs = network.relayPortRetryIntervalMs ?? RELAY_PORT_RETRY_INTERVAL_MS;
38840
39272
  this.serverId = serverId;
38841
39273
  this.serverName = serverName;
38842
39274
  this.onError = onError;
@@ -38853,7 +39285,7 @@ class MobileGateway {
38853
39285
  } catch (error) {
38854
39286
  storeFailure = toMessage(error);
38855
39287
  }
38856
- const gateway = new MobileGateway(options.serverId, options.serverName ?? defaultServerName(), options.onError, options.chord, store, storeFailure, storeFailure === undefined ? { status: "disabled" } : { status: "error", error: `mobile store unavailable: ${storeFailure}` });
39288
+ const gateway = new MobileGateway(options.serverId, options.serverName ?? defaultServerName(), options.onError, options.chord, store, storeFailure, storeFailure === undefined ? { status: "disabled" } : { status: "error", error: `mobile store unavailable: ${storeFailure}` }, options.network);
38857
39289
  gateway.runtime = options.runtime;
38858
39290
  if (store !== undefined) {
38859
39291
  const config = store.getState();
@@ -38925,12 +39357,30 @@ class MobileGateway {
38925
39357
  }
38926
39358
  return false;
38927
39359
  }
38928
- adminState() {
39360
+ async adminState() {
38929
39361
  const store = this.requireStore();
38930
39362
  const config = store.getState();
39363
+ let candidates;
39364
+ let wsl = null;
39365
+ if (this.wslEnvironment === undefined) {
39366
+ candidates = mobileListenCandidates();
39367
+ } else {
39368
+ candidates = [];
39369
+ let error = null;
39370
+ if (this.windowsAddresses === undefined)
39371
+ error = WSL_INTEROP_UNAVAILABLE;
39372
+ else {
39373
+ try {
39374
+ candidates = await this.windowsAddresses();
39375
+ } catch (caught) {
39376
+ error = `\u8BFB\u53D6 Windows \u5C40\u57DF\u7F51\u5730\u5740\u5931\u8D25\uFF1A${toMessage(caught)}`;
39377
+ }
39378
+ }
39379
+ wsl = { interop: this.windowsAddresses !== undefined, error };
39380
+ }
38931
39381
  return {
38932
39382
  state: this.stateValue,
38933
- candidates: prioritizeListenCandidate(mobileListenCandidates(), config.lastConnectedAddress),
39383
+ candidates: prioritizeListenCandidate(candidates, config.lastConnectedAddress),
38934
39384
  port: config.port,
38935
39385
  certificate: this.certificate === undefined ? null : { fingerprint: this.certificate.fingerprint, expiresAt: this.certificate.expiresAt },
38936
39386
  devices: config.devices.map((device) => ({
@@ -38940,7 +39390,8 @@ class MobileGateway {
38940
39390
  lastSeenAt: device.lastSeenAt,
38941
39391
  online: this.deviceOnline(device.deviceId)
38942
39392
  })),
38943
- lastConnectedAddress: config.lastConnectedAddress
39393
+ lastConnectedAddress: config.lastConnectedAddress,
39394
+ wsl
38944
39395
  };
38945
39396
  }
38946
39397
  async regenerateCertificate() {
@@ -39020,9 +39471,10 @@ class MobileGateway {
39020
39471
  if (store?.getState().devices.some((device) => device.deviceId === auth.deviceId)) {
39021
39472
  store.touchDevice(auth.deviceId, Date.now()).catch((error) => this.onError?.(toError(error)));
39022
39473
  }
39023
- if (store !== undefined && typeof localAddress === "string" && isIpv4Literal(localAddress)) {
39024
- if (store.getState().lastConnectedAddress !== localAddress) {
39025
- store.rememberConnectedAddress(localAddress).catch((error) => this.onError?.(toError(error)));
39474
+ const learned = this.advertisedAddress(localAddress);
39475
+ if (store !== undefined && typeof learned === "string" && isIpv4Literal(learned)) {
39476
+ if (store.getState().lastConnectedAddress !== learned) {
39477
+ store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
39026
39478
  }
39027
39479
  }
39028
39480
  const clientHost = createHost2({
@@ -39106,6 +39558,13 @@ class MobileGateway {
39106
39558
  for (const connection of this.connections.values())
39107
39559
  connection.sendNotification(method, params);
39108
39560
  }
39561
+ advertisedAddress(localAddress) {
39562
+ const state = this.stateValue;
39563
+ if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
39564
+ return state.address;
39565
+ }
39566
+ return localAddress;
39567
+ }
39109
39568
  requireStore() {
39110
39569
  if (this.storeHandle === undefined)
39111
39570
  throw new Error(`mobile store is unavailable: ${this.storeFailure ?? "unknown error"}`);
@@ -39118,6 +39577,7 @@ class MobileGateway {
39118
39577
  await this.stopListening();
39119
39578
  this.stateValue = { status: "starting" };
39120
39579
  try {
39580
+ const plan = await this.planListen(address);
39121
39581
  const certificate = await this.certificateValue();
39122
39582
  const server = createServer2({ cert: certificate.certPem, key: certificate.keyPem }, (request, response) => {
39123
39583
  this.handleRequest(request, response);
@@ -39145,11 +39605,10 @@ class MobileGateway {
39145
39605
  };
39146
39606
  server.once("error", onFailure);
39147
39607
  server.once("listening", onListening);
39148
- server.listen(port, address);
39608
+ server.listen(port, plan.bindAddress);
39149
39609
  });
39150
39610
  this.server = server;
39151
39611
  this.wss = wss;
39152
- this.stateValue = { status: "listening", address, port };
39153
39612
  server.on("error", (error) => {
39154
39613
  this.stateValue = { status: "error", error: toMessage(error) };
39155
39614
  this.onError?.(error);
@@ -39157,6 +39616,9 @@ class MobileGateway {
39157
39616
  return;
39158
39617
  });
39159
39618
  });
39619
+ if (plan.mode === "wsl-relay")
39620
+ await this.startRelayFor(address, port, plan.bindAddress);
39621
+ this.stateValue = plan.mode === "wsl-relay" ? { status: "listening", address, port, mode: "wsl-relay", bindAddress: plan.bindAddress } : { status: "listening", address, port };
39160
39622
  } catch (error) {
39161
39623
  await this.stopListening().catch(() => {
39162
39624
  return;
@@ -39165,6 +39627,66 @@ class MobileGateway {
39165
39627
  throw error;
39166
39628
  }
39167
39629
  }
39630
+ async planListen(address) {
39631
+ if (this.wslEnvironment === undefined)
39632
+ return { mode: "direct", bindAddress: address };
39633
+ if (this.windowsAddresses === undefined)
39634
+ throw new Error(WSL_INTEROP_UNAVAILABLE);
39635
+ let candidates;
39636
+ try {
39637
+ candidates = await this.windowsAddresses();
39638
+ } catch (error) {
39639
+ throw new Error(`\u8BFB\u53D6 Windows \u5C40\u57DF\u7F51\u5730\u5740\u5931\u8D25\uFF1A${toMessage(error)}`);
39640
+ }
39641
+ if (!candidates.includes(address)) {
39642
+ throw new Error(`${address} \u4E0D\u662F\u5F53\u524D\u53EF\u7528\u7684 Windows \u5C40\u57DF\u7F51\u5730\u5740\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u540E\u518D\u5F00\u542F\u3002`);
39643
+ }
39644
+ const interfaces = this.localInterfaces?.();
39645
+ if (wslOwnAddresses(interfaces).includes(address))
39646
+ return { mode: "direct", bindAddress: address };
39647
+ const bindAddress = primaryWslAddress(interfaces);
39648
+ if (bindAddress === undefined)
39649
+ throw new Error("WSL \u5185\u6CA1\u6709\u53EF\u7528\u7684\u7F51\u7EDC\u5730\u5740\uFF0C\u65E0\u6CD5\u5EFA\u7ACB\u624B\u673A\u5165\u53E3\u3002");
39650
+ return { mode: "wsl-relay", bindAddress };
39651
+ }
39652
+ async startRelayFor(address, port, bindAddress) {
39653
+ const deadline = Date.now() + this.relayPortRetryMs;
39654
+ let relay;
39655
+ for (;; ) {
39656
+ try {
39657
+ relay = await this.startRelay({
39658
+ powershell: this.wslEnvironment?.powershell ?? "powershell.exe",
39659
+ listenAddress: address,
39660
+ listenPort: port,
39661
+ targetAddress: bindAddress,
39662
+ targetPort: port
39663
+ });
39664
+ break;
39665
+ } catch (error) {
39666
+ if (error instanceof RelayError && error.code === "port_in_use" && Date.now() + this.relayPortRetryIntervalMs < deadline) {
39667
+ await new Promise((resolve) => setTimeout(resolve, this.relayPortRetryIntervalMs));
39668
+ continue;
39669
+ }
39670
+ throw new Error(describeRelayError(error, address, port, bindAddress));
39671
+ }
39672
+ }
39673
+ this.relay = relay;
39674
+ relay.exited.then((reason) => this.onRelayExited(relay, reason));
39675
+ }
39676
+ onRelayExited(relay, reason) {
39677
+ this.enqueue(async () => {
39678
+ if (this.relay !== relay)
39679
+ return;
39680
+ this.relay = undefined;
39681
+ this.pairingValue?.invalidate();
39682
+ await this.stopListening().catch(() => {
39683
+ return;
39684
+ });
39685
+ const message = `Windows \u4E2D\u7EE7\u610F\u5916\u9000\u51FA\uFF08${reason}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u5F00\u542F\u624B\u673A\u8BBF\u95EE\u3002`;
39686
+ this.stateValue = { status: "error", error: message };
39687
+ this.onError?.(new Error(message));
39688
+ });
39689
+ }
39168
39690
  async certificateValue() {
39169
39691
  if (this.certificate === undefined) {
39170
39692
  this.certificate = await MobileCertificate.load(this.requireStore(), this.serverId);
@@ -39172,6 +39694,12 @@ class MobileGateway {
39172
39694
  return this.certificate;
39173
39695
  }
39174
39696
  async stopListening() {
39697
+ const relay = this.relay;
39698
+ this.relay = undefined;
39699
+ if (relay !== undefined)
39700
+ await relay.stop().catch(() => {
39701
+ return;
39702
+ });
39175
39703
  for (const connection of [...this.connections.values()])
39176
39704
  connection.close(1001, "listener_stopped");
39177
39705
  const server = this.server;
@@ -39276,42 +39804,26 @@ ${JSON.stringify({ code, message })}`);
39276
39804
  return run;
39277
39805
  }
39278
39806
  }
39279
- var VIRTUAL_INTERFACE_PREFIXES, VIRTUAL_INTERFACE_KEYWORDS;
39807
+ var RELAY_PORT_RETRY_MS = 1e4, RELAY_PORT_RETRY_INTERVAL_MS = 1000, WSL_INTEROP_UNAVAILABLE = "WSL \u7684 Windows \u4E92\u64CD\u4F5C\u4E0D\u53EF\u7528\uFF08\u627E\u4E0D\u5230 powershell.exe\uFF09\uFF0C\u65E0\u6CD5\u8BFB\u53D6 Windows \u5C40\u57DF\u7F51\u5730\u5740\u3002\u8BF7\u542F\u7528 WSL \u4E92\u64CD\u4F5C\uFF0C\u6216\u76F4\u63A5\u5728 Windows \u4E0A\u8FD0\u884C neko\u3002";
39280
39808
  var init_listener = __esm(() => {
39281
39809
  init_contract();
39282
39810
  init_link();
39283
39811
  init_adapter();
39284
39812
  init_auth();
39285
39813
  init_certificate();
39814
+ init_network();
39286
39815
  init_pairing();
39287
39816
  init_protocol();
39817
+ init_relay();
39288
39818
  init_store();
39289
- VIRTUAL_INTERFACE_PREFIXES = [
39290
- "anpi",
39291
- "awdl",
39292
- "bridge",
39293
- "docker",
39294
- "gif",
39295
- "ipsec",
39296
- "llw",
39297
- "ppp",
39298
- "stf",
39299
- "tap",
39300
- "tun",
39301
- "utun",
39302
- "vboxnet",
39303
- "veth",
39304
- "virbr",
39305
- "vmnet"
39306
- ];
39307
- VIRTUAL_INTERFACE_KEYWORDS = ["hyper-v", "virtual", "virtualbox", "vmware", "vpn"];
39819
+ init_wsl();
39308
39820
  });
39309
39821
 
39310
39822
  // packages/gateway/src/port-file.ts
39311
39823
  import { readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
39312
- import { join as join10 } from "path";
39824
+ import { join as join11 } from "path";
39313
39825
  function gatewayPortFilePath(nekoDir, serverId) {
39314
- return join10(nekoDir, `${serverId}.port`);
39826
+ return join11(nekoDir, `${serverId}.port`);
39315
39827
  }
39316
39828
  async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
39317
39829
  await writeFile4(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
@@ -40428,7 +40940,7 @@ var init_esm = __esm(() => {
40428
40940
  import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
40429
40941
  import { mkdir as mkdir6, readdir as readdir2, rename as rename4, rm as rm5, stat as stat3, writeFile as writeFile5 } from "fs/promises";
40430
40942
  import { homedir as homedir7 } from "os";
40431
- import { dirname as dirname8, join as join11 } from "path";
40943
+ import { dirname as dirname8, join as join12 } from "path";
40432
40944
  function createSkillMarketClient(options) {
40433
40945
  const home = options.home ?? homedir7();
40434
40946
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -40545,11 +41057,11 @@ function createSkillMarketClient(options) {
40545
41057
  } catch {
40546
41058
  return;
40547
41059
  }
40548
- await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm5(join11(parent, n), { recursive: true, force: true }).catch(() => {})));
41060
+ await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm5(join12(parent, n), { recursive: true, force: true }).catch(() => {})));
40549
41061
  };
40550
41062
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
40551
41063
  const skillsDir = skillsDirFor(scope, workspaceRoot);
40552
- const target = join11(skillsDir, name);
41064
+ const target = join12(skillsDir, name);
40553
41065
  const exists = await stat3(target).catch(() => {
40554
41066
  return;
40555
41067
  }) !== undefined;
@@ -40558,10 +41070,10 @@ function createSkillMarketClient(options) {
40558
41070
  const skill = await detail(name);
40559
41071
  const files = unpack(skill, await download(skill));
40560
41072
  await sweepTempDirs(skillsDir);
40561
- const temp = join11(dirname8(skillsDir), `.tmp-${randomUUID3()}`);
41073
+ const temp = join12(dirname8(skillsDir), `.tmp-${randomUUID3()}`);
40562
41074
  try {
40563
41075
  for (const [relative, bytes] of files) {
40564
- const dest = join11(temp, ...relative.split("/"));
41076
+ const dest = join12(temp, ...relative.split("/"));
40565
41077
  await mkdir6(dirname8(dest), { recursive: true });
40566
41078
  await writeFile5(dest, bytes);
40567
41079
  }
@@ -40574,7 +41086,7 @@ function createSkillMarketClient(options) {
40574
41086
  }
40575
41087
  let shadowedBy;
40576
41088
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
40577
- const projectDir = join11(projectSkillsDir(workspaceRoot), name);
41089
+ const projectDir = join12(projectSkillsDir(workspaceRoot), name);
40578
41090
  if (await stat3(projectDir).catch(() => {
40579
41091
  return;
40580
41092
  }) !== undefined)
@@ -40584,7 +41096,7 @@ function createSkillMarketClient(options) {
40584
41096
  };
40585
41097
  const uninstall = async ({ name, scope, workspaceRoot }) => {
40586
41098
  const skillsDir = skillsDirFor(scope, workspaceRoot);
40587
- const target = join11(skillsDir, name);
41099
+ const target = join12(skillsDir, name);
40588
41100
  if (await stat3(target).catch(() => {
40589
41101
  return;
40590
41102
  }) === undefined)
@@ -40616,7 +41128,7 @@ function createSkillMarketClient(options) {
40616
41128
  uninstall
40617
41129
  };
40618
41130
  }
40619
- var SkillMarketError, CATALOG_TTL_MS = 60000, ZIP_CACHE_MAX = 16, MAX_PACKAGE_BYTES, userSkillsDir2 = (home) => join11(home, ".agents", "skills"), projectSkillsDir = (workspaceRoot) => join11(workspaceRoot, ".agents", "skills"), activeLevelOf = (state) => state.project ? "project" : state.user ? "user" : state.builtin ? "builtin" : null, normalizeEntry = (raw) => {
41131
+ var SkillMarketError, CATALOG_TTL_MS = 60000, ZIP_CACHE_MAX = 16, MAX_PACKAGE_BYTES, userSkillsDir2 = (home) => join12(home, ".agents", "skills"), projectSkillsDir = (workspaceRoot) => join12(workspaceRoot, ".agents", "skills"), activeLevelOf = (state) => state.project ? "project" : state.user ? "user" : state.builtin ? "builtin" : null, normalizeEntry = (raw) => {
40620
41132
  const row = raw ?? {};
40621
41133
  const metadata = row.metadata;
40622
41134
  const source = metadata !== null && typeof metadata === "object" ? metadata.source : undefined;
@@ -40647,9 +41159,9 @@ var init_skill_market = __esm(() => {
40647
41159
  });
40648
41160
 
40649
41161
  // packages/gateway/src/workspaces.ts
40650
- import { constants as constants2 } from "fs";
41162
+ import { constants as constants3 } from "fs";
40651
41163
  import { access, readdir as readdir3, stat as stat4 } from "fs/promises";
40652
- import { dirname as dirname9, isAbsolute as isAbsolute3, join as join12 } from "path";
41164
+ import { dirname as dirname9, isAbsolute as isAbsolute3, join as join13 } from "path";
40653
41165
  async function browseWorkspaces(options) {
40654
41166
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
40655
41167
  if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
@@ -40676,13 +41188,13 @@ async function browseWorkspaces(options) {
40676
41188
  let next = offset;
40677
41189
  for (;next < entries.length && directories.length < 100; next++) {
40678
41190
  const entry = entries[next];
40679
- const target = join12(path, entry.name);
41191
+ const target = join13(path, entry.name);
40680
41192
  if (!entry.isDirectory() && !entry.isSymbolicLink())
40681
41193
  continue;
40682
41194
  try {
40683
41195
  if (!(await stat4(target)).isDirectory())
40684
41196
  continue;
40685
- await access(target, constants2.R_OK | constants2.X_OK);
41197
+ await access(target, constants3.R_OK | constants3.X_OK);
40686
41198
  directories.push({ name: entry.name, path: target });
40687
41199
  } catch {
40688
41200
  directories.push({ name: entry.name, path: target, error: "\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE" });
@@ -40961,7 +41473,7 @@ var init_server_services = __esm(() => {
40961
41473
  // packages/gateway/src/gateway.ts
40962
41474
  import { randomUUID as randomUUID5 } from "crypto";
40963
41475
  import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
40964
- import { basename as basename6, dirname as dirname10, join as join13 } from "path";
41476
+ import { basename as basename6, dirname as dirname10, join as join14 } from "path";
40965
41477
  import { createHost as createHost3 } from "@earendil-works/chord";
40966
41478
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
40967
41479
  async function startGateway(options = {}) {
@@ -40974,7 +41486,7 @@ async function startGateway(options = {}) {
40974
41486
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
40975
41487
  const nekoDir = dirname10(storagePath);
40976
41488
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
40977
- const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join13(nekoDir, `${serverId}.sock`);
41489
+ const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join14(nekoDir, `${serverId}.sock`);
40978
41490
  const runtime = await createNekoRuntime({
40979
41491
  context,
40980
41492
  onReport: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
@@ -41134,7 +41646,7 @@ function serverIdFromSocketPath(socketPath) {
41134
41646
  return isServerId(candidate) ? candidate : undefined;
41135
41647
  }
41136
41648
  async function loadOrCreateServerId(nekoDir) {
41137
- const file = join13(nekoDir, SERVER_ID_FILENAME);
41649
+ const file = join14(nekoDir, SERVER_ID_FILENAME);
41138
41650
  try {
41139
41651
  const existing = (await readFile8(file, "utf8")).trim();
41140
41652
  if (existing !== "")
@@ -41170,20 +41682,89 @@ function gatewayCommand(args) {
41170
41682
  ]
41171
41683
  };
41172
41684
  }
41173
- async function stopGatewayProcess(child) {
41685
+ async function stopGatewayProcess(child, options = {}) {
41174
41686
  if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
41175
41687
  return;
41176
41688
  await new Promise((resolve) => {
41177
- const timer = setTimeout(() => child.kill("SIGKILL"), 3000);
41689
+ let forcedTimer;
41690
+ const timer = setTimeout(() => {
41691
+ child.kill("SIGKILL");
41692
+ forcedTimer = setTimeout(resolve, options.forcedMs ?? FORCED_STOP_MS);
41693
+ }, options.gracefulMs ?? GRACEFUL_STOP_MS);
41178
41694
  child.once("exit", () => {
41179
41695
  clearTimeout(timer);
41696
+ clearTimeout(forcedTimer);
41180
41697
  resolve();
41181
41698
  });
41182
41699
  child.kill("SIGTERM");
41183
41700
  });
41184
41701
  }
41702
+ var GRACEFUL_STOP_MS = 3000, FORCED_STOP_MS = 2000;
41185
41703
  var init_process = () => {};
41186
41704
 
41705
+ // packages/gateway/src/shutdown.ts
41706
+ function shutdownSignals(platform) {
41707
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
41708
+ if (platform === "win32")
41709
+ signals.push("SIGBREAK");
41710
+ return signals;
41711
+ }
41712
+ function installShutdown(options = {}) {
41713
+ const proc = options.process ?? process;
41714
+ const exit = options.exit ?? ((code) => process.exit(code));
41715
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
41716
+ const controller = new AbortController;
41717
+ let cleanup;
41718
+ let cleaning = false;
41719
+ let timer;
41720
+ const runCleanup = () => {
41721
+ if (cleaning || cleanup === undefined)
41722
+ return;
41723
+ cleaning = true;
41724
+ cleanup().then(() => exit(0), (error) => {
41725
+ options.log?.(error instanceof Error ? error.message : String(error));
41726
+ exit(1);
41727
+ });
41728
+ };
41729
+ const onSignal = () => {
41730
+ if (controller.signal.aborted) {
41731
+ options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
41732
+ exit(130);
41733
+ return;
41734
+ }
41735
+ options.log?.(`
41736
+ \u6B63\u5728\u5173\u95ED\u2026`);
41737
+ controller.abort();
41738
+ timer = setTimeout(() => {
41739
+ options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
41740
+ exit(1);
41741
+ }, timeoutMs);
41742
+ timer.unref?.();
41743
+ runCleanup();
41744
+ };
41745
+ const installed = [];
41746
+ for (const signal of shutdownSignals(proc.platform)) {
41747
+ try {
41748
+ proc.on(signal, onSignal);
41749
+ installed.push(signal);
41750
+ } catch {}
41751
+ }
41752
+ return {
41753
+ signal: controller.signal,
41754
+ setCleanup(next) {
41755
+ cleanup = next;
41756
+ if (controller.signal.aborted)
41757
+ runCleanup();
41758
+ },
41759
+ dispose() {
41760
+ clearTimeout(timer);
41761
+ for (const signal of installed)
41762
+ proc.off(signal, onSignal);
41763
+ }
41764
+ };
41765
+ }
41766
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
41767
+
41187
41768
  // packages/gateway/src/index.ts
41188
41769
  var init_src2 = __esm(() => {
41189
41770
  init_client();
@@ -41199,13 +41780,13 @@ var init_src2 = __esm(() => {
41199
41780
 
41200
41781
  // packages/tui/src/gateway.ts
41201
41782
  import { spawn as spawn3 } from "child_process";
41202
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
41203
- import { basename as basename7, dirname as dirname11, join as join14 } from "path";
41783
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
41784
+ import { basename as basename7, dirname as dirname11, join as join15 } from "path";
41204
41785
  async function connectToGateway(options = {}) {
41205
41786
  const config = loadNekoConfig(options.configDir);
41206
41787
  const storagePath = options.storagePath ?? config.storagePath ?? DEFAULT_STORAGE_PATH;
41207
41788
  const nekoDir = dirname11(storagePath);
41208
- const fileServerId = readServerId(join14(nekoDir, SERVER_ID_FILENAME2));
41789
+ const fileServerId = readServerId(join15(nekoDir, SERVER_ID_FILENAME2));
41209
41790
  const platform = options.platform ?? process.platform;
41210
41791
  if (platform === "win32") {
41211
41792
  if (fileServerId !== undefined) {
@@ -41222,8 +41803,8 @@ async function connectToGateway(options = {}) {
41222
41803
  }
41223
41804
  return spawnAndConnect({ ...options, platform, storagePath, socketPath: undefined, nekoDir, fileServerId });
41224
41805
  }
41225
- const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join14(nekoDir, `${fileServerId}.sock`) : undefined);
41226
- const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join14(nekoDir, SERVER_ID_FILENAME2));
41806
+ const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join15(nekoDir, `${fileServerId}.sock`) : undefined);
41807
+ const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join15(nekoDir, SERVER_ID_FILENAME2));
41227
41808
  if (socketPath !== undefined && existsSync3(socketPath)) {
41228
41809
  const serverId = serverIdFor(socketPath);
41229
41810
  if (serverId !== undefined) {
@@ -41257,7 +41838,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41257
41838
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
41258
41839
  if (plan.platform === "win32") {
41259
41840
  const timeoutAt = Date.now() + timeoutMs;
41260
- const serverId = plan.fileServerId ?? await waitForValue2(() => readServerId(join14(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41841
+ const serverId = plan.fileServerId ?? await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41261
41842
  let lastError = new Error("Gateway port file unavailable");
41262
41843
  while (Date.now() < timeoutAt) {
41263
41844
  if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
@@ -41294,11 +41875,11 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41294
41875
  }
41295
41876
  let socketPath = plan.socketPath;
41296
41877
  if (socketPath === undefined) {
41297
- const generated = await waitForValue2(() => readServerId(join14(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41298
- socketPath = join14(plan.nekoDir, `${generated}.sock`);
41878
+ const generated = await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41879
+ socketPath = join15(plan.nekoDir, `${generated}.sock`);
41299
41880
  }
41300
41881
  await waitForValue2(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
41301
- const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join14(plan.nekoDir, SERVER_ID_FILENAME2));
41882
+ const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2));
41302
41883
  if (serverId === undefined) {
41303
41884
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
41304
41885
  }
@@ -41350,7 +41931,7 @@ function gatewayArgs(plan) {
41350
41931
  }
41351
41932
  function readServerId(file) {
41352
41933
  try {
41353
- const value = readFileSync2(file, "utf8").trim();
41934
+ const value = readFileSync3(file, "utf8").trim();
41354
41935
  return isServerId(value) ? value : undefined;
41355
41936
  } catch {
41356
41937
  return;
@@ -41554,7 +42135,7 @@ var init_help_ui = __esm(() => {
41554
42135
  });
41555
42136
 
41556
42137
  // packages/tui/src/image-input.ts
41557
- import { execFile as execFile2 } from "child_process";
42138
+ import { execFile as execFile3 } from "child_process";
41558
42139
  import { readFile as readFile9, stat as stat5 } from "fs/promises";
41559
42140
  import { homedir as homedir8 } from "os";
41560
42141
  import { basename as basename8, resolve as resolve5 } from "path";
@@ -41657,7 +42238,7 @@ function isClipboardShortcut(data, platform = process.platform) {
41657
42238
  }
41658
42239
  var exec;
41659
42240
  var init_image_input = __esm(() => {
41660
- exec = promisify2(execFile2);
42241
+ exec = promisify2(execFile3);
41661
42242
  });
41662
42243
 
41663
42244
  // packages/tui/src/layout.ts
@@ -43648,12 +44229,12 @@ var init_main = __esm(async () => {
43648
44229
  });
43649
44230
 
43650
44231
  // packages/web/src/open-browser.ts
43651
- import { execFile as execFile3 } from "child_process";
44232
+ import { execFile as execFile4 } from "child_process";
43652
44233
  function openBrowser(url) {
43653
44234
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32.exe" : "xdg-open";
43654
44235
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
43655
44236
  return new Promise((resolve, reject) => {
43656
- execFile3(command, args, { timeout: 8000, windowsHide: true }, (error) => {
44237
+ execFile4(command, args, { timeout: 8000, windowsHide: true }, (error) => {
43657
44238
  if (error)
43658
44239
  reject(error);
43659
44240
  else
@@ -43667,7 +44248,7 @@ var init_open_browser = () => {};
43667
44248
  import { spawn as spawn4 } from "child_process";
43668
44249
  import { randomUUID as randomUUID6 } from "crypto";
43669
44250
  import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
43670
- import { dirname as dirname12, join as join15 } from "path";
44251
+ import { dirname as dirname12, join as join16 } from "path";
43671
44252
  async function ensureNekoGateway(options) {
43672
44253
  const timeoutMs = options.spawnTimeoutMs ?? 30000;
43673
44254
  const timeoutAt = Date.now() + timeoutMs;
@@ -43678,6 +44259,7 @@ async function ensureNekoGateway(options) {
43678
44259
  if (discovered !== undefined && await probeWs(discovered, serverId, timeoutAt)) {
43679
44260
  return makeHandle(serverId, undefined, discovered, null, nekoDir);
43680
44261
  }
44262
+ throwIfAborted(options.signal);
43681
44263
  const child = spawnGatewayProcess(options, null, serverId);
43682
44264
  pipeChildLogs(child, options.onLog);
43683
44265
  let spawnError;
@@ -43687,6 +44269,7 @@ async function ensureNekoGateway(options) {
43687
44269
  try {
43688
44270
  let lastError = new Error("gateway \u672A\u5C31\u7EEA");
43689
44271
  while (Date.now() < timeoutAt) {
44272
+ throwIfAborted(options.signal);
43690
44273
  if (spawnError !== undefined)
43691
44274
  throw spawnError;
43692
44275
  if (child.exitCode !== null || child.signalCode !== null) {
@@ -43703,7 +44286,7 @@ async function ensureNekoGateway(options) {
43703
44286
  lastError = error;
43704
44287
  }
43705
44288
  }
43706
- await sleep2(Math.max(0, Math.min(PROBE_INTERVAL_MS, timeoutAt - Date.now())));
44289
+ await sleep2(Math.max(0, Math.min(PROBE_INTERVAL_MS, timeoutAt - Date.now())), options.signal);
43707
44290
  }
43708
44291
  throw new Error(`\u7B49\u5F85 neko gateway \u5C31\u7EEA\u8D85\u65F6\uFF08${options.spawnTimeoutMs ?? 30000}ms\uFF09\uFF1A${lastError instanceof Error ? lastError.message : String(lastError)}`);
43709
44292
  } catch (error) {
@@ -43711,7 +44294,7 @@ async function ensureNekoGateway(options) {
43711
44294
  throw error;
43712
44295
  }
43713
44296
  }
43714
- const socketPath = options.socketPath ?? join15(nekoDir, `${serverId}.sock`);
44297
+ const socketPath = options.socketPath ?? join16(nekoDir, `${serverId}.sock`);
43715
44298
  const host = options.host ?? "127.0.0.1";
43716
44299
  const port = options.port ?? 8321;
43717
44300
  const wsUrl = `ws://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
@@ -43721,6 +44304,7 @@ async function ensureNekoGateway(options) {
43721
44304
  }
43722
44305
  return makeHandle(serverId, socketPath, wsUrl, null, nekoDir);
43723
44306
  }
44307
+ throwIfAborted(options.signal);
43724
44308
  const child = spawnGatewayProcess(options, socketPath, serverId);
43725
44309
  pipeChildLogs(child, options.onLog);
43726
44310
  let spawnError;
@@ -43730,6 +44314,7 @@ async function ensureNekoGateway(options) {
43730
44314
  try {
43731
44315
  let lastError = new Error("gateway \u672A\u5C31\u7EEA");
43732
44316
  while (Date.now() < timeoutAt) {
44317
+ throwIfAborted(options.signal);
43733
44318
  if (spawnError !== undefined)
43734
44319
  throw spawnError;
43735
44320
  if (child.exitCode !== null || child.signalCode !== null) {
@@ -43744,7 +44329,7 @@ async function ensureNekoGateway(options) {
43744
44329
  throw error;
43745
44330
  lastError = error;
43746
44331
  }
43747
- await sleep2(Math.max(0, Math.min(PROBE_INTERVAL_MS, timeoutAt - Date.now())));
44332
+ await sleep2(Math.max(0, Math.min(PROBE_INTERVAL_MS, timeoutAt - Date.now())), options.signal);
43748
44333
  }
43749
44334
  throw new Error(`\u7B49\u5F85 neko gateway \u5C31\u7EEA\u8D85\u65F6\uFF08${options.spawnTimeoutMs ?? 30000}ms\uFF09\uFF1A${lastError instanceof Error ? lastError.message : String(lastError)}`);
43750
44335
  } catch (error) {
@@ -43831,7 +44416,7 @@ function makeHandle(serverId, socketPath, wsUrl, spawned, nekoDir) {
43831
44416
  };
43832
44417
  }
43833
44418
  async function loadOrCreateServerId2(nekoDir) {
43834
- const file = join15(nekoDir, SERVER_ID_FILENAME3);
44419
+ const file = join16(nekoDir, SERVER_ID_FILENAME3);
43835
44420
  try {
43836
44421
  const existing = (await readFile10(file, "utf8")).trim();
43837
44422
  if (existing !== "")
@@ -43843,8 +44428,22 @@ async function loadOrCreateServerId2(nekoDir) {
43843
44428
  `, { mode: 384 });
43844
44429
  return created;
43845
44430
  }
43846
- function sleep2(ms) {
43847
- return new Promise((resolve) => setTimeout(resolve, ms));
44431
+ function throwIfAborted(signal) {
44432
+ if (signal?.aborted)
44433
+ throw new Error("neko gateway \u542F\u52A8\u88AB\u4E2D\u65AD");
44434
+ }
44435
+ function sleep2(ms, signal) {
44436
+ return new Promise((resolve) => {
44437
+ if (signal?.aborted)
44438
+ return resolve();
44439
+ const timer = setTimeout(done, ms);
44440
+ function done() {
44441
+ clearTimeout(timer);
44442
+ signal?.removeEventListener("abort", done);
44443
+ resolve();
44444
+ }
44445
+ signal?.addEventListener("abort", done, { once: true });
44446
+ });
43848
44447
  }
43849
44448
  var SERVER_ID_FILENAME3 = "gateway-server-id", PROBE_INTERVAL_MS = 400, PROBE_TIMEOUT_MS = 5000;
43850
44449
  var init_gateway_spawn = __esm(() => {
@@ -43855,7 +44454,7 @@ var init_gateway_spawn = __esm(() => {
43855
44454
  // packages/web/src/mobile-admin.ts
43856
44455
  import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "crypto";
43857
44456
  import { ChordError as ChordError7, createHost as createHost4 } from "@earendil-works/chord";
43858
- function createMobileAdminCaller(gateway, nekoDir) {
44457
+ function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
43859
44458
  const serverId = gateway.serverId;
43860
44459
  let tokenPromise;
43861
44460
  const readToken = () => {
@@ -43865,10 +44464,18 @@ function createMobileAdminCaller(gateway, nekoDir) {
43865
44464
  });
43866
44465
  return tokenPromise;
43867
44466
  };
43868
- return async (method, params) => {
44467
+ const run = async (method, params, signal) => {
43869
44468
  const token = await readToken();
44469
+ signal.throwIfAborted();
43870
44470
  const link = gateway.socketPath !== undefined ? await createUnixLink(gateway.socketPath) : createWsLink(gateway.wsUrl);
43871
44471
  const host = createHost4({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
44472
+ const release = () => {
44473
+ host.close().catch(() => {
44474
+ return;
44475
+ });
44476
+ link.close("aborted");
44477
+ };
44478
+ signal.addEventListener("abort", release, { once: true });
43872
44479
  try {
43873
44480
  const admin = host.get(NekoMobileAdmin, { token }, { at: serverId });
43874
44481
  const call = admin[method];
@@ -43884,12 +44491,42 @@ function createMobileAdminCaller(gateway, nekoDir) {
43884
44491
  admin.release?.();
43885
44492
  }
43886
44493
  } finally {
44494
+ signal.removeEventListener("abort", release);
43887
44495
  await host.close().catch(() => {
43888
44496
  return;
43889
44497
  });
43890
44498
  link.close("done");
43891
44499
  }
43892
44500
  };
44501
+ const inflight = new Set;
44502
+ const caller = async (method, params) => {
44503
+ const timeoutMs = method === "enable" ? timeouts.enableTimeoutMs ?? MOBILE_ADMIN_ENABLE_TIMEOUT_MS : timeouts.callTimeoutMs ?? MOBILE_ADMIN_CALL_TIMEOUT_MS;
44504
+ const controller = new AbortController;
44505
+ inflight.add(controller);
44506
+ const timer = setTimeout(() => controller.abort(new MobileAdminTimeoutError(method, timeoutMs)), timeoutMs);
44507
+ try {
44508
+ const work = run(method, params, controller.signal);
44509
+ work.catch(() => {
44510
+ return;
44511
+ });
44512
+ return await new Promise((resolve, reject) => {
44513
+ const onAbort = () => reject(controller.signal.reason);
44514
+ if (controller.signal.aborted)
44515
+ onAbort();
44516
+ else
44517
+ controller.signal.addEventListener("abort", onAbort, { once: true });
44518
+ work.then(resolve, reject);
44519
+ });
44520
+ } finally {
44521
+ clearTimeout(timer);
44522
+ inflight.delete(controller);
44523
+ }
44524
+ };
44525
+ caller.abortAll = () => {
44526
+ for (const controller of [...inflight])
44527
+ controller.abort(new MobileAdminAbortedError);
44528
+ };
44529
+ return caller;
43893
44530
  }
43894
44531
  function writeJson(response, status, body) {
43895
44532
  response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
@@ -43966,7 +44603,7 @@ async function handleMobileAdminRequest(request, response, context) {
43966
44603
  if (error instanceof GatewayUnsupportedError)
43967
44604
  supported = false;
43968
44605
  else
43969
- throw error;
44606
+ return respondWithCaller(response, Promise.reject(error));
43970
44607
  }
43971
44608
  writeJson(response, 200, { csrf: context.csrf, supported });
43972
44609
  return true;
@@ -44024,6 +44661,14 @@ async function respondWithCaller(response, result) {
44024
44661
  writeJson(response, 502, { code: error.code, message: error.message });
44025
44662
  return true;
44026
44663
  }
44664
+ if (error instanceof MobileAdminTimeoutError) {
44665
+ writeJson(response, 504, { code: "gateway_timeout", message: error.message });
44666
+ return true;
44667
+ }
44668
+ if (error instanceof MobileAdminAbortedError) {
44669
+ writeJson(response, 503, { code: "shutting_down", message: error.message });
44670
+ return true;
44671
+ }
44027
44672
  throw error;
44028
44673
  }
44029
44674
  return true;
@@ -44041,7 +44686,7 @@ function createMobileAdminContext(options) {
44041
44686
  allowedHosts: hosts
44042
44687
  };
44043
44688
  }
44044
- var BODY_LIMIT_BYTES = 4096, GatewayUnsupportedError, WRITE_ROUTES, READ_ROUTES;
44689
+ var BODY_LIMIT_BYTES = 4096, GatewayUnsupportedError, MobileAdminTimeoutError, MobileAdminAbortedError, MOBILE_ADMIN_CALL_TIMEOUT_MS = 1e4, MOBILE_ADMIN_ENABLE_TIMEOUT_MS = 30000, WRITE_ROUTES, READ_ROUTES;
44045
44690
  var init_mobile_admin = __esm(() => {
44046
44691
  init_src2();
44047
44692
  GatewayUnsupportedError = class GatewayUnsupportedError extends Error {
@@ -44050,6 +44695,18 @@ var init_mobile_admin = __esm(() => {
44050
44695
  this.name = "GatewayUnsupportedError";
44051
44696
  }
44052
44697
  };
44698
+ MobileAdminTimeoutError = class MobileAdminTimeoutError extends Error {
44699
+ constructor(method, timeoutMs) {
44700
+ super(`mobile admin call "${method}" timed out after ${timeoutMs}ms`);
44701
+ this.name = "MobileAdminTimeoutError";
44702
+ }
44703
+ };
44704
+ MobileAdminAbortedError = class MobileAdminAbortedError extends Error {
44705
+ constructor() {
44706
+ super("mobile admin call was cancelled because the web server is shutting down");
44707
+ this.name = "MobileAdminAbortedError";
44708
+ }
44709
+ };
44053
44710
  WRITE_ROUTES = {
44054
44711
  "/api/mobile/enable": "enable",
44055
44712
  "/api/mobile/disable": "disable",
@@ -44075,9 +44732,11 @@ async function startWebServer(options = {}) {
44075
44732
  const storagePath = options.gateway?.storagePath ?? DEFAULT_STORAGE_PATH;
44076
44733
  const gateway = await ensureNekoGateway({
44077
44734
  ...options.gateway ?? {},
44078
- storagePath
44735
+ storagePath,
44736
+ ...options.signal !== undefined ? { signal: options.signal } : {}
44079
44737
  });
44080
44738
  let mobileAdmin;
44739
+ let closing;
44081
44740
  const server = createServer3((request, response) => {
44082
44741
  const context = mobileAdmin;
44083
44742
  if (context === undefined) {
@@ -44098,12 +44757,24 @@ async function startWebServer(options = {}) {
44098
44757
  response.end(error instanceof Error ? error.message : String(error));
44099
44758
  });
44100
44759
  });
44760
+ const sockets = new Set;
44761
+ server.on("connection", (socket) => {
44762
+ sockets.add(socket);
44763
+ socket.once("close", () => sockets.delete(socket));
44764
+ });
44101
44765
  const port = options.port ?? DEFAULT_PORT2;
44102
44766
  const host = options.host ?? DEFAULT_HOST2;
44103
- await new Promise((resolveListen, rejectListen) => {
44104
- server.once("error", rejectListen);
44105
- server.listen(port, host, resolveListen);
44106
- });
44767
+ try {
44768
+ await new Promise((resolveListen, rejectListen) => {
44769
+ server.once("error", rejectListen);
44770
+ server.listen(port, host, resolveListen);
44771
+ });
44772
+ } catch (error) {
44773
+ await gateway.close().catch(() => {
44774
+ return;
44775
+ });
44776
+ throw error;
44777
+ }
44107
44778
  const address = server.address();
44108
44779
  const actualPort = typeof address === "object" && address !== null ? address.port : port;
44109
44780
  mobileAdmin = createMobileAdminContext({
@@ -44116,11 +44787,16 @@ async function startWebServer(options = {}) {
44116
44787
  url: `http://${host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "[::1]" : host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${actualPort}`,
44117
44788
  port: actualPort,
44118
44789
  gateway,
44119
- async close() {
44120
- await new Promise((resolveClose, rejectClose) => {
44121
- server.close((error) => error ? rejectClose(error) : resolveClose());
44122
- });
44123
- await gateway.close();
44790
+ close() {
44791
+ closing ??= (async () => {
44792
+ mobileAdmin?.caller.abortAll?.();
44793
+ const closed = new Promise((resolveClose) => server.close(() => resolveClose()));
44794
+ for (const socket of sockets)
44795
+ socket.destroy();
44796
+ await closed;
44797
+ await gateway.close();
44798
+ })();
44799
+ return closing;
44124
44800
  }
44125
44801
  };
44126
44802
  }
@@ -44342,13 +45018,22 @@ var USAGE2 = `\u7528\u6CD5: neko web [\u9009\u9879]
44342
45018
  --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF08\u9002\u7528\u4E8E\u8FDC\u7A0B\u6216\u65E0\u684C\u9762\u73AF\u5883\uFF09
44343
45019
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
44344
45020
 
44345
- \u53EF\u5728 Web \u8BBE\u7F6E\u9875\u914D\u7F6E Key\uFF1B\u7F51\u5173\u5B50\u8FDB\u7A0B\u4E5F\u4F1A\u7EE7\u627F DEEPSEEK_API_KEY\u3002`;
45021
+ \u53EF\u5728 Web \u8BBE\u7F6E\u9875\u914D\u7F6E Key\uFF1B\u7F51\u5173\u5B50\u8FDB\u7A0B\u4E5F\u4F1A\u7EE7\u627F DEEPSEEK_API_KEY\u3002`, server, shutdown;
44346
45022
  var init_main2 = __esm(async () => {
45023
+ init_src2();
44347
45024
  init_open_browser();
44348
45025
  init_server();
45026
+ shutdown = installShutdown({
45027
+ log: (message) => console.log(message),
45028
+ exit: (code) => {
45029
+ server?.gateway.spawned?.kill("SIGKILL");
45030
+ process.exit(code);
45031
+ }
45032
+ });
44349
45033
  try {
44350
45034
  const options = parseArgs2(process.argv.slice(2));
44351
- const server = await startWebServer({
45035
+ server = await startWebServer({
45036
+ signal: shutdown.signal,
44352
45037
  ...options.port !== undefined ? { port: options.port } : {},
44353
45038
  ...options.host !== undefined ? { host: options.host } : {},
44354
45039
  gateway: {
@@ -44361,27 +45046,23 @@ var init_main2 = __esm(async () => {
44361
45046
  onLog: (line) => console.log(`[gateway] ${line}`)
44362
45047
  }
44363
45048
  });
44364
- console.log(`[web] neko web \u5DF2\u542F\u52A8: ${server.url}`);
44365
- console.log(`[web] gateway: ${server.gateway.wsUrl}\uFF08serverId: ${server.gateway.serverId}\uFF09`);
45049
+ const running = server;
45050
+ shutdown.setCleanup(() => running.close());
45051
+ console.log(`[web] neko web \u5DF2\u542F\u52A8: ${running.url}`);
45052
+ console.log(`[web] gateway: ${running.gateway.wsUrl}\uFF08serverId: ${running.gateway.serverId}\uFF09`);
44366
45053
  console.log("[web] \u6309 Ctrl+C \u505C\u6B62\u3002");
44367
- const shutdown = () => {
44368
- console.log(`
44369
- \u6B63\u5728\u5173\u95ED\u2026`);
44370
- server.close().then(() => process.exit(0)).catch((error) => {
44371
- console.error(error instanceof Error ? error.message : String(error));
44372
- process.exit(1);
44373
- });
44374
- };
44375
- process.on("SIGINT", shutdown);
44376
- process.on("SIGTERM", shutdown);
44377
45054
  if (options.open) {
44378
- openBrowser(server.url).catch(() => {
44379
- console.warn(`\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE: ${server.url}`);
45055
+ openBrowser(running.url).catch(() => {
45056
+ console.warn(`\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE: ${running.url}`);
44380
45057
  });
44381
45058
  }
44382
45059
  } catch (error) {
45060
+ if (shutdown.signal.aborted) {
45061
+ process.exit(0);
45062
+ }
44383
45063
  console.error(error instanceof Error ? error.message : String(error));
44384
45064
  process.exitCode = 1;
45065
+ shutdown.dispose();
44385
45066
  }
44386
45067
  });
44387
45068
 
@@ -44489,15 +45170,7 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
44489
45170
  \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
44490
45171
 
44491
45172
  // packages/gateway/src/main.ts
44492
- var argv, options, gateway, stopping = false, stop = () => {
44493
- if (stopping)
44494
- return;
44495
- stopping = true;
44496
- gateway.close().then(() => process.exit(0)).catch((error) => {
44497
- console.error("\u5173\u95ED\u5931\u8D25:", error instanceof Error ? error.message : String(error));
44498
- process.exit(1);
44499
- });
44500
- };
45173
+ var argv, shutdown2, options, gateway;
44501
45174
  var init_main3 = __esm(async () => {
44502
45175
  init_gateway();
44503
45176
  argv = process.argv.slice(2);
@@ -44505,6 +45178,10 @@ var init_main3 = __esm(async () => {
44505
45178
  console.log(GATEWAY_USAGE);
44506
45179
  process.exit(0);
44507
45180
  }
45181
+ shutdown2 = installShutdown({
45182
+ log: (message) => console.log(message),
45183
+ exit: (code) => process.exit(code)
45184
+ });
44508
45185
  options = parseGatewayArgs(argv);
44509
45186
  gateway = await startGateway({
44510
45187
  ...options,
@@ -44516,8 +45193,7 @@ var init_main3 = __esm(async () => {
44516
45193
  if (gateway.wsUrl !== undefined)
44517
45194
  console.log(` WebSocket: ${gateway.wsUrl}`);
44518
45195
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
44519
- process.on("SIGINT", stop);
44520
- process.on("SIGTERM", stop);
45196
+ shutdown2.setCleanup(() => gateway.close());
44521
45197
  });
44522
45198
 
44523
45199
  // packages/cli/src/main.ts