@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
@@ -0,0 +1,31 @@
1
+ import { hasExactOwnKeys, isPlainObject } from "./json-shape.js";
2
+ export const AUTOSTART_STATES = [
3
+ "on",
4
+ "off",
5
+ "stale",
6
+ "disabled",
7
+ "unknown",
8
+ "unsupported",
9
+ ];
10
+ export function parseAutostartStatus(value) {
11
+ if (!isPlainObject(value) ||
12
+ !hasExactOwnKeys(value, ["state", "canEnable", "reason"]) ||
13
+ !AUTOSTART_STATES.some((state) => state === value.state) ||
14
+ typeof value.canEnable !== "boolean" ||
15
+ (value.reason !== null && typeof value.reason !== "string")) {
16
+ throw new Error("Invalid autostart status");
17
+ }
18
+ return {
19
+ state: value.state,
20
+ canEnable: value.canEnable,
21
+ reason: value.reason,
22
+ };
23
+ }
24
+ export function parseAutostartEnabled(value) {
25
+ if (!isPlainObject(value) ||
26
+ !hasExactOwnKeys(value, ["enabled"]) ||
27
+ typeof value.enabled !== "boolean") {
28
+ throw new Error("Expected an object containing only an enabled boolean");
29
+ }
30
+ return value.enabled;
31
+ }
@@ -0,0 +1,10 @@
1
+ import "./node-version-guard.js";
2
+ import { startAtLogin } from "./autostart/start.js";
3
+ import { errorMessage } from "./error-utils.js";
4
+ try {
5
+ await startAtLogin();
6
+ }
7
+ catch (error) {
8
+ console.error(`[sash] Login startup failed: ${errorMessage(error)}`);
9
+ process.exitCode = 1;
10
+ }
@@ -0,0 +1,69 @@
1
+ import path from "node:path";
2
+ import { autostartContext, } from "./autostart/context.js";
3
+ import { installationIssue } from "./autostart/installation.js";
4
+ import { windowsAutostart } from "./autostart/windows.js";
5
+ import { errorMessage } from "./error-utils.js";
6
+ import { StateMutationQueue } from "./state-lock.js";
7
+ export class AutostartUnavailableError extends Error {
8
+ }
9
+ export class AutostartService {
10
+ context;
11
+ backend;
12
+ queue;
13
+ checkInstallation;
14
+ constructor(options = {}) {
15
+ this.context = autostartContext(options);
16
+ this.backend =
17
+ options.backend ??
18
+ (this.context.platform === "win32" ? windowsAutostart(this.context) : undefined);
19
+ this.queue = new StateMutationQueue(path.join(this.context.controlDir, "registration.lock"));
20
+ this.checkInstallation = options.checkInstallation ?? installationIssue;
21
+ }
22
+ async inspect() {
23
+ if (!this.backend) {
24
+ return {
25
+ state: "unsupported",
26
+ canEnable: false,
27
+ reason: `Autostart is not supported on ${this.context.platform}`,
28
+ };
29
+ }
30
+ const issue = this.checkInstallation(this.context);
31
+ try {
32
+ return {
33
+ state: await this.backend.inspect(),
34
+ canEnable: issue === null,
35
+ reason: issue,
36
+ };
37
+ }
38
+ catch (error) {
39
+ return {
40
+ state: "unknown",
41
+ canEnable: issue === null,
42
+ reason: errorMessage(error),
43
+ };
44
+ }
45
+ }
46
+ async set(enabled) {
47
+ const backend = this.backend;
48
+ if (!backend) {
49
+ throw new AutostartUnavailableError(`Autostart is not supported on ${this.context.platform}`);
50
+ }
51
+ return this.queue.run("configure autostart", async () => {
52
+ if (enabled) {
53
+ const issue = this.checkInstallation(this.context);
54
+ if (issue)
55
+ throw new AutostartUnavailableError(issue);
56
+ }
57
+ await backend.set(enabled);
58
+ if (!enabled) {
59
+ const issue = this.checkInstallation(this.context);
60
+ return { state: "off", canEnable: issue === null, reason: issue };
61
+ }
62
+ const status = await this.inspect();
63
+ if (status.state !== "on") {
64
+ throw new Error(`Could not verify autostart: ${status.reason ?? status.state}`);
65
+ }
66
+ return status;
67
+ });
68
+ }
69
+ }
package/dist/cli.js CHANGED
@@ -3,13 +3,13 @@ import "./node-version-guard.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { Command, CommanderError, InvalidArgumentError } from "commander";
6
+ import { Argument, Command, CommanderError, InvalidArgumentError } from "commander";
7
7
  import { withCliErrors } from "./cli-errors.js";
8
+ import { runAuto } from "./commands/auto.js";
8
9
  import { runRestart, runStart, runStop } from "./commands/lifecycle.js";
9
10
  import { runLogs } from "./commands/logs.js";
10
11
  import { runStatus } from "./commands/status.js";
11
12
  import { runUpdate } from "./commands/update.js";
12
- import { runUpgrade } from "./commands/upgrade.js";
13
13
  import { runWeb } from "./commands/web.js";
14
14
  import { parseLogLineCount } from "./log-follow.js";
15
15
  function packageVersion() {
@@ -35,7 +35,6 @@ Examples:
35
35
  $ sash web open the web dashboard
36
36
  $ sash status show runtime state, endpoints, and system proxy status
37
37
  $ sash update upgrade the core binary
38
- $ sash upgrade upgrade Sash itself via npm
39
38
 
40
39
  Data directory: %LOCALAPPDATA%\\Sash (Windows), ~/Library/Application Support/Sash (macOS),
41
40
  $XDG_DATA_HOME/sash (Linux). Override with the SASH_HOME environment variable.`);
@@ -49,8 +48,13 @@ program
49
48
  .action(withCliErrors(() => runStop()));
50
49
  program
51
50
  .command("restart")
52
- .description("restart the daemon and core")
51
+ .description("apply saved configuration and restart the core")
53
52
  .action(withCliErrors(() => runRestart()));
53
+ program
54
+ .command("auto")
55
+ .description("inspect or set automatic startup at login for the current user")
56
+ .addArgument(new Argument("[mode]", "set autostart or inspect its state").choices(["on", "off", "status"]))
57
+ .action(withCliErrors((mode) => runAuto(mode)));
54
58
  program
55
59
  .command("status")
56
60
  .description("show runtime state, versions, endpoints, and system proxy status")
@@ -63,26 +67,22 @@ program
63
67
  .option("-f, --follow", "follow the log output")
64
68
  .option("--errors", "read the stderr log instead of stdout")
65
69
  .option("--daemon", "read sashd daemon logs instead of core logs")
70
+ .option("--startup", "read login startup diagnostics")
66
71
  .action(withCliErrors((opts) => runLogs({
67
72
  lines: opts.lines ?? 50,
68
73
  follow: opts.follow,
69
74
  errors: opts.errors,
70
75
  daemon: opts.daemon,
76
+ startup: opts.startup,
71
77
  })));
72
78
  program
73
79
  .command("update")
74
80
  .description("upgrade the core binary")
75
81
  .option("--version <tag>", "install a specific core version, e.g. v1.19.30")
76
- .option("--force", "reinstall the target version and repair inconsistent Core state")
77
82
  .action(withCliErrors((opts) => runUpdate(opts)));
78
- program
79
- .command("upgrade")
80
- .description("upgrade Sash itself via npm")
81
- .option("--version <version>", "install a specific Sash version")
82
- .action(withCliErrors((opts) => runUpgrade(opts)));
83
83
  program
84
84
  .command("web")
85
- .description("open the web dashboard (installs/starts components as needed)")
85
+ .description("open the web dashboard without starting the core")
86
86
  .option("--no-open", "print the URL without opening a browser")
87
87
  .action(withCliErrors((opts) => runWeb({ noOpen: !opts.open })));
88
88
  program
@@ -0,0 +1,15 @@
1
+ import { AutostartService } from "../autostart.js";
2
+ import { log } from "../log.js";
3
+ import { ensureManagement } from "../runtime-owner.js";
4
+ import { runtimeContext } from "./shared.js";
5
+ export async function runAuto(mode = "status", controller = {
6
+ inspect: () => new AutostartService().inspect(),
7
+ set: async (enabled) => (await ensureManagement(runtimeContext())).client.setAutostart(enabled),
8
+ }) {
9
+ const status = mode === "status" ? await controller.inspect() : await controller.set(mode === "on");
10
+ log.kv("autostart", status.state);
11
+ if (status.reason)
12
+ log.warn(status.reason);
13
+ if (mode === "status" && status.state === "unknown")
14
+ process.exitCode = 2;
15
+ }
@@ -6,25 +6,15 @@ export async function runStart() {
6
6
  const { owner, result } = await ensureRunning(ctx);
7
7
  log.ok(`core started (PID=${result.pid}${result.version ? `, version ${result.version}` : ""})`);
8
8
  printEndpoints(ctx, owner.daemon.port);
9
- if (ctx.settings.systemProxy)
10
- log.ok("system proxy enabled");
11
9
  }
12
10
  export async function runStop() {
13
- const ctx = runtimeContext();
14
- const { wasRunning } = await stopRuntime(ctx);
15
- if (wasRunning) {
16
- log.ok("sash stopped; system proxy restored to its pre-Sash state");
17
- }
18
- else {
19
- log.info("sash is not running; stale runtime state was reconciled");
20
- }
11
+ const result = await stopRuntime(runtimeContext());
12
+ log.info(result.wasRunning ? "sash stopped; previous system proxy state restored" : "sash is stopped");
21
13
  }
22
- export async function runRestart(deps = {}) {
14
+ export async function runRestart() {
23
15
  const ctx = runtimeContext();
24
- const { owner, result, daemonPid, daemonWasRunning } = await restartRuntime(ctx, deps);
25
- const verb = daemonWasRunning ? "restarted" : "started";
26
- log.ok(`sashd ${verb} (PID=${daemonPid})`);
27
- log.ok(`core ${verb} (PID=${result.pid}${result.version ? `, version ${result.version}` : ""})`);
16
+ const { owner, result } = await restartRuntime(ctx);
17
+ log.ok(`core restarted (PID=${result.pid})`);
28
18
  printEndpoints(ctx, owner.daemon.port);
29
19
  }
30
20
  export function printEndpoints(ctx, daemonPort) {
@@ -1,16 +1,23 @@
1
1
  import fs from "node:fs";
2
2
  import { log } from "../log.js";
3
3
  import { followLogFile, logCursorAtEnd, normalizeLines } from "../log-follow.js";
4
+ import { sashLayout } from "../paths.js";
4
5
  import { tailFile } from "../process.js";
5
6
  import { runtimeContext } from "./shared.js";
6
7
  export async function runLogs(opts = {}) {
7
- const ctx = runtimeContext();
8
+ if (opts.startup && (opts.daemon || opts.errors)) {
9
+ throw new Error("--startup cannot be combined with --daemon or --errors");
10
+ }
11
+ const layout = opts.startup ? sashLayout() : runtimeContext().layout;
8
12
  let file;
9
- if (opts.daemon) {
10
- file = opts.errors ? ctx.layout.daemonErrLogFile : ctx.layout.daemonLogFile;
13
+ if (opts.startup) {
14
+ file = layout.sashLogFile;
15
+ }
16
+ else if (opts.daemon) {
17
+ file = opts.errors ? layout.daemonErrLogFile : layout.daemonLogFile;
11
18
  }
12
19
  else {
13
- file = opts.errors ? ctx.layout.coreErrLogFile : ctx.layout.coreLogFile;
20
+ file = opts.errors ? layout.coreErrLogFile : layout.coreLogFile;
14
21
  }
15
22
  const lines = normalizeLines(opts.lines);
16
23
  const cursor = logCursorAtEnd(file);
@@ -1,8 +1,6 @@
1
+ import { loadSettings } from "../app-state.js";
1
2
  import { sashLayout } from "../paths.js";
2
- import { loadSettings } from "../settings.js";
3
- export { runOfflineMutation } from "../offline-mutation.js";
4
3
  export function runtimeContext() {
5
4
  const layout = sashLayout();
6
- const settings = loadSettings(layout);
7
- return { layout, settings };
5
+ return { layout, settings: loadSettings(layout) };
8
6
  }
@@ -13,6 +13,10 @@ export async function runStatus(opts = {}, collect = () => collectRuntimeStatus(
13
13
  if (status.queryError)
14
14
  log.warn(`status incomplete: ${status.queryError}`);
15
15
  log.kv("root", status.paths.root);
16
+ log.kv("autostart", status.autostart.state);
17
+ if (status.autostart.state === "stale" || status.autostart.state === "disabled") {
18
+ log.warn("Run sash auto on to repair the login startup registration");
19
+ }
16
20
  log.kv("config", status.paths.config);
17
21
  log.kv("mixed port", status.endpoints.mixedProxy);
18
22
  log.kv("proxy desired", status.systemProxy.desired ? "on" : "off");
@@ -26,7 +30,7 @@ export async function runStatus(opts = {}, collect = () => collectRuntimeStatus(
26
30
  log.kv("os proxy", formatObservedProxy(status.systemProxy.osObserved));
27
31
  log.kv("sash api", status.endpoints.daemonApi);
28
32
  log.kv("dashboard", status.endpoints.dashboard);
29
- log.kv("active profile", status.activeProfile
33
+ log.kv("selected profile", status.activeProfile
30
34
  ? `${status.activeProfile.name} (${status.activeProfile.url || "local file"})`
31
35
  : "(none)");
32
36
  log.kv("core version", status.core.installedVersion || "(not installed)");
@@ -1,5 +1,9 @@
1
- import { runCoreUpdate } from "../core-update-service.js";
1
+ import { log } from "../log.js";
2
+ import { ensureManagement } from "../runtime-owner.js";
2
3
  import { runtimeContext } from "./shared.js";
3
- export async function runUpdate(opts = {}, deps = {}) {
4
- await runCoreUpdate(runtimeContext(), opts, deps);
4
+ export async function runUpdate(opts = {}) {
5
+ const owner = await ensureManagement(runtimeContext());
6
+ log.info("downloading and verifying Core; the dashboard remains available");
7
+ const result = await owner.client.updateCore(opts.version);
8
+ log.ok(`core updated to ${result.version}`);
5
9
  }
@@ -1,61 +1,25 @@
1
1
  import { openInBrowser } from "../browser.js";
2
2
  import { log } from "../log.js";
3
- import { resolveRuntimeOwner } from "../runtime-owner.js";
3
+ import { ensureManagement } from "../runtime-owner.js";
4
4
  import { writeBootstrapFile } from "../web-bootstrap.js";
5
- import { runStart } from "./lifecycle.js";
6
5
  import { runtimeContext } from "./shared.js";
7
- export function dashboardUrl(daemonPort) {
8
- return `http://127.0.0.1:${daemonPort}/ui/`;
6
+ export function dashboardUrl(port) {
7
+ return `http://127.0.0.1:${port}/ui/`;
9
8
  }
10
9
  export async function runWeb(opts = {}, deps = {}) {
11
- const logger = deps.log ?? log;
12
- const resolve = deps.resolveRuntimeOwner ?? resolveRuntimeOwner;
13
10
  const ctx = (deps.runtimeContext ?? runtimeContext)();
14
- let owner = await resolve(ctx);
15
- let coreRunning = null;
16
- if (owner.kind === "daemon") {
17
- try {
18
- coreRunning = (await owner.client.status()).core.running;
19
- }
20
- catch {
21
- logger.warn("Core state is unavailable; opening the dashboard for recovery.");
22
- }
23
- }
24
- if (owner.kind === "offline" || coreRunning === false) {
25
- logger.info("sash is not running; starting it first...");
26
- let started = false;
27
- try {
28
- await (deps.runStart ?? runStart)();
29
- started = true;
30
- }
31
- catch (error) {
32
- try {
33
- owner = await resolve(ctx);
34
- }
35
- catch {
36
- throw error;
37
- }
38
- if (owner.kind !== "daemon")
39
- throw error;
40
- logger.warn(`Core startup failed; opening the dashboard for recovery: ${error instanceof Error ? error.message : String(error)}`);
41
- }
42
- if (started)
43
- owner = await resolve(ctx);
44
- }
45
- if (owner.kind !== "daemon") {
46
- throw new Error("sashd did not become healthy before opening the dashboard");
47
- }
11
+ const owner = await (deps.ensureManagement ?? ensureManagement)(ctx);
48
12
  const url = dashboardUrl(owner.daemon.port);
49
- if (opts.noOpen) {
50
- logger.ok(`dashboard: ${url}`);
51
- logger.info("run 'sash web' without --no-open to authorize a browser session");
52
- return;
13
+ const logger = deps.log ?? log;
14
+ if (!opts.noOpen) {
15
+ const bootstrap = await owner.client.createWebBootstrap();
16
+ const file = (deps.writeBootstrap ?? writeBootstrapFile)(ctx.layout, {
17
+ dashboardUrl: url,
18
+ ...bootstrap,
19
+ });
20
+ (deps.openInBrowser ?? openInBrowser)(file.fileUrl);
53
21
  }
54
- const bootstrap = await owner.client.createWebBootstrap();
55
- const file = (deps.writeBootstrap ?? writeBootstrapFile)(ctx.layout, {
56
- dashboardUrl: url,
57
- ...bootstrap,
58
- });
59
- (deps.openInBrowser ?? openInBrowser)(file.fileUrl);
22
+ else
23
+ logger.info("run 'sash web' without --no-open to authorize a browser session");
60
24
  logger.ok(`dashboard: ${url}`);
61
25
  }