@mmmbuto/nexuscli 0.9.8 → 0.9.9
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 +32 -10
- package/bin/nexuscli.js +12 -0
- package/lib/cli/config.js +3 -0
- package/lib/cli/engines.js +5 -0
- package/lib/cli/model.js +2 -0
- package/lib/cli/start.js +37 -24
- package/lib/cli/stop.js +12 -41
- package/lib/cli/update.js +28 -0
- package/lib/cli/workspaces.js +4 -0
- package/lib/utils/restart-warning.js +18 -0
- package/lib/utils/server.js +88 -0
- package/lib/utils/update-check.js +173 -0
- package/lib/utils/update-runner.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,22 +22,22 @@ NexusCLI is a lightweight, Termux-first AI cockpit that orchestrates Claude Code
|
|
|
22
22
|
|
|
23
23
|
<p align="center">
|
|
24
24
|
<img src="docs/assets/screenshots/nexuscli-multilang-preview.png" width="45%" />
|
|
25
|
-
<img src="docs/assets/screenshots/nexuscli-mobile-
|
|
25
|
+
<img src="docs/assets/screenshots/nexuscli-mobile-glm.png" width="45%" />
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
## Highlights (v0.9.
|
|
30
|
+
## Highlights (v0.9.9)
|
|
31
31
|
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
32
|
+
- **Auto-update**: Update check on start (npm + GitHub) with interactive prompt
|
|
33
|
+
- **Update command**: `nexuscli update` / `nexuscli upgrade` (stop → update → restart)
|
|
34
|
+
- **Live default model**: UI refreshes config on focus/interval without restart
|
|
35
|
+
- **Restart warnings**: CLI warns when changes require stop/start
|
|
36
36
|
|
|
37
|
-
### Stable (v0.9.
|
|
37
|
+
### Stable (v0.9.9)
|
|
38
38
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
39
|
+
- Update check is cached and non-blocking for normal startup
|
|
40
|
+
- GitHub-only releases show a notice without prompting
|
|
41
41
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
@@ -124,10 +124,14 @@ nexuscli start
|
|
|
124
124
|
| `nexuscli logs` | View server logs |
|
|
125
125
|
| `nexuscli users` | Users |
|
|
126
126
|
| `nexuscli setup-termux` | Termux helpers (services, paths) |
|
|
127
|
+
| `nexuscli update` | Update NexusCLI and restart server |
|
|
128
|
+
| `nexuscli upgrade` | Alias for update |
|
|
127
129
|
| `nexuscli uninstall` | Remove NexusCLI |
|
|
128
130
|
|
|
129
131
|
---
|
|
130
132
|
|
|
133
|
+
> **Note**: On `nexuscli start`, an update check runs (cached) and will prompt in interactive shells.
|
|
134
|
+
|
|
131
135
|
## API Keys
|
|
132
136
|
|
|
133
137
|
Configure API keys for additional providers:
|
|
@@ -164,7 +168,7 @@ NexusCLI is designed primarily for **Termux** on Android devices.
|
|
|
164
168
|
### Stack
|
|
165
169
|
|
|
166
170
|
- **Termux** - primary runtime environment
|
|
167
|
-
- **tmux** - session management
|
|
171
|
+
- **tmux (optional)** - session management (user-managed)
|
|
168
172
|
- **Node.js + SSE** - lightweight backend
|
|
169
173
|
- **React** - minimal UI
|
|
170
174
|
|
|
@@ -180,6 +184,24 @@ It is a **research and learning tool**.
|
|
|
180
184
|
|
|
181
185
|
---
|
|
182
186
|
|
|
187
|
+
## Battery / Keep-Alive (Android)
|
|
188
|
+
|
|
189
|
+
Android can kill background processes aggressively. NexusCLI does not keep
|
|
190
|
+
engine CLIs alive in background; it spawns them on demand and resumes sessions.
|
|
191
|
+
|
|
192
|
+
**Keep NexusCLI alive (when you need it running):**
|
|
193
|
+
- Disable battery optimization for Termux (Android Settings → Battery → Unrestricted).
|
|
194
|
+
- Enable wake-lock + notifications (via `nexuscli config`).
|
|
195
|
+
- Install Termux:Boot to auto-restart after reboot or app kill.
|
|
196
|
+
- Keep a persistent notification (Termux:API helps prevent background kill).
|
|
197
|
+
|
|
198
|
+
**Reduce battery usage (when you don’t need it always-on):**
|
|
199
|
+
- Stop the server when idle: `nexuscli stop`.
|
|
200
|
+
- Disable wake-lock and notifications when not needed.
|
|
201
|
+
- Prefer lighter models and lower reasoning settings.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
183
205
|
## API Endpoints
|
|
184
206
|
|
|
185
207
|
| Endpoint | Engine | Description |
|
package/bin/nexuscli.js
CHANGED
|
@@ -22,6 +22,7 @@ const workspacesCommand = require('../lib/cli/workspaces');
|
|
|
22
22
|
const usersCommand = require('../lib/cli/users');
|
|
23
23
|
const uninstallCommand = require('../lib/cli/uninstall');
|
|
24
24
|
const setupTermuxCommand = require('../lib/cli/setup-termux');
|
|
25
|
+
const updateCommand = require('../lib/cli/update');
|
|
25
26
|
const { modelCommand } = require('../lib/cli/model');
|
|
26
27
|
|
|
27
28
|
program
|
|
@@ -122,6 +123,17 @@ program
|
|
|
122
123
|
.description('Prepare for uninstallation (optional data removal)')
|
|
123
124
|
.action(uninstallCommand);
|
|
124
125
|
|
|
126
|
+
// nexuscli update / upgrade
|
|
127
|
+
program
|
|
128
|
+
.command('update')
|
|
129
|
+
.description('Update NexusCLI and restart server')
|
|
130
|
+
.action(updateCommand);
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('upgrade')
|
|
134
|
+
.description('Alias for update')
|
|
135
|
+
.action(updateCommand);
|
|
136
|
+
|
|
125
137
|
// Parse arguments
|
|
126
138
|
program.parse();
|
|
127
139
|
|
package/lib/cli/config.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
getConfigKeys
|
|
14
14
|
} = require('../config/manager');
|
|
15
15
|
const { PATHS } = require('../utils/paths');
|
|
16
|
+
const { warnIfServerRunning } = require('../utils/restart-warning');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* List all config values
|
|
@@ -123,6 +124,7 @@ async function config(action, key, value) {
|
|
|
123
124
|
|
|
124
125
|
if (setConfigValue(key, value)) {
|
|
125
126
|
console.log(chalk.green(` ✓ ${key} = ${value}`));
|
|
127
|
+
warnIfServerRunning();
|
|
126
128
|
} else {
|
|
127
129
|
console.log(chalk.red(` ✗ Failed to set ${key}`));
|
|
128
130
|
}
|
|
@@ -168,6 +170,7 @@ async function config(action, key, value) {
|
|
|
168
170
|
setConfigValue('termux.notifications', answers.notifications);
|
|
169
171
|
|
|
170
172
|
console.log(chalk.green(' ✓ Configuration updated'));
|
|
173
|
+
warnIfServerRunning();
|
|
171
174
|
console.log('');
|
|
172
175
|
return;
|
|
173
176
|
}
|
package/lib/cli/engines.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
const { isInitialized, getConfig, setConfigValue } = require('../config/manager');
|
|
12
12
|
const { HOME } = require('../utils/paths');
|
|
13
|
+
const { warnIfServerRunning } = require('../utils/restart-warning');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Detect available engines (TRI CLI v0.4.0)
|
|
@@ -211,6 +212,7 @@ async function addEngine() {
|
|
|
211
212
|
setConfigValue('engines.claude.enabled', true);
|
|
212
213
|
setConfigValue('engines.claude.model', model);
|
|
213
214
|
console.log(chalk.green(` ✓ Claude configured (${model})`));
|
|
215
|
+
warnIfServerRunning();
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
if (engine === 'codex') {
|
|
@@ -231,12 +233,14 @@ async function addEngine() {
|
|
|
231
233
|
setConfigValue('engines.codex.enabled', true);
|
|
232
234
|
setConfigValue('engines.codex.model', model);
|
|
233
235
|
console.log(chalk.green(` ✓ Codex configured (${model})`));
|
|
236
|
+
warnIfServerRunning();
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
if (engine === 'gemini') {
|
|
237
240
|
setConfigValue('engines.gemini.enabled', true);
|
|
238
241
|
setConfigValue('engines.gemini.model', 'gemini-3-pro-preview');
|
|
239
242
|
console.log(chalk.green(` ✓ Gemini configured (gemini-3-pro-preview)`));
|
|
243
|
+
warnIfServerRunning();
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
if (engine === 'qwen') {
|
|
@@ -254,6 +258,7 @@ async function addEngine() {
|
|
|
254
258
|
setConfigValue('engines.qwen.enabled', true);
|
|
255
259
|
setConfigValue('engines.qwen.model', model);
|
|
256
260
|
console.log(chalk.green(` ✓ QWEN configured (${model})`));
|
|
261
|
+
warnIfServerRunning();
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
|
package/lib/cli/model.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
isValidModelId,
|
|
15
15
|
getAllModels
|
|
16
16
|
} = require('../config/models');
|
|
17
|
+
const { warnIfServerRunning } = require('../utils/restart-warning');
|
|
17
18
|
|
|
18
19
|
async function modelCommand(modelId) {
|
|
19
20
|
if (!isInitialized()) {
|
|
@@ -68,6 +69,7 @@ async function modelCommand(modelId) {
|
|
|
68
69
|
if (success) {
|
|
69
70
|
console.log(chalk.green('✓') + ' Default model set to: ' + chalk.cyan(modelId));
|
|
70
71
|
console.log(chalk.dim('The frontend will now auto-select this model on load.'));
|
|
72
|
+
warnIfServerRunning('If the UI is already open, refresh or restart to apply.');
|
|
71
73
|
} else {
|
|
72
74
|
console.error(chalk.red('✗') + ' Failed to save config');
|
|
73
75
|
process.exit(1);
|
package/lib/cli/start.js
CHANGED
|
@@ -11,29 +11,9 @@ const readline = require('readline');
|
|
|
11
11
|
const { isInitialized, getConfig } = require('../config/manager');
|
|
12
12
|
const { PATHS } = require('../utils/paths');
|
|
13
13
|
const { isTermux, acquireWakeLock, sendNotification } = require('../utils/termux');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
function isServerRunning() {
|
|
19
|
-
if (!fs.existsSync(PATHS.PID_FILE)) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const pid = parseInt(fs.readFileSync(PATHS.PID_FILE, 'utf8').trim());
|
|
25
|
-
|
|
26
|
-
// Check if process exists
|
|
27
|
-
process.kill(pid, 0);
|
|
28
|
-
return pid;
|
|
29
|
-
} catch {
|
|
30
|
-
// Process doesn't exist, clean up stale PID file
|
|
31
|
-
try {
|
|
32
|
-
fs.unlinkSync(PATHS.PID_FILE);
|
|
33
|
-
} catch {}
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
14
|
+
const { getServerPid } = require('../utils/server');
|
|
15
|
+
const { getUpdateInfo } = require('../utils/update-check');
|
|
16
|
+
const { runUpdateAndRestart } = require('../utils/update-runner');
|
|
37
17
|
|
|
38
18
|
/**
|
|
39
19
|
* Write PID file
|
|
@@ -169,7 +149,7 @@ async function start(options) {
|
|
|
169
149
|
}
|
|
170
150
|
|
|
171
151
|
// Check if already running
|
|
172
|
-
const runningPid =
|
|
152
|
+
const runningPid = getServerPid();
|
|
173
153
|
if (runningPid) {
|
|
174
154
|
console.log(chalk.yellow(`Server already running (PID: ${runningPid})`));
|
|
175
155
|
console.log(`Run ${chalk.cyan('nexuscli stop')} to stop it.`);
|
|
@@ -177,6 +157,39 @@ async function start(options) {
|
|
|
177
157
|
process.exit(1);
|
|
178
158
|
}
|
|
179
159
|
|
|
160
|
+
// Auto-update check (unless explicitly skipped)
|
|
161
|
+
const skipUpdateCheck = process.env.NEXUSCLI_SKIP_UPDATE_CHECK === '1';
|
|
162
|
+
if (!skipUpdateCheck) {
|
|
163
|
+
try {
|
|
164
|
+
const updateInfo = await getUpdateInfo();
|
|
165
|
+
|
|
166
|
+
if (updateInfo.updateAvailable) {
|
|
167
|
+
const latest = updateInfo.npmVersion || updateInfo.latestVersion;
|
|
168
|
+
console.log(chalk.yellow(`Update available: ${updateInfo.currentVersion} → ${latest}`));
|
|
169
|
+
|
|
170
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
171
|
+
const shouldUpdate = await askYesNo(` Update now? ${chalk.gray('(Y/n)')} `);
|
|
172
|
+
if (shouldUpdate) {
|
|
173
|
+
const result = await runUpdateAndRestart({ restartArgs: process.argv.slice(2) });
|
|
174
|
+
if (result.ok) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(chalk.yellow(' Update failed. Continuing with current version...'));
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.log(chalk.gray(' Non-interactive session: skipping update prompt.'));
|
|
181
|
+
}
|
|
182
|
+
console.log('');
|
|
183
|
+
} else if (updateInfo.githubNewer && !updateInfo.npmNewer && updateInfo.githubVersion) {
|
|
184
|
+
console.log(chalk.gray(`GitHub release ${updateInfo.githubVersion} is available (npm not updated yet).`));
|
|
185
|
+
console.log('');
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.log(chalk.gray(`Update check skipped: ${err.message}`));
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
180
193
|
const config = getConfig();
|
|
181
194
|
const port = options.port || config.server.port;
|
|
182
195
|
config.server.port = port;
|
package/lib/cli/stop.js
CHANGED
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const { PATHS } = require('../utils/paths');
|
|
9
|
-
const { isTermux, releaseWakeLock, sendNotification } = require('../utils/termux');
|
|
10
|
-
const { getConfig } = require('../config/manager');
|
|
6
|
+
const { stopServer } = require('../utils/server');
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
9
|
* Main stop command
|
|
@@ -15,50 +11,25 @@ const { getConfig } = require('../config/manager');
|
|
|
15
11
|
async function stop() {
|
|
16
12
|
console.log('');
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const result = stopServer();
|
|
15
|
+
|
|
16
|
+
if (!result.running) {
|
|
20
17
|
console.log(chalk.yellow('No running daemon found.'));
|
|
21
18
|
console.log('');
|
|
22
19
|
return;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Try to kill the process
|
|
29
|
-
try {
|
|
30
|
-
process.kill(pid, 'SIGTERM');
|
|
31
|
-
console.log(chalk.green(` ✓ Server stopped (PID: ${pid})`));
|
|
32
|
-
} catch (err) {
|
|
33
|
-
if (err.code === 'ESRCH') {
|
|
34
|
-
console.log(chalk.yellow(' Process not found (may have already stopped)'));
|
|
35
|
-
} else {
|
|
36
|
-
throw err;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Remove PID file
|
|
41
|
-
fs.unlinkSync(PATHS.PID_FILE);
|
|
42
|
-
|
|
43
|
-
// Release wake lock on Termux
|
|
44
|
-
const config = getConfig();
|
|
45
|
-
if (isTermux() && config.termux?.wake_lock) {
|
|
46
|
-
releaseWakeLock();
|
|
47
|
-
console.log(chalk.gray(' Wake-lock released'));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Send notification on Termux
|
|
51
|
-
if (isTermux() && config.termux?.notifications) {
|
|
52
|
-
sendNotification('NexusCLI', 'Server stopped');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
console.log('');
|
|
56
|
-
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.log(chalk.red(` ✗ Error stopping server: ${err.message}`));
|
|
22
|
+
if (result.error) {
|
|
23
|
+
console.log(chalk.red(` ✗ Error stopping server: ${result.error.message}`));
|
|
59
24
|
console.log('');
|
|
60
25
|
process.exit(1);
|
|
61
26
|
}
|
|
27
|
+
|
|
28
|
+
console.log(chalk.green(` ✓ Server stopped (PID: ${result.pid})`));
|
|
29
|
+
if (result.wakeLockReleased) {
|
|
30
|
+
console.log(chalk.gray(' Wake-lock released'));
|
|
31
|
+
}
|
|
32
|
+
console.log('');
|
|
62
33
|
}
|
|
63
34
|
|
|
64
35
|
module.exports = stop;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nexuscli update - Update NexusCLI and restart
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { getUpdateInfo } = require('../utils/update-check');
|
|
7
|
+
const { runUpdateAndRestart } = require('../utils/update-runner');
|
|
8
|
+
|
|
9
|
+
async function updateCommand() {
|
|
10
|
+
console.log('');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const info = await getUpdateInfo({ force: true });
|
|
14
|
+
if (info.latestVersion) {
|
|
15
|
+
console.log(chalk.bold(`Current: ${info.currentVersion}`));
|
|
16
|
+
console.log(chalk.bold(`Latest: ${info.latestVersion}`));
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
const result = await runUpdateAndRestart({ restartArgs: ['start'] });
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.exit(result.restartExitCode ?? 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = updateCommand;
|
package/lib/cli/workspaces.js
CHANGED
|
@@ -15,6 +15,7 @@ const path = require('path');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const { getConfig, setConfigValue, getConfigValue } = require('../config/manager');
|
|
18
|
+
const { warnIfServerRunning } = require('../utils/restart-warning');
|
|
18
19
|
|
|
19
20
|
// Directories to scan for sessions
|
|
20
21
|
const SCAN_ROOTS = [
|
|
@@ -124,6 +125,7 @@ function setDefault(workspacePath) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
console.log(chalk.green(`\n✅ Default workspace set to: ${collapsed}\n`));
|
|
128
|
+
warnIfServerRunning();
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/**
|
|
@@ -159,6 +161,7 @@ function addWorkspace(workspacePath) {
|
|
|
159
161
|
setConfigValue('workspaces.paths', paths);
|
|
160
162
|
|
|
161
163
|
console.log(chalk.green(`\n✅ Added workspace: ${collapsed}\n`));
|
|
164
|
+
warnIfServerRunning();
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
/**
|
|
@@ -195,6 +198,7 @@ function removeWorkspace(workspacePath) {
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
console.log(chalk.green(`\n✅ Removed workspace: ${collapsed}\n`));
|
|
201
|
+
warnIfServerRunning();
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI warning helper for settings that require restart
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { getServerPid } = require('./server');
|
|
7
|
+
|
|
8
|
+
function warnIfServerRunning(message) {
|
|
9
|
+
const pid = getServerPid();
|
|
10
|
+
if (!pid) return false;
|
|
11
|
+
const note = message || 'Changes will apply after restart (nexuscli stop && nexuscli start).';
|
|
12
|
+
console.log(chalk.yellow(` ⚠ Server running (PID: ${pid}). ${note}`));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
warnIfServerRunning
|
|
18
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server control helpers (PID-based)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { PATHS } = require('./paths');
|
|
7
|
+
const { isTermux, releaseWakeLock, sendNotification } = require('./termux');
|
|
8
|
+
const { getConfig } = require('../config/manager');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get running server PID (cleans stale PID file)
|
|
12
|
+
*/
|
|
13
|
+
function getServerPid() {
|
|
14
|
+
if (!fs.existsSync(PATHS.PID_FILE)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const pid = parseInt(fs.readFileSync(PATHS.PID_FILE, 'utf8').trim(), 10);
|
|
20
|
+
if (!Number.isFinite(pid)) {
|
|
21
|
+
fs.unlinkSync(PATHS.PID_FILE);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if process exists
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return pid;
|
|
28
|
+
} catch {
|
|
29
|
+
// Process doesn't exist or PID invalid - cleanup
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(PATHS.PID_FILE);
|
|
32
|
+
} catch {}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if server is running
|
|
39
|
+
*/
|
|
40
|
+
function isServerRunning() {
|
|
41
|
+
return getServerPid() !== null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stop the server if running
|
|
46
|
+
*/
|
|
47
|
+
function stopServer() {
|
|
48
|
+
const pid = getServerPid();
|
|
49
|
+
if (!pid) {
|
|
50
|
+
return { running: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 'SIGTERM');
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code !== 'ESRCH') {
|
|
57
|
+
return { running: true, pid, error: err };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Remove PID file
|
|
62
|
+
try {
|
|
63
|
+
fs.unlinkSync(PATHS.PID_FILE);
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
66
|
+
// Release wake lock + notification (Termux)
|
|
67
|
+
const config = getConfig();
|
|
68
|
+
const wakeLockReleased = isTermux() && config.termux?.wake_lock
|
|
69
|
+
? releaseWakeLock()
|
|
70
|
+
: false;
|
|
71
|
+
const notificationSent = isTermux() && config.termux?.notifications
|
|
72
|
+
? sendNotification('NexusCLI', 'Server stopped')
|
|
73
|
+
: false;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
running: true,
|
|
77
|
+
pid,
|
|
78
|
+
stopped: true,
|
|
79
|
+
wakeLockReleased,
|
|
80
|
+
notificationSent
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
getServerPid,
|
|
86
|
+
isServerRunning,
|
|
87
|
+
stopServer
|
|
88
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update check utilities (npm + GitHub) with simple caching
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { PATHS, ensureDirectories } = require('./paths');
|
|
8
|
+
const pkg = require('../../package.json');
|
|
9
|
+
|
|
10
|
+
const CACHE_FILE = path.join(PATHS.DATA_DIR, 'version.json');
|
|
11
|
+
const CHECK_INTERVAL_MS = 20 * 60 * 60 * 1000;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
13
|
+
|
|
14
|
+
function normalizeVersion(raw) {
|
|
15
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
16
|
+
const match = raw.trim().match(/(\d+\.\d+\.\d+)/);
|
|
17
|
+
return match ? match[1] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseSemver(v) {
|
|
21
|
+
if (!v) return null;
|
|
22
|
+
const parts = v.trim().split('.');
|
|
23
|
+
if (parts.length < 3) return null;
|
|
24
|
+
const major = Number(parts[0]);
|
|
25
|
+
const minor = Number(parts[1]);
|
|
26
|
+
const patch = Number(parts[2]);
|
|
27
|
+
if (![major, minor, patch].every(Number.isFinite)) return null;
|
|
28
|
+
return [major, minor, patch];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareSemver(a, b) {
|
|
32
|
+
const av = parseSemver(a);
|
|
33
|
+
const bv = parseSemver(b);
|
|
34
|
+
if (!av || !bv) return null;
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
if (av[i] > bv[i]) return 1;
|
|
37
|
+
if (av[i] < bv[i]) return -1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isNewer(latest, current) {
|
|
43
|
+
const cmp = compareSemver(latest, current);
|
|
44
|
+
if (cmp === null) return null;
|
|
45
|
+
return cmp === 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readCache() {
|
|
49
|
+
if (!fs.existsSync(CACHE_FILE)) return null;
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
52
|
+
return JSON.parse(content);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeCache(info) {
|
|
59
|
+
ensureDirectories();
|
|
60
|
+
try {
|
|
61
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(info, null, 2), 'utf8');
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function fetchJson(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
68
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error(`HTTP ${res.status}`);
|
|
72
|
+
}
|
|
73
|
+
return res.json();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchNpmLatest(timeoutMs) {
|
|
77
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`;
|
|
78
|
+
const data = await fetchJson(registryUrl, { headers: { 'Accept': 'application/json' } }, timeoutMs);
|
|
79
|
+
const distTags = data['dist-tags'] || {};
|
|
80
|
+
return normalizeVersion(distTags.latest || distTags.stable);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function fetchGithubLatest(timeoutMs) {
|
|
84
|
+
const url = 'https://api.github.com/repos/DioNanos/nexuscli/releases/latest';
|
|
85
|
+
const data = await fetchJson(url, {
|
|
86
|
+
headers: {
|
|
87
|
+
'User-Agent': 'nexuscli',
|
|
88
|
+
'Accept': 'application/vnd.github+json'
|
|
89
|
+
}
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
return normalizeVersion(data.tag_name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pickLatest(a, b) {
|
|
95
|
+
if (a && !b) return a;
|
|
96
|
+
if (b && !a) return b;
|
|
97
|
+
if (!a && !b) return null;
|
|
98
|
+
const cmp = compareSemver(a, b);
|
|
99
|
+
if (cmp === null) return a || b;
|
|
100
|
+
return cmp >= 0 ? a : b;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildInfo(info, { usedCache = false, error = null } = {}) {
|
|
104
|
+
const currentVersion = pkg.version;
|
|
105
|
+
const npmVersion = info?.npm_version || null;
|
|
106
|
+
const githubVersion = info?.github_version || null;
|
|
107
|
+
const latestVersion = info?.latest_version || pickLatest(npmVersion, githubVersion);
|
|
108
|
+
const npmNewer = isNewer(npmVersion, currentVersion);
|
|
109
|
+
const githubNewer = isNewer(githubVersion, currentVersion);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
currentVersion,
|
|
113
|
+
npmVersion,
|
|
114
|
+
githubVersion,
|
|
115
|
+
latestVersion,
|
|
116
|
+
npmNewer,
|
|
117
|
+
githubNewer,
|
|
118
|
+
updateAvailable: npmNewer === true,
|
|
119
|
+
usedCache,
|
|
120
|
+
error
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getUpdateInfo({ force = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
125
|
+
const cache = readCache();
|
|
126
|
+
const lastCheckedAt = cache?.last_checked_at ? Date.parse(cache.last_checked_at) : null;
|
|
127
|
+
const cacheFresh = lastCheckedAt && (Date.now() - lastCheckedAt) < CHECK_INTERVAL_MS;
|
|
128
|
+
|
|
129
|
+
if (!force && cache && cacheFresh) {
|
|
130
|
+
return buildInfo(cache, { usedCache: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let npmVersion = null;
|
|
134
|
+
let githubVersion = null;
|
|
135
|
+
const errors = [];
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
npmVersion = await fetchNpmLatest(timeoutMs);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
errors.push(`npm: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
githubVersion = await fetchGithubLatest(timeoutMs);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
errors.push(`github: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!npmVersion && !githubVersion) {
|
|
150
|
+
if (cache) {
|
|
151
|
+
return buildInfo(cache, { usedCache: true, error: errors.join('; ') });
|
|
152
|
+
}
|
|
153
|
+
return buildInfo(null, { usedCache: false, error: errors.join('; ') });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const latestVersion = pickLatest(npmVersion, githubVersion);
|
|
157
|
+
const info = {
|
|
158
|
+
latest_version: latestVersion,
|
|
159
|
+
npm_version: npmVersion,
|
|
160
|
+
github_version: githubVersion,
|
|
161
|
+
last_checked_at: new Date().toISOString()
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
writeCache(info);
|
|
165
|
+
return buildInfo(info, { usedCache: false });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
getUpdateInfo,
|
|
170
|
+
normalizeVersion,
|
|
171
|
+
compareSemver,
|
|
172
|
+
isNewer
|
|
173
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update + restart helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { spawn, spawnSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const pkg = require('../../package.json');
|
|
8
|
+
const { getServerPid, stopServer } = require('./server');
|
|
9
|
+
|
|
10
|
+
function runUpdateCommand() {
|
|
11
|
+
console.log(chalk.cyan(`Running npm update for ${pkg.name}...`));
|
|
12
|
+
const result = spawnSync('npm', ['update', '-g', pkg.name], { stdio: 'inherit' });
|
|
13
|
+
|
|
14
|
+
if (result.error) {
|
|
15
|
+
console.log(chalk.red(`Update failed: ${result.error.message}`));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
20
|
+
console.log(chalk.red(`Update exited with code ${result.status}`));
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function restartCli(restartArgs = []) {
|
|
28
|
+
const args = restartArgs.length > 0 ? restartArgs : ['start'];
|
|
29
|
+
const env = { ...process.env, NEXUSCLI_SKIP_UPDATE_CHECK: '1' };
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const child = spawn('nexuscli', args, { stdio: 'inherit', env });
|
|
33
|
+
child.on('exit', (code) => resolve(code ?? 0));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runUpdateAndRestart({ restartArgs = [] } = {}) {
|
|
38
|
+
const runningPid = getServerPid();
|
|
39
|
+
if (runningPid) {
|
|
40
|
+
console.log(chalk.yellow(`Stopping NexusCLI (PID: ${runningPid})...`));
|
|
41
|
+
const stopResult = stopServer();
|
|
42
|
+
if (stopResult.error) {
|
|
43
|
+
console.log(chalk.red(`Failed to stop server: ${stopResult.error.message}`));
|
|
44
|
+
return { ok: false, error: stopResult.error };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const updated = runUpdateCommand();
|
|
49
|
+
if (!updated) {
|
|
50
|
+
return { ok: false, error: new Error('Update failed') };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.green('✓ Update complete. Restarting NexusCLI...'));
|
|
54
|
+
const restartExitCode = await restartCli(restartArgs);
|
|
55
|
+
return { ok: true, restartExitCode };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
runUpdateCommand,
|
|
60
|
+
restartCli,
|
|
61
|
+
runUpdateAndRestart
|
|
62
|
+
};
|