@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
@@ -0,0 +1,59 @@
1
+ import path from "node:path";
2
+ import { assertLauncherValue, launcherFilesExist, } from "./context.js";
3
+ import { readRegistration, registerFile, removeRegistration } from "./files.js";
4
+ import { readWindowsRegistration, setWindowsRegistration, windowsSystemPath, } from "./windows-registry.js";
5
+ export function windowsLauncherContents(node, entry, dataDir) {
6
+ for (const value of [node, entry, dataDir]) {
7
+ assertLauncherValue(value);
8
+ if (value.includes('"'))
9
+ throw new Error("Windows autostart paths cannot contain quotes");
10
+ }
11
+ const script = [
12
+ "' Sash login launcher; generated by sash auto.",
13
+ "Option Explicit",
14
+ "Dim shell, environment",
15
+ 'Set shell = CreateObject("WScript.Shell")',
16
+ 'Set environment = shell.Environment("PROCESS")',
17
+ `environment("SASH_HOME") = "${dataDir}"`,
18
+ `environment("SASH_AUTOSTART_NODE") = "${node}"`,
19
+ `environment("SASH_AUTOSTART_ENTRY") = "${entry}"`,
20
+ 'shell.Run """%SASH_AUTOSTART_NODE%"" ""%SASH_AUTOSTART_ENTRY%""", 0, False',
21
+ "",
22
+ ].join("\r\n");
23
+ return Buffer.from(`\uFEFF${script}`, "utf16le");
24
+ }
25
+ export function windowsAutostart(ctx) {
26
+ const file = path.join(ctx.controlDir, "start.vbs");
27
+ const launcherCommand = () => {
28
+ const wscript = windowsSystemPath(ctx, "wscript.exe");
29
+ for (const value of [wscript, file]) {
30
+ assertLauncherValue(value);
31
+ if (value.includes('"'))
32
+ throw new Error("Windows autostart paths cannot contain quotes");
33
+ }
34
+ return `"${wscript}" //B //Nologo "${file}"`;
35
+ };
36
+ const contents = () => windowsLauncherContents(ctx.nodePath, ctx.entryPath, ctx.dataDir);
37
+ return {
38
+ async inspect() {
39
+ const registration = await readWindowsRegistration(ctx);
40
+ if (registration.command === null)
41
+ return "off";
42
+ if (!launcherFilesExist(ctx) ||
43
+ registration.command.toLowerCase() !== launcherCommand().toLowerCase() ||
44
+ !readRegistration(file)?.equals(contents())) {
45
+ return "stale";
46
+ }
47
+ return registration.disabled ? "disabled" : "on";
48
+ },
49
+ async set(enabled) {
50
+ if (enabled) {
51
+ await registerFile(file, contents(), () => setWindowsRegistration(ctx, launcherCommand()));
52
+ }
53
+ else {
54
+ await setWindowsRegistration(ctx, null);
55
+ removeRegistration(file);
56
+ }
57
+ },
58
+ };
59
+ }
@@ -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
+ }
@@ -1,47 +1,21 @@
1
1
  import { log } from "../log.js";
2
2
  import { ensureRunning, restartRuntime, stopRuntime } from "../runtime-owner.js";
3
- import { tunPrivilegeGuidance } from "../tun-guidance.js";
4
3
  import { runtimeContext } from "./shared.js";
5
4
  export async function runStart() {
6
5
  const ctx = runtimeContext();
7
6
  const { owner, result } = await ensureRunning(ctx);
8
7
  log.ok(`core started (PID=${result.pid}${result.version ? `, version ${result.version}` : ""})`);
9
8
  printEndpoints(ctx, owner.daemon.port);
10
- reportTunState(ctx, result);
11
- if (ctx.settings.systemProxy)
12
- log.ok("system proxy enabled");
13
9
  }
14
10
  export async function runStop() {
15
- const ctx = runtimeContext();
16
- const { wasRunning } = await stopRuntime(ctx);
17
- if (wasRunning) {
18
- log.ok("sash stopped; system proxy restored to its pre-Sash state");
19
- }
20
- else {
21
- log.info("sash is not running; stale runtime state was reconciled");
22
- }
11
+ const result = await stopRuntime(runtimeContext());
12
+ log.info(result.wasRunning ? "sash stopped; previous system proxy state restored" : "sash is stopped");
23
13
  }
24
- export async function runRestart(deps = {}) {
14
+ export async function runRestart() {
25
15
  const ctx = runtimeContext();
26
- const { owner, result, daemonPid, daemonWasRunning } = await restartRuntime(ctx, deps);
27
- const verb = daemonWasRunning ? "restarted" : "started";
28
- log.ok(`sashd ${verb} (PID=${daemonPid})`);
29
- 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})`);
30
18
  printEndpoints(ctx, owner.daemon.port);
31
- reportTunState(ctx, result);
32
- }
33
- function reportTunState(ctx, result) {
34
- if (!ctx.settings.tun)
35
- return;
36
- if (result.tunActive === true) {
37
- log.ok("TUN active");
38
- }
39
- else if (result.tunActive === false) {
40
- log.warn(`TUN was requested but is inactive; the core remains available without TUN. ${tunPrivilegeGuidance("runtime-inactive", { root: ctx.layout.root })}`);
41
- }
42
- else {
43
- log.warn(`TUN was requested, but its runtime state could not be verified. ${tunPrivilegeGuidance("runtime-inactive", { root: ctx.layout.root })}`);
44
- }
45
19
  }
46
20
  export function printEndpoints(ctx, daemonPort) {
47
21
  log.kv("mixed port", `127.0.0.1:${ctx.settings.mixedPort}`);
@@ -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
  }
@@ -1,6 +1,5 @@
1
1
  import { log } from "../log.js";
2
- import { collectRuntimeStatus, formatObservedProxy, formatTunObservation, markIncompleteObservation, runtimeStatusHeadline, shouldShowTunGuidance, } from "../status.js";
3
- import { tunPrivilegeGuidance } from "../tun-guidance.js";
2
+ import { collectRuntimeStatus, formatObservedProxy, markIncompleteObservation, runtimeStatusHeadline, } from "../status.js";
4
3
  import { runtimeContext } from "./shared.js";
5
4
  export async function runStatus(opts = {}, collect = () => collectRuntimeStatus(runtimeContext())) {
6
5
  const status = await collect();
@@ -14,6 +13,10 @@ export async function runStatus(opts = {}, collect = () => collectRuntimeStatus(
14
13
  if (status.queryError)
15
14
  log.warn(`status incomplete: ${status.queryError}`);
16
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
+ }
17
20
  log.kv("config", status.paths.config);
18
21
  log.kv("mixed port", status.endpoints.mixedProxy);
19
22
  log.kv("proxy desired", status.systemProxy.desired ? "on" : "off");
@@ -27,13 +30,9 @@ export async function runStatus(opts = {}, collect = () => collectRuntimeStatus(
27
30
  log.kv("os proxy", formatObservedProxy(status.systemProxy.osObserved));
28
31
  log.kv("sash api", status.endpoints.daemonApi);
29
32
  log.kv("dashboard", status.endpoints.dashboard);
30
- log.kv("active profile", status.activeProfile
33
+ log.kv("selected profile", status.activeProfile
31
34
  ? `${status.activeProfile.name} (${status.activeProfile.url || "local file"})`
32
35
  : "(none)");
33
- log.kv("tun", formatTunObservation(status));
34
36
  log.kv("core version", status.core.installedVersion || "(not installed)");
35
- if (shouldShowTunGuidance(status)) {
36
- log.warn(tunPrivilegeGuidance("runtime-inactive", { root: status.paths.root }));
37
- }
38
37
  markIncompleteObservation(status.complete);
39
38
  }
@@ -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,33 +1,25 @@
1
1
  import { openInBrowser } from "../browser.js";
2
2
  import { log } from "../log.js";
3
- import { resolveRuntimeOwner } from "../runtime-owner.js";
4
- import { runStart } from "./lifecycle.js";
3
+ import { ensureManagement } from "../runtime-owner.js";
4
+ import { writeBootstrapFile } from "../web-bootstrap.js";
5
5
  import { runtimeContext } from "./shared.js";
6
- export function dashboardUrl(daemonPort) {
7
- return `http://127.0.0.1:${daemonPort}/ui/`;
6
+ export function dashboardUrl(port) {
7
+ return `http://127.0.0.1:${port}/ui/`;
8
8
  }
9
- export async function runWeb(opts = {}) {
10
- const ctx = runtimeContext();
11
- let owner = await resolveRuntimeOwner(ctx);
12
- let coreRunning = false;
13
- if (owner.kind === "daemon") {
14
- try {
15
- coreRunning = (await owner.client.status()).core.running;
16
- }
17
- catch {
18
- coreRunning = false;
19
- }
20
- }
21
- if (!coreRunning) {
22
- log.info("sash is not running; starting it first...");
23
- await runStart();
24
- owner = await resolveRuntimeOwner(ctx);
25
- }
26
- if (owner.kind !== "daemon") {
27
- throw new Error("sashd did not become healthy before opening the dashboard");
28
- }
9
+ export async function runWeb(opts = {}, deps = {}) {
10
+ const ctx = (deps.runtimeContext ?? runtimeContext)();
11
+ const owner = await (deps.ensureManagement ?? ensureManagement)(ctx);
29
12
  const url = dashboardUrl(owner.daemon.port);
30
- log.ok(`dashboard: ${url}`);
31
- if (!opts.noOpen)
32
- openInBrowser(url);
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);
21
+ }
22
+ else
23
+ logger.info("run 'sash web' without --no-open to authorize a browser session");
24
+ logger.ok(`dashboard: ${url}`);
33
25
  }