@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
@@ -1,87 +1,48 @@
1
- import { assertCoreInstallationConsistent, coreInstalled, installCore } from "./core.js";
2
- import { recoverCoreInstallTransaction } from "./core-install-transaction.js";
3
- import { recoverCoordinatedCoreUpdate } from "./core-update-coordination.js";
1
+ import fs from "node:fs";
2
+ import { loadSettings } from "./app-state.js";
4
3
  import { SashDaemonClient } from "./daemon-client.js";
5
- import { ensureDaemon, evaluateDaemon, prepareDaemonMaintenance, spawnDaemon, stopDaemonFromCli, } from "./daemon-lifecycle.js";
6
- import { log } from "./log.js";
7
- import { runOfflineMutation } from "./offline-mutation.js";
8
- import { withStateLock } from "./state-lock.js";
9
- export async function resolveRuntimeOwner(ctx, dependencies = {}) {
10
- const daemon = await (dependencies.evaluateDaemon ?? evaluateDaemon)(ctx.layout, ctx.settings);
11
- if (daemon.kind === "healthy") {
12
- const client = (dependencies.clientFactory ?? ((port, secret) => new SashDaemonClient(port, secret)))(daemon.port, ctx.settings.daemonSecret);
13
- return { kind: "daemon", daemon, client };
14
- }
4
+ import { ensureDaemon, evaluateDaemon, stopDaemonFromCli, } from "./daemon-lifecycle.js";
5
+ export async function resolveRuntimeOwner(ctx, deps = {}) {
6
+ const daemon = await (deps.evaluateDaemon ?? evaluateDaemon)(ctx.layout, ctx.settings);
7
+ if (daemon.kind === "healthy")
8
+ return {
9
+ kind: "daemon",
10
+ daemon,
11
+ client: (deps.clientFactory ?? ((port, secret) => new SashDaemonClient(port, secret)))(daemon.port, ctx.settings.daemonSecret),
12
+ };
15
13
  return daemon.kind === "stopped" ? { kind: "offline", daemon } : { kind: "unhealthy", daemon };
16
14
  }
17
- export async function ensureCore(ctx) {
18
- recoverCoreInstallTransaction(ctx.layout);
19
- recoverCoordinatedCoreUpdate(ctx.layout);
20
- assertCoreInstallationConsistent(ctx.layout);
21
- if (coreInstalled(ctx.layout))
22
- return;
23
- log.info("mihomo core not installed; downloading latest release...");
24
- const { version } = await installCore({ layout: ctx.layout });
25
- log.ok(`mihomo core ${version} installed`);
26
- }
27
- function withRuntimeOperation(ctx, purpose, action) {
28
- return withStateLock(ctx.layout.runtimeOperationLockFile, { purpose, timeoutMs: 120_000 }, () => action());
29
- }
30
- async function healthyDaemonOwner(ctx) {
15
+ export async function ensureManagement(ctx) {
16
+ await ensureDaemon({ layout: ctx.layout, settings: ctx.settings });
17
+ ctx.settings = loadSettings(ctx.layout);
31
18
  const owner = await resolveRuntimeOwner(ctx);
32
- if (owner.kind !== "daemon") {
33
- throw new Error("sashd did not become healthy after startup");
34
- }
19
+ if (owner.kind !== "daemon")
20
+ throw new Error("sashd did not become healthy");
35
21
  return owner;
36
22
  }
37
23
  export async function ensureRunning(ctx) {
38
- return withRuntimeOperation(ctx, "start runtime", async () => {
39
- const initialOwner = await resolveRuntimeOwner(ctx);
40
- if (initialOwner.kind === "unhealthy") {
41
- throw new Error("sashd is already starting or unresponsive; refusing a competing start");
42
- }
43
- if (initialOwner.kind === "offline") {
44
- await runOfflineMutation(ctx, "install Core before start", () => ensureCore(ctx));
45
- }
46
- await ensureDaemon({ layout: ctx.layout, settings: ctx.settings });
47
- const owner = await healthyDaemonOwner(ctx);
48
- const result = await owner.client.startCore();
49
- return { owner, result };
50
- });
24
+ const owner = await ensureManagement(ctx);
25
+ return { owner, result: await owner.client.startCore() };
51
26
  }
52
27
  export async function stopRuntime(ctx) {
53
- return withRuntimeOperation(ctx, "stop runtime", async () => {
54
- const owner = await resolveRuntimeOwner(ctx);
55
- if (owner.kind !== "offline") {
56
- const stopped = await stopDaemonFromCli({ layout: ctx.layout, settings: ctx.settings });
57
- if (!stopped)
58
- throw new Error("sashd could not be stopped safely");
59
- }
60
- await runOfflineMutation(ctx, "finalize stopped runtime", () => undefined, {
61
- reconcileRuntime: {
62
- ...(owner.kind !== "offline" && owner.daemon.legacyOwnership ? { legacyDaemon: true } : {}),
63
- },
64
- });
65
- return {
66
- wasRunning: owner.kind !== "offline",
67
- legacyDaemon: owner.kind !== "offline" && owner.daemon.legacyOwnership === true,
68
- };
69
- });
28
+ const initial = await resolveRuntimeOwner(ctx);
29
+ if (initial.kind === "unhealthy")
30
+ throw new Error("sashd is unresponsive or its ownership is unknown; refusing an unverified stop");
31
+ if (initial.kind === "offline") {
32
+ const leftovers = [
33
+ ctx.layout.pidFile,
34
+ ctx.layout.systemProxyStateFile,
35
+ ctx.layout.coreUpdateTransactionFile,
36
+ ];
37
+ if (!leftovers.some((file) => fs.existsSync(file)))
38
+ return { wasRunning: false };
39
+ await ensureManagement(ctx);
40
+ }
41
+ if (!(await stopDaemonFromCli({ layout: ctx.layout, settings: ctx.settings })))
42
+ throw new Error("sashd shutdown could not be verified");
43
+ return { wasRunning: initial.kind === "daemon" };
70
44
  }
71
- export async function restartRuntime(ctx, deps = {}) {
72
- return withRuntimeOperation(ctx, "restart runtime", async () => {
73
- const maintenance = await prepareDaemonMaintenance(ctx.layout, ctx.settings, "runtime restart", deps);
74
- if (maintenance.legacyDaemon) {
75
- await runOfflineMutation(ctx, "clean up legacy runtime", () => undefined, {
76
- reconcileRuntime: { legacyDaemon: true },
77
- });
78
- }
79
- if (!maintenance.daemonWasRunning) {
80
- await runOfflineMutation(ctx, "install Core before restart", () => ensureCore(ctx));
81
- }
82
- const { pid: daemonPid } = await spawnDaemon({ layout: ctx.layout, settings: ctx.settings });
83
- const owner = await healthyDaemonOwner(ctx);
84
- const result = await owner.client.startCore();
85
- return { owner, result, daemonPid, daemonWasRunning: maintenance.daemonWasRunning };
86
- });
45
+ export async function restartRuntime(ctx) {
46
+ const owner = await ensureManagement(ctx);
47
+ return { owner, result: await owner.client.restartCore() };
87
48
  }
@@ -1,4 +1,5 @@
1
- import { parseApiErrorBody, parseCoreReloadResult, parseCoreStartResult, parseDaemonStatus, parseHealthInfo, parseProfileActionResponse, parseProfileActivateResponse, parseProfileContentResponse, parseProfileRemoveResponse, parseProfileRenameResponse, parseProfilesIndex, parseProfilesUpdateAllResponse, parseProfileUpdateResponse, parsePublicSettings, parseSettingsFileContent, parseSettingsWriteResult, parseShutdownResult, parseSystemProxyStatusResponse, parseWebBootstrapInfo, parseWebSessionInfo, } from "./contracts.js";
1
+ import { parseAutostartStatus } from "./autostart-contract.js";
2
+ import { parseApiErrorBody, parseCoreStartResult, parseCoreUpdateResponse, parseDaemonStatus, parseHealthInfo, parseProfileActionResponse, parseProfileActivateResponse, parseProfileContentResponse, parseProfileRemoveResponse, parseProfileRenameResponse, parseProfilesIndex, parseProfilesUpdateAllResponse, parseProfileUpdateResponse, parsePublicSettings, parseSettingsWriteResult, parseSystemProxyStatusResponse, parseWebBootstrapInfo, parseWebSessionInfo, } from "./contracts.js";
2
3
  const defaultFetch = async (url, init) => {
3
4
  const response = await fetch(url, {
4
5
  method: init.method,
@@ -8,6 +9,7 @@ const defaultFetch = async (url, init) => {
8
9
  });
9
10
  return { status: response.status, text: () => response.text() };
10
11
  };
12
+ const CORE_OPERATION_TIMEOUT_MS = 20 * 60_000;
11
13
  export class SashApiError extends Error {
12
14
  status;
13
15
  code;
@@ -96,22 +98,48 @@ export class SashClient {
96
98
  }));
97
99
  }
98
100
  async status(fresh = false) {
99
- return parseDaemonStatus(await this.request(fresh ? "/sash/daemon/status?fresh=1" : "/sash/daemon/status"));
101
+ return parseDaemonStatus(await this.request(fresh ? "/sash/daemon/status?fresh=1" : "/sash/daemon/status", {
102
+ timeoutMs: 8000,
103
+ }));
100
104
  }
101
105
  async shutdown() {
102
- return parseShutdownResult(await this.request("/sash/daemon/shutdown", { method: "POST", timeoutMs: 45_000 }));
106
+ await this.request("/sash/daemon/shutdown", { method: "POST", timeoutMs: 45_000 });
107
+ }
108
+ async autostartStatus() {
109
+ return parseAutostartStatus(await this.request("/sash/autostart", { timeoutMs: 15_000, attempts: 1 }));
110
+ }
111
+ async setAutostart(enabled) {
112
+ return parseAutostartStatus(await this.request("/sash/autostart", {
113
+ method: "PUT",
114
+ body: { enabled },
115
+ timeoutMs: 60_000,
116
+ attempts: 1,
117
+ }));
103
118
  }
104
119
  async startCore() {
105
- return parseCoreStartResult(await this.request("/sash/core/start", { method: "POST", timeoutMs: 15_000 }));
120
+ return parseCoreStartResult(await this.request("/sash/core/start", {
121
+ method: "POST",
122
+ timeoutMs: CORE_OPERATION_TIMEOUT_MS,
123
+ }));
106
124
  }
107
125
  async stopCore() {
108
126
  await this.request("/sash/core/stop", { method: "POST", timeoutMs: 30_000 });
109
127
  }
110
128
  async restartCore() {
111
- return parseCoreStartResult(await this.request("/sash/core/restart", { method: "POST", timeoutMs: 30_000 }));
129
+ return parseCoreStartResult(await this.request("/sash/core/restart", {
130
+ method: "POST",
131
+ timeoutMs: CORE_OPERATION_TIMEOUT_MS,
132
+ }));
112
133
  }
113
- async reloadCoreConfig() {
114
- return parseCoreReloadResult(await this.request("/sash/core/reload", { method: "POST", timeoutMs: 30_000 }));
134
+ async updateCore(version) {
135
+ return parseCoreUpdateResponse(await this.request("/sash/core/update", {
136
+ method: "POST",
137
+ body: version ? { version } : {},
138
+ timeoutMs: CORE_OPERATION_TIMEOUT_MS,
139
+ }));
140
+ }
141
+ async setMode(mode) {
142
+ await this.request("/sash/core/mode", { method: "PUT", body: { mode } });
115
143
  }
116
144
  async proxyStatus(fresh = false) {
117
145
  return parseSystemProxyStatusResponse(await this.request(fresh ? "/sash/proxy?fresh=1" : "/sash/proxy"));
@@ -122,19 +150,12 @@ export class SashClient {
122
150
  async patchSettings(patch) {
123
151
  return parseSettingsWriteResult(await this.request("/sash/settings", { method: "PATCH", body: patch, timeoutMs: 45_000 }));
124
152
  }
125
- async getSettingsFile() {
126
- return parseSettingsFileContent(await this.request("/sash/settings/file"));
127
- }
128
- async writeSettingsFile(content) {
129
- return parseSettingsWriteResult(await this.request("/sash/settings/file", {
130
- method: "PUT",
131
- body: { content },
132
- timeoutMs: 45_000,
133
- }));
134
- }
135
153
  async listProfiles() {
136
154
  return parseProfilesIndex(await this.request("/sash/profiles"));
137
155
  }
156
+ async reorderProfiles(ids) {
157
+ return parseProfilesIndex(await this.request("/sash/profiles/order", { method: "PUT", body: { ids } }));
158
+ }
138
159
  async addProfile(url, opts = {}) {
139
160
  return parseProfileActionResponse(await this.request("/sash/profiles", {
140
161
  method: "POST",
@@ -168,10 +189,10 @@ export class SashClient {
168
189
  async getProfileContent(id) {
169
190
  return parseProfileContentResponse(await this.request(`/sash/profiles/${id}/content`));
170
191
  }
171
- async writeProfileContent(id, content) {
192
+ async writeProfileContent(id, content, revision) {
172
193
  return parseProfileUpdateResponse(await this.request(`/sash/profiles/${id}/content`, {
173
194
  method: "PUT",
174
- body: { content },
195
+ body: { content, revision },
175
196
  timeoutMs: 30_000,
176
197
  }));
177
198
  }
@@ -1,238 +1,43 @@
1
- import { commitManagedStateTransaction } from "./managed-state-transaction.js";
2
- import { ProfileConflictError, } from "./profile-service.js";
3
- import { sameSettings, validateSettingsCandidate } from "./settings.js";
1
+ import { validateSettingsCandidate } from "./settings.js";
4
2
  export class SettingsInputError extends Error {
5
3
  }
6
- export class SettingsConflictError extends Error {
7
- }
8
4
  export class CoreUnhealthyError extends Error {
9
5
  }
10
- const CORE_SETTING_KEYS = ["mixedPort", "controller", "secret", "allowLan"];
6
+ export { StateConflictError as SettingsConflictError } from "./app-state.js";
11
7
  export class SettingsService {
12
8
  options;
13
9
  constructor(options) {
14
10
  this.options = options;
15
11
  }
16
12
  async apply(patch) {
17
- const previous = { ...this.options.getCommitted() };
18
- let candidate;
19
- try {
20
- candidate = validateSettingsCandidate(mergePatch(previous, patch));
21
- }
22
- catch (err) {
23
- throw new SettingsInputError(err.message);
24
- }
25
- const restartRequired = candidate.daemonPort !== previous.daemonPort;
26
- const proxyChanged = candidate.systemProxy !== previous.systemProxy;
27
- const coreChanged = CORE_SETTING_KEYS.some((key) => candidate[key] !== previous[key]);
28
- const daemonChanged = restartRequired || candidate.daemonSecret !== previous.daemonSecret;
29
- let committed = previous;
30
- if (coreChanged || daemonChanged) {
31
- const staged = { ...candidate, systemProxy: previous.systemProxy };
32
- committed = coreChanged
33
- ? await this.commitCoreChange(previous, staged)
34
- : await this.commitSettingsOnly(previous, staged);
35
- }
36
- if (proxyChanged || patch.systemProxy === false) {
37
- committed = await this.commitSystemProxy(committed, candidate);
38
- }
39
- return { settings: committed, restartRequired };
40
- }
41
- async applyFileSettings(next) {
42
- const current = this.options.getCommitted();
43
- const patch = {};
44
- if (next.mixedPort !== current.mixedPort)
45
- patch.mixedPort = next.mixedPort;
46
- if (next.controller !== current.controller)
47
- patch.controller = next.controller;
48
- if (next.secret !== current.secret)
49
- patch.secret = next.secret;
50
- if (next.tun !== current.tun)
51
- patch.tun = next.tun;
52
- if (next.allowLan !== current.allowLan)
53
- patch.allowLan = next.allowLan;
54
- if (next.systemProxy !== current.systemProxy)
55
- patch.systemProxy = next.systemProxy;
56
- if (next.daemonPort !== current.daemonPort)
57
- patch.daemonPort = next.daemonPort;
58
- if (next.daemonSecret !== current.daemonSecret)
59
- patch.daemonSecret = next.daemonSecret;
60
- return this.apply(patch);
61
- }
62
- async commitSettingsOnly(previous, candidate) {
63
- return this.options.commit("update daemon settings", async () => {
64
- this.assertCurrent(previous);
65
- this.options.setRuntime(candidate);
66
- try {
67
- await commitManagedStateTransaction(this.options.layout, { settings: candidate }, undefined);
68
- }
69
- catch (err) {
70
- this.options.setRuntime(previous);
71
- throw err;
72
- }
73
- this.options.setCommitted(candidate);
74
- return candidate;
75
- });
76
- }
77
- async commitCoreChange(previous, candidate) {
78
- for (let attempt = 0;; attempt += 1) {
79
- const prepared = await this.options.profiles.prepareActiveConfig(candidate, previous);
80
- let retryableProfileConflict;
81
- let callbackEntered = false;
82
- try {
83
- return await this.options.commit("update settings", async () => {
84
- this.assertCurrent(previous);
85
- try {
86
- return await this.options.profiles.withPreparedActivePublication(prepared, async (publication) => {
87
- callbackEntered = true;
88
- return this.commitCoreSettings(previous, candidate, publication);
89
- });
90
- }
91
- catch (err) {
92
- if (err instanceof ProfileConflictError && !callbackEntered) {
93
- retryableProfileConflict = err;
94
- }
95
- throw err;
96
- }
97
- });
98
- }
99
- catch (err) {
100
- if (err !== retryableProfileConflict || attempt >= 1)
101
- throw err;
102
- }
103
- }
104
- }
105
- async commitSystemProxy(previous, candidate) {
106
- if (!candidate.systemProxy) {
107
- return this.options.commit("disable system proxy", async () => {
108
- this.assertCurrent(previous);
109
- this.options.setRuntime(candidate);
110
- try {
111
- if (!sameSettings(previous, candidate)) {
112
- await commitManagedStateTransaction(this.options.layout, { settings: candidate }, undefined);
113
- }
114
- }
115
- catch (err) {
116
- this.options.setRuntime(previous);
117
- throw err;
118
- }
119
- this.options.setCommitted(candidate);
120
- if (this.options.lifecycle)
121
- await this.options.lifecycle.reconcileSystemProxy();
122
- else
123
- await this.options.releaseSystemProxy?.();
124
- return candidate;
125
- });
126
- }
127
- const core = await this.options.supervisor?.status();
128
- if (!core?.running || !core.healthy) {
129
- throw new CoreUnhealthyError("Cannot enable system proxy: core is not healthy");
130
- }
131
- return this.options.commit("enable system proxy", async () => {
132
- this.assertCurrent(previous);
133
- this.options.setRuntime(candidate);
13
+ return this.options.commit("save settings", async () => {
14
+ const state = this.options.state.snapshot();
15
+ let settings;
134
16
  try {
135
- await commitManagedStateTransaction(this.options.layout, {
136
- settings: candidate,
137
- applyRuntime: () => this.requireLifecycle().reconcileSystemProxy(),
138
- }, undefined);
139
- this.options.setCommitted(candidate);
140
- return candidate;
141
- }
142
- catch (err) {
143
- this.options.setRuntime(previous);
144
- try {
145
- await this.requireLifecycle().reconcileSystemProxy();
146
- }
147
- catch (rollback) {
148
- throw new Error(`${err.message}; system proxy rollback failed: ${rollback.message}`);
149
- }
150
- throw err;
151
- }
17
+ settings = validateSettingsCandidate({ ...state.settings, ...patch });
18
+ }
19
+ catch (error) {
20
+ throw new SettingsInputError(error instanceof Error ? error.message : String(error));
21
+ }
22
+ if (patch.systemProxy === true) {
23
+ const core = await this.options.supervisor.status();
24
+ if (!core.running || !core.healthy)
25
+ throw new CoreUnhealthyError("Cannot enable system proxy: Core is not healthy");
26
+ }
27
+ this.options.state.assertCurrent(state.revision);
28
+ const changed = settings.mixedPort !== state.settings.mixedPort ||
29
+ settings.allowLan !== state.settings.allowLan ||
30
+ settings.systemProxy !== state.settings.systemProxy;
31
+ const saved = changed ? this.options.state.commit({ ...state, settings }) : state;
32
+ if (patch.systemProxy !== undefined) {
33
+ await this.options.lifecycle.reconcileSystemProxy();
34
+ }
35
+ const running = this.options.lifecycle.settings();
36
+ return {
37
+ settings: saved.settings,
38
+ restartRequired: saved.settings.mixedPort !== running.mixedPort ||
39
+ saved.settings.allowLan !== running.allowLan,
40
+ };
152
41
  });
153
42
  }
154
- async commitCoreSettings(previous, candidate, publication) {
155
- const wasRunning = this.options.supervisor?.isRunning() ?? false;
156
- this.options.setRuntime(candidate);
157
- try {
158
- await commitManagedStateTransaction(this.options.layout, {
159
- ...(publication.index ? { index: publication.index } : {}),
160
- ...(publication.profile
161
- ? {
162
- profile: {
163
- id: publication.profile.id,
164
- yamlText: publication.profile.yamlText,
165
- },
166
- }
167
- : {}),
168
- config: publication.config,
169
- settings: candidate,
170
- reloadRuntime: false,
171
- applyRuntime: async () => {
172
- if (!wasRunning)
173
- return;
174
- await this.requireLifecycle().restart();
175
- },
176
- }, undefined);
177
- this.options.setCommitted(candidate);
178
- return candidate;
179
- }
180
- catch (err) {
181
- this.options.setRuntime(previous);
182
- const rollbackErrors = [];
183
- try {
184
- await this.options.profiles.commitPreparedActiveReload(publication.rollback, {
185
- reloadRuntime: false,
186
- boundary: "already-held",
187
- });
188
- }
189
- catch (rollback) {
190
- rollbackErrors.push(`config rollback failed: ${rollback.message}`);
191
- }
192
- if (wasRunning) {
193
- try {
194
- if (this.options.supervisor?.isRunning())
195
- await this.requireLifecycle().restart();
196
- else
197
- await this.requireLifecycle().start();
198
- }
199
- catch (rollback) {
200
- rollbackErrors.push(`runtime rollback failed: ${rollback.message}`);
201
- }
202
- }
203
- if (rollbackErrors.length)
204
- throw new Error(`${err.message}; ${rollbackErrors.join("; ")}`, { cause: err });
205
- throw err;
206
- }
207
- }
208
- requireLifecycle() {
209
- if (!this.options.lifecycle)
210
- throw new Error("No runtime lifecycle is available for this settings mutation");
211
- return this.options.lifecycle;
212
- }
213
- assertCurrent(previous) {
214
- if (!sameSettings(this.options.getCommitted(), previous)) {
215
- throw new SettingsConflictError("Settings changed while preparing configuration");
216
- }
217
- }
218
- }
219
- function mergePatch(previous, patch) {
220
- const candidate = { ...previous };
221
- if (patch.mixedPort !== undefined)
222
- candidate.mixedPort = patch.mixedPort;
223
- if (patch.controller !== undefined)
224
- candidate.controller = patch.controller;
225
- if (patch.secret !== undefined)
226
- candidate.secret = patch.secret;
227
- if (patch.tun !== undefined)
228
- candidate.tun = patch.tun;
229
- if (patch.allowLan !== undefined)
230
- candidate.allowLan = patch.allowLan;
231
- if (patch.systemProxy !== undefined)
232
- candidate.systemProxy = patch.systemProxy;
233
- if (patch.daemonPort !== undefined)
234
- candidate.daemonPort = patch.daemonPort;
235
- if (patch.daemonSecret !== undefined)
236
- candidate.daemonSecret = patch.daemonSecret;
237
- return candidate;
238
43
  }