@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 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-terminal.png" width="45%" />
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.8)
30
+ ## Highlights (v0.9.9)
31
31
 
32
- - **QWEN CLI**: Integrated Qwen Code CLI with `coder-model` and `vision-model`
33
- - **Statusbar Realtime**: Qwen tool events streamed live in the UI
34
- - **Light Theme**: Higher contrast + correct mobile statusbar colors
35
- - **GLM-4.7**: Updated Z.ai model integration for Claude-compatible routing
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.8)
37
+ ### Stable (v0.9.9)
38
38
 
39
- - Jobs runner restored with Termux-safe execution
40
- - Cleaner job SSE errors in UI
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
  }
@@ -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
- * Check if server is already running
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 = isServerRunning();
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 fs = require('fs');
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
- // Check PID file
19
- if (!fs.existsSync(PATHS.PID_FILE)) {
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
- try {
26
- const pid = parseInt(fs.readFileSync(PATHS.PID_FILE, 'utf8').trim());
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;
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini/Qwen)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {