@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/docs/frontend.md CHANGED
@@ -1,135 +1,89 @@
1
- # Frontend Architecture & WebUI Design
2
-
3
- The WebUI is a Vue 3 application built with Vite and bundled into `dist/ui/`. sashd serves it at `http://127.0.0.1:19090/ui/`; `<root>/ui/index.html` can override the bundled dashboard.
4
-
5
- ---
6
-
7
- ## 1. Build and Quality Gates
8
-
9
- - `npm run build:ui` calls the Vite Node API through `scripts/build-ui.mjs`.
10
- - `npm run typecheck:web` runs `vue-tsc`, including Vue templates.
11
- - `npm test` runs server type checking, WebUI type checking, backend tests and WebUI TypeScript tests.
12
- - Biome checks WebUI TypeScript and Vite configuration. Vue templates are type-checked by `vue-tsc` and compiled by Vite.
13
- - Vite 6 builds the bundled dashboard on the declared Node.js 24 baseline.
14
- - CI runs lint, server/WebUI type checks, all tests, production builds and actual tarball pack/install/CLI/UI smoke on Windows, macOS and Linux with Node.js 24.
15
-
16
- The `cn-font-split` dependency has a scoped `koffi: 2.16.3` override. The [Koffi changelog](https://koffi.dev/changelog) documents Node.js 24.14+ teardown fixes relevant to the native font splitter.
17
-
18
- Workflows using `npm ci --ignore-scripts` explicitly initialize the tested native font subsetter with `node node_modules/cn-font-split/dist/cli.js i default@7.6.8` before building. The npm wrapper and native subsetter have separate versions; 7.6.8 is the native release used by the validated builds. This runs the selected build tool directly while keeping automatic dependency lifecycle hooks disabled.
19
-
20
- `npx tsx scripts/web-auth-ui-verify.mts` checks the real private-file/browser/daemon authorization exchange in both engines, including bare URLs, refresh, replay, daemon restart, reauthorization and the Settings/Overview layouts without TUN controls. It uses isolated temporary roots and ports, a fake Core and system proxy. Screenshots and a report remain in the OS temporary directory.
21
-
22
- ---
23
-
24
- ## 2. Source Layout
25
-
26
- ```text
27
- web/src/
28
- ├── api/index.ts typed REST client and WebSocket reconnect logic
29
- ├── api/session.ts bootstrap exchange and per-tab credential ownership
30
- ├── components/
31
- │ ├── AppSidebar.vue
32
- │ ├── ConfirmDialog.vue
33
- │ ├── Icon.vue semantic icon wrapper
34
- │ ├── icons.ts tree-shaken Remix Icon component mapping
35
- │ ├── ProxyGroupSection.vue reusable group header/node grid
36
- │ ├── TrafficChart.vue
37
- │ └── ...
38
- ├── i18n/ Chinese source messages and typed English mirror
39
- ├── stores/index.ts runtime state, refresh actions and polling
40
- ├── stores/state-ownership.ts pure generations, cleanup and sync helpers
41
- ├── styles/main.css design tokens and shared utility/component styles
42
- ├── types/index.ts core-controller response types; daemon types are shared
43
- ├── views/
44
- │ ├── ConnectionView.vue read-only browser authorization instructions
45
- │ ├── OverviewView.vue status, traffic, modes and proxy groups
46
- │ ├── ProfilesView.vue download/import/update/select/delete profiles
47
- │ ├── LogsView.vue
48
- │ ├── ConnectionsView.vue
49
- │ ├── RulesView.vue
50
- │ └── SettingsView.vue
51
- ├── App.vue shell, view selection and stream lifecycle
52
- ├── theme.ts persisted system/light/dark theme state
53
- └── router.ts small hash router
54
- ```
1
+ # Frontend Architecture
55
2
 
56
- The shell follows a classic compact desktop-console layout: a 25px title strip sits above a 170px text-navigation sidebar with live traffic, recessed active-item geometry, runtime and Core status. Below 900px it becomes a compact title strip plus safe-area-aware bottom navigation. Light and Dark themes use flat neutral surfaces, dense rows, restrained shadows and distinct mode/semantic state colors. Profiles use a full-width download toolbar and compact profile grid; Logs use an edge-to-edge stream; Connections use colored metadata tags; Settings use labeled flat sections. Overview intentionally remains a roughly one-third/two-thirds combined General + Proxies workspace: common runtime controls and the GLOBAL/RULE/DIRECT selector stay on the left while the mode-driven proxy groups or DIRECT state render on the right. `styles/main.css` owns theme tokens, component states and reduced-motion behavior without a runtime-loaded UI dependency. Functional icons use selected, tree-shaken `@remixicon/vue` line components behind the local semantic `Icon.vue` API; the Sash brand mark remains project-owned. `THIRD_PARTY_NOTICES.md` records the bundled Vue MIT and Remix Icon notices, and the complete Remix Icon License v1.0 remains in `docs/remix-icon-license.txt`; both are included in the npm tarball. Long connection and rule sets remain paginated and switch to narrow-screen layouts.
3
+ Sash retains its Vue 3 / Vite WebUI, built into `dist/ui/` and served by the daemon. Existing LXGW WenKai Lite font assets, appearance and Unicode-range splitting remain unchanged. No framework replacement or new UI/state framework is introduced.
57
4
 
58
- Daemon/profile contracts live in `src/contracts.ts` and are imported as types by both `src/daemon-client.ts` and the WebUI. This prevents manually duplicated `SashStatus` and profile response shapes.
5
+ ## State ownership
59
6
 
60
- ---
7
+ `stores/state.ts` is the single shared shallow-reactive state. Large collections are replaced by reference. `stores/index.ts` only exports the public actions/selectors; it is not another state store.
61
8
 
62
- ## 3. Runtime State Ownership
9
+ | Module | Responsibility |
10
+ | --- | --- |
11
+ | `stores/runtime-actions.ts` | Non-overlapping status polling, boot changes and settings actions |
12
+ | `stores/profile-actions.ts` | Saved profile mutations and metadata refresh |
13
+ | `stores/core-actions.ts` | Independent Core resource loads and mode/node/connection controls |
14
+ | `stores/state-ownership.ts` | Request generations, runtime identity and pure selectors |
15
+ | `stores/telemetry.ts` | Traffic history and batched, bounded log records |
16
+ | `api/session.ts` | One-time bootstrap, per-tab session and daemon identity |
17
+ | `api/index.ts` | Shared daemon client, Core queries and streams |
63
18
 
64
- `web/src/stores/index.ts` owns status, profiles, proxy groups, rules, connections, traffic history, manual delay results, logs, operation flags and toasts. Pure ownership/generation helpers live in `web/src/stores/state-ownership.ts` and are covered by browser-free `node:test` tests. A pinned `happy-dom` dev dependency supplies the minimal DOM behavior harness for mounting a real Vue `createApp` render path and asserting reactive notice transitions; no second test runner or coverage-only gate is introduced.
19
+ Three values determine refresh ownership:
65
20
 
66
- Canonical actions include:
21
+ - `daemon.bootId`: a new daemon invalidates old authorization and metadata revision comparisons.
22
+ - `revisions.profiles`: saved-state changes refresh profile metadata. It does not invalidate Core resources.
23
+ - `revisions.runtime`: with `bootId`, identifies the Core runtime. Replacement clears Core resources, traffic and manual latency results.
67
24
 
68
- - `refreshStatus()`
69
- - `refreshConnections()`
70
- - `refreshProxies()`
71
- - `refreshProfiles()`
72
- - `refreshRuntimeState()`
73
- - profile mutation actions such as `updateProfile()` and `activateProfile()`
74
- - runtime intent actions such as `setOutboundMode()` and `selectGroupProxy()`
75
- - `startRuntimePolling()`
25
+ Each resource has its own loaded flag and error. A failed rules query does not discard working node data. Failures for the same runtime retain prior data and show degradation; a new runtime cannot inherit old data. Per-domain request generations discard late successes and errors. Mutation results from a replaced daemon/Core cannot overwrite its successor.
76
26
 
77
- Polling is self-scheduling with `setTimeout` after the previous cycle completes. It slows to a 15-second interval while the page is hidden and refreshes immediately after returning to the foreground. Domain request generations discard both successful responses and resource errors made stale by a newer refresh or user mutation; old polls cannot overwrite a committed settings response. Core-specific API calls require an authorized browser session and status reporting `running && healthy`.
27
+ ## Refresh and performance
78
28
 
79
- Daemon reachability, profile revision and Core snapshots have separate ownership. A successful `/sash/daemon/status` keeps the daemon online even when a downstream Core gateway request returns 502. Profiles track their last fetched daemon revision independently and refresh on revision changes even while Core is stopped. A daemon restart resets that revision comparison.
29
+ Status polls every two seconds after the previous cycle finishes. Hidden or unauthorized tabs use a slower interval; returning to the foreground refreshes promptly. Session initialization runs on entry/reconnect, with public status boot identity detecting daemon restarts.
80
30
 
81
- The Core owner is the daemon boot plus Core PID/start time. A stopped/unhealthy Core or unreachable daemon clears proxy groups, rules, connections/totals and traffic rates/history. A same-owner Core API failure preserves the last complete configs/proxies/rules/connections snapshot, marks it degraded and retries; a changed owner clears the old snapshot before fetching, so failed replacement data cannot be shown under the new owner. Profile revision changes request a new snapshot without prematurely discarding same-owner data.
31
+ | Visible page | Core requests |
32
+ | --- | --- |
33
+ | Overview | Config/mode and proxies approximately every third cycle; connections approximately every fifth cycle |
34
+ | Connections | Connections each cycle |
35
+ | Rules | Rules on entry/runtime change, then cached |
36
+ | Profiles / Settings | No background Core tables |
37
+ | Logs | Log stream while visible |
82
38
 
83
- Manual latency results are stored separately from polled proxy payloads. Normal proxy polling therefore preserves a test result; a successful profile snapshot generation or Core owner change invalidates stale delay results.
39
+ Entering a page loads its resources immediately. Profile saves only refresh management metadata. Explicit Apply refreshes the resources visible after Core replacement. Traffic is one shared stream for visible consumers; traffic/log sockets pause when the page is hidden or the runtime/session is unavailable.
84
40
 
85
- Views call store actions rather than maintaining separate copies of status/proxy/rule refresh orchestration. Same-domain controls use store operation flags, while independent group/node latency tests remain concurrent.
41
+ Existing optimizations remain: async route/editor chunks, shallow collections, collapsed node groups unmounting their cards, paginated connections/rules, separate manual latency results, and roughly 100 ms log batches capped at 600 rows. Font slices load only for rendered glyph ranges.
86
42
 
87
- ---
43
+ ## Views and shared controls
88
44
 
89
- ## 4. Profiles and Overview Components
45
+ `App.vue` owns the shell, route composition, session gate, global pending-configuration bar and stream lifetime. Existing Overview, Profiles, Logs, Connections, Rules and Settings pages remain.
90
46
 
91
- The Profiles page provides:
47
+ `CoreControls.vue` and `useCoreControl()` share busy state and Apply/start/stop actions across the Overview, Settings and pending bar. Applying while running asks for confirmation because it restarts Core. Stopping Core keeps management open.
92
48
 
93
- - URL download and clipboard paste
94
- - local YAML import
95
- - Update All and per-profile update
96
- - active profile selection
97
- - quota/expiry display from provider metadata
98
- - persisted update errors
99
- - delete confirmation
49
+ Profile selection and edits are saved first. The pending bar indicates that the running configuration differs. Overview displays the applied profile/port; Profiles shows the saved selection. The YAML editor submits the content revision read on open, preventing another tab's later edits from being overwritten. The raw `sash.json` editor is removed.
100
50
 
101
- The Overview proxy panel uses `ProxyGroupSection.vue` for manual, automatic and GLOBAL groups. This keeps node-card rendering, delay labels, UDP/current markers and group test controls in one component. Group and node latency tests track independent in-flight sets, so tests for different groups/nodes can run concurrently. Mode and proxy-selection mutations are latest-request-wins and disable only their matching control domain.
51
+ Settings drafts remain local until Save and survive status polling. A saved port/LAN change waits for Apply. The system-proxy switch performs its own operation and remains available for recovery when Core is stopped. The login-startup card uses the observed OS registration and explicit enable/remove actions.
102
52
 
103
- System proxy controls are target-state based: enabling requires a running, healthy Core; disabling remains available when desired/applied/OS-observed state indicates cleanup may still be required, including while the Core is stopped.
53
+ `PageHeader`, `ProxyGroupSection`, the shared code editor, pagination, confirmation service and focus/scroll-lock composables remain reusable building blocks. Light/dark themes, Chinese/English copy and mobile navigation are retained. There is no generic table framework or event bus.
104
54
 
105
- ---
55
+ ## Authorization and transport
106
56
 
107
- ## 5. API and Streaming
57
+ `src/contracts.ts` and `src/sash-client.ts` define the browser-safe daemon protocol. Runtime imports do not pull Node-specific implementations into the browser. Core types describe only the data the UI uses.
108
58
 
109
- `web/src/api/session.ts` consumes and immediately removes the one-time `sash web` fragment, then redeems it exactly once. Concurrent initialization shares that exchange. Private session credentials and their public daemon nonce are saved in `sessionStorage`, allowing a tab to reload without exposing credentials in ordinary URLs. Storage denial falls back to memory. Health remains public discovery only; a different boot nonce clears the stored session. Stale completions and old-token `401` responses cannot replace or revoke a newer authorization. Without authorization, the shell renders `ConnectionView`, continues public reachability/status polling and does not open Core streams or expose mutation controls.
59
+ The browser consumes and immediately removes the private handoff fragment, exchanges it once, and stores its session with the issuing daemon identity in `sessionStorage`. Concurrent initialization shares the exchange; storage denial falls back to memory. Old responses cannot resurrect or revoke a newer session. A bare URL shows connection instructions and never gains control through public health.
110
60
 
111
- `web/src/api/index.ts`:
61
+ Core HTTP requests carry `X-Sash-Token`; streams use private WebSocket subprotocol authentication. The daemon replaces these credentials with its internal controller bearer. Frames require finite nonnegative counters or known textual log records. Each stream owns one reconnect timer and ignores frames from older runtime generations.
112
62
 
113
- - distinguishes JSON and `204 No Content` endpoints through overloads rather than `undefined as T`;
114
- - reads an error response body once and surfaces JSON or plain-text messages;
115
- - parses WebSocket frames separately from consumer callbacks, validates finite non-negative traffic counters and known textual log records, and silently drops malformed frames;
116
- - maintains at most one reconnect timer per stream;
117
- - binds stream callbacks to the current runtime generation and stops stream ownership when the daemon session or healthy Core is unavailable;
118
- - resets stale traffic rates/history when the current traffic stream disconnects without allowing an older socket's close event to clear a replacement stream;
119
- - sends `X-Sash-Token` on HTTP requests after initialization.
63
+ ## Development and verification
120
64
 
121
- Streams:
65
+ ```sh
66
+ npm run typecheck
67
+ npm run lint
68
+ npm test
69
+ npm run build
70
+ node --import tsx scripts/ui-verify.mts
71
+ node --import tsx scripts/profile-ui-verify.mts
72
+ node --import tsx scripts/web-auth-ui-verify.mts
73
+ node --import tsx scripts/autostart-ui-verify.mts
74
+ ```
122
75
 
123
- - `api.connectTraffic(callback)` `/core/api/traffic`
124
- - `api.connectLogs(callback)` → `/core/api/logs`
76
+ The browser scripts use isolated data, non-default ports and fake Core/OS adapters in Chromium and Firefox. The main script checks saved/applied state, authentication, font loading, request counts and layouts with 300 nodes, 10,000 rules and 500 connections. Other scripts retain focused profile, private-file authorization and autostart interaction coverage. These are behavioral checks, not a real-Core CPU/memory benchmark.
125
77
 
126
- The controller secret is never available to browser code; sashd injects it server-side.
78
+ For local source development:
127
79
 
128
- ---
80
+ ```sh
81
+ node scripts/dev.mjs web
82
+ node scripts/dev.mjs build # rebuild UI, then refresh the page
83
+ node scripts/dev.mjs stop # before loading backend changes
84
+ node scripts/dev.mjs web
85
+ ```
129
86
 
130
- ## 6. Interaction State
87
+ The launcher uses a separate `-dev` data directory and initial ports `18890`, `18990`, `28990`. `SASH_DEV_HOME` selects another absolute directory. It passes development defaults to the daemon without writing settings itself. `web` starts management only; `restart` applies configuration to Core. A backend code change requires stopping and starting the daemon.
131
88
 
132
- - Settings derive mixed-port dirty state from the committed value, preserve edited drafts across polling and offer reset. LAN and system-proxy mutations immediately adopt the committed response. If the subsequent runtime refresh fails, the UI reports that settings were saved and verification is temporarily unavailable. TUN controls and service administration are outside the 0.1.1 dashboard.
133
- - Logs receive monotonic IDs before entering the capped 600-row buffer, providing stable Vue keys and an update sequence even when length remains constant.
134
- - The global confirm service settles a previous pending Promise before opening another dialog; Escape, route changes and component unmount cancel the active confirmation.
135
- - The global banner distinguishes an unreachable daemon from a degraded same-owner Core snapshot and an unavailable new-owner snapshot.
89
+ The font build continues to use `cn-font-split`, native subsetter `default@7.6.8` and the scoped `koffi: 2.16.3` override. Install the native subsetter explicitly when dependency lifecycle scripts are disabled. Bundled Vue/Remix Icon licenses remain in `THIRD_PARTY_NOTICES.md` and `docs/remix-icon-license.txt`.
package/docs/usage.md CHANGED
@@ -1,201 +1,129 @@
1
1
  # Sash User & Operations Guide
2
2
 
3
- Sash is a lightweight command-line companion and web dashboard for managing a rule-based network core.
3
+ Sash is a lightweight network toolbox for developers, learning and research, with a CLI and built-in web dashboard. Windows is the primary desktop platform.
4
4
 
5
- ---
6
-
7
- ## 1. Quick Start
5
+ ## Start and configure
8
6
 
9
7
  ```sh
10
- sash start
11
- sash web # open the dashboard: download a subscription, pick nodes, toggle the system proxy
8
+ sash web # start management and authorize the browser; Core stays stopped
9
+ sash start # install Core if missing, apply saved configuration and start
12
10
  sash status
11
+ sash stop # restore the prior proxy, stop Core and exit the daemon
13
12
  ```
14
13
 
15
- Profiles, the system proxy and all runtime settings are managed from the web dashboard; the CLI covers lifecycle, logs and upgrades.
16
-
17
- ---
18
-
19
- ## 2. CLI Command Reference
20
-
21
- ### Lifecycle Management
22
-
23
- | Command | Description |
24
- | :--- | :--- |
25
- | `sash start` | Install missing components, ensure the background daemon is running, then reconcile/start the core. It is safe to repeat. |
26
- | `sash stop` | Restore the pre-Sash system proxy, stop the Core and shut down the daemon; exits with an error if safe shutdown cannot be verified. |
27
- | `sash restart` | Restart the whole runtime: the daemon exits through its maintenance boundary and a fresh daemon starts the core. |
28
- | `sash status [--json]` | Show daemon/core state, active profile, endpoints and system proxy state; incomplete observations exit with code 2. |
29
- | `sash logs [-n N] [-f] [--errors] [--daemon]` | View core or daemon logs; `-f` follows new output. |
30
-
31
- ### Status JSON and exit codes
32
-
33
- `sash status --json` emits the versioned `schemaVersion: 1` contract below. Unobservable runtime values are `null`; they are never changed to `false` merely because a query timed out.
34
-
35
- ```json
36
- {
37
- "schemaVersion": 1,
38
- "complete": true,
39
- "healthy": true,
40
- "queryError": null,
41
- "daemon": {
42
- "state": "healthy",
43
- "running": true,
44
- "healthy": true,
45
- "pid": 1234,
46
- "port": 19090
47
- },
48
- "core": {
49
- "running": true,
50
- "healthy": true,
51
- "pid": 1235,
52
- "version": "v1.19.30",
53
- "installedVersion": "v1.19.30"
54
- },
55
- "systemProxy": {
56
- "desired": false,
57
- "daemonApplied": false,
58
- "osObserved": {
59
- "supported": true,
60
- "enabled": false,
61
- "server": null,
62
- "details": null
63
- }
64
- },
65
- "uiInstalled": true,
66
- "endpoints": {
67
- "mixedProxy": "127.0.0.1:7890",
68
- "controller": "127.0.0.1:9090",
69
- "daemonApi": "http://127.0.0.1:19090",
70
- "dashboard": "http://127.0.0.1:19090/ui/"
71
- },
72
- "activeProfile": null,
73
- "tun": {
74
- "desired": false,
75
- "active": false
76
- },
77
- "paths": {
78
- "root": "<data directory>",
79
- "config": "<data directory>/config.yaml"
80
- }
81
- }
82
- ```
14
+ In **Profiles**, import YAML or download a remote profile, then select it. Selection, content edits, downloads and scheduled updates are saved without changing the running Core. Click **Apply configuration** to use the saved configuration. This restarts Core and briefly interrupts connections. Overview shows the profile and port actually applied.
83
15
 
84
- `healthy` is the observed daemon/Core controller health and is `null` when daemon runtime status cannot be queried. `complete` covers observability of all contract fields; `queryError` explains an incomplete result. Exit codes are stable:
85
-
86
- | Exit code | Meaning |
87
- | :--- | :--- |
88
- | `0` | The status is complete, including a known stopped state. |
89
- | `2` | Status output was produced, but daemon/Core/OS state could not be fully observed. |
90
- | `1` | The command itself failed, for example because local state is corrupt. |
16
+ If validation fails, the previous Core keeps running. If starting the new configuration fails, your saved edits remain and the dashboard stays available for correction. There is no automatic rollback of saved edits.
91
17
 
92
- Text output follows the same distinction: an unresponsive daemon is reported as unavailable, never with a success marker. `sash status` prints separate daemon, desired, daemon-applied and OS-observed proxy lines and uses exit code 2 when the daemon is alive but unresponsive.
18
+ ## Commands
93
19
 
94
- ### Log following
20
+ | Command | Behavior |
21
+ | --- | --- |
22
+ | `sash start` | Ensure management exists; start with saved configuration if stopped. Repeated starts check the running Core and proxy intent. |
23
+ | `sash restart` | Apply saved configuration and restart Core; keep the daemon and browser sessions. |
24
+ | `sash stop` | Restore proxy, stop Core and exit management. Report an error if safe shutdown cannot be verified. |
25
+ | `sash status [--json]` | Read runtime, endpoint, saved profile, proxy and autostart observations. Bare `sash` does the same. |
26
+ | `sash web` | Start management if needed and authorize/open the dashboard. |
27
+ | `sash web --no-open` | Start management and print its address without authorizing a browser. |
28
+ | `sash update [--version TAG]` | Download, verify and install a Core release through the daemon. |
29
+ | `sash auto [on\|off\|status]` | Set or inspect Windows login startup. No argument means status. |
30
+ | `sash logs [-n N] [-f] [--errors] [--daemon]` | Read Core or daemon logs; follow waits for creation and handles rotation. |
31
+ | `sash logs --startup [-n N] [-f]` | Read login attempts, including settings errors before daemon startup. |
32
+ | `sash version` | Print the package version. |
95
33
 
96
- `logs -f` behaves like a bounded `tail -F`: it waits when the selected file or log directory does not exist yet, follows appended bytes, restarts at byte zero after truncation or file replacement/rotation, and releases its watcher/timer on SIGINT or SIGTERM. `-n` accepts only canonical positive decimal integers such as `1` or `100`; zero, signs, whitespace, fractions, numeric prefixes and values above JavaScript's safe-integer limit are rejected.
34
+ WebUI **Stop Core** keeps the management process open. To reload updated Sash program code, use stop followed by start/web. `restart` only replaces Core.
97
35
 
98
- Sash does not blindly turn off an existing proxy. It stores a private ownership journal before takeover and restores only while managed OS values still match the original/Sash transition. If another application changes those values, Sash refuses to overwrite them. Windows and macOS include manual and automatic proxy state; Linux system-proxy automation currently requires GNOME `gsettings`.
36
+ ## Browser access
99
37
 
100
- ### Web Dashboard
38
+ Run `sash web` as the same user and with the same `SASH_HOME` as the instance. A private local handoff authorizes the browser without printing credentials. It expires after 90 seconds and works once; rerun the command if needed.
101
39
 
102
- | Command | Description |
103
- | :--- | :--- |
104
- | `sash web` | Authorize this browser and open `http://127.0.0.1:19090/ui/`. |
105
- | `sash web --no-open` | Print the dashboard URL without opening or authorizing a browser. |
40
+ An authorized tab survives refresh and Core restarts/updates. A daemon restart needs a new authorization. If browser storage is disabled, authorization lasts only for the current page. Opening a bare dashboard address displays connection instructions.
106
41
 
107
- Run `sash web` as the user who runs Sash, with the same `SASH_HOME`. It uses a private local handoff to authorize the browser automatically. A bare dashboard address shows a read-only connection page; it cannot obtain control access from public health information. The handoff expires after 90 seconds and works once. If it expires before the browser opens, rerun the command. An authorized tab can be refreshed, but a daemon restart requires a new `sash web` authorization. Browser storage restrictions keep the session in memory only, so those browsers also require authorization after a page reload.
42
+ ## Settings and profiles
108
43
 
109
- ### Profiles
44
+ The Settings page saves the mixed proxy port and LAN access for the next Apply. The system-proxy switch takes effect separately and requires a healthy Core to enable. A failed disable keeps the saved off intent; retry it after resolving the OS problem.
110
45
 
111
- Profiles are managed from the WebUI Profiles page: download from a subscription URL, import a local YAML file, update one or all profiles, rename, edit content, switch the active profile and delete. Remote profiles use the update interval advertised by the provider, defaulting to 24 hours. The daemon checks for due updates every 15 minutes.
46
+ Remote profiles use the provider's update interval, defaulting to 24 hours. The daemon checks for due updates every 15 minutes. Updates save new content and indicate pending Apply. Identical content does not create a new content revision. Rename and reorder do not affect running data or latency results.
112
47
 
113
- ### Settings
48
+ The profile editor rejects a save if another edit changed its content revision. Reopen the current content before retrying. The raw application settings file has no online editor.
114
49
 
115
- Runtime settings (`mixedPort`, `allowLan`, `systemProxy`) are managed from the WebUI Settings page, and the entire `sash.json` can be edited as JSON from the same page ("Edit settings file"); invalid documents are rejected without touching the disk. `daemonSecret` changes apply immediately; `daemonPort` changes are saved but require a manual `sash restart` to rebind the listener.
50
+ Login startup is managed through the OS registration, not a boolean in the settings file. Enabling it requires a direct global npm installation. See [Automatic Startup](./autostart.md).
116
51
 
117
- ### Maintenance & Upgrades
52
+ ## State format and data
118
53
 
119
- | Command | Description |
120
- | :--- | :--- |
121
- | `sash update [--version V] [--force]` | Download and validate a replacement core, then swap it in transactionally. |
122
- | `sash upgrade [--version V]` | Upgrade the Sash package through npm; requires `sashd` to be stopped first. |
123
- | `sash version` | Print the Sash package version. |
54
+ This branch requires a new schema-2 manifest and provides no migration from older releases. Stop the old instance using its existing installation, keep any profile YAML you need, then use a fresh data directory and import those files normally. Old state is never silently overwritten.
124
55
 
125
- Core updates download and validate before shutdown, then use an authenticated maintenance request that atomically snapshots whether Core was running while restoring proxy state and stopping `sashd`. After daemon exit, a durable update journal records the previous/target install records before the executable swap. A previously running Core is health-checked immediately; when Core was stopped, `<core>.bak` and the journal remain until the next managed `sash start` passes controller health/version checks. A failed first start restores the previous binary and install record before attempting to restart it. Downloads require official GitHub SHA-256 asset metadata; mirrors are accepted only as transports for bytes matching that digest, and all mirror/redirect/body attempts share a bounded absolute deadline. Archives are capped at 128 MiB, Windows ZIPs must contain the expected upstream Core executable basename, staged binaries must report the exact requested version, and the staged Core validates the freshly generated active configuration before publication.
56
+ `sash.json` contains `{schemaVersion: 2, revision, settings, profiles}`. `profiles` contains the saved `activeId` and metadata list. The following keys live inside `settings`:
126
57
 
127
- `sash upgrade --version` accepts only a strict npm semver such as `1.2.3` (without a `v` prefix) or a safe dist-tag such as `latest`/`next`. Package specs, paths, ranges and control characters are rejected.
58
+ | Key | Initial value | Meaning |
59
+ | --- | --- | --- |
60
+ | `mixedPort` | `7890` | HTTP/SOCKS mixed proxy port |
61
+ | `allowLan` | `false` | Accept proxy traffic from other devices |
62
+ | `systemProxy` | `false` | Desired Windows system-proxy state |
63
+ | `controller` | `127.0.0.1:9090` | Internal loopback controller address |
64
+ | `daemonPort` | `19090` | Management API and dashboard port |
65
+ | `secret` | random | Private controller credential |
66
+ | `daemonSecret` | random | Private CLI credential |
128
67
 
129
- ---
68
+ Controller/daemon addresses and credentials can be edited only while Sash is stopped, then read at daemon startup. All three ports must differ. Secrets cannot be blank. Invalid, unknown-field, oversized or unsupported-format state is rejected intact. There are no TUN or legacy subscription settings.
130
69
 
131
- ## 3. Configuration Reference (`sash.json`)
132
-
133
- | Key | Default | Description |
134
- | :--- | :--- | :--- |
135
- | `schemaVersion` | `1` | On-disk settings schema; managed by Sash. |
136
- | `mixedPort` | `7890` | Local HTTP/SOCKS5 mixed inbound port. |
137
- | `controller` | `127.0.0.1:9090` | Internal controller listen address; only loopback hosts are accepted. |
138
- | `daemonPort` | `19090` | Daemon API and WebUI port. |
139
- | `secret` | *(random)* | Internal controller secret. It is never returned by the public status API. |
140
- | `daemonSecret` | *(random)* | CLI bearer secret for state-changing daemon requests. |
141
- | `systemProxy` | `false` | Desired OS-level system proxy state. |
142
- | `tun` | `false` | Legacy compatibility field; always off in 0.1.1. Existing true values migrate to false on load; new true values are rejected. |
143
- | `allowLan` | `false` | Accept proxy traffic from other devices. |
144
-
145
- A legacy `subscriptionUrl` key is migrated once into `profiles/index.json` and then removed. It has priority over legacy `config.yaml` import. If no `profiles/index.json` has ever been created, startup/offline initialization may import an existing `config.yaml` once as the active local profile named `Imported config` (`url: ""`, updates disabled). A present empty index opts out. To avoid importing Sash's own generated default, the file must be valid core-format YAML and contain non-default routing content after managed keys are removed: nonempty proxies/providers, or nonempty rules/groups differing from the DIRECT-only default. The runtime `config.yaml` is kept unchanged during import; later profile application re-renders and validates it. Invalid YAML/config fails closed without overwriting the file.
70
+ | Platform | Default directory |
71
+ | --- | --- |
72
+ | Windows | `%LOCALAPPDATA%\Sash` |
73
+ | macOS | `~/Library/Application Support/Sash` |
74
+ | Linux | `$XDG_DATA_HOME/sash` or `~/.local/share/sash` |
146
75
 
147
- Installed core version metadata lives in `state/install.json`, not in `sash.json`.
76
+ Use an absolute `SASH_HOME` to select another directory. Use a local filesystem supporting atomic rename and hard links.
77
+
78
+ ```text
79
+ sash.json settings, profiles, selection and saved-state revision
80
+ profiles/<id>/<revision>.yaml immutable source content
81
+ runtime/config.yaml generated runtime configuration
82
+ bin/ Core executable and temporary update backup
83
+ state/install.json installed version
84
+ state/core-update-transaction.json active binary update/recovery
85
+ state/system-proxy.json original proxy snapshot and recovery phase
86
+ state/sash.pid, state/sashd.pid process discovery records
87
+ state/sashd*.lock daemon singleton/startup ownership
88
+ logs/ Core, daemon and login diagnostics
89
+ ui/ optional custom dashboard override
90
+ ```
148
91
 
149
- Malformed, future-version or unknown-field `sash.json` documents and malformed `profiles/index.json` files are rejected without being overwritten. Secrets cannot be blank or contain control characters, the controller must remain loopback-only, and the mixed, controller and daemon ports must all differ. Repair or move a damaged file explicitly instead of relying on silent defaults.
92
+ The manifest and sources use atomic publication. Old source revisions may be cleaned after successful saves; this is not a version-history feature. Do not edit generated runtime configuration. POSIX private state/logs use `0600`.
150
93
 
151
- Settings changes are prepared as an all-or-nothing candidate: active configuration is validated before settings/config publication, and a failed restart restores the previous candidate where possible. Turning the system proxy off persists the desired off state before OS cleanup; if cleanup fails, toggle the system proxy off again from the WebUI after resolving the OS error.
94
+ ## Updates
152
95
 
153
- ---
96
+ Core updates keep the dashboard available and preserve whether Core was running. Even an initially stopped update performs a temporary startup/health check, then stops again. `.bak` is retained until the new binary passes verification and the original running state is restored. Failure rolls back the executable and install record; saved profiles/settings are not part of this transaction.
154
97
 
155
- ## 4. Network Scope in 0.1.1
98
+ Downloads require official SHA-256 metadata, trusted HTTPS origins and bounded extraction. If verification cannot complete, the update fails rather than executing unverifiable bytes.
156
99
 
157
- This release supports local HTTP/SOCKS endpoints, profile rules and reversible system-proxy integration. TUN and Windows Service Mode are developed on the [feat/tun-service-mode branch](https://github.com/ming-kang/Sash/tree/feat/tun-service-mode).
100
+ Update Sash itself through npm:
158
101
 
159
- The dashboard has no TUN switch or service installation controls. Settings updates reject `tun: true`; loading an otherwise valid older settings file migrates that value to `false` atomically. Generated configurations explicitly set `tun.enable: false`, including on reload. Profiles containing a separate TUN listener are rejected before publication. Original profile files and profile-owned DNS/provider settings remain unchanged. The JSON status contract retains its TUN observation fields for compatibility.
102
+ ```sh
103
+ sash stop
104
+ npm install -g @astralyn/sash
105
+ sash start
106
+ sash web
107
+ ```
160
108
 
161
- ---
109
+ `sash upgrade` and `sash update --force` are removed. Damaged installations are diagnosed and preserved; use a clean data directory for reinstalling after stopping the existing instance.
162
110
 
163
- ## 5. Data Directory Layout
111
+ ## Status and troubleshooting
164
112
 
165
- | Platform | Default Path |
166
- | :--- | :--- |
167
- | Windows | `%LOCALAPPDATA%\Sash` |
168
- | macOS | `~/Library/Application Support/Sash` |
169
- | Linux | `$XDG_DATA_HOME/sash` or `~/.local/share/sash` |
113
+ `sash status --json` uses `schemaVersion: 2`. It includes `complete`, `healthy`, `queryError`, daemon/Core state, desired/applied/observed proxy state, autostart, endpoints, saved active profile and paths. Unknown observations remain `null`; no TUN fields are emitted. The running proxy endpoint comes from applied settings.
170
114
 
171
- Override the root with an absolute `SASH_HOME` path.
172
-
173
- - `bin/`: installed core executable; `.bak` is retained during an update transaction.
174
- - `config.yaml`: active runtime configuration rendered from the active profile or the DIRECT-only default; qualifying pre-profile files are preserved during one-time import.
175
- - `sash.json`: Sash settings and local control secrets.
176
- - `profiles/index.json`: profile metadata and active profile id.
177
- - `profiles/<id>.yaml`: validated local copy of each downloaded/imported profile.
178
- - `state/sashd.pid`, `state/sash.pid`: atomic daemon/Core discovery records.
179
- - `state/system-proxy.json`: pre-takeover proxy snapshot and ownership phase.
180
- - `state/install.json`: canonical installed Core version record.
181
- - `state/core-install-transaction.json`: first-install publication journal; interrupted publishing rolls back, while a committed marker is only cleared.
182
- - `state/core-update-transaction.json`: previous/target install records and update phase; retained with `.bak` until managed runtime health and restoration succeed.
183
- - `state/*.lock`: daemon, runtime, mutation, settings and proxy ownership leases.
184
- - `logs/`: core and daemon stdout/stderr logs.
185
- - `ui/` *(optional)*: custom dashboard override.
186
-
187
- State files are written with mode `0o600` on POSIX where applicable. `SASH_HOME` must be on a local filesystem supporting atomic rename and hard links.
188
-
189
- ---
190
-
191
- ## 6. Troubleshooting
192
-
193
- - **System proxy recovery is blocked:** another application changed managed values or the ownership journal is corrupt. Keep the Core running, inspect `state/system-proxy.json` and the current OS proxy, then repair explicitly; Sash will not overwrite an unrecognized state.
194
- - **Daemon/Core ownership is corrupt:** inspect `state/*.lock` and PID records. Sash intentionally fails closed instead of deleting uncertain ownership records.
195
- - **Profile update failed:** inspect the profile card's error or use its update button; generated candidates are checked by the installed Core before commit, and the last valid running config remains active on validation/reload failure.
196
- - **Corrupt settings/profile index:** repair the JSON file or move it aside; Sash intentionally does not overwrite corrupt state.
197
- - **Daemon errors:** `sash logs --daemon --errors`.
198
- - **Core errors:** `sash logs --errors`. Log tails and follow-mode reads use bounded chunks, so large logs do not require one whole-file allocation.
199
- - **Shutdown returned an error:** cleanup was not completed; the daemon remains listening and scheduled profile updates remain active. Resolve the reported proxy/Core issue and retry `sash stop`.
200
- - **Core binary/metadata mismatch:** Sash will not execute a binary unless `state/install.json` is valid and agrees that an installation exists. An interrupted `.unlock-probe` is restored automatically when it is the only copy; if both files exist with different bytes, Sash preserves both and fails closed. Inspect them explicitly or run `sash update --force` after resolving the conflict.
201
- - **Force a validated core reinstall:** `sash update --force`.
115
+ | Exit code | Meaning |
116
+ | --- | --- |
117
+ | `0` | Complete observation, including a known stopped state |
118
+ | `2` | Output produced, but some runtime/OS observation is unavailable |
119
+ | `1` | The command failed, for example due to corrupt local state |
120
+
121
+ - **Pending configuration:** click Apply or run `sash restart`. Saving alone does not change Core.
122
+ - **Apply failed:** inspect the displayed error and `sash logs --errors`; correct the saved profile and apply again.
123
+ - **Proxy restoration blocked:** keep the ownership journal and inspect the current Windows settings. Sash will not overwrite third-party changes or stop a healthy Core while restoration fails.
124
+ - **Daemon ownership unknown:** inspect its logs and PID/lease records; Sash will not kill an unverified process or start a competitor.
125
+ - **Interrupted update:** stop and start the daemon so its startup recovery can run. Corrupt or unrecognized backup/metadata files are preserved for inspection.
126
+ - **Login startup failed:** read `sash auto status` and `sash logs --startup`; repair the entry with `sash auto on`.
127
+ - **Shutdown failed:** the management API remains available for retry. Resolve the reported proxy/Core failure and repeat `sash stop`.
128
+
129
+ Windows proxy/PAC restoration and login startup are the only desktop integrations. Basic Core/CLI operation remains portable. TUN and service mode are outside this branch; generated configuration always disables TUN and rejects a separate TUN listener.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astralyn/sash",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A network toolbox for developers, learning, and research, with a command-line companion and built-in web dashboard.",
5
5
  "license": "MIT",
6
6
  "author": "ming-kang",
@@ -55,7 +55,6 @@
55
55
  "dependencies": {
56
56
  "adm-zip": "^0.6.0",
57
57
  "commander": "^13.1.0",
58
- "cross-spawn": "^7.0.6",
59
58
  "undici": "^7.29.0",
60
59
  "yaml": "^2.7.0"
61
60
  },
@@ -70,13 +69,14 @@
70
69
  "@codemirror/theme-one-dark": "^6.1.3",
71
70
  "@remixicon/vue": "^4.9.0",
72
71
  "@types/adm-zip": "^0.5.7",
73
- "@types/cross-spawn": "^6.0.6",
74
72
  "@types/node": "^24.0.0",
73
+ "@types/sortablejs": "1.15.9",
75
74
  "@vitejs/plugin-vue": "^5.2.4",
76
75
  "cn-font-split": "^7.4.3",
77
76
  "codemirror": "^6.0.2",
78
77
  "happy-dom": "20.13.0",
79
78
  "playwright": "^1.62.1",
79
+ "sortablejs": "1.15.7",
80
80
  "tsx": "^4.19.3",
81
81
  "typescript": "^5.8.2",
82
82
  "vite": "^6.4.3",
@@ -1,43 +0,0 @@
1
- import spawn from "cross-spawn";
2
- import { log } from "../log.js";
3
- import { buildSanitizedEnv } from "../process.js";
4
- import { resolveRuntimeOwner } from "../runtime-owner.js";
5
- import { runtimeContext } from "./shared.js";
6
- const PACKAGE_NAME = "@astralyn/sash";
7
- const STRICT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
8
- const SAFE_DIST_TAG = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
9
- export function validateUpgradeVersion(value) {
10
- if (value !== value.trim() || !value) {
11
- throw new Error(`Invalid Sash package version or dist-tag: ${JSON.stringify(value)}`);
12
- }
13
- if (STRICT_SEMVER.test(value))
14
- return value;
15
- if (SAFE_DIST_TAG.test(value) && !/^v?\d+(?:\.|$)/i.test(value))
16
- return value;
17
- throw new Error(`Invalid Sash package version or dist-tag: ${JSON.stringify(value)}`);
18
- }
19
- export function buildUpgradeSpawnOptions(sourceEnv = process.env) {
20
- return { stdio: "inherit", env: buildSanitizedEnv(sourceEnv) };
21
- }
22
- export async function runUpgrade(opts = {}) {
23
- const ctx = runtimeContext();
24
- const owner = await resolveRuntimeOwner(ctx);
25
- if (owner.kind !== "offline") {
26
- throw new Error("stop Sash with `sash stop` before upgrading the package");
27
- }
28
- const version = validateUpgradeVersion(opts.version ?? "latest");
29
- const target = `${PACKAGE_NAME}@${version}`;
30
- log.info(`upgrading Sash via npm: ${target}`);
31
- const code = await new Promise((resolve) => {
32
- const child = spawn("npm", ["install", "-g", target], buildUpgradeSpawnOptions());
33
- child.on("error", (err) => {
34
- log.error(`failed to launch npm: ${err.message}`);
35
- resolve(1);
36
- });
37
- child.on("close", (exitCode) => resolve(exitCode ?? 1));
38
- });
39
- if (code !== 0) {
40
- throw new Error(`npm exited with code ${code}. Try running the command manually: npm install -g ${target}`);
41
- }
42
- log.ok("Sash upgraded. Run `sash version` to verify, then start it normally.");
43
- }