@astralyn/sash 0.1.0 → 0.1.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 (108) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +22 -7
  3. package/THIRD_PARTY_NOTICES.md +9 -0
  4. package/dist/api.js +14 -45
  5. package/dist/app-state.js +99 -0
  6. package/dist/autostart/command.js +24 -0
  7. package/dist/autostart/context.js +43 -0
  8. package/dist/autostart/files.js +60 -0
  9. package/dist/autostart/installation.js +48 -0
  10. package/dist/autostart/start.js +47 -0
  11. package/dist/autostart/windows-registry.js +88 -0
  12. package/dist/autostart/windows.js +59 -0
  13. package/dist/autostart-contract.js +31 -0
  14. package/dist/autostart-entry.js +10 -0
  15. package/dist/autostart.js +69 -0
  16. package/dist/cli.js +11 -11
  17. package/dist/commands/auto.js +15 -0
  18. package/dist/commands/lifecycle.js +5 -31
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +6 -7
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +19 -27
  24. package/dist/contracts.js +171 -335
  25. package/dist/core-config-validation.js +22 -38
  26. package/dist/core-update.js +126 -762
  27. package/dist/core.js +20 -46
  28. package/dist/daemon/app.js +132 -113
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +6 -1
  32. package/dist/daemon/handlers/autostart.js +18 -0
  33. package/dist/daemon/handlers/core.js +32 -9
  34. package/dist/daemon/handlers/daemon.js +32 -6
  35. package/dist/daemon/handlers/profiles.js +12 -2
  36. package/dist/daemon/handlers/settings.js +9 -36
  37. package/dist/daemon/router.js +40 -27
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +6 -3
  40. package/dist/daemon/web-auth.js +52 -0
  41. package/dist/daemon-auth.js +2 -2
  42. package/dist/daemon-client.js +18 -2
  43. package/dist/daemon-http.js +7 -0
  44. package/dist/daemon-lifecycle.js +19 -196
  45. package/dist/github.js +7 -2
  46. package/dist/http.js +7 -5
  47. package/dist/log-follow.js +2 -1
  48. package/dist/mihomo-config.js +7 -9
  49. package/dist/paths.js +5 -7
  50. package/dist/profile-model.js +87 -0
  51. package/dist/profile-service.js +250 -587
  52. package/dist/profiles.js +43 -171
  53. package/dist/runtime-lifecycle.js +112 -194
  54. package/dist/runtime-owner.js +37 -76
  55. package/dist/sash-client.js +63 -21
  56. package/dist/settings-service.js +29 -228
  57. package/dist/settings.js +61 -285
  58. package/dist/status.js +24 -40
  59. package/dist/supervisor.js +1 -14
  60. package/dist/sysproxy/common.js +5 -45
  61. package/dist/sysproxy/factory.js +24 -65
  62. package/dist/sysproxy/snapshot.js +24 -257
  63. package/dist/sysproxy.js +1 -4
  64. package/dist/system-proxy-manager.js +37 -35
  65. package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  66. package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
  67. package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-cj1tcZej.js} +1 -1
  68. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  69. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  70. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  71. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  72. package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CtKvlckZ.js} +1 -1
  73. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  74. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  75. package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
  76. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  77. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  78. package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
  79. package/dist/ui/index.html +2 -2
  80. package/dist/web-bootstrap.js +113 -0
  81. package/docs/architecture-proposal.md +109 -0
  82. package/docs/autostart.md +101 -0
  83. package/docs/backend.md +92 -216
  84. package/docs/frontend.md +61 -97
  85. package/docs/usage.md +88 -186
  86. package/package.json +10 -6
  87. package/dist/commands/upgrade.js +0 -43
  88. package/dist/core-install-transaction.js +0 -114
  89. package/dist/core-update-coordination.js +0 -105
  90. package/dist/core-update-service.js +0 -245
  91. package/dist/managed-state-transaction.js +0 -377
  92. package/dist/offline-mutation.js +0 -58
  93. package/dist/profile-migration.js +0 -101
  94. package/dist/runtime-recovery.js +0 -31
  95. package/dist/sysproxy/darwin.js +0 -287
  96. package/dist/sysproxy/gnome.js +0 -181
  97. package/dist/sysproxy/legacy.js +0 -58
  98. package/dist/tun-guidance.js +0 -11
  99. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  100. package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
  101. package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
  102. package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
  103. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  104. package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
  105. package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
  106. package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
  107. package/dist/ui/assets/index-B61V60w_.js +0 -19
  108. package/dist/ui/assets/index-bkyxJG8J.css +0 -1
package/dist/settings.js CHANGED
@@ -1,301 +1,29 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import { atomicWriteFileSync } from "./fs-atomic.js";
4
2
  import { isPlainObject } from "./json-shape.js";
5
- import { sashLayout } from "./paths.js";
6
- import { acquireStateLockSync } from "./state-lock.js";
7
- const PUBLIC_SETTINGS_KEYS = [
8
- "mixedPort",
9
- "controller",
10
- "tun",
11
- "allowLan",
12
- "daemonPort",
13
- "systemProxy",
14
- ];
15
- function exhaustiveKeys() {
16
- return (keys) => keys;
17
- }
18
- const CANONICAL_SETTINGS_KEYS = exhaustiveKeys()([
19
- "schemaVersion",
20
- "subscriptionUrl",
21
- "mixedPort",
22
- "controller",
23
- "secret",
24
- "tun",
25
- "allowLan",
26
- "daemonPort",
27
- "daemonSecret",
28
- "systemProxy",
29
- ]);
30
- const LEGACY_SETTINGS_KEYS = ["coreVersion", "uiVersion"];
31
- const ACCEPTED_SETTINGS_KEYS = new Set([
32
- ...CANONICAL_SETTINGS_KEYS,
33
- ...LEGACY_SETTINGS_KEYS,
34
- ]);
35
- export function publicSettings(settings) {
36
- return {
37
- mixedPort: settings.mixedPort,
38
- controller: settings.controller,
39
- tun: settings.tun,
40
- allowLan: settings.allowLan,
41
- daemonPort: settings.daemonPort,
42
- systemProxy: settings.systemProxy,
43
- };
44
- }
45
- export function sameSettings(a, b) {
46
- return CANONICAL_SETTINGS_KEYS.every((key) => a[key] === b[key]);
47
- }
3
+ const development = process.env.SASH_DEVELOPMENT === "1";
48
4
  export const DEFAULT_SETTINGS = {
49
- schemaVersion: 1,
50
- mixedPort: 7890,
51
- controller: "127.0.0.1:9090",
5
+ mixedPort: development ? 18890 : 7890,
6
+ controller: development ? "127.0.0.1:18990" : "127.0.0.1:9090",
52
7
  secret: "",
53
- tun: false,
54
8
  allowLan: false,
55
- daemonPort: 19090,
9
+ daemonPort: development ? 28990 : 19090,
56
10
  daemonSecret: "",
57
11
  systemProxy: false,
58
12
  };
59
- function hasOwn(object, key) {
60
- return Object.hasOwn(object, key);
61
- }
62
- function invalidSettings(file, message) {
63
- return new Error(`Settings are invalid: ${file}: ${message}`);
64
- }
65
- function readRequiredField(source, key, defaultValue, allowMissing, file, parse) {
66
- if (!hasOwn(source, key)) {
67
- if (!allowMissing)
68
- throw invalidSettings(file, `${key} is required`);
69
- return { value: defaultValue, missing: true };
70
- }
71
- const value = source[key];
72
- if (value === null)
73
- throw invalidSettings(file, `${key} must not be null`);
74
- return { value: parse(value, key, file), missing: false };
75
- }
76
- function readOptionalString(source, key, file) {
77
- if (!hasOwn(source, key))
78
- return undefined;
79
- const value = source[key];
80
- if (value === null)
81
- throw invalidSettings(file, `${key} must not be null`);
82
- return parseString(value, key, file);
83
- }
84
- function parseString(value, key, file) {
85
- if (typeof value !== "string")
86
- throw invalidSettings(file, `${key} must be a string`);
87
- return value;
88
- }
89
- function hasSecretControlCharacter(secret) {
90
- for (const char of secret) {
91
- const code = char.charCodeAt(0);
92
- if (code <= 31 || code === 127)
93
- return true;
94
- }
95
- return false;
96
- }
97
- function parseSecret(value, key, file) {
98
- const secret = parseString(value, key, file);
99
- if (!secret.trim())
100
- throw invalidSettings(file, `${key} must not be blank`);
101
- if (hasSecretControlCharacter(secret)) {
102
- throw invalidSettings(file, `${key} must not contain control characters`);
103
- }
104
- return secret;
105
- }
106
- function parseBoolean(value, key, file) {
107
- if (typeof value !== "boolean")
108
- throw invalidSettings(file, `${key} must be a boolean`);
109
- return value;
110
- }
111
- function parsePort(value, key, file) {
112
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 65_535) {
113
- throw invalidSettings(file, `${key} must be an integer from 1 to 65535`);
114
- }
115
- return value;
116
- }
117
- function parseController(value, key, file) {
118
- const controller = parseString(value, key, file);
119
- const address = parseControllerAddress(controller);
120
- if (!address) {
121
- throw invalidSettings(file, `${key} must be a loopback host:port controller address`);
122
- }
123
- return address.canonical;
124
- }
125
- function validateSettingsRelations(settings, file) {
126
- const controller = parseControllerAddress(settings.controller);
127
- if (!controller) {
128
- throw invalidSettings(file, "controller must be a loopback host:port controller address");
129
- }
130
- const ports = [
131
- ["mixedPort", settings.mixedPort],
132
- ["daemonPort", settings.daemonPort],
133
- ["controller", controller.port],
134
- ];
135
- for (let i = 0; i < ports.length; i++) {
136
- for (let j = i + 1; j < ports.length; j++) {
137
- if (ports[i]?.[1] === ports[j]?.[1]) {
138
- throw invalidSettings(file, `${ports[i]?.[0]} and ${ports[j]?.[0]} must use different ports`);
139
- }
140
- }
141
- }
142
- }
143
- function parseSchemaVersion(source, allowMissing, file) {
144
- if (!hasOwn(source, "schemaVersion")) {
145
- if (!allowMissing)
146
- throw invalidSettings(file, "schemaVersion is required");
147
- return { value: 0, missing: true };
148
- }
149
- const value = source.schemaVersion;
150
- if (value === null)
151
- throw invalidSettings(file, "schemaVersion must not be null");
152
- if (typeof value !== "number" || !Number.isInteger(value)) {
153
- throw invalidSettings(file, "schemaVersion must be an integer");
154
- }
155
- if (value === 0 || value === 1)
156
- return { value, missing: false };
157
- if (value > 1) {
158
- throw invalidSettings(file, `schemaVersion ${value} is from a future version`);
159
- }
160
- throw invalidSettings(file, "schemaVersion must be 0 or 1");
161
- }
162
- function parseSettings(document, file, allowMissing) {
163
- if (!isPlainObject(document)) {
164
- throw invalidSettings(file, "JSON root must be a plain object");
165
- }
166
- for (const key of Object.keys(document)) {
167
- if (!ACCEPTED_SETTINGS_KEYS.has(key)) {
168
- throw invalidSettings(file, `unknown field ${JSON.stringify(key)}`);
169
- }
170
- }
171
- const schemaVersion = parseSchemaVersion(document, allowMissing, file);
172
- const subscriptionUrl = readOptionalString(document, "subscriptionUrl", file);
173
- const mixedPort = readRequiredField(document, "mixedPort", DEFAULT_SETTINGS.mixedPort, allowMissing, file, parsePort);
174
- const controller = readRequiredField(document, "controller", DEFAULT_SETTINGS.controller, allowMissing, file, parseController);
175
- const secret = readRequiredField(document, "secret", DEFAULT_SETTINGS.secret, allowMissing, file, parseSecret);
176
- const tun = readRequiredField(document, "tun", DEFAULT_SETTINGS.tun, allowMissing, file, parseBoolean);
177
- const allowLan = readRequiredField(document, "allowLan", DEFAULT_SETTINGS.allowLan, allowMissing, file, parseBoolean);
178
- const daemonPort = readRequiredField(document, "daemonPort", DEFAULT_SETTINGS.daemonPort, allowMissing, file, parsePort);
179
- const daemonSecret = readRequiredField(document, "daemonSecret", DEFAULT_SETTINGS.daemonSecret, allowMissing, file, parseSecret);
180
- const systemProxy = readRequiredField(document, "systemProxy", DEFAULT_SETTINGS.systemProxy, allowMissing, file, parseBoolean);
181
- let needsRewrite = schemaVersion.missing ||
182
- schemaVersion.value === 0 ||
183
- mixedPort.missing ||
184
- controller.missing ||
185
- controller.value !== document.controller ||
186
- secret.missing ||
187
- tun.missing ||
188
- allowLan.missing ||
189
- daemonPort.missing ||
190
- daemonSecret.missing ||
191
- systemProxy.missing;
192
- for (const key of LEGACY_SETTINGS_KEYS) {
193
- if (!hasOwn(document, key))
194
- continue;
195
- const value = document[key];
196
- if (value === null)
197
- throw invalidSettings(file, `${key} must not be null`);
198
- parseString(value, key, file);
199
- needsRewrite = true;
200
- }
201
- const normalizedSecret = secret.missing ? generateSecret() : secret.value;
202
- const normalizedDaemonSecret = daemonSecret.missing ? generateSecret() : daemonSecret.value;
203
- const settings = {
204
- schemaVersion: 1,
205
- mixedPort: mixedPort.value,
206
- controller: controller.value,
207
- secret: normalizedSecret,
208
- tun: tun.value,
209
- allowLan: allowLan.value,
210
- daemonPort: daemonPort.value,
211
- daemonSecret: normalizedDaemonSecret,
212
- systemProxy: systemProxy.value,
213
- };
214
- if (subscriptionUrl !== undefined)
215
- settings.subscriptionUrl = subscriptionUrl;
216
- validateSettingsRelations(settings, file);
217
- return { settings, needsRewrite };
13
+ export function generateSecret() {
14
+ return crypto.randomBytes(24).toString("hex");
218
15
  }
219
- function readSettingsFile(file) {
220
- let text;
221
- try {
222
- text = fs.readFileSync(file, "utf8");
223
- }
224
- catch (err) {
225
- if (err.code === "ENOENT") {
226
- return { exists: false, document: {} };
227
- }
228
- throw err;
229
- }
230
- try {
231
- return { exists: true, document: JSON.parse(text) };
232
- }
233
- catch (err) {
234
- throw new Error(`Settings file is invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`);
235
- }
16
+ export function initialSettings() {
17
+ return { ...DEFAULT_SETTINGS, secret: generateSecret(), daemonSecret: generateSecret() };
236
18
  }
237
- function canonicalSettings(settings) {
238
- const canonical = {
239
- schemaVersion: 1,
19
+ export function publicSettings(settings) {
20
+ return {
240
21
  mixedPort: settings.mixedPort,
241
22
  controller: settings.controller,
242
- secret: settings.secret,
243
- tun: settings.tun,
244
23
  allowLan: settings.allowLan,
245
24
  daemonPort: settings.daemonPort,
246
- daemonSecret: settings.daemonSecret,
247
25
  systemProxy: settings.systemProxy,
248
26
  };
249
- if (settings.subscriptionUrl !== undefined)
250
- canonical.subscriptionUrl = settings.subscriptionUrl;
251
- return canonical;
252
- }
253
- function writeCanonicalSettings(settings, layout) {
254
- atomicWriteFileSync(layout.settingsFile, `${JSON.stringify(canonicalSettings(settings), null, 2)}\n`);
255
- }
256
- function loadSettingsUnlocked(layout) {
257
- const source = readSettingsFile(layout.settingsFile);
258
- const parsed = parseSettings(source.document, layout.settingsFile, true);
259
- if (!source.exists || parsed.needsRewrite) {
260
- writeCanonicalSettings(parsed.settings, layout);
261
- }
262
- return parsed.settings;
263
- }
264
- export function generateSecret() {
265
- return crypto.randomBytes(24).toString("hex");
266
- }
267
- export function loadSettings(layout = sashLayout()) {
268
- const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
269
- try {
270
- return loadSettingsUnlocked(layout);
271
- }
272
- finally {
273
- lease.release();
274
- }
275
- }
276
- export function saveSettings(settings, layout = sashLayout()) {
277
- const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
278
- try {
279
- const parsed = parseSettings(settings, layout.settingsFile, false);
280
- writeCanonicalSettings(parsed.settings, layout);
281
- return parsed.settings;
282
- }
283
- finally {
284
- lease.release();
285
- }
286
- }
287
- export function parseSettingsText(text, file = "sash.json") {
288
- let document;
289
- try {
290
- document = JSON.parse(text);
291
- }
292
- catch (err) {
293
- throw new Error(`Settings file is invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
294
- }
295
- return parseSettings(document, file, false).settings;
296
- }
297
- export function validateSettingsCandidate(candidate) {
298
- return parseSettings(candidate, "candidate settings", false).settings;
299
27
  }
300
28
  export function parseControllerAddress(value) {
301
29
  const match = value.trim().match(/^(127\.0\.0\.1|localhost|\[::1\]):(\d+)$/i);
@@ -303,12 +31,60 @@ export function parseControllerAddress(value) {
303
31
  const rawPort = match?.[2];
304
32
  if (!rawHost || !rawPort)
305
33
  return undefined;
306
- const port = Number.parseInt(rawPort, 10);
34
+ const port = Number(rawPort);
307
35
  if (port < 1 || port > 65_535 || String(port) !== rawPort)
308
36
  return undefined;
309
- if (rawHost === "[::1]") {
37
+ if (rawHost === "[::1]")
310
38
  return { host: "::1", port, canonical: `[::1]:${port}` };
311
- }
312
39
  const host = rawHost === "localhost" ? "localhost" : "127.0.0.1";
313
40
  return { host, port, canonical: `${host}:${port}` };
314
41
  }
42
+ export function validateSettingsCandidate(value) {
43
+ if (!isPlainObject(value))
44
+ throw new Error("Settings must be a plain object");
45
+ const keys = Object.keys(DEFAULT_SETTINGS);
46
+ if (Object.keys(value).some((key) => !keys.includes(key))) {
47
+ throw new Error("Settings contain an unknown field");
48
+ }
49
+ for (const key of keys) {
50
+ if (!Object.hasOwn(value, key))
51
+ throw new Error(`Settings field ${key} is required`);
52
+ }
53
+ const port = (key) => {
54
+ const number = value[key];
55
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > 65_535) {
56
+ throw new Error(`${key} must be an integer from 1 to 65535`);
57
+ }
58
+ return number;
59
+ };
60
+ const secret = (key) => {
61
+ const text = value[key];
62
+ if (typeof text !== "string" || !text.trim())
63
+ throw new Error(`${key} must not be blank`);
64
+ if ([...text].some((char) => char.charCodeAt(0) <= 31 || char.charCodeAt(0) === 127)) {
65
+ throw new Error(`${key} must not contain control characters`);
66
+ }
67
+ return text;
68
+ };
69
+ const boolean = (key) => {
70
+ if (typeof value[key] !== "boolean")
71
+ throw new Error(`${key} must be a boolean`);
72
+ return value[key];
73
+ };
74
+ const controller = typeof value.controller === "string" && parseControllerAddress(value.controller);
75
+ if (!controller)
76
+ throw new Error("controller must be a loopback host:port address");
77
+ const result = {
78
+ mixedPort: port("mixedPort"),
79
+ controller: controller.canonical,
80
+ secret: secret("secret"),
81
+ allowLan: boolean("allowLan"),
82
+ daemonPort: port("daemonPort"),
83
+ daemonSecret: secret("daemonSecret"),
84
+ systemProxy: boolean("systemProxy"),
85
+ };
86
+ if (new Set([result.mixedPort, result.daemonPort, controller.port]).size !== 3) {
87
+ throw new Error("mixedPort, daemonPort and controller must use different ports");
88
+ }
89
+ return result;
90
+ }
package/dist/status.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { AutostartService } from "./autostart.js";
1
2
  import { currentCoreVersion } from "./core.js";
2
3
  import { SashDaemonClient } from "./daemon-client.js";
3
4
  import { evaluateDaemon, } from "./daemon-lifecycle.js";
4
- import { ProfileService } from "./profile-service.js";
5
+ import { getActiveProfile, loadProfiles } from "./profiles.js";
5
6
  import { SystemProxyManager } from "./system-proxy-manager.js";
6
7
  import { uiInstalled } from "./webui.js";
7
- export const CLI_STATUS_SCHEMA_VERSION = 1;
8
+ export const CLI_STATUS_SCHEMA_VERSION = 2;
8
9
  function errorText(err) {
9
10
  const message = err instanceof Error ? err.message : String(err);
10
11
  return (message
@@ -106,16 +107,13 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
106
107
  : currentCoreVersion(context.layout);
107
108
  const profile = dependencies.activeProfile
108
109
  ? dependencies.activeProfile(context)
109
- : new ProfileService({
110
- layout: context.layout,
111
- settings: () => context.settings,
112
- }).active();
110
+ : getActiveProfile(loadProfiles(context.layout));
113
111
  let coreRunning = daemonState.running ? null : false;
114
112
  let coreHealthy = daemonState.running ? null : false;
115
113
  let corePid = null;
116
114
  let coreVersion = null;
117
- let tunActive = daemonState.running ? null : false;
118
115
  let desiredProxy = context.settings.systemProxy;
116
+ let mixedEndpoint = `127.0.0.1:${context.settings.mixedPort}`;
119
117
  let proxySource;
120
118
  let queriedDaemon = false;
121
119
  if (daemonState.kind === "healthy") {
@@ -124,6 +122,11 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
124
122
  queriedDaemon = true;
125
123
  daemon = daemonObservation(daemonState, "healthy");
126
124
  desiredProxy = status.systemProxy.desired;
125
+ mixedEndpoint = status.core.running
126
+ ? status.configuration.appliedSettings
127
+ ? `127.0.0.1:${status.configuration.appliedSettings.mixedPort}`
128
+ : "unknown"
129
+ : `127.0.0.1:${status.settings.mixedPort}`;
127
130
  proxySource = {
128
131
  applied: status.systemProxy.applied,
129
132
  appliedKnown: status.systemProxy.appliedKnown,
@@ -137,7 +140,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
137
140
  else if (!status.core.running) {
138
141
  coreRunning = false;
139
142
  coreHealthy = false;
140
- tunActive = false;
141
143
  }
142
144
  else {
143
145
  coreRunning = true;
@@ -147,7 +149,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
147
149
  typeof status.core.version === "string" && status.core.version
148
150
  ? status.core.version
149
151
  : null;
150
- tunActive = typeof status.core.tunActive === "boolean" ? status.core.tunActive : null;
151
152
  if (coreHealthy === null)
152
153
  addError(errors, "Core health is unavailable");
153
154
  else if (!coreHealthy)
@@ -156,8 +157,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
156
157
  addError(errors, "Core PID is unavailable");
157
158
  if (coreVersion === null)
158
159
  addError(errors, "Core runtime version is unavailable");
159
- if (tunActive === null)
160
- addError(errors, "Core TUN state is unavailable");
161
160
  }
162
161
  }
163
162
  catch (err) {
@@ -179,11 +178,24 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
179
178
  : null;
180
179
  const activeProfile = profile ? { id: profile.id, name: profile.name, url: profile.url } : null;
181
180
  const daemonPort = daemon.port || context.settings.daemonPort;
181
+ let autostart;
182
+ try {
183
+ autostart = await (dependencies.inspectAutostart
184
+ ? dependencies.inspectAutostart(context)
185
+ : new AutostartService({ layout: context.layout }).inspect());
186
+ }
187
+ catch (error) {
188
+ autostart = { state: "unknown", canEnable: false, reason: errorText(error) };
189
+ }
190
+ if (autostart.state === "unknown") {
191
+ addError(errors, `Autostart query failed: ${autostart.reason ?? "unknown error"}`);
192
+ }
182
193
  return {
183
194
  schemaVersion: CLI_STATUS_SCHEMA_VERSION,
184
195
  complete: errors.length === 0,
185
196
  healthy,
186
197
  queryError: errors.length > 0 ? errors.join("; ") : null,
198
+ autostart,
187
199
  daemon: { ...daemon, port: daemonPort },
188
200
  core: {
189
201
  running: coreRunning,
@@ -199,16 +211,12 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
199
211
  },
200
212
  uiInstalled: dependencies.hasUi ? dependencies.hasUi(context) : uiInstalled(context.layout),
201
213
  endpoints: {
202
- mixedProxy: `127.0.0.1:${context.settings.mixedPort}`,
214
+ mixedProxy: mixedEndpoint,
203
215
  controller: context.settings.controller,
204
216
  daemonApi: `http://127.0.0.1:${daemonPort}`,
205
217
  dashboard: `http://127.0.0.1:${daemonPort}/ui/`,
206
218
  },
207
219
  activeProfile,
208
- tun: {
209
- desired: context.settings.tun,
210
- active: tunActive,
211
- },
212
220
  paths: {
213
221
  root: context.layout.root,
214
222
  config: context.layout.configFile,
@@ -243,30 +251,6 @@ export function runtimeStatusHeadline(status) {
243
251
  text: `sashd running (PID=${status.daemon.pid}), core stopped`,
244
252
  };
245
253
  }
246
- export function formatTunObservation(status) {
247
- if (!status.tun.desired) {
248
- if (status.tun.active === true)
249
- return "off (runtime active)";
250
- return status.tun.active === null && status.core.running !== false
251
- ? "off (runtime unknown)"
252
- : "off";
253
- }
254
- if (status.core.running === false)
255
- return "on (core stopped)";
256
- if (status.core.running === null)
257
- return "on (runtime unknown)";
258
- if (status.tun.active === true)
259
- return "on (active)";
260
- if (status.tun.active === false)
261
- return "on (inactive)";
262
- return "on (unverified)";
263
- }
264
- export function shouldShowTunGuidance(status) {
265
- return (status.tun.desired &&
266
- status.core.running === true &&
267
- status.core.healthy === true &&
268
- status.tun.active !== true);
269
- }
270
254
  export function formatObservedProxy(state) {
271
255
  if (state.enabled === null)
272
256
  return "unknown";
@@ -79,14 +79,6 @@ export class CoreSupervisor {
79
79
  }
80
80
  return terminated;
81
81
  }
82
- async probeTunActive(api) {
83
- try {
84
- return await api.getTunActive();
85
- }
86
- catch {
87
- return undefined;
88
- }
89
- }
90
82
  async start() {
91
83
  if (this.child && this.isAlive(this.child.pid ?? -1)) {
92
84
  throw new Error(`Core is already running (PID=${this.child.pid})`);
@@ -166,12 +158,10 @@ export class CoreSupervisor {
166
158
  }
167
159
  healthyProbes++;
168
160
  if (healthyProbes >= 2 && this.isAlive(pid)) {
169
- const tunActive = await this.probeTunActive(api);
170
161
  if (this.isAlive(pid)) {
171
162
  return {
172
163
  pid,
173
164
  version,
174
- ...(tunActive !== undefined ? { tunActive } : {}),
175
165
  };
176
166
  }
177
167
  }
@@ -241,13 +231,11 @@ export class CoreSupervisor {
241
231
  const api = new MihomoApi(settings.controller, settings.secret);
242
232
  let healthy = false;
243
233
  let version;
244
- let tunActive;
245
234
  try {
246
- version = await api.version();
235
+ version = await api.version({ deadlineMs: 1000, attempts: 1 });
247
236
  healthy = true;
248
237
  if (!this.ownsCore(ownership))
249
238
  return { running: false };
250
- tunActive = await this.probeTunActive(api);
251
239
  }
252
240
  catch {
253
241
  healthy = false;
@@ -260,7 +248,6 @@ export class CoreSupervisor {
260
248
  startedAt: this.childStartedAt,
261
249
  healthy,
262
250
  version,
263
- ...(tunActive !== undefined ? { tunActive } : {}),
264
251
  };
265
252
  }
266
253
  isRunning() {
@@ -1,9 +1,8 @@
1
1
  import { errorMessage } from "../error-utils.js";
2
2
  import { isPlainObject } from "../json-shape.js";
3
- import { findExecutableOnPath, runSanitizedCommandAsync, windowsSystemExecutable, } from "../process.js";
3
+ import { runSanitizedCommandAsync, windowsSystemExecutable } from "../process.js";
4
4
  import { DEFAULT_BYPASS_LIST } from "./types.js";
5
5
  const MAX_PROXY_STRING_LENGTH = 4096;
6
- const MAX_SERVICE_NAME_LENGTH = 512;
7
6
  export { errorMessage, isPlainObject };
8
7
  export function parseProxyString(value, label, allowEmpty = true) {
9
8
  if (typeof value !== "string") {
@@ -20,19 +19,6 @@ export function parseProxyString(value, label, allowEmpty = true) {
20
19
  }
21
20
  return value;
22
21
  }
23
- export function parseServiceName(value, label) {
24
- const service = parseProxyString(value, label, false);
25
- if (service.length > MAX_SERVICE_NAME_LENGTH) {
26
- throw new Error(`Invalid system proxy snapshot: ${label} has an invalid length`);
27
- }
28
- return service;
29
- }
30
- export function parseSnapshotPort(value, label) {
31
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65_535) {
32
- throw new Error(`Invalid system proxy snapshot: ${label} must be an integer from 0 to 65535`);
33
- }
34
- return value;
35
- }
36
22
  export function parseProxyEnable(value) {
37
23
  if (value === null)
38
24
  return null;
@@ -41,37 +27,8 @@ export function parseProxyEnable(value) {
41
27
  }
42
28
  return value;
43
29
  }
44
- export function compareStrings(a, b) {
45
- if (a < b)
46
- return -1;
47
- if (a > b)
48
- return 1;
49
- return 0;
50
- }
51
- export function isSystemProxySupported(platform = process.platform) {
52
- if (platform === "win32" || platform === "darwin")
53
- return true;
54
- if (platform !== "linux")
55
- return false;
56
- return findExecutableOnPath("gsettings") !== undefined;
57
- }
58
- function resolveSystemProxyCommand(command) {
59
- if (process.platform === "win32" && command.toLowerCase() === "reg.exe") {
60
- return windowsSystemExecutable("reg.exe");
61
- }
62
- if (process.platform === "darwin" && command === "networksetup") {
63
- return "/usr/sbin/networksetup";
64
- }
65
- if (process.platform === "linux" && command === "gsettings") {
66
- const resolved = findExecutableOnPath(command);
67
- if (!resolved)
68
- throw new Error("gsettings is not available on this system");
69
- return resolved;
70
- }
71
- return command;
72
- }
73
30
  export function runCmd(cmd, args, timeoutMs = 5000) {
74
- return runSanitizedCommandAsync(resolveSystemProxyCommand(cmd), args, { timeoutMs });
31
+ return runSanitizedCommandAsync(cmd.toLowerCase() === "reg.exe" ? windowsSystemExecutable("reg.exe") : cmd, args, { timeoutMs });
75
32
  }
76
33
  export function normalizeEnableOptions(opts) {
77
34
  if (!isPlainObject(opts)) {
@@ -97,3 +54,6 @@ export function normalizeEnableOptions(opts) {
97
54
  export function formatHostPort(host, port) {
98
55
  return host.includes(":") && !host.startsWith("[") ? `[${host}]:${port}` : `${host}:${port}`;
99
56
  }
57
+ export function isSystemProxySupported(platform = process.platform) {
58
+ return platform === "win32";
59
+ }