@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.
- package/CHANGELOG.md +84 -0
- package/README.md +89 -158
- package/bin/nexuscli.js +12 -0
- package/frontend/dist/assets/{index-D8XkscmI.js → index-Bztt9hew.js} +1704 -1704
- package/frontend/dist/assets/{index-CoLEGBO4.css → index-Dj7jz2fy.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +1 -1
- package/lib/cli/api.js +19 -1
- package/lib/cli/config.js +27 -5
- package/lib/cli/engines.js +84 -202
- package/lib/cli/init.js +56 -2
- package/lib/cli/model.js +17 -7
- 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/config/manager.js +112 -8
- package/lib/config/models.js +388 -192
- package/lib/server/db/migrations/001_ultra_light_schema.sql +1 -1
- package/lib/server/db/migrations/006_runtime_lane_tracking.sql +79 -0
- package/lib/server/lib/getPty.js +51 -0
- package/lib/server/lib/pty-adapter.js +101 -57
- package/lib/server/lib/pty-provider.js +63 -0
- package/lib/server/lib/pty-utils-loader.js +136 -0
- package/lib/server/middleware/auth.js +27 -4
- package/lib/server/models/Conversation.js +7 -3
- package/lib/server/models/Message.js +29 -5
- package/lib/server/routes/chat.js +27 -4
- package/lib/server/routes/codex.js +35 -8
- package/lib/server/routes/config.js +9 -1
- package/lib/server/routes/gemini.js +24 -5
- package/lib/server/routes/jobs.js +15 -156
- package/lib/server/routes/models.js +12 -10
- package/lib/server/routes/qwen.js +26 -7
- package/lib/server/routes/runtimes.js +68 -0
- package/lib/server/server.js +3 -0
- package/lib/server/services/claude-wrapper.js +60 -62
- package/lib/server/services/cli-loader.js +1 -1
- package/lib/server/services/codex-wrapper.js +79 -10
- package/lib/server/services/gemini-wrapper.js +9 -4
- package/lib/server/services/job-runner.js +156 -0
- package/lib/server/services/qwen-wrapper.js +26 -11
- package/lib/server/services/runtime-manager.js +467 -0
- package/lib/server/services/session-importer.js +6 -1
- package/lib/server/services/session-manager.js +56 -14
- package/lib/server/services/workspace-manager.js +121 -0
- package/lib/server/tests/integration.test.js +12 -0
- package/lib/server/tests/runtime-manager.test.js +46 -0
- package/lib/server/tests/runtime-persistence.test.js +97 -0
- package/lib/setup/postinstall-pty-check.js +183 -0
- package/lib/setup/postinstall.js +60 -41
- package/lib/utils/restart-warning.js +18 -0
- package/lib/utils/server.js +88 -0
- package/lib/utils/termux.js +1 -1
- package/lib/utils/update-check.js +153 -0
- package/lib/utils/update-runner.js +62 -0
- package/package.json +6 -5
package/lib/cli/engines.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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 (
|
|
36
|
+
console.log(chalk.bold('║ AI Engines (Runtime-Aware) ║'));
|
|
65
37
|
console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
const choices = []
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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: '
|
|
202
|
-
message:
|
|
203
|
-
choices:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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:
|
|
221
|
-
choices:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|