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/gateway.js CHANGED
@@ -16593,7 +16593,7 @@ function parseGatewayArgs(argv) {
16593
16593
  // packages/gateway/src/gateway.ts
16594
16594
  import { randomUUID as randomUUID5 } from "crypto";
16595
16595
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
16596
- import { basename as basename4, dirname as dirname9, join as join12 } from "path";
16596
+ import { basename as basename4, dirname as dirname9, join as join13 } from "path";
16597
16597
  import { createHost as createHost3 } from "@earendil-works/chord";
16598
16598
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
16599
16599
 
@@ -21124,7 +21124,9 @@ var listenerStateSchema = Type5.Union([
21124
21124
  Type5.Object({
21125
21125
  status: Type5.Literal("listening"),
21126
21126
  address: Type5.String(),
21127
- port: Type5.Integer({ minimum: 1, maximum: 65535 })
21127
+ port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21128
+ bindAddress: Type5.Optional(Type5.String()),
21129
+ mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21128
21130
  }),
21129
21131
  Type5.Object({ status: Type5.Literal("error"), error: Type5.String() }),
21130
21132
  Type5.Object({ status: Type5.Literal("stopping") })
@@ -21153,7 +21155,11 @@ var NekoMobileAdmin = contract("neko.mobile-admin", {
21153
21155
  Type5.Null()
21154
21156
  ]),
21155
21157
  devices: Type5.Array(adminDeviceSchema),
21156
- lastConnectedAddress: Type5.Union([Type5.String(), Type5.Null()])
21158
+ lastConnectedAddress: Type5.Union([Type5.String(), Type5.Null()]),
21159
+ wsl: Type5.Optional(Type5.Union([
21160
+ Type5.Object({ interop: Type5.Boolean(), error: Type5.Union([Type5.String(), Type5.Null()]) }),
21161
+ Type5.Null()
21162
+ ]))
21157
21163
  })
21158
21164
  ],
21159
21165
  enable: [
@@ -21164,7 +21170,9 @@ var NekoMobileAdmin = contract("neko.mobile-admin", {
21164
21170
  Type5.Object({
21165
21171
  status: Type5.Literal("listening"),
21166
21172
  address: Type5.String(),
21167
- port: Type5.Integer({ minimum: 1, maximum: 65535 })
21173
+ port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21174
+ bindAddress: Type5.Optional(Type5.String()),
21175
+ mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21168
21176
  })
21169
21177
  ],
21170
21178
  disable: [Type5.Null(), listenerStateSchema],
@@ -21250,7 +21258,7 @@ function createMobileAdminFactory(deps) {
21250
21258
  // packages/gateway/src/mobile/listener.ts
21251
21259
  import { randomUUID as randomUUID2 } from "crypto";
21252
21260
  import { createServer } from "https";
21253
- import { hostname, networkInterfaces } from "os";
21261
+ import { hostname } from "os";
21254
21262
  import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
21255
21263
  import { WebSocketServer } from "ws";
21256
21264
 
@@ -21813,7 +21821,7 @@ import { Check as Check2 } from "typebox/value";
21813
21821
  import { createHost } from "@earendil-works/chord";
21814
21822
 
21815
21823
  // packages/core/src/application-version.ts
21816
- var RUNNING_VERSION = "0.4.0";
21824
+ var RUNNING_VERSION = "0.4.2";
21817
21825
  var APPLICATION_PROTOCOL = 4;
21818
21826
  function stableVersion(value) {
21819
21827
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
@@ -23749,6 +23757,70 @@ class MobileCertificate {
23749
23757
  }
23750
23758
  }
23751
23759
 
23760
+ // packages/gateway/src/mobile/network.ts
23761
+ import { networkInterfaces } from "os";
23762
+ var VIRTUAL_INTERFACE_PREFIXES = [
23763
+ "anpi",
23764
+ "awdl",
23765
+ "bridge",
23766
+ "docker",
23767
+ "gif",
23768
+ "ipsec",
23769
+ "llw",
23770
+ "ppp",
23771
+ "stf",
23772
+ "tap",
23773
+ "tun",
23774
+ "utun",
23775
+ "vboxnet",
23776
+ "veth",
23777
+ "virbr",
23778
+ "vmnet"
23779
+ ];
23780
+ var VIRTUAL_INTERFACE_KEYWORDS = ["hyper-v", "virtual", "virtualbox", "vmware", "vpn", "wsl"];
23781
+ function isVirtualInterfaceName(name) {
23782
+ const lower = name.toLowerCase();
23783
+ if (VIRTUAL_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix)))
23784
+ return true;
23785
+ return VIRTUAL_INTERFACE_KEYWORDS.some((keyword) => lower.includes(keyword));
23786
+ }
23787
+ function isBenchmarkAddress(address) {
23788
+ return address.startsWith("198.18.") || address.startsWith("198.19.");
23789
+ }
23790
+ function collectListenCandidates(interfaces, skipVirtual) {
23791
+ const candidates = [];
23792
+ for (const [name, infos] of Object.entries(interfaces)) {
23793
+ if (infos === undefined)
23794
+ continue;
23795
+ if (skipVirtual && isVirtualInterfaceName(name))
23796
+ continue;
23797
+ for (const info of infos) {
23798
+ if (info.family !== "IPv4" || info.internal)
23799
+ continue;
23800
+ if (info.address.startsWith("169.254."))
23801
+ continue;
23802
+ if (skipVirtual && isBenchmarkAddress(info.address))
23803
+ continue;
23804
+ candidates.push(info.address);
23805
+ }
23806
+ }
23807
+ return candidates.sort();
23808
+ }
23809
+ function mobileListenCandidates(interfaces) {
23810
+ const source = interfaces ?? networkInterfaces();
23811
+ const filtered = collectListenCandidates(source, true);
23812
+ return filtered.length > 0 ? filtered : collectListenCandidates(source, false);
23813
+ }
23814
+ function prioritizeListenCandidate(candidates, lastConnectedAddress) {
23815
+ if (lastConnectedAddress === null || !candidates.includes(lastConnectedAddress))
23816
+ return [...candidates];
23817
+ return [lastConnectedAddress, ...candidates.filter((candidate) => candidate !== lastConnectedAddress)];
23818
+ }
23819
+ function isIpv4Literal(address) {
23820
+ const parts = address.split(".");
23821
+ return parts.length === 4 && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
23822
+ }
23823
+
23752
23824
  // packages/gateway/src/mobile/pairing.ts
23753
23825
  import { createHash as createHash5, randomBytes as randomBytes3 } from "crypto";
23754
23826
  var PAIRING_CODE_TTL_MS = 120000;
@@ -23942,71 +24014,376 @@ class MobilePairing {
23942
24014
  }
23943
24015
  }
23944
24016
 
23945
- // packages/gateway/src/mobile/listener.ts
23946
- function defaultServerName() {
23947
- const name = hostname().trim();
23948
- return (name === "" ? "neko-computer" : name).slice(0, 64);
24017
+ // packages/gateway/src/mobile/relay.ts
24018
+ import { spawn as nodeSpawn } from "child_process";
24019
+ class RelayError extends Error {
24020
+ code;
24021
+ constructor(code, message) {
24022
+ super(message);
24023
+ this.name = "RelayError";
24024
+ this.code = code;
24025
+ }
23949
24026
  }
23950
- var VIRTUAL_INTERFACE_PREFIXES = [
23951
- "anpi",
23952
- "awdl",
23953
- "bridge",
23954
- "docker",
23955
- "gif",
23956
- "ipsec",
23957
- "llw",
23958
- "ppp",
23959
- "stf",
23960
- "tap",
23961
- "tun",
23962
- "utun",
23963
- "vboxnet",
23964
- "veth",
23965
- "virbr",
23966
- "vmnet"
23967
- ];
23968
- var VIRTUAL_INTERFACE_KEYWORDS = ["hyper-v", "virtual", "virtualbox", "vmware", "vpn"];
23969
- function isVirtualInterfaceName(name) {
23970
- const lower = name.toLowerCase();
23971
- if (VIRTUAL_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix)))
23972
- return true;
23973
- return VIRTUAL_INTERFACE_KEYWORDS.some((keyword) => lower.includes(keyword));
24027
+ var RELAY_HEARTBEAT_INTERVAL_MS = 2000;
24028
+ var RELAY_WATCHDOG_MS = 8000;
24029
+ var START_TIMEOUT_MS = 20000;
24030
+ var STOP_GRACE_MS = 1000;
24031
+ var STDERR_TAIL_BYTES = 2000;
24032
+ var RELAY_SOURCE = `using System;
24033
+ using System.Diagnostics;
24034
+ using System.Net;
24035
+ using System.Net.Sockets;
24036
+ using System.Threading;
24037
+
24038
+ public static class NekoRelay
24039
+ {
24040
+ static readonly Stopwatch Clock = Stopwatch.StartNew();
24041
+ static long lastBeatMs = 0;
24042
+
24043
+ static void Touch() { Interlocked.Exchange(ref lastBeatMs, Clock.ElapsedMilliseconds); }
24044
+ static string Flat(string text) { return text.Replace("\\r", " ").Replace("\\n", " "); }
24045
+ static void Close(TcpClient client) { if (client != null) { try { client.Close(); } catch (Exception) { } } }
24046
+
24047
+ static void Copy(TcpClient from, TcpClient to)
24048
+ {
24049
+ byte[] buffer = new byte[16384];
24050
+ try
24051
+ {
24052
+ System.IO.Stream source = from.GetStream();
24053
+ System.IO.Stream sink = to.GetStream();
24054
+ int count;
24055
+ while ((count = source.Read(buffer, 0, buffer.Length)) > 0) { sink.Write(buffer, 0, count); }
24056
+ }
24057
+ catch (Exception) { }
24058
+ try { to.Client.Shutdown(SocketShutdown.Send); } catch (Exception) { }
24059
+ }
24060
+
24061
+ static void Pump(object state)
24062
+ {
24063
+ object[] args = (object[])state;
24064
+ TcpClient client = (TcpClient)args[0];
24065
+ TcpClient target = null;
24066
+ try
24067
+ {
24068
+ target = new TcpClient();
24069
+ target.NoDelay = true;
24070
+ client.NoDelay = true;
24071
+ target.Connect((string)args[1], (int)args[2]);
24072
+ Thread upstream = new Thread(delegate() { Copy(client, target); });
24073
+ upstream.IsBackground = true;
24074
+ upstream.Start();
24075
+ Copy(target, client);
24076
+ }
24077
+ catch (Exception) { }
24078
+ finally { Close(client); Close(target); }
24079
+ }
24080
+
24081
+ public static void Run(string listenIp, int listenPort, string targetIp, int targetPort, int watchdogMs)
24082
+ {
24083
+ TcpListener listener = new TcpListener(IPAddress.Parse(listenIp), listenPort);
24084
+ listener.ExclusiveAddressUse = true;
24085
+ try { listener.Start(128); }
24086
+ catch (SocketException e)
24087
+ {
24088
+ string code = (e.SocketErrorCode == SocketError.AddressAlreadyInUse || e.SocketErrorCode == SocketError.AccessDenied) ? "port_in_use" : "listen_failed";
24089
+ Console.Out.WriteLine("ERROR " + code + " " + Flat(e.Message));
24090
+ Console.Out.Flush();
24091
+ Environment.Exit(2);
24092
+ }
24093
+ try
24094
+ {
24095
+ TcpClient probe = new TcpClient();
24096
+ IAsyncResult pending = probe.BeginConnect(targetIp, targetPort, null, null);
24097
+ if (!pending.AsyncWaitHandle.WaitOne(3000)) { throw new TimeoutException("connect timed out"); }
24098
+ probe.EndConnect(pending);
24099
+ probe.Close();
24100
+ }
24101
+ catch (Exception e)
24102
+ {
24103
+ Console.Out.WriteLine("ERROR target_unreachable " + Flat(e.Message));
24104
+ Console.Out.Flush();
24105
+ Environment.Exit(3);
24106
+ }
24107
+ Touch();
24108
+ Thread input = new Thread(delegate()
24109
+ {
24110
+ try { while (Console.In.ReadLine() != null) { Touch(); } } catch (Exception) { }
24111
+ Environment.Exit(0);
24112
+ });
24113
+ input.IsBackground = true;
24114
+ input.Start();
24115
+ Thread watchdog = new Thread(delegate()
24116
+ {
24117
+ while (true)
24118
+ {
24119
+ Thread.Sleep(1000);
24120
+ if (Clock.ElapsedMilliseconds - Interlocked.Read(ref lastBeatMs) > watchdogMs) { Environment.Exit(0); }
24121
+ }
24122
+ });
24123
+ watchdog.IsBackground = true;
24124
+ watchdog.Start();
24125
+ Console.Out.WriteLine("READY");
24126
+ Console.Out.Flush();
24127
+ while (true)
24128
+ {
24129
+ TcpClient accepted;
24130
+ try { accepted = listener.AcceptTcpClient(); }
24131
+ catch (Exception) { Environment.Exit(4); return; }
24132
+ ThreadPool.QueueUserWorkItem(Pump, new object[] { accepted, targetIp, targetPort });
24133
+ }
24134
+ }
24135
+ }`;
24136
+ function assertPort2(port, label) {
24137
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
24138
+ throw new Error(`relay ${label} port is invalid`);
23974
24139
  }
23975
- function isBenchmarkAddress(address) {
23976
- return address.startsWith("198.18.") || address.startsWith("198.19.");
24140
+ function buildRelayScript(endpoints, watchdogMs = RELAY_WATCHDOG_MS) {
24141
+ if (!isIpv4Literal(endpoints.listenAddress) || !isIpv4Literal(endpoints.targetAddress)) {
24142
+ throw new Error("relay addresses must be IPv4 literals");
24143
+ }
24144
+ assertPort2(endpoints.listenPort, "listen");
24145
+ assertPort2(endpoints.targetPort, "target");
24146
+ if (!Number.isInteger(watchdogMs) || watchdogMs < 1000)
24147
+ throw new Error("relay watchdog is invalid");
24148
+ return [
24149
+ "$ErrorActionPreference='Stop'",
24150
+ `$src=@'
24151
+ ${RELAY_SOURCE}
24152
+ '@`,
24153
+ "Add-Type -TypeDefinition $src -Language CSharp",
24154
+ `[NekoRelay]::Run('${endpoints.listenAddress}',${endpoints.listenPort},'${endpoints.targetAddress}',${endpoints.targetPort},${watchdogMs})`
24155
+ ].join(`
24156
+ `);
23977
24157
  }
23978
- function collectListenCandidates(interfaces, skipVirtual) {
23979
- const candidates = [];
23980
- for (const [name, infos] of Object.entries(interfaces)) {
23981
- if (infos === undefined)
23982
- continue;
23983
- if (skipVirtual && isVirtualInterfaceName(name))
24158
+ function encodeRelayCommand(script) {
24159
+ return Buffer.from(script, "utf16le").toString("base64");
24160
+ }
24161
+ function describeExit(child) {
24162
+ return child.signalCode !== null ? `signal ${child.signalCode}` : `exit code ${child.exitCode}`;
24163
+ }
24164
+ async function startWindowsRelay(options) {
24165
+ const watchdogMs = options.watchdogMs ?? RELAY_WATCHDOG_MS;
24166
+ const script = buildRelayScript(options, watchdogMs);
24167
+ const spawn = options.spawn ?? nodeSpawn;
24168
+ const child = spawn(options.powershell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodeRelayCommand(script)], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
24169
+ let stderrTail = "";
24170
+ child.stderr?.on("data", (chunk) => {
24171
+ stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_TAIL_BYTES);
24172
+ });
24173
+ child.stdin?.on("error", () => {});
24174
+ const exited = new Promise((resolve) => {
24175
+ child.once("error", (error) => resolve(`spawn error: ${error.message}`));
24176
+ child.once("exit", () => resolve(describeExit(child)));
24177
+ });
24178
+ let heartbeat;
24179
+ let stopping;
24180
+ const stop = () => {
24181
+ stopping ??= (async () => {
24182
+ clearInterval(heartbeat);
24183
+ child.stdin?.end();
24184
+ const grace = options.stopGraceMs ?? STOP_GRACE_MS;
24185
+ const done = await Promise.race([exited.then(() => true), delay2(grace).then(() => false)]);
24186
+ if (!done) {
24187
+ child.kill("SIGKILL");
24188
+ await Promise.race([exited, delay2(grace)]);
24189
+ }
24190
+ })();
24191
+ return stopping;
24192
+ };
24193
+ const ready = new Promise((resolve, reject) => {
24194
+ let buffer = "";
24195
+ const timer = setTimeout(() => reject(new RelayError("timeout", "Windows relay did not become ready in time")), options.startTimeoutMs ?? START_TIMEOUT_MS);
24196
+ const finish = (action) => {
24197
+ clearTimeout(timer);
24198
+ action();
24199
+ };
24200
+ child.stdout?.on("data", (chunk) => {
24201
+ buffer += chunk.toString("utf8");
24202
+ for (;; ) {
24203
+ const newline = buffer.indexOf(`
24204
+ `);
24205
+ if (newline < 0)
24206
+ break;
24207
+ const line = buffer.slice(0, newline).trim();
24208
+ buffer = buffer.slice(newline + 1);
24209
+ if (line === "READY")
24210
+ finish(resolve);
24211
+ else if (line.startsWith("ERROR ")) {
24212
+ const [, code = "start_failed", ...rest] = line.split(" ");
24213
+ const known = ["port_in_use", "listen_failed", "target_unreachable"];
24214
+ const mapped = known.find((item) => item === code) ?? "start_failed";
24215
+ finish(() => reject(new RelayError(mapped, rest.join(" ") || code)));
24216
+ }
24217
+ }
24218
+ });
24219
+ exited.then((reason) => {
24220
+ const detail = stderrTail.trim();
24221
+ finish(() => reject(new RelayError("start_failed", `Windows relay exited before it was ready (${reason})${detail === "" ? "" : `: ${detail}`}`)));
24222
+ });
24223
+ });
24224
+ try {
24225
+ await ready;
24226
+ } catch (error) {
24227
+ await stop();
24228
+ throw error;
24229
+ }
24230
+ heartbeat = setInterval(() => {
24231
+ child.stdin?.write(`
24232
+ `);
24233
+ }, options.heartbeatMs ?? RELAY_HEARTBEAT_INTERVAL_MS);
24234
+ heartbeat.unref?.();
24235
+ return { exited, stop };
24236
+ }
24237
+ function delay2(ms) {
24238
+ return new Promise((resolve) => setTimeout(resolve, ms));
24239
+ }
24240
+
24241
+ // packages/gateway/src/mobile/wsl.ts
24242
+ import { execFile } from "child_process";
24243
+ import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync2 } from "fs";
24244
+ import { networkInterfaces as networkInterfaces2 } from "os";
24245
+ import { delimiter, join as join8 } from "path";
24246
+ var POWERSHELL_FALLBACK = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
24247
+ var defaultProbeDeps = {
24248
+ platform: process.platform,
24249
+ env: process.env,
24250
+ readProcVersion() {
24251
+ try {
24252
+ return readFileSync2("/proc/version", "utf8");
24253
+ } catch {
24254
+ return;
24255
+ }
24256
+ },
24257
+ isExecutable(path) {
24258
+ try {
24259
+ accessSync2(path, constants2.X_OK);
24260
+ return true;
24261
+ } catch {
24262
+ return false;
24263
+ }
24264
+ }
24265
+ };
24266
+ function detectWsl(overrides = {}) {
24267
+ const deps = { ...defaultProbeDeps, ...overrides };
24268
+ if (deps.platform !== "linux")
24269
+ return;
24270
+ const { env } = deps;
24271
+ const version = deps.readProcVersion() ?? "";
24272
+ const inWsl = (env.WSL_DISTRO_NAME ?? "") !== "" || (env.WSL_INTEROP ?? "") !== "" || /microsoft|wsl/i.test(version);
24273
+ if (!inWsl)
24274
+ return;
24275
+ const candidates = [
24276
+ ...(env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join8(dir, "powershell.exe")),
24277
+ POWERSHELL_FALLBACK
24278
+ ];
24279
+ return { powershell: candidates.find((candidate) => deps.isExecutable(candidate)) };
24280
+ }
24281
+ var WINDOWS_ADDRESSES_SCRIPT = [
24282
+ "$ErrorActionPreference='Stop'",
24283
+ "$r=@()",
24284
+ "foreach($n in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()){",
24285
+ "if($n.OperationalStatus -ne 'Up'){continue}",
24286
+ "if($n.NetworkInterfaceType -eq 'Loopback' -or $n.NetworkInterfaceType -eq 'Tunnel'){continue}",
24287
+ "foreach($a in $n.GetIPProperties().UnicastAddresses){",
24288
+ "if($a.Address.AddressFamily -eq 'InterNetwork'){$r+=[pscustomobject]@{name=$n.Name;address=$a.Address.IPAddressToString}}",
24289
+ "}}",
24290
+ "ConvertTo-Json -InputObject @($r) -Compress"
24291
+ ].join(`
24292
+ `);
24293
+ var WSL_ADAPTER_PATTERN = /wsl|hyper-v|vethernet/i;
24294
+ var runPowerShell = (powershell, script, timeoutMs) => new Promise((resolve, reject) => {
24295
+ execFile(powershell, ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024, encoding: "utf8" }, (error, stdout) => error ? reject(error) : resolve(stdout));
24296
+ });
24297
+ function parseWindowsInterfaces(output) {
24298
+ const text = output.replace(/^\uFEFF/, "").trim();
24299
+ if (text === "")
24300
+ return [];
24301
+ const parsed = JSON.parse(text);
24302
+ const items = Array.isArray(parsed) ? parsed : [parsed];
24303
+ const result = [];
24304
+ for (const item of items) {
24305
+ if (typeof item !== "object" || item === null)
23984
24306
  continue;
23985
- for (const info of infos) {
23986
- if (info.family !== "IPv4" || info.internal)
23987
- continue;
23988
- if (info.address.startsWith("169.254."))
23989
- continue;
23990
- if (skipVirtual && isBenchmarkAddress(info.address))
23991
- continue;
23992
- candidates.push(info.address);
24307
+ const { name, address } = item;
24308
+ if (typeof name === "string" && typeof address === "string" && isIpv4Literal(address)) {
24309
+ result.push({ name, address });
23993
24310
  }
23994
24311
  }
23995
- return candidates.sort();
24312
+ return result;
23996
24313
  }
23997
- function mobileListenCandidates(interfaces) {
23998
- const source = interfaces ?? networkInterfaces();
23999
- const filtered = collectListenCandidates(source, true);
24000
- return filtered.length > 0 ? filtered : collectListenCandidates(source, false);
24314
+ function toInterfaceDict(interfaces) {
24315
+ const dict = {};
24316
+ for (const { name, address } of interfaces) {
24317
+ const list = dict[name] ?? [];
24318
+ dict[name] = list;
24319
+ list.push({
24320
+ address,
24321
+ netmask: "255.255.255.0",
24322
+ family: "IPv4",
24323
+ mac: "00:00:00:00:00:00",
24324
+ internal: false,
24325
+ cidr: `${address}/24`
24326
+ });
24327
+ }
24328
+ return dict;
24001
24329
  }
24002
- function prioritizeListenCandidate(candidates, lastConnectedAddress) {
24003
- if (lastConnectedAddress === null || !candidates.includes(lastConnectedAddress))
24004
- return [...candidates];
24005
- return [lastConnectedAddress, ...candidates.filter((candidate) => candidate !== lastConnectedAddress)];
24330
+
24331
+ class WindowsAddressDiscovery {
24332
+ powershell;
24333
+ run;
24334
+ cacheMs;
24335
+ timeoutMs;
24336
+ now;
24337
+ cached;
24338
+ pending;
24339
+ constructor(options) {
24340
+ this.powershell = options.powershell;
24341
+ this.run = options.run ?? runPowerShell;
24342
+ this.cacheMs = options.cacheMs ?? 15000;
24343
+ this.timeoutMs = options.timeoutMs ?? 1e4;
24344
+ this.now = options.now ?? Date.now;
24345
+ }
24346
+ invalidate() {
24347
+ this.cached = undefined;
24348
+ }
24349
+ candidates() {
24350
+ if (this.cached !== undefined && this.now() - this.cached.at < this.cacheMs) {
24351
+ return Promise.resolve([...this.cached.addresses]);
24352
+ }
24353
+ this.pending ??= this.read().finally(() => {
24354
+ this.pending = undefined;
24355
+ });
24356
+ return this.pending.then((addresses) => [...addresses]);
24357
+ }
24358
+ async read() {
24359
+ const output = await this.run(this.powershell, WINDOWS_ADDRESSES_SCRIPT, this.timeoutMs);
24360
+ const usable = parseWindowsInterfaces(output).filter((item) => !WSL_ADAPTER_PATTERN.test(item.name));
24361
+ const addresses = mobileListenCandidates(toInterfaceDict(usable));
24362
+ this.cached = { at: this.now(), addresses };
24363
+ return addresses;
24364
+ }
24006
24365
  }
24007
- function isIpv4Literal(address) {
24008
- const parts = address.split(".");
24009
- return parts.length === 4 && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
24366
+ function wslOwnAddresses(interfaces = networkInterfaces2()) {
24367
+ const result = [];
24368
+ for (const infos of Object.values(interfaces)) {
24369
+ for (const info of infos ?? []) {
24370
+ if (info.family === "IPv4" && !info.internal)
24371
+ result.push(info.address);
24372
+ }
24373
+ }
24374
+ return result;
24375
+ }
24376
+ function primaryWslAddress(interfaces = networkInterfaces2()) {
24377
+ const eth0 = (interfaces.eth0 ?? []).find((info) => info.family === "IPv4" && !info.internal);
24378
+ return eth0?.address ?? wslOwnAddresses(interfaces)[0];
24379
+ }
24380
+
24381
+ // packages/gateway/src/mobile/listener.ts
24382
+ var RELAY_PORT_RETRY_MS = 1e4;
24383
+ var RELAY_PORT_RETRY_INTERVAL_MS = 1000;
24384
+ function defaultServerName() {
24385
+ const name = hostname().trim();
24386
+ return (name === "" ? "neko-computer" : name).slice(0, 64);
24010
24387
  }
24011
24388
  function assertListenAddress(address) {
24012
24389
  if (!isIpv4Literal(address))
@@ -24018,6 +24395,23 @@ function assertListenAddress(address) {
24018
24395
  function toError(error) {
24019
24396
  return error instanceof Error ? error : new Error(String(error));
24020
24397
  }
24398
+ var 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";
24399
+ function describeRelayError(error, address, port, bindAddress) {
24400
+ if (!(error instanceof RelayError))
24401
+ return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${toMessage(error)}`;
24402
+ switch (error.code) {
24403
+ case "port_in_use":
24404
+ 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`;
24405
+ case "target_unreachable":
24406
+ return `Windows \u65E0\u6CD5\u8FDE\u63A5\u5230 WSL \u5185\u7684\u76D1\u542C\u5730\u5740 ${bindAddress}:${port}\uFF08${error.message}\uFF09\u3002`;
24407
+ case "timeout":
24408
+ return "Windows \u4E2D\u7EE7\u542F\u52A8\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
24409
+ case "listen_failed":
24410
+ return `Windows \u4E2D\u7EE7\u65E0\u6CD5\u5728 ${address}:${port} \u4E0A\u76D1\u542C\uFF1A${error.message}`;
24411
+ default:
24412
+ return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
24413
+ }
24414
+ }
24021
24415
  function isMobilePrincipal2(principal) {
24022
24416
  return typeof principal === "object" && principal !== null && principal.kind === "mobile";
24023
24417
  }
@@ -24047,7 +24441,26 @@ class MobileGateway {
24047
24441
  stateValue;
24048
24442
  queue = Promise.resolve();
24049
24443
  closing;
24050
- constructor(serverId, serverName, onError, chord, store, storeFailure, initial) {
24444
+ relay;
24445
+ wslEnvironment;
24446
+ windowsAddresses;
24447
+ startRelay;
24448
+ localInterfaces;
24449
+ relayPortRetryMs;
24450
+ relayPortRetryIntervalMs;
24451
+ constructor(serverId, serverName, onError, chord, store, storeFailure, initial, network = {}) {
24452
+ this.wslEnvironment = network.wsl === null ? undefined : network.wsl ?? detectWsl();
24453
+ const powershell = this.wslEnvironment?.powershell;
24454
+ if (network.windowsCandidates !== undefined)
24455
+ this.windowsAddresses = network.windowsCandidates;
24456
+ else if (powershell !== undefined) {
24457
+ const discovery = new WindowsAddressDiscovery({ powershell });
24458
+ this.windowsAddresses = () => discovery.candidates();
24459
+ }
24460
+ this.startRelay = network.startRelay ?? startWindowsRelay;
24461
+ this.localInterfaces = network.localInterfaces;
24462
+ this.relayPortRetryMs = network.relayPortRetryMs ?? RELAY_PORT_RETRY_MS;
24463
+ this.relayPortRetryIntervalMs = network.relayPortRetryIntervalMs ?? RELAY_PORT_RETRY_INTERVAL_MS;
24051
24464
  this.serverId = serverId;
24052
24465
  this.serverName = serverName;
24053
24466
  this.onError = onError;
@@ -24064,7 +24477,7 @@ class MobileGateway {
24064
24477
  } catch (error) {
24065
24478
  storeFailure = toMessage(error);
24066
24479
  }
24067
- 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}` });
24480
+ 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);
24068
24481
  gateway.runtime = options.runtime;
24069
24482
  if (store !== undefined) {
24070
24483
  const config = store.getState();
@@ -24136,12 +24549,30 @@ class MobileGateway {
24136
24549
  }
24137
24550
  return false;
24138
24551
  }
24139
- adminState() {
24552
+ async adminState() {
24140
24553
  const store = this.requireStore();
24141
24554
  const config = store.getState();
24555
+ let candidates;
24556
+ let wsl = null;
24557
+ if (this.wslEnvironment === undefined) {
24558
+ candidates = mobileListenCandidates();
24559
+ } else {
24560
+ candidates = [];
24561
+ let error = null;
24562
+ if (this.windowsAddresses === undefined)
24563
+ error = WSL_INTEROP_UNAVAILABLE;
24564
+ else {
24565
+ try {
24566
+ candidates = await this.windowsAddresses();
24567
+ } catch (caught) {
24568
+ error = `\u8BFB\u53D6 Windows \u5C40\u57DF\u7F51\u5730\u5740\u5931\u8D25\uFF1A${toMessage(caught)}`;
24569
+ }
24570
+ }
24571
+ wsl = { interop: this.windowsAddresses !== undefined, error };
24572
+ }
24142
24573
  return {
24143
24574
  state: this.stateValue,
24144
- candidates: prioritizeListenCandidate(mobileListenCandidates(), config.lastConnectedAddress),
24575
+ candidates: prioritizeListenCandidate(candidates, config.lastConnectedAddress),
24145
24576
  port: config.port,
24146
24577
  certificate: this.certificate === undefined ? null : { fingerprint: this.certificate.fingerprint, expiresAt: this.certificate.expiresAt },
24147
24578
  devices: config.devices.map((device) => ({
@@ -24151,7 +24582,8 @@ class MobileGateway {
24151
24582
  lastSeenAt: device.lastSeenAt,
24152
24583
  online: this.deviceOnline(device.deviceId)
24153
24584
  })),
24154
- lastConnectedAddress: config.lastConnectedAddress
24585
+ lastConnectedAddress: config.lastConnectedAddress,
24586
+ wsl
24155
24587
  };
24156
24588
  }
24157
24589
  async regenerateCertificate() {
@@ -24231,9 +24663,10 @@ class MobileGateway {
24231
24663
  if (store?.getState().devices.some((device) => device.deviceId === auth.deviceId)) {
24232
24664
  store.touchDevice(auth.deviceId, Date.now()).catch((error) => this.onError?.(toError(error)));
24233
24665
  }
24234
- if (store !== undefined && typeof localAddress === "string" && isIpv4Literal(localAddress)) {
24235
- if (store.getState().lastConnectedAddress !== localAddress) {
24236
- store.rememberConnectedAddress(localAddress).catch((error) => this.onError?.(toError(error)));
24666
+ const learned = this.advertisedAddress(localAddress);
24667
+ if (store !== undefined && typeof learned === "string" && isIpv4Literal(learned)) {
24668
+ if (store.getState().lastConnectedAddress !== learned) {
24669
+ store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
24237
24670
  }
24238
24671
  }
24239
24672
  const clientHost = createHost2({
@@ -24317,6 +24750,13 @@ class MobileGateway {
24317
24750
  for (const connection of this.connections.values())
24318
24751
  connection.sendNotification(method, params);
24319
24752
  }
24753
+ advertisedAddress(localAddress) {
24754
+ const state = this.stateValue;
24755
+ if (state.status === "listening" && state.mode === "wsl-relay" && localAddress === state.bindAddress) {
24756
+ return state.address;
24757
+ }
24758
+ return localAddress;
24759
+ }
24320
24760
  requireStore() {
24321
24761
  if (this.storeHandle === undefined)
24322
24762
  throw new Error(`mobile store is unavailable: ${this.storeFailure ?? "unknown error"}`);
@@ -24329,6 +24769,7 @@ class MobileGateway {
24329
24769
  await this.stopListening();
24330
24770
  this.stateValue = { status: "starting" };
24331
24771
  try {
24772
+ const plan = await this.planListen(address);
24332
24773
  const certificate = await this.certificateValue();
24333
24774
  const server = createServer({ cert: certificate.certPem, key: certificate.keyPem }, (request, response) => {
24334
24775
  this.handleRequest(request, response);
@@ -24356,11 +24797,10 @@ class MobileGateway {
24356
24797
  };
24357
24798
  server.once("error", onFailure);
24358
24799
  server.once("listening", onListening);
24359
- server.listen(port, address);
24800
+ server.listen(port, plan.bindAddress);
24360
24801
  });
24361
24802
  this.server = server;
24362
24803
  this.wss = wss;
24363
- this.stateValue = { status: "listening", address, port };
24364
24804
  server.on("error", (error) => {
24365
24805
  this.stateValue = { status: "error", error: toMessage(error) };
24366
24806
  this.onError?.(error);
@@ -24368,6 +24808,9 @@ class MobileGateway {
24368
24808
  return;
24369
24809
  });
24370
24810
  });
24811
+ if (plan.mode === "wsl-relay")
24812
+ await this.startRelayFor(address, port, plan.bindAddress);
24813
+ this.stateValue = plan.mode === "wsl-relay" ? { status: "listening", address, port, mode: "wsl-relay", bindAddress: plan.bindAddress } : { status: "listening", address, port };
24371
24814
  } catch (error) {
24372
24815
  await this.stopListening().catch(() => {
24373
24816
  return;
@@ -24376,6 +24819,66 @@ class MobileGateway {
24376
24819
  throw error;
24377
24820
  }
24378
24821
  }
24822
+ async planListen(address) {
24823
+ if (this.wslEnvironment === undefined)
24824
+ return { mode: "direct", bindAddress: address };
24825
+ if (this.windowsAddresses === undefined)
24826
+ throw new Error(WSL_INTEROP_UNAVAILABLE);
24827
+ let candidates;
24828
+ try {
24829
+ candidates = await this.windowsAddresses();
24830
+ } catch (error) {
24831
+ throw new Error(`\u8BFB\u53D6 Windows \u5C40\u57DF\u7F51\u5730\u5740\u5931\u8D25\uFF1A${toMessage(error)}`);
24832
+ }
24833
+ if (!candidates.includes(address)) {
24834
+ 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`);
24835
+ }
24836
+ const interfaces = this.localInterfaces?.();
24837
+ if (wslOwnAddresses(interfaces).includes(address))
24838
+ return { mode: "direct", bindAddress: address };
24839
+ const bindAddress = primaryWslAddress(interfaces);
24840
+ if (bindAddress === undefined)
24841
+ throw new Error("WSL \u5185\u6CA1\u6709\u53EF\u7528\u7684\u7F51\u7EDC\u5730\u5740\uFF0C\u65E0\u6CD5\u5EFA\u7ACB\u624B\u673A\u5165\u53E3\u3002");
24842
+ return { mode: "wsl-relay", bindAddress };
24843
+ }
24844
+ async startRelayFor(address, port, bindAddress) {
24845
+ const deadline = Date.now() + this.relayPortRetryMs;
24846
+ let relay;
24847
+ for (;; ) {
24848
+ try {
24849
+ relay = await this.startRelay({
24850
+ powershell: this.wslEnvironment?.powershell ?? "powershell.exe",
24851
+ listenAddress: address,
24852
+ listenPort: port,
24853
+ targetAddress: bindAddress,
24854
+ targetPort: port
24855
+ });
24856
+ break;
24857
+ } catch (error) {
24858
+ if (error instanceof RelayError && error.code === "port_in_use" && Date.now() + this.relayPortRetryIntervalMs < deadline) {
24859
+ await new Promise((resolve) => setTimeout(resolve, this.relayPortRetryIntervalMs));
24860
+ continue;
24861
+ }
24862
+ throw new Error(describeRelayError(error, address, port, bindAddress));
24863
+ }
24864
+ }
24865
+ this.relay = relay;
24866
+ relay.exited.then((reason) => this.onRelayExited(relay, reason));
24867
+ }
24868
+ onRelayExited(relay, reason) {
24869
+ this.enqueue(async () => {
24870
+ if (this.relay !== relay)
24871
+ return;
24872
+ this.relay = undefined;
24873
+ this.pairingValue?.invalidate();
24874
+ await this.stopListening().catch(() => {
24875
+ return;
24876
+ });
24877
+ const message = `Windows \u4E2D\u7EE7\u610F\u5916\u9000\u51FA\uFF08${reason}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u5F00\u542F\u624B\u673A\u8BBF\u95EE\u3002`;
24878
+ this.stateValue = { status: "error", error: message };
24879
+ this.onError?.(new Error(message));
24880
+ });
24881
+ }
24379
24882
  async certificateValue() {
24380
24883
  if (this.certificate === undefined) {
24381
24884
  this.certificate = await MobileCertificate.load(this.requireStore(), this.serverId);
@@ -24383,6 +24886,12 @@ class MobileGateway {
24383
24886
  return this.certificate;
24384
24887
  }
24385
24888
  async stopListening() {
24889
+ const relay = this.relay;
24890
+ this.relay = undefined;
24891
+ if (relay !== undefined)
24892
+ await relay.stop().catch(() => {
24893
+ return;
24894
+ });
24386
24895
  for (const connection of [...this.connections.values()])
24387
24896
  connection.close(1001, "listener_stopped");
24388
24897
  const server = this.server;
@@ -24490,9 +24999,9 @@ ${JSON.stringify({ code, message })}`);
24490
24999
 
24491
25000
  // packages/gateway/src/port-file.ts
24492
25001
  import { readFile as readFile5, rm as rm2, writeFile as writeFile2 } from "fs/promises";
24493
- import { join as join8 } from "path";
25002
+ import { join as join9 } from "path";
24494
25003
  function gatewayPortFilePath(nekoDir, serverId) {
24495
- return join8(nekoDir, `${serverId}.port`);
25004
+ return join9(nekoDir, `${serverId}.port`);
24496
25005
  }
24497
25006
  async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
24498
25007
  await writeFile2(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
@@ -24516,7 +25025,7 @@ import {
24516
25025
  // packages/core/src/application-updates.ts
24517
25026
  import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
24518
25027
  import { homedir as homedir5 } from "os";
24519
- import { dirname as dirname5, join as join9 } from "path";
25028
+ import { dirname as dirname5, join as join10 } from "path";
24520
25029
  import { fileURLToPath as fileURLToPath2 } from "url";
24521
25030
  async function abortable(work, signal) {
24522
25031
  signal.throwIfAborted();
@@ -24580,7 +25089,7 @@ class UpdateChecker {
24580
25089
  return this.#work;
24581
25090
  }
24582
25091
  get #cachePath() {
24583
- return this.#options.cachePath ?? join9(homedir5(), ".neko", "update-check.json");
25092
+ return this.#options.cachePath ?? join10(homedir5(), ".neko", "update-check.json");
24584
25093
  }
24585
25094
  async#check(force) {
24586
25095
  await this.local();
@@ -25360,7 +25869,7 @@ function serveSessions(host, runtime, context) {
25360
25869
  import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
25361
25870
  import { mkdir as mkdir4, readdir as readdir2, rename as rename4, rm as rm4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
25362
25871
  import { homedir as homedir6 } from "os";
25363
- import { dirname as dirname6, join as join10 } from "path";
25872
+ import { dirname as dirname6, join as join11 } from "path";
25364
25873
 
25365
25874
  // node_modules/fflate/esm/index.mjs
25366
25875
  import { createRequire } from "module";
@@ -25783,8 +26292,8 @@ class SkillMarketError extends Error {
25783
26292
  var CATALOG_TTL_MS = 60000;
25784
26293
  var ZIP_CACHE_MAX = 16;
25785
26294
  var MAX_PACKAGE_BYTES = 32 * 1024 * 1024;
25786
- var userSkillsDir2 = (home) => join10(home, ".agents", "skills");
25787
- var projectSkillsDir = (workspaceRoot) => join10(workspaceRoot, ".agents", "skills");
26295
+ var userSkillsDir2 = (home) => join11(home, ".agents", "skills");
26296
+ var projectSkillsDir = (workspaceRoot) => join11(workspaceRoot, ".agents", "skills");
25788
26297
  var activeLevelOf = (state) => state.project ? "project" : state.user ? "user" : state.builtin ? "builtin" : null;
25789
26298
  var normalizeEntry = (raw) => {
25790
26299
  const row = raw ?? {};
@@ -25918,11 +26427,11 @@ function createSkillMarketClient(options) {
25918
26427
  } catch {
25919
26428
  return;
25920
26429
  }
25921
- await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm4(join10(parent, n), { recursive: true, force: true }).catch(() => {})));
26430
+ await Promise.all(names.filter((n) => n.startsWith(".tmp-")).map((n) => rm4(join11(parent, n), { recursive: true, force: true }).catch(() => {})));
25922
26431
  };
25923
26432
  const install = async ({ name, scope, workspaceRoot, overwrite }) => {
25924
26433
  const skillsDir = skillsDirFor(scope, workspaceRoot);
25925
- const target = join10(skillsDir, name);
26434
+ const target = join11(skillsDir, name);
25926
26435
  const exists = await stat2(target).catch(() => {
25927
26436
  return;
25928
26437
  }) !== undefined;
@@ -25931,10 +26440,10 @@ function createSkillMarketClient(options) {
25931
26440
  const skill = await detail(name);
25932
26441
  const files = unpack(skill, await download(skill));
25933
26442
  await sweepTempDirs(skillsDir);
25934
- const temp = join10(dirname6(skillsDir), `.tmp-${randomUUID3()}`);
26443
+ const temp = join11(dirname6(skillsDir), `.tmp-${randomUUID3()}`);
25935
26444
  try {
25936
26445
  for (const [relative, bytes] of files) {
25937
- const dest = join10(temp, ...relative.split("/"));
26446
+ const dest = join11(temp, ...relative.split("/"));
25938
26447
  await mkdir4(dirname6(dest), { recursive: true });
25939
26448
  await writeFile4(dest, bytes);
25940
26449
  }
@@ -25947,7 +26456,7 @@ function createSkillMarketClient(options) {
25947
26456
  }
25948
26457
  let shadowedBy;
25949
26458
  if (scope === "user" && workspaceRoot !== undefined && workspaceRoot !== "") {
25950
- const projectDir = join10(projectSkillsDir(workspaceRoot), name);
26459
+ const projectDir = join11(projectSkillsDir(workspaceRoot), name);
25951
26460
  if (await stat2(projectDir).catch(() => {
25952
26461
  return;
25953
26462
  }) !== undefined)
@@ -25957,7 +26466,7 @@ function createSkillMarketClient(options) {
25957
26466
  };
25958
26467
  const uninstall = async ({ name, scope, workspaceRoot }) => {
25959
26468
  const skillsDir = skillsDirFor(scope, workspaceRoot);
25960
- const target = join10(skillsDir, name);
26469
+ const target = join11(skillsDir, name);
25961
26470
  if (await stat2(target).catch(() => {
25962
26471
  return;
25963
26472
  }) === undefined)
@@ -25991,9 +26500,9 @@ function createSkillMarketClient(options) {
25991
26500
  }
25992
26501
 
25993
26502
  // packages/gateway/src/workspaces.ts
25994
- import { constants as constants2 } from "fs";
26503
+ import { constants as constants3 } from "fs";
25995
26504
  import { access, readdir as readdir3, stat as stat3 } from "fs/promises";
25996
- import { dirname as dirname7, isAbsolute as isAbsolute3, join as join11 } from "path";
26505
+ import { dirname as dirname7, isAbsolute as isAbsolute3, join as join12 } from "path";
25997
26506
  async function browseWorkspaces(options) {
25998
26507
  const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
25999
26508
  if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
@@ -26020,13 +26529,13 @@ async function browseWorkspaces(options) {
26020
26529
  let next = offset;
26021
26530
  for (;next < entries.length && directories.length < 100; next++) {
26022
26531
  const entry = entries[next];
26023
- const target = join11(path, entry.name);
26532
+ const target = join12(path, entry.name);
26024
26533
  if (!entry.isDirectory() && !entry.isSymbolicLink())
26025
26534
  continue;
26026
26535
  try {
26027
26536
  if (!(await stat3(target)).isDirectory())
26028
26537
  continue;
26029
- await access(target, constants2.R_OK | constants2.X_OK);
26538
+ await access(target, constants3.R_OK | constants3.X_OK);
26030
26539
  directories.push({ name: entry.name, path: target });
26031
26540
  } catch {
26032
26541
  directories.push({ name: entry.name, path: target, error: "\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE" });
@@ -26505,7 +27014,7 @@ async function startGateway(options = {}) {
26505
27014
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
26506
27015
  const nekoDir = dirname9(storagePath);
26507
27016
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
26508
- const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join12(nekoDir, `${serverId}.sock`);
27017
+ const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join13(nekoDir, `${serverId}.sock`);
26509
27018
  const runtime = await createNekoRuntime({
26510
27019
  context,
26511
27020
  onReport: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
@@ -26665,7 +27174,7 @@ function serverIdFromSocketPath(socketPath) {
26665
27174
  return isServerId(candidate) ? candidate : undefined;
26666
27175
  }
26667
27176
  async function loadOrCreateServerId(nekoDir) {
26668
- const file = join12(nekoDir, SERVER_ID_FILENAME);
27177
+ const file = join13(nekoDir, SERVER_ID_FILENAME);
26669
27178
  try {
26670
27179
  const existing = (await readFile7(file, "utf8")).trim();
26671
27180
  if (existing !== "")
@@ -26678,12 +27187,79 @@ async function loadOrCreateServerId(nekoDir) {
26678
27187
  return created;
26679
27188
  }
26680
27189
 
27190
+ // packages/gateway/src/shutdown.ts
27191
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
27192
+ function shutdownSignals(platform) {
27193
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
27194
+ if (platform === "win32")
27195
+ signals.push("SIGBREAK");
27196
+ return signals;
27197
+ }
27198
+ function installShutdown(options = {}) {
27199
+ const proc = options.process ?? process;
27200
+ const exit = options.exit ?? ((code) => process.exit(code));
27201
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
27202
+ const controller = new AbortController;
27203
+ let cleanup;
27204
+ let cleaning = false;
27205
+ let timer;
27206
+ const runCleanup = () => {
27207
+ if (cleaning || cleanup === undefined)
27208
+ return;
27209
+ cleaning = true;
27210
+ cleanup().then(() => exit(0), (error) => {
27211
+ options.log?.(error instanceof Error ? error.message : String(error));
27212
+ exit(1);
27213
+ });
27214
+ };
27215
+ const onSignal = () => {
27216
+ if (controller.signal.aborted) {
27217
+ options.log?.("\u518D\u6B21\u6536\u5230\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
27218
+ exit(130);
27219
+ return;
27220
+ }
27221
+ options.log?.(`
27222
+ \u6B63\u5728\u5173\u95ED\u2026`);
27223
+ controller.abort();
27224
+ timer = setTimeout(() => {
27225
+ options.log?.("\u5173\u95ED\u8D85\u65F6\uFF0C\u5F3A\u5236\u9000\u51FA\u3002");
27226
+ exit(1);
27227
+ }, timeoutMs);
27228
+ timer.unref?.();
27229
+ runCleanup();
27230
+ };
27231
+ const installed = [];
27232
+ for (const signal of shutdownSignals(proc.platform)) {
27233
+ try {
27234
+ proc.on(signal, onSignal);
27235
+ installed.push(signal);
27236
+ } catch {}
27237
+ }
27238
+ return {
27239
+ signal: controller.signal,
27240
+ setCleanup(next) {
27241
+ cleanup = next;
27242
+ if (controller.signal.aborted)
27243
+ runCleanup();
27244
+ },
27245
+ dispose() {
27246
+ clearTimeout(timer);
27247
+ for (const signal of installed)
27248
+ proc.off(signal, onSignal);
27249
+ }
27250
+ };
27251
+ }
27252
+
26681
27253
  // packages/gateway/src/main.ts
26682
27254
  var argv = process.argv.slice(2);
26683
27255
  if (argv.includes("-h") || argv.includes("--help")) {
26684
27256
  console.log(GATEWAY_USAGE);
26685
27257
  process.exit(0);
26686
27258
  }
27259
+ var shutdown = installShutdown({
27260
+ log: (message) => console.log(message),
27261
+ exit: (code) => process.exit(code)
27262
+ });
26687
27263
  var options = parseGatewayArgs(argv);
26688
27264
  var gateway = await startGateway({
26689
27265
  ...options,
@@ -26695,15 +27271,4 @@ if (gateway.socketPath !== undefined)
26695
27271
  if (gateway.wsUrl !== undefined)
26696
27272
  console.log(` WebSocket: ${gateway.wsUrl}`);
26697
27273
  console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
26698
- var stopping = false;
26699
- var stop = () => {
26700
- if (stopping)
26701
- return;
26702
- stopping = true;
26703
- gateway.close().then(() => process.exit(0)).catch((error) => {
26704
- console.error("\u5173\u95ED\u5931\u8D25:", error instanceof Error ? error.message : String(error));
26705
- process.exit(1);
26706
- });
26707
- };
26708
- process.on("SIGINT", stop);
26709
- process.on("SIGTERM", stop);
27274
+ shutdown.setCleanup(() => gateway.close());