@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
package/docs/frontend.md CHANGED
@@ -1,125 +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
- ---
17
-
18
- ## 2. Source Layout
19
-
20
- ```text
21
- web/src/
22
- ├── api/index.ts typed REST client and WebSocket reconnect logic
23
- ├── components/
24
- │ ├── AppSidebar.vue
25
- │ ├── ConfirmDialog.vue
26
- │ ├── Icon.vue semantic icon wrapper
27
- │ ├── icons.ts tree-shaken Remix Icon component mapping
28
- │ ├── ProxyGroupSection.vue reusable group header/node grid
29
- │ ├── TrafficChart.vue
30
- │ └── ...
31
- ├── i18n/ Chinese source messages and typed English mirror
32
- ├── stores/index.ts runtime state, refresh actions and polling
33
- ├── stores/state-ownership.ts pure generations, cleanup and sync helpers
34
- ├── styles/main.css design tokens and shared utility/component styles
35
- ├── types/index.ts core-controller response types; daemon types are shared
36
- ├── views/
37
- │ ├── OverviewView.vue status, traffic, modes and proxy groups
38
- │ ├── ProfilesView.vue download/import/update/select/delete profiles
39
- │ ├── LogsView.vue
40
- │ ├── ConnectionsView.vue
41
- │ ├── RulesView.vue
42
- │ └── SettingsView.vue
43
- ├── App.vue shell, view selection and stream lifecycle
44
- ├── theme.ts persisted system/light/dark theme state
45
- └── router.ts small hash router
46
- ```
1
+ # Frontend Architecture
2
+
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.
47
4
 
48
- 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.
5
+ ## State ownership
49
6
 
50
- 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.
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.
51
8
 
52
- ---
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 |
53
18
 
54
- ## 3. Runtime State Ownership
19
+ Three values determine refresh ownership:
55
20
 
56
- `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.
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.
57
24
 
58
- Canonical actions include:
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.
59
26
 
60
- - `refreshStatus()`
61
- - `refreshConnections()`
62
- - `refreshProxies()`
63
- - `refreshProfiles()`
64
- - `refreshRuntimeState()`
65
- - profile mutation actions such as `updateProfile()` and `activateProfile()`
66
- - runtime intent actions such as `setOutboundMode()` and `selectGroupProxy()`
67
- - `startRuntimePolling()`
27
+ ## Refresh and performance
68
28
 
69
- 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 responses made stale by a newer refresh or user mutation. Core-specific API calls are made only after status reports `running && healthy`.
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.
70
30
 
71
- Daemon reachability, profile revision and Core snapshots have separate ownership. A successful `/sash/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.
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 |
72
38
 
73
- 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.
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.
74
40
 
75
- 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.
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.
76
42
 
77
- 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.
43
+ ## Views and shared controls
78
44
 
79
- ---
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.
80
46
 
81
- ## 4. Profiles and Overview Components
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.
82
48
 
83
- The Profiles page provides:
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.
84
50
 
85
- - URL download and clipboard paste
86
- - local YAML import
87
- - Update All and per-profile update
88
- - active profile selection
89
- - quota/expiry display from provider metadata
90
- - persisted update errors
91
- - delete confirmation
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.
92
52
 
93
- 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.
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.
94
54
 
95
- 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.
55
+ ## Authorization and transport
96
56
 
97
- ---
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.
98
58
 
99
- ## 5. API and Streaming
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.
100
60
 
101
- `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.
102
62
 
103
- - distinguishes JSON and `204 No Content` endpoints through overloads rather than `undefined as T`;
104
- - reads an error response body once and surfaces JSON or plain-text messages;
105
- - parses WebSocket frames separately from consumer callbacks, validates finite non-negative traffic counters and known textual log records, and silently drops malformed frames;
106
- - maintains at most one reconnect timer per stream;
107
- - binds stream callbacks to the current runtime generation and stops stream ownership when the daemon session or healthy Core is unavailable;
108
- - resets stale traffic rates/history when the current traffic stream disconnects without allowing an older socket's close event to clear a replacement stream;
109
- - sends `X-Sash-Token` on HTTP requests after initialization.
63
+ ## Development and verification
110
64
 
111
- 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
+ ```
112
75
 
113
- - `api.connectTraffic(callback)` `/core/api/traffic`
114
- - `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.
115
77
 
116
- The controller secret is never available to browser code; sashd injects it server-side.
78
+ For local source development:
117
79
 
118
- ---
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
+ ```
119
86
 
120
- ## 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.
121
88
 
122
- - Settings derive mixed-port dirty state by comparing the draft with the last committed value, preserve a genuinely edited draft across polling, and expose an explicit reset action. Successful `allow-lan` and TUN responses commit the returned settings snapshot directly before any follow-up refresh. TUN controls separately derive active, inactive, unverified, pending-start and desired/runtime-mismatch presentation from `settings.tun` plus `core.tunActive`; failed online activation never advances the committed switch, and the returned error explains that the whole Sash daemon—not only its Core child—must be restarted with elevated privileges.
123
- - Logs receive monotonic IDs before entering the capped 600-row buffer, providing stable Vue keys and an update sequence even when length remains constant.
124
- - The global confirm service settles a previous pending Promise before opening another dialog; Escape, route changes and component unmount cancel the active confirmation.
125
- - 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,227 +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:
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.
85
17
 
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. |
18
+ ## Commands
91
19
 
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.
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. |
93
33
 
94
- ### Log following
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.
95
35
 
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.
36
+ ## Browser access
97
37
 
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`.
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.
99
39
 
100
- ### Web Dashboard
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.
101
41
 
102
- | Command | Description |
103
- | :--- | :--- |
104
- | `sash web` | Open `http://127.0.0.1:19090/ui/`. |
105
- | `sash web --no-open` | Print the dashboard URL without opening a browser. |
42
+ ## Settings and profiles
106
43
 
107
- ### 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.
108
45
 
109
- 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.
110
47
 
111
- ### 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.
112
49
 
113
- Runtime settings (`mixed-port`, `tun`, `allow-lan`, `system-proxy`) 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).
114
51
 
115
- ### Maintenance & Upgrades
52
+ ## State format and data
116
53
 
117
- | Command | Description |
118
- | :--- | :--- |
119
- | `sash update [--version V] [--force]` | Download and validate a replacement core, then swap it in transactionally. |
120
- | `sash upgrade [--version V]` | Upgrade the Sash package through npm; requires `sashd` to be stopped first. |
121
- | `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.
122
55
 
123
- 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`:
124
57
 
125
- `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 |
126
67
 
127
- ---
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.
128
69
 
129
- ## 3. Configuration Reference (`sash.json`)
130
-
131
- | Key | Default | Description |
132
- | :--- | :--- | :--- |
133
- | `schemaVersion` | `1` | On-disk settings schema; managed by Sash. |
134
- | `mixedPort` | `7890` | Local HTTP/SOCKS5 mixed inbound port. The CLI key is `mixed-port`. |
135
- | `controller` | `127.0.0.1:9090` | Internal controller listen address; only loopback hosts are accepted. |
136
- | `daemonPort` | `19090` | Daemon API and WebUI port. |
137
- | `secret` | *(random)* | Internal controller secret. It is never returned by the public status API. |
138
- | `daemonSecret` | *(random)* | CLI bearer secret for state-changing daemon requests. |
139
- | `systemProxy` | `false` | Desired OS-level system proxy state. |
140
- | `tun` | `false` | Enable the TUN inbound; requires elevated privileges. |
141
- | `allowLan` | `false` | Accept proxy traffic from other devices. |
142
-
143
- 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` |
144
75
 
145
- 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
+ ```
146
91
 
147
- 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`.
148
93
 
149
- 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
150
95
 
151
- ---
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.
152
97
 
153
- ## 4. TUN Mode
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.
154
99
 
155
- TUN requires the whole Sash daemon to run with elevated privileges. Stop the current daemon and save the setting while Sash is offline — toggle it in the WebUI Settings page before stopping, or set `"tun": true` in `sash.json` directly:
100
+ Update Sash itself through npm:
156
101
 
157
102
  ```sh
158
103
  sash stop
159
- # ensure TUN is on (WebUI Settings page, or edit sash.json)
160
- ```
161
-
162
- On Windows, open PowerShell as Administrator:
163
-
164
- ```powershell
104
+ npm install -g @astralyn/sash
165
105
  sash start
106
+ sash web
166
107
  ```
167
108
 
168
- The default `%LOCALAPPDATA%\Sash` data root remains the same when the current Windows user elevates. Only copy an explicitly customized `SASH_HOME` into the Administrator shell.
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.
169
110
 
170
- On macOS or Linux, `sudo` can change the default home directory. Read the current data root and pass it explicitly while starting Sash as root:
111
+ ## Status and troubleshooting
171
112
 
172
- ```sh
173
- sash status # note the printed data root
174
- sudo env SASH_HOME='<data root printed above>' "$(command -v sash)" start
175
- ```
176
-
177
- While that elevated daemon is running on macOS or Linux, use the same `sudo` and `SASH_HOME` prefix for later lifecycle commands such as `status` or `stop`, so they target the same runtime and can read its protected state.
178
-
179
- If TUN was already saved as on, just start Sash elevated. Because a full `sash restart` replaces the daemon itself, running it from the elevated shell is equivalent to `sash stop` + `sash start` here; restarting only the Core from the dashboard does not elevate `sashd`.
180
-
181
- Sash distinguishes the desired setting from the Core's actual runtime state: `sash status` reports `on (active)`, `on (inactive)`, `on (unverified)` or `on (runtime unknown)`, and `sash status --json` reports `tun.desired` separately from `tun.active` (`true`, `false` or `null`). Privilege guidance is shown only after a responsive, healthy running Core explicitly reports inactive or unverified TUN state.
182
-
183
- When a running Core is switched from TUN off to on, Sash reads back `tun.enable` from the Core before committing the setting. If the Core remains inactive or cannot be verified, the settings/config transaction and prior runtime are restored. An inactive result includes the platform-appropriate elevated `sash restart` instructions above. A TUN setting saved while the Core is stopped can only be verified on the next start; startup leaves the ordinary proxy Core available and reports any inactive or unverified TUN state explicitly. If an elevated start still leaves TUN inactive, inspect the Core error log.
184
-
185
- Do not enable TUN in automated smoke tests.
186
-
187
- ---
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.
188
114
 
189
- ## 5. Data Directory Layout
190
-
191
- | Platform | Default Path |
192
- | :--- | :--- |
193
- | Windows | `%LOCALAPPDATA%\Sash` |
194
- | macOS | `~/Library/Application Support/Sash` |
195
- | Linux | `$XDG_DATA_HOME/sash` or `~/.local/share/sash` |
196
-
197
- Override the root with an absolute `SASH_HOME` path.
198
-
199
- - `bin/`: installed core executable; `.bak` is retained during an update transaction.
200
- - `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.
201
- - `sash.json`: Sash settings and local control secrets.
202
- - `profiles/index.json`: profile metadata and active profile id.
203
- - `profiles/<id>.yaml`: validated local copy of each downloaded/imported profile.
204
- - `state/sashd.pid`, `state/sash.pid`: atomic daemon/Core discovery records.
205
- - `state/system-proxy.json`: pre-takeover proxy snapshot and ownership phase.
206
- - `state/install.json`: canonical installed Core version record.
207
- - `state/core-install-transaction.json`: first-install publication journal; interrupted publishing rolls back, while a committed marker is only cleared.
208
- - `state/core-update-transaction.json`: previous/target install records and update phase; retained with `.bak` until managed runtime health and restoration succeed.
209
- - `state/*.lock`: daemon, runtime, mutation, settings and proxy ownership leases.
210
- - `logs/`: core and daemon stdout/stderr logs.
211
- - `ui/` *(optional)*: custom dashboard override.
212
-
213
- 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.
214
-
215
- ---
216
-
217
- ## 6. Troubleshooting
218
-
219
- - **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.
220
- - **Daemon/Core ownership is corrupt:** inspect `state/*.lock` and PID records. Sash intentionally fails closed instead of deleting uncertain ownership records.
221
- - **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.
222
- - **Corrupt settings/profile index:** repair the JSON file or move it aside; Sash intentionally does not overwrite corrupt state.
223
- - **Daemon errors:** `sash logs --daemon --errors`.
224
- - **Core errors:** `sash logs --errors`. Log tails and follow-mode reads use bounded chunks, so large logs do not require one whole-file allocation.
225
- - **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`.
226
- - **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.
227
- - **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,7 +1,7 @@
1
1
  {
2
2
  "name": "@astralyn/sash",
3
- "version": "0.1.0",
4
- "description": "A lightweight command-line companion for a rule-based network core and its web dashboard. For developers and advanced users.",
3
+ "version": "0.1.2",
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",
7
7
  "type": "module",
@@ -47,8 +47,7 @@
47
47
  "network",
48
48
  "toolbox",
49
49
  "rules",
50
- "dashboard",
51
- "tun"
50
+ "dashboard"
52
51
  ],
53
52
  "publishConfig": {
54
53
  "access": "public"
@@ -56,23 +55,28 @@
56
55
  "dependencies": {
57
56
  "adm-zip": "^0.6.0",
58
57
  "commander": "^13.1.0",
59
- "cross-spawn": "^7.0.6",
60
58
  "undici": "^7.29.0",
61
59
  "yaml": "^2.7.0"
62
60
  },
61
+ "overrides": {
62
+ "cn-font-split": {
63
+ "koffi": "2.16.3"
64
+ }
65
+ },
63
66
  "devDependencies": {
64
67
  "@biomejs/biome": "^2.5.5",
65
68
  "@codemirror/legacy-modes": "^6.5.4",
66
69
  "@codemirror/theme-one-dark": "^6.1.3",
67
70
  "@remixicon/vue": "^4.9.0",
68
71
  "@types/adm-zip": "^0.5.7",
69
- "@types/cross-spawn": "^6.0.6",
70
72
  "@types/node": "^24.0.0",
73
+ "@types/sortablejs": "1.15.9",
71
74
  "@vitejs/plugin-vue": "^5.2.4",
72
75
  "cn-font-split": "^7.4.3",
73
76
  "codemirror": "^6.0.2",
74
77
  "happy-dom": "20.13.0",
75
78
  "playwright": "^1.62.1",
79
+ "sortablejs": "1.15.7",
76
80
  "tsx": "^4.19.3",
77
81
  "typescript": "^5.8.2",
78
82
  "vite": "^6.4.3",