@astralyn/sash 0.1.1 → 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 (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +17 -5
  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 -15
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +5 -1
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +14 -50
  24. package/dist/contracts.js +168 -348
  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 +129 -117
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +3 -0
  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 +17 -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 +26 -21
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +5 -2
  40. package/dist/daemon-client.js +13 -3
  41. package/dist/daemon-http.js +6 -0
  42. package/dist/daemon-lifecycle.js +19 -196
  43. package/dist/github.js +7 -2
  44. package/dist/http.js +7 -5
  45. package/dist/mihomo-config.js +2 -1
  46. package/dist/paths.js +5 -7
  47. package/dist/profile-model.js +87 -0
  48. package/dist/profile-service.js +250 -587
  49. package/dist/profiles.js +43 -171
  50. package/dist/runtime-lifecycle.js +112 -194
  51. package/dist/runtime-owner.js +37 -76
  52. package/dist/sash-client.js +40 -19
  53. package/dist/settings-service.js +29 -224
  54. package/dist/settings.js +61 -289
  55. package/dist/status.js +24 -16
  56. package/dist/supervisor.js +1 -14
  57. package/dist/sysproxy/common.js +5 -45
  58. package/dist/sysproxy/factory.js +24 -65
  59. package/dist/sysproxy/snapshot.js +24 -257
  60. package/dist/sysproxy.js +1 -4
  61. package/dist/system-proxy-manager.js +37 -35
  62. package/dist/ui/assets/{ConnectionsView-CfaOG2JV.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  63. package/dist/ui/assets/{LogsView-BDiRATXN.js → LogsView-2f542P8z.js} +1 -1
  64. package/dist/ui/assets/{PaginationFooter-DTytr1iu.js → PaginationFooter-cj1tcZej.js} +1 -1
  65. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  66. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  67. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  68. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  69. package/dist/ui/assets/{RulesView-CjEyCN1A.js → RulesView-CtKvlckZ.js} +1 -1
  70. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  71. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  72. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  73. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  74. package/dist/ui/assets/{theme-BwDBMsKO.js → theme-D40MF8cQ.js} +1 -1
  75. package/dist/ui/index.html +2 -2
  76. package/docs/architecture-proposal.md +109 -0
  77. package/docs/autostart.md +101 -0
  78. package/docs/backend.md +92 -224
  79. package/docs/frontend.md +60 -106
  80. package/docs/usage.md +92 -164
  81. package/package.json +3 -3
  82. package/dist/commands/upgrade.js +0 -43
  83. package/dist/core-install-transaction.js +0 -114
  84. package/dist/core-update-coordination.js +0 -105
  85. package/dist/core-update-service.js +0 -245
  86. package/dist/managed-state-transaction.js +0 -375
  87. package/dist/offline-mutation.js +0 -58
  88. package/dist/profile-migration.js +0 -101
  89. package/dist/runtime-recovery.js +0 -31
  90. package/dist/sysproxy/darwin.js +0 -287
  91. package/dist/sysproxy/gnome.js +0 -181
  92. package/dist/sysproxy/legacy.js +0 -58
  93. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  94. package/dist/ui/assets/CodeEditorModal-D1zn-jRS.js +0 -14
  95. package/dist/ui/assets/ProfileEditorDialog-CyyP6OF1.js +0 -1
  96. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  97. package/dist/ui/assets/ProfilesView-hFa7-O72.js +0 -2
  98. package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +0 -1
  99. package/dist/ui/assets/SettingsView-D5QvJId6.css +0 -1
  100. package/dist/ui/assets/SettingsView-DsRntRfn.js +0 -2
  101. package/dist/ui/assets/index-CxQTf_P9.css +0 -1
  102. package/dist/ui/assets/index-_mb4OfYp.js +0 -19
package/dist/settings.js CHANGED
@@ -1,305 +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
- if (tun.value && !allowMissing) {
179
- throw invalidSettings(file, "TUN is unavailable in this release; tun must be false");
180
- }
181
- const daemonPort = readRequiredField(document, "daemonPort", DEFAULT_SETTINGS.daemonPort, allowMissing, file, parsePort);
182
- const daemonSecret = readRequiredField(document, "daemonSecret", DEFAULT_SETTINGS.daemonSecret, allowMissing, file, parseSecret);
183
- const systemProxy = readRequiredField(document, "systemProxy", DEFAULT_SETTINGS.systemProxy, allowMissing, file, parseBoolean);
184
- let needsRewrite = schemaVersion.missing ||
185
- schemaVersion.value === 0 ||
186
- mixedPort.missing ||
187
- controller.missing ||
188
- controller.value !== document.controller ||
189
- secret.missing ||
190
- tun.missing ||
191
- tun.value ||
192
- allowLan.missing ||
193
- daemonPort.missing ||
194
- daemonSecret.missing ||
195
- systemProxy.missing;
196
- for (const key of LEGACY_SETTINGS_KEYS) {
197
- if (!hasOwn(document, key))
198
- continue;
199
- const value = document[key];
200
- if (value === null)
201
- throw invalidSettings(file, `${key} must not be null`);
202
- parseString(value, key, file);
203
- needsRewrite = true;
204
- }
205
- const normalizedSecret = secret.missing ? generateSecret() : secret.value;
206
- const normalizedDaemonSecret = daemonSecret.missing ? generateSecret() : daemonSecret.value;
207
- const settings = {
208
- schemaVersion: 1,
209
- mixedPort: mixedPort.value,
210
- controller: controller.value,
211
- secret: normalizedSecret,
212
- tun: false,
213
- allowLan: allowLan.value,
214
- daemonPort: daemonPort.value,
215
- daemonSecret: normalizedDaemonSecret,
216
- systemProxy: systemProxy.value,
217
- };
218
- if (subscriptionUrl !== undefined)
219
- settings.subscriptionUrl = subscriptionUrl;
220
- validateSettingsRelations(settings, file);
221
- return { settings, needsRewrite };
13
+ export function generateSecret() {
14
+ return crypto.randomBytes(24).toString("hex");
222
15
  }
223
- function readSettingsFile(file) {
224
- let text;
225
- try {
226
- text = fs.readFileSync(file, "utf8");
227
- }
228
- catch (err) {
229
- if (err.code === "ENOENT") {
230
- return { exists: false, document: {} };
231
- }
232
- throw err;
233
- }
234
- try {
235
- return { exists: true, document: JSON.parse(text) };
236
- }
237
- catch (err) {
238
- throw new Error(`Settings file is invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`);
239
- }
16
+ export function initialSettings() {
17
+ return { ...DEFAULT_SETTINGS, secret: generateSecret(), daemonSecret: generateSecret() };
240
18
  }
241
- function canonicalSettings(settings) {
242
- const canonical = {
243
- schemaVersion: 1,
19
+ export function publicSettings(settings) {
20
+ return {
244
21
  mixedPort: settings.mixedPort,
245
22
  controller: settings.controller,
246
- secret: settings.secret,
247
- tun: settings.tun,
248
23
  allowLan: settings.allowLan,
249
24
  daemonPort: settings.daemonPort,
250
- daemonSecret: settings.daemonSecret,
251
25
  systemProxy: settings.systemProxy,
252
26
  };
253
- if (settings.subscriptionUrl !== undefined)
254
- canonical.subscriptionUrl = settings.subscriptionUrl;
255
- return canonical;
256
- }
257
- function writeCanonicalSettings(settings, layout) {
258
- atomicWriteFileSync(layout.settingsFile, `${JSON.stringify(canonicalSettings(settings), null, 2)}\n`);
259
- }
260
- function loadSettingsUnlocked(layout) {
261
- const source = readSettingsFile(layout.settingsFile);
262
- const parsed = parseSettings(source.document, layout.settingsFile, true);
263
- if (!source.exists || parsed.needsRewrite) {
264
- writeCanonicalSettings(parsed.settings, layout);
265
- }
266
- return parsed.settings;
267
- }
268
- export function generateSecret() {
269
- return crypto.randomBytes(24).toString("hex");
270
- }
271
- export function loadSettings(layout = sashLayout()) {
272
- const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
273
- try {
274
- return loadSettingsUnlocked(layout);
275
- }
276
- finally {
277
- lease.release();
278
- }
279
- }
280
- export function saveSettings(settings, layout = sashLayout()) {
281
- const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
282
- try {
283
- const parsed = parseSettings(settings, layout.settingsFile, false);
284
- writeCanonicalSettings(parsed.settings, layout);
285
- return parsed.settings;
286
- }
287
- finally {
288
- lease.release();
289
- }
290
- }
291
- export function parseSettingsText(text, file = "sash.json") {
292
- let document;
293
- try {
294
- document = JSON.parse(text);
295
- }
296
- catch (err) {
297
- throw new Error(`Settings file is invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
298
- }
299
- return parseSettings(document, file, false).settings;
300
- }
301
- export function validateSettingsCandidate(candidate) {
302
- return parseSettings(candidate, "candidate settings", false).settings;
303
27
  }
304
28
  export function parseControllerAddress(value) {
305
29
  const match = value.trim().match(/^(127\.0\.0\.1|localhost|\[::1\]):(\d+)$/i);
@@ -307,12 +31,60 @@ export function parseControllerAddress(value) {
307
31
  const rawPort = match?.[2];
308
32
  if (!rawHost || !rawPort)
309
33
  return undefined;
310
- const port = Number.parseInt(rawPort, 10);
34
+ const port = Number(rawPort);
311
35
  if (port < 1 || port > 65_535 || String(port) !== rawPort)
312
36
  return undefined;
313
- if (rawHost === "[::1]") {
37
+ if (rawHost === "[::1]")
314
38
  return { host: "::1", port, canonical: `[::1]:${port}` };
315
- }
316
39
  const host = rawHost === "localhost" ? "localhost" : "127.0.0.1";
317
40
  return { host, port, canonical: `${host}:${port}` };
318
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,
@@ -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
+ }