@mmmbuto/nexuscrew 0.2.5 → 0.4.1
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.
- package/README.md +63 -163
- package/bin/nexuscrew.js +1 -296
- package/frontend/dist/assets/index-B41xYEDb.js +81 -0
- package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
- package/frontend/dist/icon-maskable.svg +4 -0
- package/frontend/dist/icon.svg +4 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/manifest.json +24 -0
- package/frontend/dist/sw.js +3 -0
- package/frontend/index.html +20 -0
- package/lib/auth/token.js +26 -0
- package/lib/config.js +25 -0
- package/lib/pty/attach.js +30 -0
- package/lib/pty/provider.js +33 -0
- package/lib/server.js +62 -0
- package/lib/tmux/actions.js +27 -0
- package/lib/tmux/list.js +33 -0
- package/lib/ws/bridge.js +66 -0
- package/package.json +20 -24
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
- package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
- package/lib/config/paths.js +0 -30
- package/lib/server/routes/hosts.js +0 -82
- package/lib/server/routes/launchers.js +0 -17
- package/lib/server/routes/models.js +0 -26
- package/lib/server/routes/runtime.js +0 -14
- package/lib/server/routes/send.js +0 -236
- package/lib/server/routes/sessions.js +0 -104
- package/lib/server/routes/status.js +0 -62
- package/lib/server/routes/tmux.js +0 -91
- package/lib/server/server.js +0 -189
- package/lib/services/engine-discovery.js +0 -317
- package/lib/services/log-watcher.js +0 -279
- package/lib/services/session-store.js +0 -198
- package/lib/services/tmux-manager.js +0 -288
package/README.md
CHANGED
|
@@ -1,201 +1,101 @@
|
|
|
1
1
|
# NexusCrew
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.
|
|
3
|
+
[](https://www.npmjs.com/package/@mmmbuto/nexuscrew)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](#requirements)
|
|
7
|
+
[](#access-model--read-this)
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
A faithful **browser tmux client**. It attaches to your live tmux sessions over a **real PTY**
|
|
10
|
+
and streams them to a mobile-friendly web UI — full color, copy-mode scroll, special keys,
|
|
11
|
+
panes, windows. tmux does the work; the browser is just a faithful client.
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
> **NexusCrew does not create, own, or recover sessions — it only attaches.**
|
|
8
14
|
|
|
9
15
|
---
|
|
10
16
|
|
|
11
|
-
##
|
|
17
|
+
## What it is (v0.4 "pty-core")
|
|
12
18
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
19
|
+
- Runs a small server on the host where your tmux sessions live.
|
|
20
|
+
- Each attach spawns a real PTY running `tmux attach` and bridges its bytes over a WebSocket
|
|
21
|
+
to [xterm.js](https://xtermjs.org) in the browser. **No screenshots, no polling.**
|
|
22
|
+
- **localhost-only**: the server binds `127.0.0.1` and refuses any non-loopback bind.
|
|
23
|
+
- **Stateless**: tmux *is* the persistence. No database, no accounts.
|
|
24
|
+
- **Universal**: a PTY is a PTY — a coding agent, a REPL, a plain shell, anything tmux holds.
|
|
18
25
|
|
|
19
|
-
##
|
|
26
|
+
## Requirements
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
- **Node.js ≥ 18**
|
|
29
|
+
- **tmux** on the host (3.4+; the non-destructive `ignore-size` attach is honored on 3.4 and
|
|
30
|
+
later)
|
|
31
|
+
- A PTY backend is resolved automatically per platform: `node-pty` (Linux/macOS),
|
|
32
|
+
`@lydell/node-pty-linux-x64` (prebuilt Linux x64), and the native arm64 provider on Android/Termux.
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
- Lets the UI attach chat to an existing tmux session
|
|
25
|
-
- Detects runnable launchers from:
|
|
26
|
-
- PATH binaries
|
|
27
|
-
- live shell aliases/functions
|
|
28
|
-
- generic shell config fallbacks
|
|
29
|
-
- Creates a new standalone tmux session only when you explicitly ask for it
|
|
30
|
-
- Stores conversation state locally in SQLite
|
|
31
|
-
- Streams output back through SSE
|
|
34
|
+
## Access model — read this
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- It does not keep a hardcoded list of AI models or wrappers
|
|
37
|
-
- It does not assume a user-specific home like `/home/dag`
|
|
38
|
-
- It does not treat shell-file-only detections as runnable unless the active shell confirms them
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Installation
|
|
43
|
-
|
|
44
|
-
### Quick Start
|
|
45
|
-
|
|
46
|
-
Install:
|
|
36
|
+
NexusCrew binds **only to `127.0.0.1`**. There is no built-in network exposure, no public
|
|
37
|
+
tunnel, no TLS, no login server. You reach it by **bringing the loopback port to you over a
|
|
38
|
+
channel you control**:
|
|
47
39
|
|
|
48
40
|
```bash
|
|
49
|
-
|
|
41
|
+
# from your laptop/phone, tunnel the loopback port over SSH
|
|
42
|
+
ssh -L 41820:127.0.0.1:41820 user@your-host
|
|
43
|
+
# then open http://localhost:41820/#token=<token printed by the server>
|
|
50
44
|
```
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
autossh reverse tunnels or a VPN work the same way. A short-lived **local token** (0600 file,
|
|
47
|
+
auto-generated, printed once at startup) is a second factor on top of your SSH/VPN gate. The
|
|
48
|
+
token travels in the URL **fragment** (`#token=…`), so it never reaches the server logs.
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
50
|
+
> **Exposing the app publicly (reverse proxy, network bind, port forward to the internet) is
|
|
51
|
+
> unsupported and unsafe.** The whole security model is "localhost + a tunnel you control".
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
## Install & run
|
|
59
54
|
|
|
60
55
|
```bash
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Open:
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
http://localhost:41820
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Termux Quick Start
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
pkg install tmux nodejs
|
|
74
|
-
npm install -g @mmmbuto/nexuscrew
|
|
75
|
-
nexuscrew init
|
|
76
|
-
nexuscrew start
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Notes:
|
|
80
|
-
|
|
81
|
-
- the package uses a Termux-compatible SQLite dependency path during install
|
|
82
|
-
- detailed install notes live in [docs/INSTALLATION.md](docs/INSTALLATION.md)
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## Runtime Model
|
|
87
|
-
|
|
88
|
-
NexusCrew now works with standalone tmux sessions.
|
|
89
|
-
|
|
90
|
-
That means:
|
|
56
|
+
# install the test channel
|
|
57
|
+
npm install -g @mmmbuto/nexuscrew@next
|
|
91
58
|
|
|
92
|
-
|
|
93
|
-
- chat sends to an existing tmux session
|
|
94
|
-
- new tmux sessions are created explicitly from a detected launcher
|
|
95
|
-
- session output is piped to log files and watched by the server
|
|
96
|
-
|
|
97
|
-
Architecture:
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
Browser <--SSE--> Express server <--tmux--> active tmux sessions
|
|
101
|
-
|- claude-work
|
|
102
|
-
|- codex-debug
|
|
103
|
-
`- qwen-sandbox
|
|
59
|
+
nexuscrew # prints the URL + token, binds 127.0.0.1:41820
|
|
104
60
|
```
|
|
105
61
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
## Launcher Discovery
|
|
109
|
-
|
|
110
|
-
Discovery is environment-agnostic and shell-agnostic.
|
|
111
|
-
|
|
112
|
-
Sources, in order:
|
|
62
|
+
Then tunnel in (see above) and open the printed URL with `#token=…`.
|
|
113
63
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
3. PATH binaries
|
|
64
|
+
Env knobs: `NEXUSCREW_PORT` (default 41820), `NEXUSCREW_TOKEN_FILE`, `NEXUSCREW_TMUX`,
|
|
65
|
+
`NEXUSCREW_READONLY=1`. Set `NEXUSCREW_DEBUG=1` to log the resolved PTY provider at startup.
|
|
117
66
|
|
|
118
|
-
|
|
67
|
+
## Mobile KeyBar
|
|
119
68
|
|
|
120
|
-
|
|
69
|
+
The awkward tmux gestures, as buttons: **scroll** (enters copy-mode; then PgUp/↑/↓, `q` to
|
|
70
|
+
exit), **window** prev/next, **pane** left/right, **esc**, **Ctrl-C**, **detach**.
|
|
121
71
|
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
- `qwen`
|
|
126
|
-
- `custom`
|
|
72
|
+
Window and pane navigation run as **server-side, allowlisted tmux commands** on the active
|
|
73
|
+
session — they are *not* emulated with client-side prefix keys, which are fragile and depend
|
|
74
|
+
on each host's key bindings.
|
|
127
75
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- `runnable: true` means the launcher is valid for explicit tmux creation now
|
|
131
|
-
- `runnable: false` means it was detected from shell config metadata only
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Commands
|
|
136
|
-
|
|
137
|
-
| Command | Description |
|
|
138
|
-
|---------|-------------|
|
|
139
|
-
| `nexuscrew init` | Create config, hosts file, and logs directory |
|
|
140
|
-
| `nexuscrew start` | Start the web server |
|
|
141
|
-
| `nexuscrew stop` | Stop the server |
|
|
142
|
-
| `nexuscrew status` | Show server, tmux, and host status |
|
|
143
|
-
| `nexuscrew sessions` | List active tmux sessions |
|
|
144
|
-
| `nexuscrew attach -s <name>` | Attach to a tmux session directly |
|
|
145
|
-
| `nexuscrew engines` | Print discovered launchers |
|
|
146
|
-
| `nexuscrew config --list` | Show config |
|
|
147
|
-
| `nexuscrew config --set key=value` | Update config |
|
|
148
|
-
| `nexuscrew hosts list` | List stored hosts |
|
|
149
|
-
| `nexuscrew hosts add ...` | Add an SSH host entry |
|
|
150
|
-
| `nexuscrew hosts remove ...` | Remove an SSH host entry |
|
|
151
|
-
|
|
152
|
-
---
|
|
76
|
+
## Non-destructive by default (`ignore-size`)
|
|
153
77
|
|
|
154
|
-
|
|
78
|
+
Multiple clients on one tmux session share the window geometry. To avoid a small phone
|
|
79
|
+
shrinking the window for the real terminal you also have attached, NexusCrew attaches with
|
|
80
|
+
`-f ignore-size` by default: **the browser never resizes a session that a real terminal is
|
|
81
|
+
holding.** On a screen smaller than the session you'll see a clipped view (expected). Pass
|
|
82
|
+
`takeSize` only when you deliberately want the browser to drive the size.
|
|
155
83
|
|
|
156
|
-
|
|
157
|
-
- [Configuration Guide](docs/CONFIGURATION.md)
|
|
158
|
-
- [Current State](docs/CURRENT_STATE.md)
|
|
159
|
-
- [Changelog](CHANGELOG.md)
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Support Matrix
|
|
164
|
-
|
|
165
|
-
| Area | State |
|
|
166
|
-
|------|----------------|
|
|
167
|
-
| Local tmux session listing | Verified |
|
|
168
|
-
| Explicit local tmux creation | Verified |
|
|
169
|
-
| Runtime launcher discovery | Verified |
|
|
170
|
-
| Bucketed launcher discovery | Verified |
|
|
171
|
-
| Shell-file-only launcher gating | Verified |
|
|
172
|
-
| Host-scoped tmux/session selection | Implemented |
|
|
173
|
-
| SSE send flow | Improved, basic smoke tested |
|
|
174
|
-
| Interrupt flow | Improved, not deeply stress-tested |
|
|
175
|
-
| Remote host registry | Present |
|
|
176
|
-
| Remote host execution | Implemented, pending live smoke |
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Development
|
|
84
|
+
## Develop
|
|
181
85
|
|
|
182
86
|
```bash
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
npm run build
|
|
187
|
-
npm run dev
|
|
87
|
+
npm test # node --test (config, tmux list, pty attach smoke, ws bridge, token)
|
|
88
|
+
npm run build # builds the frontend into frontend/dist
|
|
89
|
+
node bin/nexuscrew.js
|
|
188
90
|
```
|
|
189
91
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
## Known Gaps
|
|
92
|
+
## Status
|
|
193
93
|
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
---
|
|
94
|
+
v0.4 "pty-core" is published on the **`next`** dist-tag for install-and-try on real devices
|
|
95
|
+
(Android/Termux included). The `latest` tag is reserved for a verified stable release.
|
|
198
96
|
|
|
199
97
|
## License
|
|
200
98
|
|
|
201
|
-
MIT
|
|
99
|
+
MIT © DioNanos
|
|
100
|
+
|
|
101
|
+
*Per aspera ad astra.*
|
package/bin/nexuscrew.js
CHANGED
|
@@ -1,298 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
|
-
const { Command } = require('commander');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const { execSync, spawn } = require('child_process');
|
|
9
|
-
|
|
10
|
-
const EngineDiscovery = require('../lib/services/engine-discovery');
|
|
11
|
-
const TmuxManager = require('../lib/services/tmux-manager');
|
|
12
|
-
const { getConfigDir, getConfigPath, getHostsPath, getDefaultLogDir } = require('../lib/config/paths');
|
|
13
|
-
const packageJson = require('../package.json');
|
|
14
|
-
|
|
15
|
-
const CONFIG_DIR = getConfigDir();
|
|
16
|
-
const CONFIG_PATH = getConfigPath();
|
|
17
|
-
const HOSTS_PATH = getHostsPath();
|
|
18
|
-
|
|
19
|
-
const DEFAULT_PORT = '41820';
|
|
20
|
-
|
|
21
|
-
const program = new Command();
|
|
22
|
-
program
|
|
23
|
-
.name('nexuscrew')
|
|
24
|
-
.description('tmux-based AI cockpit')
|
|
25
|
-
.version(packageJson.version);
|
|
26
|
-
|
|
27
|
-
program.command('init')
|
|
28
|
-
.description('First-time setup')
|
|
29
|
-
.option('--silent', 'Suppress setup output')
|
|
30
|
-
.action((opts) => {
|
|
31
|
-
const log = (...args) => {
|
|
32
|
-
if (!opts.silent) console.log(...args);
|
|
33
|
-
};
|
|
34
|
-
const error = (...args) => {
|
|
35
|
-
if (!opts.silent) console.error(...args);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
39
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
40
|
-
log('Created ~/.nexuscrew/');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
44
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify({
|
|
45
|
-
port: Number(DEFAULT_PORT),
|
|
46
|
-
logDir: getDefaultLogDir(),
|
|
47
|
-
autoDiscovery: true,
|
|
48
|
-
preferredShell: '',
|
|
49
|
-
extraShellSources: []
|
|
50
|
-
}, null, 2));
|
|
51
|
-
log('Created config.json');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!fs.existsSync(HOSTS_PATH)) {
|
|
55
|
-
fs.writeFileSync(HOSTS_PATH, JSON.stringify([
|
|
56
|
-
{ name: 'local', type: 'local', default: true }
|
|
57
|
-
], null, 2));
|
|
58
|
-
log('Created hosts.json');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const logDir = getDefaultLogDir();
|
|
62
|
-
if (!fs.existsSync(logDir)) {
|
|
63
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
execSync('command -v tmux >/dev/null 2>&1', { stdio: 'pipe' });
|
|
68
|
-
log('tmux: OK');
|
|
69
|
-
} catch {
|
|
70
|
-
error('tmux not found! Install: apt install tmux (or pkg install tmux)');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
log('\nDone! Run: nexuscrew start');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
program.command('start')
|
|
77
|
-
.description('Start the server')
|
|
78
|
-
.option('-p, --port <port>', 'Port', DEFAULT_PORT)
|
|
79
|
-
.option('-d, --dev', 'Dev mode (no frontend)')
|
|
80
|
-
.action((opts) => {
|
|
81
|
-
const env = { ...process.env, PORT: opts.port };
|
|
82
|
-
if (opts.dev) env.NEXUSCREW_DEV = '1';
|
|
83
|
-
|
|
84
|
-
const args = [path.join(__dirname, '..', 'lib', 'server', 'server.js')];
|
|
85
|
-
const child = spawn('node', args, {
|
|
86
|
-
env,
|
|
87
|
-
stdio: 'inherit',
|
|
88
|
-
detached: false
|
|
89
|
-
});
|
|
90
|
-
child.on('exit', (code) => process.exit(code || 0));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
program.command('stop')
|
|
94
|
-
.description('Stop the server')
|
|
95
|
-
.action(() => {
|
|
96
|
-
try {
|
|
97
|
-
const pid = execSync(`lsof -ti :${DEFAULT_PORT} 2>/dev/null`).toString().trim();
|
|
98
|
-
if (pid) {
|
|
99
|
-
execSync(`kill ${pid}`);
|
|
100
|
-
console.log('Server stopped');
|
|
101
|
-
} else {
|
|
102
|
-
console.log('Server not running');
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
console.log('Server not running');
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
program.command('status')
|
|
110
|
-
.description('Show server and tmux status')
|
|
111
|
-
.action(() => {
|
|
112
|
-
console.log('=== NexusCrew Status ===\n');
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const pid = execSync(`lsof -ti :${DEFAULT_PORT} 2>/dev/null`).toString().trim();
|
|
116
|
-
console.log(`Server: RUNNING (pid ${pid})`);
|
|
117
|
-
} catch {
|
|
118
|
-
console.log('Server: STOPPED');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const sessions = execSync("tmux list-sessions -F '#{session_name} #{?session_attached,*, }' 2>/dev/null").toString().trim();
|
|
123
|
-
console.log('\nActive tmux sessions:');
|
|
124
|
-
sessions.split('\n').filter(Boolean).forEach((session) => console.log(` ${session}`));
|
|
125
|
-
} catch {
|
|
126
|
-
console.log('\nActive tmux sessions: none');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (fs.existsSync(HOSTS_PATH)) {
|
|
130
|
-
const hosts = JSON.parse(fs.readFileSync(HOSTS_PATH, 'utf8'));
|
|
131
|
-
console.log('\nHosts:');
|
|
132
|
-
hosts.forEach((host) => {
|
|
133
|
-
if (host.type === 'local') {
|
|
134
|
-
console.log(` ${host.name}: local`);
|
|
135
|
-
} else {
|
|
136
|
-
const status = testSSH(host) ? 'OK' : 'UNREACHABLE';
|
|
137
|
-
console.log(` ${host.name}: ${host.user}@${host.host}${host.port ? ':' + host.port : ''} [${status}]`);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
program.command('attach')
|
|
144
|
-
.description('Attach to a tmux session')
|
|
145
|
-
.requiredOption('-s, --session <name>', 'Session name')
|
|
146
|
-
.action((opts) => {
|
|
147
|
-
try {
|
|
148
|
-
execSync(`tmux attach -t ${shellQuote(opts.session)}`, { stdio: 'inherit' });
|
|
149
|
-
} catch {
|
|
150
|
-
console.error(`Session not found: ${opts.session}`);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
program.command('sessions')
|
|
155
|
-
.description('List active tmux sessions')
|
|
156
|
-
.action(() => {
|
|
157
|
-
try {
|
|
158
|
-
const out = execSync(
|
|
159
|
-
"tmux list-sessions -F '#{session_name} [#{session_windows} windows] #{?session_attached,(attached),}' 2>/dev/null"
|
|
160
|
-
).toString().trim();
|
|
161
|
-
console.log(out || 'No tmux sessions found');
|
|
162
|
-
} catch {
|
|
163
|
-
console.log('No tmux sessions found');
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
program.command('config')
|
|
168
|
-
.description('View/edit configuration')
|
|
169
|
-
.option('-l, --list', 'Show current config')
|
|
170
|
-
.option('-s, --set <key=value>', 'Set a config value')
|
|
171
|
-
.action((opts) => {
|
|
172
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
173
|
-
console.error('Run nexuscrew init first');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
177
|
-
|
|
178
|
-
if (opts.set) {
|
|
179
|
-
const [key, ...rest] = opts.set.split('=');
|
|
180
|
-
const value = rest.join('=');
|
|
181
|
-
let parsedValue = value;
|
|
182
|
-
if (value === 'true') parsedValue = true;
|
|
183
|
-
if (value === 'false') parsedValue = false;
|
|
184
|
-
if (!Number.isNaN(Number(value)) && value.trim() !== '') parsedValue = Number(value);
|
|
185
|
-
config[key] = parsedValue;
|
|
186
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
187
|
-
console.log(`Set ${key} = ${value}`);
|
|
188
|
-
} else {
|
|
189
|
-
console.log(JSON.stringify(config, null, 2));
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const hostsCmd = program.command('hosts').description('Manage remote hosts');
|
|
194
|
-
|
|
195
|
-
hostsCmd.command('list')
|
|
196
|
-
.description('List configured hosts')
|
|
197
|
-
.action(() => {
|
|
198
|
-
if (!fs.existsSync(HOSTS_PATH)) {
|
|
199
|
-
console.log('No hosts configured. Run nexuscrew init.');
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
const hosts = JSON.parse(fs.readFileSync(HOSTS_PATH, 'utf8'));
|
|
203
|
-
hosts.forEach((host, i) => {
|
|
204
|
-
const status = host.type === 'local' ? 'local' : (testSSH(host) ? 'OK' : 'UNREACHABLE');
|
|
205
|
-
console.log(`[${i}] ${host.name} (${host.type}) ${host.host || ''} [${status}]${host.default ? ' *default*' : ''}`);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
hostsCmd.command('add')
|
|
210
|
-
.description('Add a remote host')
|
|
211
|
-
.requiredOption('-n, --name <name>', 'Host name')
|
|
212
|
-
.requiredOption('-h, --host <host>', 'Hostname or IP')
|
|
213
|
-
.option('-u, --user <user>', 'SSH user', os.userInfo().username)
|
|
214
|
-
.option('-p, --port <port>', 'SSH port', '22')
|
|
215
|
-
.option('-k, --key <path>', 'SSH key path')
|
|
216
|
-
.action((opts) => {
|
|
217
|
-
const hosts = fs.existsSync(HOSTS_PATH)
|
|
218
|
-
? JSON.parse(fs.readFileSync(HOSTS_PATH, 'utf8'))
|
|
219
|
-
: [];
|
|
220
|
-
hosts.push({
|
|
221
|
-
name: opts.name,
|
|
222
|
-
type: 'ssh',
|
|
223
|
-
host: opts.host,
|
|
224
|
-
user: opts.user,
|
|
225
|
-
port: opts.port,
|
|
226
|
-
key: opts.key || undefined
|
|
227
|
-
});
|
|
228
|
-
fs.writeFileSync(HOSTS_PATH, JSON.stringify(hosts, null, 2));
|
|
229
|
-
console.log(`Added host: ${opts.name}`);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
hostsCmd.command('remove')
|
|
233
|
-
.description('Remove a host')
|
|
234
|
-
.requiredOption('-n, --name <name>', 'Host name')
|
|
235
|
-
.action((opts) => {
|
|
236
|
-
let hosts = fs.existsSync(HOSTS_PATH)
|
|
237
|
-
? JSON.parse(fs.readFileSync(HOSTS_PATH, 'utf8'))
|
|
238
|
-
: [];
|
|
239
|
-
hosts = hosts.filter((host) => host.name !== opts.name);
|
|
240
|
-
fs.writeFileSync(HOSTS_PATH, JSON.stringify(hosts, null, 2));
|
|
241
|
-
console.log(`Removed host: ${opts.name}`);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
program.command('engines')
|
|
245
|
-
.description('Discover available launchers')
|
|
246
|
-
.option('-H, --host <name>', 'Host name', 'local')
|
|
247
|
-
.action((opts) => {
|
|
248
|
-
const config = readConfig();
|
|
249
|
-
const hosts = fs.existsSync(HOSTS_PATH)
|
|
250
|
-
? JSON.parse(fs.readFileSync(HOSTS_PATH, 'utf8'))
|
|
251
|
-
: [{ name: 'local', type: 'local', default: true }];
|
|
252
|
-
const tmux = new TmuxManager({
|
|
253
|
-
configDir: CONFIG_DIR,
|
|
254
|
-
logDir: config.logDir || getDefaultLogDir(),
|
|
255
|
-
preferredShell: config.preferredShell || process.env.SHELL || '/bin/sh',
|
|
256
|
-
hosts
|
|
257
|
-
});
|
|
258
|
-
const launchers = new EngineDiscovery({ ...config, tmux }).discover(opts.host || 'local');
|
|
259
|
-
console.log('\nDiscovered Launchers:\n');
|
|
260
|
-
for (const [bucket, items] of Object.entries(launchers.buckets)) {
|
|
261
|
-
if (items.length === 0) continue;
|
|
262
|
-
console.log(` ${bucket}:`);
|
|
263
|
-
for (const launcher of items) {
|
|
264
|
-
console.log(` ${launcher.label}: ${launcher.kind} [${launcher.family}]${launcher.runnable ? '' : ' (detected only)'}`);
|
|
265
|
-
console.log(` command: ${launcher.commandPreview}`);
|
|
266
|
-
console.log(` source: ${launcher.source}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
console.log('');
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
function readConfig() {
|
|
273
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
274
|
-
return { preferredShell: '', extraShellSources: [] };
|
|
275
|
-
}
|
|
276
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
277
|
-
if (config.providersPath && !config.extraShellSources) {
|
|
278
|
-
config.extraShellSources = [config.providersPath].filter(Boolean);
|
|
279
|
-
}
|
|
280
|
-
return config;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function testSSH(host) {
|
|
284
|
-
try {
|
|
285
|
-
const keyOpt = host.key ? `-i ${shellQuote(host.key)}` : '';
|
|
286
|
-
const portOpt = host.port && host.port !== '22' ? `-p ${host.port}` : '';
|
|
287
|
-
execSync(`ssh -o ConnectTimeout=3 -o BatchMode=yes ${keyOpt} ${portOpt} ${host.user}@${host.host} echo ok`, { stdio: 'pipe' });
|
|
288
|
-
return true;
|
|
289
|
-
} catch {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function shellQuote(value) {
|
|
295
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
program.parse();
|
|
3
|
+
require('../lib/server.js').start();
|