@mmmbuto/nexuscli 0.5.2 → 0.5.4
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 +7 -4
- package/bin/nexuscli.js +7 -0
- package/lib/cli/setup-termux.js +213 -0
- package/lib/server/server.js +39 -27
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
NexusCLI is an experimental, ultra-light terminal cockpit designed for
|
|
13
13
|
AI-assisted development workflows on Termux (Android).
|
|
14
14
|
|
|
15
|
-
**v0.5.
|
|
15
|
+
**v0.5.4** - Mobile-First AI Control Plane
|
|
16
16
|
|
|
17
|
-
Web UI wrapper for Claude Code, Codex CLI, and Gemini CLI.
|
|
17
|
+
Web UI wrapper for Claude Code, Codex CLI, and Gemini CLI with voice input support.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -36,6 +36,8 @@ Web UI wrapper for Claude Code, Codex CLI, and Gemini CLI.
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
38
|
- Multi-engine support (Claude, Codex, Gemini)
|
|
39
|
+
- **Voice input** (OpenAI Whisper STT)
|
|
40
|
+
- **Auto HTTPS** for remote microphone access
|
|
39
41
|
- Mobile-first responsive UI
|
|
40
42
|
- SSE streaming responses
|
|
41
43
|
- Workspace management
|
|
@@ -91,6 +93,7 @@ Open browser: `http://localhost:41800`
|
|
|
91
93
|
| `nexuscli config` | Configuration |
|
|
92
94
|
| `nexuscli api` | Manage API keys |
|
|
93
95
|
| `nexuscli users` | User management |
|
|
96
|
+
| `nexuscli setup-termux` | Bootstrap Termux (SSH, packages) |
|
|
94
97
|
|
|
95
98
|
---
|
|
96
99
|
|
|
@@ -101,13 +104,13 @@ Configure API keys for additional providers:
|
|
|
101
104
|
```bash
|
|
102
105
|
nexuscli api list # List configured keys
|
|
103
106
|
nexuscli api set deepseek <key> # DeepSeek models
|
|
104
|
-
nexuscli api set openai <key> #
|
|
107
|
+
nexuscli api set openai <key> # Voice input (Whisper STT)
|
|
105
108
|
nexuscli api set openrouter <key> # Future: Multi-provider gateway
|
|
106
109
|
nexuscli api delete <provider> # Remove key
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
> **Note**: Claude/Codex/Gemini keys are managed by their respective CLIs.
|
|
110
|
-
>
|
|
113
|
+
> OpenAI key enables voice input via Whisper. HTTPS auto-generated for remote mic access.
|
|
111
114
|
|
|
112
115
|
---
|
|
113
116
|
|
package/bin/nexuscli.js
CHANGED
|
@@ -21,6 +21,7 @@ const apiCommand = require('../lib/cli/api');
|
|
|
21
21
|
const workspacesCommand = require('../lib/cli/workspaces');
|
|
22
22
|
const usersCommand = require('../lib/cli/users');
|
|
23
23
|
const uninstallCommand = require('../lib/cli/uninstall');
|
|
24
|
+
const setupTermuxCommand = require('../lib/cli/setup-termux');
|
|
24
25
|
|
|
25
26
|
program
|
|
26
27
|
.name('nexuscli')
|
|
@@ -108,6 +109,12 @@ program
|
|
|
108
109
|
.description('Prepare for uninstallation (optional data removal)')
|
|
109
110
|
.action(uninstallCommand);
|
|
110
111
|
|
|
112
|
+
// nexuscli setup-termux
|
|
113
|
+
program
|
|
114
|
+
.command('setup-termux')
|
|
115
|
+
.description('Bootstrap Termux for remote development (SSH, packages)')
|
|
116
|
+
.action(setupTermuxCommand);
|
|
117
|
+
|
|
111
118
|
// Parse arguments
|
|
112
119
|
program.parse();
|
|
113
120
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nexuscli setup-termux - Bootstrap Termux for remote development
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const { isTermux } = require('../utils/termux');
|
|
12
|
+
const { getConfig } = require('../config/manager');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get local IP address
|
|
16
|
+
*/
|
|
17
|
+
function getLocalIP() {
|
|
18
|
+
const interfaces = os.networkInterfaces();
|
|
19
|
+
for (const name of Object.keys(interfaces)) {
|
|
20
|
+
for (const iface of interfaces[name]) {
|
|
21
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
22
|
+
return iface.address;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '127.0.0.1';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Map package names to their binary names for checking
|
|
30
|
+
const packageBinaries = {
|
|
31
|
+
'openssh': 'ssh',
|
|
32
|
+
'openssl': 'openssl',
|
|
33
|
+
'tmux': 'tmux',
|
|
34
|
+
'git': 'git',
|
|
35
|
+
'curl': 'curl',
|
|
36
|
+
'wget': 'wget'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if package is installed
|
|
41
|
+
*/
|
|
42
|
+
function isPackageInstalled(pkg) {
|
|
43
|
+
const binary = packageBinaries[pkg] || pkg;
|
|
44
|
+
try {
|
|
45
|
+
execSync(`which ${binary}`, { stdio: 'ignore' });
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Install package via pkg
|
|
54
|
+
*/
|
|
55
|
+
function installPackage(pkg) {
|
|
56
|
+
try {
|
|
57
|
+
console.log(chalk.gray(` Installing ${pkg}...`));
|
|
58
|
+
execSync(`pkg install -y ${pkg}`, { stdio: 'inherit' });
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if sshd is running
|
|
67
|
+
*/
|
|
68
|
+
function isSshdRunning() {
|
|
69
|
+
try {
|
|
70
|
+
execSync('pgrep -x sshd', { stdio: 'ignore' });
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start sshd
|
|
79
|
+
*/
|
|
80
|
+
function startSshd() {
|
|
81
|
+
try {
|
|
82
|
+
execSync('sshd', { stdio: 'ignore' });
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Setup SSH auto-start in shell rc
|
|
91
|
+
*/
|
|
92
|
+
function setupSshAutoStart() {
|
|
93
|
+
const home = process.env.HOME;
|
|
94
|
+
const rcFiles = ['.zshrc', '.bashrc'];
|
|
95
|
+
const autoStartLine = '# Auto-start SSH\npgrep -x sshd >/dev/null || sshd';
|
|
96
|
+
|
|
97
|
+
for (const rcFile of rcFiles) {
|
|
98
|
+
const rcPath = path.join(home, rcFile);
|
|
99
|
+
if (fs.existsSync(rcPath)) {
|
|
100
|
+
const content = fs.readFileSync(rcPath, 'utf8');
|
|
101
|
+
if (!content.includes('pgrep -x sshd')) {
|
|
102
|
+
fs.appendFileSync(rcPath, `\n${autoStartLine}\n`);
|
|
103
|
+
return rcFile;
|
|
104
|
+
}
|
|
105
|
+
return null; // Already configured
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Main setup command
|
|
113
|
+
*/
|
|
114
|
+
async function setupTermux() {
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
// Check Termux
|
|
118
|
+
if (!isTermux()) {
|
|
119
|
+
console.log(chalk.red('This command is for Termux only.'));
|
|
120
|
+
console.log('');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(chalk.bold('╔═══════════════════════════════════════════════════╗'));
|
|
125
|
+
console.log(chalk.bold('║ 📱 NexusCLI Termux Setup ║'));
|
|
126
|
+
console.log(chalk.bold('╚═══════════════════════════════════════════════════╝'));
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
// Essential packages
|
|
130
|
+
const packages = ['openssh', 'tmux', 'git', 'curl', 'wget', 'openssl'];
|
|
131
|
+
|
|
132
|
+
console.log(chalk.cyan('Installing packages:'));
|
|
133
|
+
for (const pkg of packages) {
|
|
134
|
+
if (isPackageInstalled(pkg)) {
|
|
135
|
+
console.log(chalk.green(` ✓ ${pkg}`));
|
|
136
|
+
} else {
|
|
137
|
+
const installed = installPackage(pkg);
|
|
138
|
+
if (installed) {
|
|
139
|
+
console.log(chalk.green(` ✓ ${pkg} installed`));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.yellow(` ⚠ ${pkg} failed`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
// Setup SSH
|
|
148
|
+
console.log(chalk.cyan('Setting up SSH:'));
|
|
149
|
+
|
|
150
|
+
// Generate host keys if needed
|
|
151
|
+
const sshDir = path.join(process.env.PREFIX, 'etc', 'ssh');
|
|
152
|
+
if (!fs.existsSync(path.join(sshDir, 'ssh_host_rsa_key'))) {
|
|
153
|
+
console.log(chalk.gray(' Generating host keys...'));
|
|
154
|
+
try {
|
|
155
|
+
execSync('ssh-keygen -A', { stdio: 'ignore' });
|
|
156
|
+
console.log(chalk.green(' ✓ Host keys generated'));
|
|
157
|
+
} catch {
|
|
158
|
+
console.log(chalk.yellow(' ⚠ Could not generate host keys'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Start sshd
|
|
163
|
+
if (isSshdRunning()) {
|
|
164
|
+
console.log(chalk.green(' ✓ SSH server already running'));
|
|
165
|
+
} else {
|
|
166
|
+
const started = startSshd();
|
|
167
|
+
if (started) {
|
|
168
|
+
console.log(chalk.green(' ✓ SSH server started'));
|
|
169
|
+
} else {
|
|
170
|
+
console.log(chalk.yellow(' ⚠ Could not start SSH server'));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Setup auto-start
|
|
175
|
+
const rcFile = setupSshAutoStart();
|
|
176
|
+
if (rcFile) {
|
|
177
|
+
console.log(chalk.green(` ✓ Auto-start added to ${rcFile}`));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.gray(' Auto-start already configured'));
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
182
|
+
|
|
183
|
+
// Get connection info
|
|
184
|
+
const ip = getLocalIP();
|
|
185
|
+
const user = process.env.USER || 'u0_a362';
|
|
186
|
+
const sshPort = 8022;
|
|
187
|
+
|
|
188
|
+
let config;
|
|
189
|
+
try {
|
|
190
|
+
config = getConfig();
|
|
191
|
+
} catch {
|
|
192
|
+
config = { server: { port: 41800 } };
|
|
193
|
+
}
|
|
194
|
+
const webPort = config.server?.port || 41800;
|
|
195
|
+
|
|
196
|
+
// Output connection info
|
|
197
|
+
console.log(chalk.bold('╔═══════════════════════════════════════════════════╗'));
|
|
198
|
+
console.log(chalk.bold('║ 🔗 Connection Info ║'));
|
|
199
|
+
console.log(chalk.bold('╚═══════════════════════════════════════════════════╝'));
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(chalk.cyan(' SSH:'));
|
|
202
|
+
console.log(chalk.white(` ssh ${user}@${ip} -p ${sshPort}`));
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(chalk.cyan(' NexusCLI:'));
|
|
205
|
+
console.log(chalk.white(` https://${ip}:${webPort}`));
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(chalk.gray(' Note: Set password with: passwd'));
|
|
208
|
+
console.log('');
|
|
209
|
+
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = setupTermux;
|
package/lib/server/server.js
CHANGED
|
@@ -169,50 +169,62 @@ async function start() {
|
|
|
169
169
|
const certDir = path.join(process.env.HOME || '', '.nexuscli', 'certs');
|
|
170
170
|
const certPath = path.join(certDir, 'cert.pem');
|
|
171
171
|
const keyPath = path.join(certDir, 'key.pem');
|
|
172
|
-
const
|
|
172
|
+
const hasHttpsCerts = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// Always start HTTP server on main port
|
|
175
|
+
const httpServer = http.createServer(app);
|
|
176
176
|
|
|
177
|
-
if (
|
|
177
|
+
// Start HTTPS server on PORT+1 if certificates exist (for remote mic access)
|
|
178
|
+
let httpsServer = null;
|
|
179
|
+
const HTTPS_PORT = parseInt(PORT) + 1;
|
|
180
|
+
|
|
181
|
+
if (hasHttpsCerts) {
|
|
178
182
|
try {
|
|
179
183
|
const httpsOptions = {
|
|
180
184
|
key: fs.readFileSync(keyPath),
|
|
181
185
|
cert: fs.readFileSync(certPath)
|
|
182
186
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
console.log('[Startup] HTTPS enabled - certificates found');
|
|
187
|
+
httpsServer = https.createServer(httpsOptions, app);
|
|
188
|
+
console.log('[Startup] HTTPS certificates found');
|
|
186
189
|
} catch (err) {
|
|
187
|
-
console.warn('[Startup] Failed to load certificates
|
|
188
|
-
server = http.createServer(app);
|
|
190
|
+
console.warn('[Startup] Failed to load certificates:', err.message);
|
|
189
191
|
}
|
|
190
|
-
} else {
|
|
191
|
-
server = http.createServer(app);
|
|
192
|
-
console.log('[Startup] HTTP mode - no certificates found');
|
|
193
|
-
console.log('[Startup] Run: ./scripts/setup-https.sh to enable HTTPS');
|
|
194
192
|
}
|
|
195
193
|
|
|
196
|
-
|
|
194
|
+
httpServer.listen(PORT, () => {
|
|
197
195
|
console.log('');
|
|
198
196
|
console.log('╔══════════════════════════════════════════════╗');
|
|
199
197
|
console.log('║ 🚀 NexusCLI Backend ║');
|
|
200
198
|
console.log('╚══════════════════════════════════════════════╝');
|
|
201
199
|
console.log('');
|
|
202
|
-
console.log(`✅
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
console.log(`✅ HTTP: http://localhost:${PORT}`);
|
|
201
|
+
|
|
202
|
+
if (httpsServer) {
|
|
203
|
+
httpsServer.listen(HTTPS_PORT, () => {
|
|
204
|
+
console.log(`🔒 HTTPS: https://localhost:${HTTPS_PORT} (for remote mic)`);
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log('Endpoints:');
|
|
207
|
+
console.log(` GET /health (public)`);
|
|
208
|
+
console.log(` POST /api/v1/auth/login (public)`);
|
|
209
|
+
console.log(` GET /api/v1/auth/me (protected)`);
|
|
210
|
+
console.log(` GET /api/v1/conversations (protected)`);
|
|
211
|
+
console.log(` POST /api/v1/conversations (protected)`);
|
|
212
|
+
console.log(` POST /api/v1/jobs (protected)`);
|
|
213
|
+
console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
|
|
214
|
+
console.log('');
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log('Endpoints:');
|
|
219
|
+
console.log(` GET /health (public)`);
|
|
220
|
+
console.log(` POST /api/v1/auth/login (public)`);
|
|
221
|
+
console.log(` GET /api/v1/auth/me (protected)`);
|
|
222
|
+
console.log(` GET /api/v1/conversations (protected)`);
|
|
223
|
+
console.log(` POST /api/v1/conversations (protected)`);
|
|
224
|
+
console.log(` POST /api/v1/jobs (protected)`);
|
|
225
|
+
console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
|
|
226
|
+
console.log('');
|
|
205
227
|
}
|
|
206
|
-
console.log('');
|
|
207
|
-
console.log('Endpoints:');
|
|
208
|
-
console.log(` GET /health (public)`);
|
|
209
|
-
console.log(` POST /api/v1/auth/login (public)`);
|
|
210
|
-
console.log(` GET /api/v1/auth/me (protected)`);
|
|
211
|
-
console.log(` GET /api/v1/conversations (protected)`);
|
|
212
|
-
console.log(` POST /api/v1/conversations (protected)`);
|
|
213
|
-
console.log(` POST /api/v1/jobs (protected)`);
|
|
214
|
-
console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
|
|
215
|
-
console.log('');
|
|
216
228
|
});
|
|
217
229
|
}
|
|
218
230
|
|