@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.
Files changed (94) hide show
  1. package/README.md +63 -163
  2. package/bin/nexuscrew.js +1 -296
  3. package/frontend/dist/assets/index-B41xYEDb.js +81 -0
  4. package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
  5. package/frontend/dist/icon-maskable.svg +4 -0
  6. package/frontend/dist/icon.svg +4 -0
  7. package/frontend/dist/index.html +11 -3
  8. package/frontend/dist/manifest.json +24 -0
  9. package/frontend/dist/sw.js +3 -0
  10. package/frontend/index.html +20 -0
  11. package/lib/auth/token.js +26 -0
  12. package/lib/config.js +25 -0
  13. package/lib/pty/attach.js +30 -0
  14. package/lib/pty/provider.js +33 -0
  15. package/lib/server.js +62 -0
  16. package/lib/tmux/actions.js +27 -0
  17. package/lib/tmux/list.js +33 -0
  18. package/lib/ws/bridge.js +66 -0
  19. package/package.json +20 -24
  20. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  24. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  25. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  26. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  27. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  28. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  29. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  30. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  32. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  36. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  37. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  38. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  39. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  40. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  42. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  47. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  58. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  59. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  61. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  62. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  63. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  64. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  65. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  66. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  67. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  68. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  69. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  70. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  71. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  72. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  73. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  74. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  75. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  76. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  77. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  78. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  79. package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
  80. package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
  81. package/lib/config/paths.js +0 -30
  82. package/lib/server/routes/hosts.js +0 -82
  83. package/lib/server/routes/launchers.js +0 -17
  84. package/lib/server/routes/models.js +0 -26
  85. package/lib/server/routes/runtime.js +0 -14
  86. package/lib/server/routes/send.js +0 -236
  87. package/lib/server/routes/sessions.js +0 -104
  88. package/lib/server/routes/status.js +0 -62
  89. package/lib/server/routes/tmux.js +0 -91
  90. package/lib/server/server.js +0 -189
  91. package/lib/services/engine-discovery.js +0 -317
  92. package/lib/services/log-watcher.js +0 -279
  93. package/lib/services/session-store.js +0 -198
  94. package/lib/services/tmux-manager.js +0 -288
package/README.md CHANGED
@@ -1,201 +1,101 @@
1
1
  # NexusCrew
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@mmmbuto/nexuscrew?style=flat-square&logo=npm)](https://www.npmjs.org/package/@mmmbuto/nexuscrew)
3
+ [![npm (next)](https://img.shields.io/npm/v/@mmmbuto/nexuscrew/next?style=flat-square&logo=npm&label=npm%40next&color=cb3837)](https://www.npmjs.com/package/@mmmbuto/nexuscrew)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
5
+ [![node](https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
6
+ [![platforms](https://img.shields.io/badge/platforms-linux%20%C2%B7%20macos%20%C2%B7%20android-555?style=flat-square)](#requirements)
7
+ [![bind](https://img.shields.io/badge/bind-127.0.0.1%20only-success?style=flat-square)](#access-model--read-this)
4
8
 
5
- NexusCrew is a tmux-first AI cockpit for Claude Code, Codex CLI, Gemini CLI, and Qwen Code.
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
- It is built around one core rule: chat targets real tmux sessions that already exist, or new tmux sessions that are created explicitly from launchers detected on the current machine.
13
+ > **NexusCrew does not create, own, or recover sessions it only attaches.**
8
14
 
9
15
  ---
10
16
 
11
- ## About
17
+ ## What it is (v0.4 "pty-core")
12
18
 
13
- - Runtime model: active tmux sessions + explicit tmux creation
14
- - Launcher discovery: runtime-driven, shell-agnostic, user-agnostic
15
- - Primary verified environment: Termux on Android
16
- - Linux/macOS: supported by design, not yet fully revalidated in the current cycle
17
- - Remote hosts: registry and connectivity exist, but end-to-end remote session flow is still experimental
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
- ## Project Scope
26
+ ## Requirements
20
27
 
21
- ### What It Does
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
- - Lists real active tmux sessions on the host
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
- ### What It Does Not Do
34
-
35
- - It does not create hidden tmux master sessions
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
- npm install -g @mmmbuto/nexuscrew
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
- Initialize local config:
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
- ```bash
55
- nexuscrew init
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
- Start the server:
53
+ ## Install & run
59
54
 
60
55
  ```bash
61
- nexuscrew start
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
- - one AI runtime = one tmux session
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
- 1. live shell introspection
115
- 2. generic shell files such as `.zshrc`, `.bashrc`, `.profile`
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
- Detection families are inferred from what is actually present, not from a static product catalog.
67
+ ## Mobile KeyBar
119
68
 
120
- Current launcher families:
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
- - `claude`
123
- - `codex`
124
- - `gemini`
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
- Important distinction:
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
- ## Documentation
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
- - [Installation Guide](docs/INSTALLATION.md)
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
- git clone https://github.com/DioNanos/nexuscrew
184
- cd nexuscrew
185
- npm install
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
- - remote tmux workflows still need live multi-host smoke before being advertised as stable
195
- - send/interrupt should still be stress-tested against long-running CLI pauses
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. See `LICENSE`.
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();