@mmmbuto/nexuscli 0.9.7004-termux → 0.10.0

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 (57) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +89 -158
  3. package/bin/nexuscli.js +12 -0
  4. package/frontend/dist/assets/{index-D8XkscmI.js → index-Bztt9hew.js} +1704 -1704
  5. package/frontend/dist/assets/{index-CoLEGBO4.css → index-Dj7jz2fy.css} +1 -1
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/dist/sw.js +1 -1
  8. package/lib/cli/api.js +19 -1
  9. package/lib/cli/config.js +27 -5
  10. package/lib/cli/engines.js +84 -202
  11. package/lib/cli/init.js +56 -2
  12. package/lib/cli/model.js +17 -7
  13. package/lib/cli/start.js +37 -24
  14. package/lib/cli/stop.js +12 -41
  15. package/lib/cli/update.js +28 -0
  16. package/lib/cli/workspaces.js +4 -0
  17. package/lib/config/manager.js +112 -8
  18. package/lib/config/models.js +388 -192
  19. package/lib/server/db/migrations/001_ultra_light_schema.sql +1 -1
  20. package/lib/server/db/migrations/006_runtime_lane_tracking.sql +79 -0
  21. package/lib/server/lib/getPty.js +51 -0
  22. package/lib/server/lib/pty-adapter.js +101 -57
  23. package/lib/server/lib/pty-provider.js +63 -0
  24. package/lib/server/lib/pty-utils-loader.js +136 -0
  25. package/lib/server/middleware/auth.js +27 -4
  26. package/lib/server/models/Conversation.js +7 -3
  27. package/lib/server/models/Message.js +29 -5
  28. package/lib/server/routes/chat.js +27 -4
  29. package/lib/server/routes/codex.js +35 -8
  30. package/lib/server/routes/config.js +9 -1
  31. package/lib/server/routes/gemini.js +24 -5
  32. package/lib/server/routes/jobs.js +15 -156
  33. package/lib/server/routes/models.js +12 -10
  34. package/lib/server/routes/qwen.js +26 -7
  35. package/lib/server/routes/runtimes.js +68 -0
  36. package/lib/server/server.js +3 -0
  37. package/lib/server/services/claude-wrapper.js +60 -62
  38. package/lib/server/services/cli-loader.js +1 -1
  39. package/lib/server/services/codex-wrapper.js +79 -10
  40. package/lib/server/services/gemini-wrapper.js +9 -4
  41. package/lib/server/services/job-runner.js +156 -0
  42. package/lib/server/services/qwen-wrapper.js +26 -11
  43. package/lib/server/services/runtime-manager.js +467 -0
  44. package/lib/server/services/session-importer.js +6 -1
  45. package/lib/server/services/session-manager.js +56 -14
  46. package/lib/server/services/workspace-manager.js +121 -0
  47. package/lib/server/tests/integration.test.js +12 -0
  48. package/lib/server/tests/runtime-manager.test.js +46 -0
  49. package/lib/server/tests/runtime-persistence.test.js +97 -0
  50. package/lib/setup/postinstall-pty-check.js +183 -0
  51. package/lib/setup/postinstall.js +60 -41
  52. package/lib/utils/restart-warning.js +18 -0
  53. package/lib/utils/server.js +88 -0
  54. package/lib/utils/termux.js +1 -1
  55. package/lib/utils/update-check.js +153 -0
  56. package/lib/utils/update-runner.js +62 -0
  57. package/package.json +6 -5
@@ -4,105 +4,49 @@
4
4
 
5
5
  const chalk = require('chalk');
6
6
  const inquirer = require('inquirer');
7
- const { execSync } = require('child_process');
8
- const fs = require('fs');
9
- const path = require('path');
10
7
 
11
8
  const { isInitialized, getConfig, setConfigValue } = require('../config/manager');
12
- const { HOME } = require('../utils/paths');
9
+ const { getCliTools } = require('../config/models');
10
+ const { warnIfServerRunning } = require('../utils/restart-warning');
11
+ const RuntimeManager = require('../server/services/runtime-manager');
13
12
 
14
- /**
15
- * Detect available engines (TRI CLI v0.4.0)
16
- */
17
- function detectEngines() {
18
- const engines = {
19
- claude: { available: false, path: null, version: null },
20
- codex: { available: false, path: null, version: null },
21
- gemini: { available: false, path: null, version: null },
22
- qwen: { available: false, path: null, version: null }
23
- };
24
-
25
- // Claude
26
- try {
27
- const claudePath = execSync('which claude 2>/dev/null', { encoding: 'utf8' }).trim();
28
- const claudeVersion = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
29
- engines.claude = { available: true, path: claudePath, version: claudeVersion };
30
- } catch {}
31
-
32
- // Codex
33
- try {
34
- const codexPath = execSync('which codex 2>/dev/null', { encoding: 'utf8' }).trim();
35
- const codexVersion = execSync('codex --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
36
- engines.codex = { available: true, path: codexPath, version: codexVersion };
37
- } catch {}
38
-
39
- // Gemini
40
- try {
41
- const geminiPath = execSync('which gemini 2>/dev/null', { encoding: 'utf8' }).trim();
42
- const geminiVersion = execSync('gemini --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
43
- engines.gemini = { available: true, path: geminiPath, version: geminiVersion };
44
- } catch {}
45
-
46
- // Qwen
47
- try {
48
- const qwenPath = execSync('which qwen 2>/dev/null', { encoding: 'utf8' }).trim();
49
- const qwenVersion = execSync('qwen --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
50
- engines.qwen = { available: true, path: qwenPath, version: qwenVersion };
51
- } catch {}
13
+ const runtimeManager = new RuntimeManager();
52
14
 
53
- return engines;
15
+ function formatAvailability(runtime) {
16
+ if (runtime.available) {
17
+ return chalk.green('available');
18
+ }
19
+ return chalk.gray('missing');
54
20
  }
55
21
 
56
22
  /**
57
23
  * List engines status (TRI CLI v0.4.0)
58
24
  */
59
- function listEngines() {
25
+ async function listEngines() {
60
26
  const config = getConfig();
61
- const detected = detectEngines();
27
+ const tools = getCliTools();
28
+ const inventory = await runtimeManager.getRuntimeInventory();
29
+ const inventoryByEngine = inventory.reduce((acc, runtime) => {
30
+ if (!acc[runtime.engine]) acc[runtime.engine] = [];
31
+ acc[runtime.engine].push(runtime);
32
+ return acc;
33
+ }, {});
62
34
 
63
35
  console.log(chalk.bold('╔═══════════════════════════════════════════╗'));
64
- console.log(chalk.bold('║ AI Engines (TRI CLI) ║'));
36
+ console.log(chalk.bold('║ AI Engines (Runtime-Aware) ║'));
65
37
  console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
66
38
 
67
- // Claude
68
- const claudeEnabled = config.engines?.claude?.enabled !== false;
69
- const claudeModel = config.engines?.claude?.model || 'sonnet';
70
- if (detected.claude.available) {
71
- const status = claudeEnabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
72
- console.log(chalk.bold(`║ Claude: ${status} (${claudeModel})`));
73
- console.log(chalk.gray(`║ ${detected.claude.version}`));
74
- } else {
75
- console.log(chalk.bold(`║ Claude: ${chalk.red('✗ not installed')}`));
76
- }
39
+ for (const [engineId, engine] of Object.entries(tools)) {
40
+ const enabled = config.engines?.[engineId]?.enabled === true || engineId === 'claude';
41
+ const defaultModel = config.engines?.[engineId]?.model || engine.models.find((m) => m.default)?.id || 'n/a';
42
+ const status = enabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
43
+ console.log(chalk.bold(`║ ${engine.name.padEnd(8)} ${status} (${defaultModel})`));
77
44
 
78
- // Codex
79
- const codexEnabled = config.engines?.codex?.enabled === true;
80
- if (detected.codex.available) {
81
- const status = codexEnabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
82
- console.log(chalk.bold(`║ Codex: ${status}`));
83
- console.log(chalk.gray(`║ ${detected.codex.version}`));
84
- } else {
85
- console.log(chalk.bold(`║ Codex: ${chalk.gray('○ not installed')}`));
86
- }
87
-
88
- // Gemini
89
- const geminiEnabled = config.engines?.gemini?.enabled === true;
90
- if (detected.gemini.available) {
91
- const status = geminiEnabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
92
- console.log(chalk.bold(`║ Gemini: ${status}`));
93
- console.log(chalk.gray(`║ ${detected.gemini.version}`));
94
- } else {
95
- console.log(chalk.bold(`║ Gemini: ${chalk.gray('○ not installed')}`));
96
- }
97
-
98
- // Qwen
99
- const qwenEnabled = config.engines?.qwen?.enabled === true;
100
- if (detected.qwen.available) {
101
- const status = qwenEnabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
102
- console.log(chalk.bold(`║ QWEN: ${status}`));
103
- console.log(chalk.gray(`║ ${detected.qwen.version}`));
104
- } else {
105
- console.log(chalk.bold(`║ QWEN: ${chalk.gray('○ not installed')}`));
45
+ for (const runtime of inventoryByEngine[engineId] || []) {
46
+ const laneLabel = runtime.laneLabel || runtime.lane;
47
+ const version = runtime.installedVersion || 'not installed';
48
+ console.log(chalk.gray(`║ ${laneLabel}: ${runtime.command} · ${formatAvailability(runtime)} · ${version}`));
49
+ }
106
50
  }
107
51
 
108
52
  console.log(chalk.bold('╚═══════════════════════════════════════════╝'));
@@ -112,75 +56,34 @@ function listEngines() {
112
56
  * Test an engine
113
57
  */
114
58
  async function testEngine(engine) {
115
- console.log(chalk.cyan(`Testing ${engine}...`));
116
-
117
- if (engine === 'claude') {
118
- try {
119
- execSync('claude --version', { stdio: 'inherit' });
120
- console.log(chalk.green(` ✓ Claude CLI is working`));
121
- return true;
122
- } catch {
123
- console.log(chalk.red(` ✗ Claude CLI failed`));
124
- return false;
125
- }
126
- }
127
-
128
- if (engine === 'codex') {
129
- try {
130
- execSync('codex --version', { stdio: 'inherit' });
131
- console.log(chalk.green(` ✓ Codex CLI is working`));
132
- return true;
133
- } catch {
134
- console.log(chalk.red(` ✗ Codex CLI not found`));
135
- return false;
136
- }
137
- }
138
-
139
- if (engine === 'gemini') {
140
- try {
141
- execSync('gemini --version', { stdio: 'inherit' });
142
- console.log(chalk.green(` ✓ Gemini CLI is working`));
143
- return true;
144
- } catch {
145
- console.log(chalk.red(` ✗ Gemini CLI not found`));
146
- return false;
147
- }
148
- }
59
+ const inventory = await runtimeManager.getRuntimeInventory();
60
+ const runtimes = inventory.filter((runtime) => runtime.engine === engine);
149
61
 
150
- if (engine === 'qwen') {
151
- try {
152
- execSync('qwen --version', { stdio: 'inherit' });
153
- console.log(chalk.green(` ✓ Qwen CLI is working`));
154
- return true;
155
- } catch {
156
- console.log(chalk.red(` ✗ Qwen CLI not found`));
157
- return false;
158
- }
62
+ console.log(chalk.cyan(`Testing ${engine} runtimes...`));
63
+ for (const runtime of runtimes) {
64
+ const status = runtime.available
65
+ ? chalk.green(` ✓ ${runtime.lane}: ${runtime.command} (${runtime.installedVersion || 'ok'})`)
66
+ : chalk.red(` ✗ ${runtime.lane}: ${runtime.command} (${runtime.error || 'missing'})`);
67
+ console.log(status);
159
68
  }
160
69
 
161
- console.log(chalk.yellow(` Engine ${engine} not testable`));
162
- return false;
70
+ return runtimes.some((runtime) => runtime.available);
163
71
  }
164
72
 
165
73
  /**
166
74
  * Add/configure an engine (TRI CLI v0.4.0)
167
75
  */
168
76
  async function addEngine() {
169
- const detected = detectEngines();
170
-
171
- const choices = [];
172
- if (detected.claude.available) {
173
- choices.push({ name: `Claude (${detected.claude.version})`, value: 'claude' });
174
- }
175
- if (detected.codex.available) {
176
- choices.push({ name: `Codex (${detected.codex.version})`, value: 'codex' });
177
- }
178
- if (detected.gemini.available) {
179
- choices.push({ name: `Gemini (${detected.gemini.version})`, value: 'gemini' });
180
- }
181
- if (detected.qwen.available) {
182
- choices.push({ name: `QWEN (${detected.qwen.version})`, value: 'qwen' });
183
- }
77
+ const tools = getCliTools();
78
+ const inventory = await runtimeManager.getRuntimeInventory();
79
+ const choices = Object.entries(tools).map(([engineId, engine]) => {
80
+ const hasRuntime = inventory.some((runtime) => runtime.engine === engineId && runtime.available);
81
+ const status = hasRuntime ? chalk.green('available') : chalk.gray('not installed');
82
+ return {
83
+ name: `${engine.name} (${status})`,
84
+ value: engineId,
85
+ };
86
+ });
184
87
 
185
88
  if (choices.length === 0) {
186
89
  console.log(chalk.yellow(' No AI engines detected.'));
@@ -195,66 +98,45 @@ async function addEngine() {
195
98
  choices
196
99
  }]);
197
100
 
198
- if (engine === 'claude') {
199
- const { model } = await inquirer.prompt([{
101
+ const engineCatalog = tools[engine];
102
+ const runtimeChoices = inventory
103
+ .filter((runtime) => runtime.engine === engine)
104
+ .map((runtime) => ({
105
+ name: `${runtime.laneLabel} (${runtime.command}${runtime.available ? ` · ${runtime.installedVersion || 'available'}` : ' · missing'})`,
106
+ value: runtime.lane,
107
+ disabled: false,
108
+ }));
109
+
110
+ const answers = await inquirer.prompt([
111
+ {
200
112
  type: 'list',
201
- name: 'model',
202
- message: 'Default Claude model:',
203
- choices: [
204
- { name: 'Haiku 4.5 (Fast)', value: 'haiku' },
205
- { name: 'Sonnet 4.5 (Balanced)', value: 'sonnet' },
206
- { name: 'Opus 4.5 (Most Intelligent)', value: 'opus' }
207
- ],
208
- default: 'sonnet'
209
- }]);
210
-
211
- setConfigValue('engines.claude.enabled', true);
212
- setConfigValue('engines.claude.model', model);
213
- console.log(chalk.green(` ✓ Claude configured (${model})`));
214
- }
215
-
216
- if (engine === 'codex') {
217
- const { model } = await inquirer.prompt([{
113
+ name: 'lane',
114
+ message: `Default ${engineCatalog.name} lane:`,
115
+ choices: runtimeChoices,
116
+ default: 'native',
117
+ },
118
+ {
218
119
  type: 'list',
219
120
  name: 'model',
220
- message: 'Default Codex model:',
221
- choices: [
222
- { name: 'GPT-5.2 (Next Gen)', value: 'gpt-5.2' },
223
- { name: 'GPT-5.1 Codex Max (Best)', value: 'gpt-5.1-codex-max' },
224
- { name: 'GPT-5.1 Codex', value: 'gpt-5.1-codex' },
225
- { name: 'GPT-5.1 Codex Mini (Fast)', value: 'gpt-5.1-codex-mini' },
226
- { name: 'GPT-5.1 (General)', value: 'gpt-5.1' }
227
- ],
228
- default: 'gpt-5.1-codex-max'
229
- }]);
230
-
231
- setConfigValue('engines.codex.enabled', true);
232
- setConfigValue('engines.codex.model', model);
233
- console.log(chalk.green(` ✓ Codex configured (${model})`));
234
- }
235
-
236
- if (engine === 'gemini') {
237
- setConfigValue('engines.gemini.enabled', true);
238
- setConfigValue('engines.gemini.model', 'gemini-3-pro-preview');
239
- console.log(chalk.green(` ✓ Gemini configured (gemini-3-pro-preview)`));
240
- }
241
-
242
- if (engine === 'qwen') {
243
- const { model } = await inquirer.prompt([{
244
- type: 'list',
245
- name: 'model',
246
- message: 'Default Qwen model:',
247
- choices: [
248
- { name: 'Coder (default)', value: 'coder-model' },
249
- { name: 'Vision', value: 'vision-model' }
250
- ],
251
- default: 'coder-model'
252
- }]);
253
-
254
- setConfigValue('engines.qwen.enabled', true);
255
- setConfigValue('engines.qwen.model', model);
256
- console.log(chalk.green(` ✓ QWEN configured (${model})`));
257
- }
121
+ message: `Default ${engineCatalog.name} model:`,
122
+ choices: (answers) => engineCatalog.models
123
+ .filter((model) => model.lane === answers.lane)
124
+ .map((model) => ({
125
+ name: model.label || model.name,
126
+ value: model.id,
127
+ })),
128
+ default: (answers) => engineCatalog.models.find((model) => model.lane === answers.lane && model.default)?.id
129
+ || engineCatalog.models.find((model) => model.lane === answers.lane)?.id,
130
+ }
131
+ ]);
132
+
133
+ const active = Array.from(new Set([...(getConfig().engines?.active || []), engine]));
134
+ setConfigValue('engines.active', active);
135
+ setConfigValue(`engines.${engine}.enabled`, true);
136
+ setConfigValue(`engines.${engine}.model`, answers.model);
137
+ setConfigValue(`engines.${engine}.lanes.${answers.lane}.enabled`, true);
138
+ console.log(chalk.green(` ✓ ${engineCatalog.name} configured (${answers.model}, ${answers.lane})`));
139
+ warnIfServerRunning();
258
140
  }
259
141
 
260
142
  /**
@@ -272,7 +154,7 @@ async function engines(action) {
272
154
 
273
155
  // No action = list
274
156
  if (!action || action === 'list') {
275
- listEngines();
157
+ await listEngines();
276
158
  console.log('');
277
159
  return;
278
160
  }
package/lib/cli/init.js CHANGED
@@ -19,6 +19,7 @@ const pkg = require('../../package.json');
19
19
  const CLAUDE_PROJECTS = path.join(os.homedir(), '.claude', 'projects');
20
20
  const CODEX_SESSIONS = path.join(os.homedir(), '.codex', 'sessions');
21
21
  const GEMINI_SESSIONS = path.join(os.homedir(), '.gemini', 'sessions');
22
+ const QWEN_PROJECTS = path.join(os.homedir(), '.qwen', 'projects');
22
23
 
23
24
  // Default workspace path - always ~/nexuswork (user can change during setup)
24
25
  const DEFAULT_WORKSPACE = path.join(os.homedir(), 'nexuswork');
@@ -96,6 +97,27 @@ function detectGeminiPath() {
96
97
  }
97
98
  }
98
99
 
100
+ function detectQwenPath() {
101
+ const candidates = [
102
+ path.join(HOME, '.local', 'bin', 'qwen'),
103
+ path.join(process.env.PREFIX || '/usr', 'bin', 'qwen'),
104
+ '/usr/local/bin/qwen',
105
+ '/usr/bin/qwen'
106
+ ];
107
+
108
+ for (const p of candidates) {
109
+ if (fs.existsSync(p)) {
110
+ return p;
111
+ }
112
+ }
113
+
114
+ try {
115
+ return execSync('which qwen', { encoding: 'utf8' }).trim();
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
99
121
  /**
100
122
  * Detect all available AI engines (TRI CLI v0.4.0)
101
123
  */
@@ -112,6 +134,10 @@ function detectEngines() {
112
134
  gemini: {
113
135
  path: detectGeminiPath(),
114
136
  hasSessions: fs.existsSync(GEMINI_SESSIONS)
137
+ },
138
+ qwen: {
139
+ path: detectQwenPath(),
140
+ hasSessions: fs.existsSync(QWEN_PROJECTS)
115
141
  }
116
142
  };
117
143
  }
@@ -120,7 +146,7 @@ function detectEngines() {
120
146
  * Count sessions for each engine (TRI CLI v0.4.0)
121
147
  */
122
148
  function countSessions() {
123
- const counts = { claude: 0, codex: 0, gemini: 0 };
149
+ const counts = { claude: 0, codex: 0, gemini: 0, qwen: 0 };
124
150
 
125
151
  // Count Claude sessions
126
152
  if (fs.existsSync(CLAUDE_PROJECTS)) {
@@ -155,6 +181,21 @@ function countSessions() {
155
181
  } catch {}
156
182
  }
157
183
 
184
+ // Count Qwen sessions
185
+ if (fs.existsSync(QWEN_PROJECTS)) {
186
+ try {
187
+ const projects = fs.readdirSync(QWEN_PROJECTS, { withFileTypes: true });
188
+ for (const project of projects) {
189
+ if (!project.isDirectory()) continue;
190
+ const chatsDir = path.join(QWEN_PROJECTS, project.name, 'chats');
191
+ if (!fs.existsSync(chatsDir)) continue;
192
+ const files = fs.readdirSync(chatsDir)
193
+ .filter(f => f.endsWith('.jsonl'));
194
+ counts.qwen += files.length;
195
+ }
196
+ } catch {}
197
+ }
198
+
158
199
  return counts;
159
200
  }
160
201
 
@@ -308,6 +349,16 @@ async function init(options) {
308
349
  console.log(chalk.gray(' ○ Gemini CLI: not installed'));
309
350
  }
310
351
 
352
+ // Qwen
353
+ if (engines.qwen.path) {
354
+ const sessInfo = sessionCounts.qwen > 0 ? chalk.gray(` (${sessionCounts.qwen} sessions)`) : '';
355
+ console.log(chalk.green(` ✓ Qwen CLI: ${engines.qwen.path}${sessInfo}`));
356
+ } else if (engines.qwen.hasSessions) {
357
+ console.log(chalk.yellow(` ⚠ Qwen: CLI not found but ${sessionCounts.qwen} sessions exist`));
358
+ } else {
359
+ console.log(chalk.gray(' ○ Qwen CLI: not installed'));
360
+ }
361
+
311
362
  console.log('');
312
363
 
313
364
  let answers = {};
@@ -323,6 +374,9 @@ async function init(options) {
323
374
  if (engines.gemini.path) {
324
375
  engineChoices.push({ name: 'Gemini CLI (Google)', value: 'gemini', checked: true });
325
376
  }
377
+ if (engines.qwen.path) {
378
+ engineChoices.push({ name: 'Qwen CLI (QwenLM)', value: 'qwen', checked: true });
379
+ }
326
380
 
327
381
  // Determine default workspace
328
382
  const existingWorkspaces = findWorkspaces();
@@ -416,7 +470,7 @@ async function init(options) {
416
470
  }
417
471
 
418
472
  // Part 4: Session scan option
419
- const totalSessions = sessionCounts.claude + sessionCounts.codex + sessionCounts.gemini;
473
+ const totalSessions = sessionCounts.claude + sessionCounts.codex + sessionCounts.gemini + sessionCounts.qwen;
420
474
  if (totalSessions > 0) {
421
475
  console.log('');
422
476
  console.log(chalk.cyan(`Found ${totalSessions} existing AI sessions.`));
package/lib/cli/model.js CHANGED
@@ -11,9 +11,12 @@ const {
11
11
  } = require('../config/manager');
12
12
  const {
13
13
  getCliTools,
14
- isValidModelId,
15
- getAllModels
14
+ isValidModelId
16
15
  } = require('../config/models');
16
+ const { warnIfServerRunning } = require('../utils/restart-warning');
17
+ const RuntimeManager = require('../server/services/runtime-manager');
18
+
19
+ const runtimeManager = new RuntimeManager();
17
20
 
18
21
  async function modelCommand(modelId) {
19
22
  if (!isInitialized()) {
@@ -23,16 +26,19 @@ async function modelCommand(modelId) {
23
26
  }
24
27
 
25
28
  const cliTools = getCliTools();
26
- const allModels = getAllModels();
27
29
 
28
- const printAvailableModels = () => {
30
+ const printAvailableModels = async () => {
31
+ const inventoryMap = await runtimeManager.getRuntimeInventoryMap();
29
32
  console.log(chalk.bold('\nAvailable models:'));
30
33
  for (const [key, cli] of Object.entries(cliTools)) {
31
34
  console.log(chalk.dim(`${cli.name}:`));
32
35
  for (const model of cli.models || []) {
33
36
  const label = model.label || model.name;
34
37
  const defaultTag = model.default ? chalk.green(' (default)') : '';
35
- console.log(` ${model.id} ${chalk.gray(`- ${label}`)}${defaultTag}`);
38
+ const runtime = inventoryMap[model.runtimeId];
39
+ const laneTag = chalk.gray(`[${model.lane}]`);
40
+ const availability = runtime?.available ? chalk.green('available') : chalk.gray('missing');
41
+ console.log(` ${model.id} ${laneTag} ${chalk.gray(`- ${label}`)} ${availability}${defaultTag}`);
36
42
  }
37
43
  }
38
44
  };
@@ -50,24 +56,28 @@ async function modelCommand(modelId) {
50
56
  console.log(chalk.dim('Usage: nexuscli model <model-id>'));
51
57
  }
52
58
 
53
- printAvailableModels();
59
+ await printAvailableModels();
54
60
 
55
61
  return;
56
62
  }
57
63
 
58
64
  if (!isValidModelId(modelId)) {
59
65
  console.log(chalk.red(`✗ Invalid model: ${modelId}`));
60
- printAvailableModels();
66
+ await printAvailableModels();
61
67
  process.exitCode = 1;
62
68
  return;
63
69
  }
64
70
 
71
+ const selection = runtimeManager.resolveRuntimeSelection({ modelId });
65
72
  // Set default model
66
73
  const success = setConfigValue('preferences.defaultModel', modelId);
74
+ setConfigValue(`engines.${selection.engine}.model`, modelId);
67
75
 
68
76
  if (success) {
69
77
  console.log(chalk.green('✓') + ' Default model set to: ' + chalk.cyan(modelId));
78
+ console.log(chalk.dim(`Lane: ${selection.lane} · Runtime: ${selection.runtimeId}`));
70
79
  console.log(chalk.dim('The frontend will now auto-select this model on load.'));
80
+ warnIfServerRunning('If the UI is already open, refresh or restart to apply.');
71
81
  } else {
72
82
  console.error(chalk.red('✗') + ' Failed to save config');
73
83
  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;