@mmmbuto/nexuscli 0.9.7005-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 -152
- 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/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-manager.js +56 -14
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
describe('RuntimeManager', () => {
|
|
6
|
+
let RuntimeManager;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env.HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
|
|
10
|
+
jest.resetModules();
|
|
11
|
+
RuntimeManager = require('../services/runtime-manager');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('resolves Claude custom provider env for Alibaba-backed models', () => {
|
|
15
|
+
const runtimeManager = new RuntimeManager();
|
|
16
|
+
const selection = runtimeManager.resolveRuntimeSelection({ modelId: 'qwen3-max-2026-01-23' });
|
|
17
|
+
|
|
18
|
+
expect(selection.engine).toBe('claude');
|
|
19
|
+
expect(selection.lane).toBe('custom');
|
|
20
|
+
expect(selection.runtimeId).toBe('claude-custom');
|
|
21
|
+
expect(selection.env.ANTHROPIC_BASE_URL).toBe('https://coding-intl.dashscope.aliyuncs.com/apps/anthropic');
|
|
22
|
+
expect(selection.env.ANTHROPIC_MODEL).toBe('qwen3-max-2026-01-23');
|
|
23
|
+
expect(selection.providerAuth).toMatchObject({
|
|
24
|
+
providerId: 'alibaba',
|
|
25
|
+
dbKey: 'alibaba',
|
|
26
|
+
envVars: ['ALIBABA_CODE_API_KEY'],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('resolves Codex custom provider config overrides for Chutes', () => {
|
|
31
|
+
const runtimeManager = new RuntimeManager();
|
|
32
|
+
const selection = runtimeManager.resolveRuntimeSelection({ modelId: 'deepseek-ai/DeepSeek-V3.2-TEE' });
|
|
33
|
+
|
|
34
|
+
expect(selection.engine).toBe('codex');
|
|
35
|
+
expect(selection.lane).toBe('custom');
|
|
36
|
+
expect(selection.runtimeId).toBe('codex-custom');
|
|
37
|
+
expect(selection.configOverrides).toContain('model_provider="chutes"');
|
|
38
|
+
expect(selection.configOverrides).toContain('model_providers.chutes.base_url="https://llm.chutes.ai/v1"');
|
|
39
|
+
expect(selection.providerAuth).toMatchObject({
|
|
40
|
+
providerId: 'chutes',
|
|
41
|
+
dbKey: 'chutes',
|
|
42
|
+
envVars: ['CHUTES_API_KEY'],
|
|
43
|
+
assignOpenAiKey: true,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
describe('Runtime-aware persistence', () => {
|
|
6
|
+
let initDb;
|
|
7
|
+
let prepare;
|
|
8
|
+
let getDb;
|
|
9
|
+
let Message;
|
|
10
|
+
let Conversation;
|
|
11
|
+
let sessionManager;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
process.env.HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
|
|
15
|
+
jest.resetModules();
|
|
16
|
+
({ initDb, prepare, getDb } = require('../db'));
|
|
17
|
+
await initDb();
|
|
18
|
+
Message = require('../models/Message');
|
|
19
|
+
Conversation = require('../models/Conversation');
|
|
20
|
+
sessionManager = require('../services/session-manager');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
try {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
if (db && typeof db.close === 'function') {
|
|
27
|
+
db.close();
|
|
28
|
+
}
|
|
29
|
+
} catch (_) {
|
|
30
|
+
// ignore cleanup errors
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Conversation.create seeds native runtime metadata in sessions', () => {
|
|
35
|
+
const conversation = Conversation.create('Runtime test', '/tmp/runtime-test');
|
|
36
|
+
const row = prepare('SELECT engine, lane, runtime_id, provider_id, model_id FROM sessions WHERE id = ?').get(conversation.id);
|
|
37
|
+
|
|
38
|
+
expect(row).toMatchObject({
|
|
39
|
+
engine: 'claude',
|
|
40
|
+
lane: 'native',
|
|
41
|
+
runtime_id: 'claude-native',
|
|
42
|
+
provider_id: 'anthropic',
|
|
43
|
+
model_id: 'sonnet',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Message.create persists lane/runtime/provider/model metadata', () => {
|
|
48
|
+
const conversation = Conversation.create('Message runtime', '/tmp/runtime-test');
|
|
49
|
+
const message = Message.create(
|
|
50
|
+
conversation.id,
|
|
51
|
+
'user',
|
|
52
|
+
'hello',
|
|
53
|
+
{ workspace: '/tmp/runtime-test' },
|
|
54
|
+
Date.now(),
|
|
55
|
+
'codex',
|
|
56
|
+
{
|
|
57
|
+
lane: 'custom',
|
|
58
|
+
runtimeId: 'codex-custom',
|
|
59
|
+
providerId: 'alibaba',
|
|
60
|
+
modelId: 'qwen3-coder-plus',
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const row = prepare('SELECT engine, lane, runtime_id, provider_id, model_id FROM messages WHERE id = ?').get(message.id);
|
|
65
|
+
expect(row).toMatchObject({
|
|
66
|
+
engine: 'codex',
|
|
67
|
+
lane: 'custom',
|
|
68
|
+
runtime_id: 'codex-custom',
|
|
69
|
+
provider_id: 'alibaba',
|
|
70
|
+
model_id: 'qwen3-coder-plus',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('SessionManager persists runtime metadata for new sessions', async () => {
|
|
75
|
+
const result = await sessionManager.getOrCreateSession(
|
|
76
|
+
'conv-runtime',
|
|
77
|
+
'codex',
|
|
78
|
+
'/tmp/runtime-test',
|
|
79
|
+
{
|
|
80
|
+
lane: 'custom',
|
|
81
|
+
runtimeId: 'codex-custom',
|
|
82
|
+
providerId: 'chutes',
|
|
83
|
+
modelId: 'deepseek-ai/DeepSeek-V3.2-TEE',
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result.isNew).toBe(true);
|
|
88
|
+
const row = prepare('SELECT engine, lane, runtime_id, provider_id, model_id FROM sessions WHERE id = ?').get(result.sessionId);
|
|
89
|
+
expect(row).toMatchObject({
|
|
90
|
+
engine: 'codex',
|
|
91
|
+
lane: 'custom',
|
|
92
|
+
runtime_id: 'codex-custom',
|
|
93
|
+
provider_id: 'chutes',
|
|
94
|
+
model_id: 'deepseek-ai/DeepSeek-V3.2-TEE',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Dependency Check for NexusCLI Post-Install
|
|
3
|
+
* Detects platform and installs appropriate PTY provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const colors = {
|
|
11
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
12
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
13
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
14
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
15
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function log(msg) { console.log(msg); }
|
|
19
|
+
function success(msg) { console.log(colors.green(` ✓ ${msg}`)); }
|
|
20
|
+
function warn(msg) { console.log(colors.yellow(` ⚠ ${msg}`)); }
|
|
21
|
+
function error(msg) { console.log(colors.red(` ✗ ${msg}`)); }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect platform
|
|
25
|
+
*/
|
|
26
|
+
function detectPlatform() {
|
|
27
|
+
const isTermux =
|
|
28
|
+
process.platform === 'android' ||
|
|
29
|
+
process.env.PREFIX?.includes('com.termux') ||
|
|
30
|
+
process.env.TERMUX_VERSION !== undefined;
|
|
31
|
+
|
|
32
|
+
const isLinuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
|
|
33
|
+
|
|
34
|
+
if (isTermux) return 'termux';
|
|
35
|
+
if (isLinuxArm64) return 'linux-arm64';
|
|
36
|
+
return 'other';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if npm package is installed
|
|
41
|
+
*/
|
|
42
|
+
function isNpmPackageInstalled(packageName) {
|
|
43
|
+
try {
|
|
44
|
+
const nodeModulesPath = path.join(__dirname, '..', '..', 'node_modules', packageName);
|
|
45
|
+
return fs.existsSync(nodeModulesPath);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if pty.node exists (native module)
|
|
53
|
+
*/
|
|
54
|
+
function checkPtyNode() {
|
|
55
|
+
try {
|
|
56
|
+
const nodeModulesPath = path.join(__dirname, '..', '..', 'node_modules');
|
|
57
|
+
|
|
58
|
+
// Check @mmmbuto/node-pty-android-arm64
|
|
59
|
+
const androidPtyPath = path.join(nodeModulesPath, '@mmmbuto', 'node-pty-android-arm64', 'build', 'Release', 'pty.node');
|
|
60
|
+
if (fs.existsSync(androidPtyPath)) {
|
|
61
|
+
return { found: true, provider: '@mmmbuto/node-pty-android-arm64', path: androidPtyPath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check @lydell/node-pty-linux-arm64
|
|
65
|
+
const linuxPtyPath = path.join(nodeModulesPath, '@lydell', 'node-pty-linux-arm64', 'build', 'Release', 'pty.node');
|
|
66
|
+
if (fs.existsSync(linuxPtyPath)) {
|
|
67
|
+
return { found: true, provider: '@lydell/node-pty-linux-arm64', path: linuxPtyPath };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { found: false };
|
|
71
|
+
} catch {
|
|
72
|
+
return { found: false };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Install PTY provider
|
|
78
|
+
*/
|
|
79
|
+
function installPtyProvider(provider) {
|
|
80
|
+
try {
|
|
81
|
+
log(` Installing ${provider}...`);
|
|
82
|
+
|
|
83
|
+
const npmCmd = process.env.npm_execpath || 'npm';
|
|
84
|
+
execSync(`${npmCmd} install ${provider}`, {
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
cwd: path.join(__dirname, '..', '..')
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
success(`${provider} installed`);
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
warn(`${provider} installation failed: ${err.message}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Main PTY check function
|
|
99
|
+
*/
|
|
100
|
+
function checkPtyDependencies() {
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(colors.cyan('Checking PTY dependencies:'));
|
|
103
|
+
|
|
104
|
+
const platform = detectPlatform();
|
|
105
|
+
let targetProvider = null;
|
|
106
|
+
|
|
107
|
+
// Determine target provider based on platform
|
|
108
|
+
if (platform === 'termux') {
|
|
109
|
+
targetProvider = '@mmmbuto/node-pty-android-arm64';
|
|
110
|
+
} else if (platform === 'linux-arm64') {
|
|
111
|
+
targetProvider = '@lydell/node-pty-linux-arm64';
|
|
112
|
+
} else {
|
|
113
|
+
log(colors.gray(' Skipped: Unsupported platform for PTY (not Termux/Linux ARM64)'));
|
|
114
|
+
log(colors.gray(' PTY will use fallback adapter (child_process)'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if PTY native module exists
|
|
119
|
+
const ptyCheck = checkPtyNode();
|
|
120
|
+
|
|
121
|
+
if (ptyCheck.found) {
|
|
122
|
+
success(`Native PTY found: ${ptyCheck.provider}`);
|
|
123
|
+
} else {
|
|
124
|
+
// Check if package is installed but pty.node is missing
|
|
125
|
+
if (isNpmPackageInstalled(targetProvider)) {
|
|
126
|
+
warn(`${targetProvider} installed but pty.node missing - may need rebuild`);
|
|
127
|
+
warn(` Run: npm rebuild ${targetProvider}`);
|
|
128
|
+
} else {
|
|
129
|
+
warn(`${targetProvider} not installed`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try to install
|
|
133
|
+
const installed = installPtyProvider(targetProvider);
|
|
134
|
+
if (installed) {
|
|
135
|
+
// Verify installation
|
|
136
|
+
const verify = checkPtyNode();
|
|
137
|
+
if (verify.found) {
|
|
138
|
+
success(`Native PTY verified: ${verify.provider}`);
|
|
139
|
+
} else {
|
|
140
|
+
warn(`PTY package installed but pty.node not found`);
|
|
141
|
+
warn(` You may need to rebuild: npm rebuild ${targetProvider}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
warn(`Cannot install ${targetProvider} - PTY will use fallback`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if pty-termux-utils is installed and has .cjs files
|
|
154
|
+
*/
|
|
155
|
+
function checkPtyTermuxUtils() {
|
|
156
|
+
try {
|
|
157
|
+
const ptyUtilsPath = path.join(__dirname, '..', '..', 'node_modules', '@mmmbuto', 'pty-termux-utils');
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(ptyUtilsPath)) {
|
|
160
|
+
warn('@mmmbuto/pty-termux-utils not installed');
|
|
161
|
+
warn(' This is required for PTY support');
|
|
162
|
+
warn(' Run: npm install @mmmbuto/pty-termux-utils');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const indexCjsPath = path.join(ptyUtilsPath, 'dist', 'index.cjs');
|
|
167
|
+
if (fs.existsSync(indexCjsPath)) {
|
|
168
|
+
success('@mmmbuto/pty-termux-utils installed with CJS support');
|
|
169
|
+
return true;
|
|
170
|
+
} else {
|
|
171
|
+
warn('@mmmbuto/pty-termux-utils installed but .cjs files missing');
|
|
172
|
+
warn(' Run: cd node_modules/@mmmbuto/pty-termux-utils && npm run build:cjs');
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
warn('Error checking @mmmbuto/pty-termux-utils: ' + err.message);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { checkPtyDependencies ,
|
|
182
|
+
checkPtyTermuxUtils
|
|
183
|
+
};
|
package/lib/setup/postinstall.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* NexusCLI Post-Install Script
|
|
4
|
-
*
|
|
4
|
+
* Cross-platform npm-first bootstrap with Termux-specific extras
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
const { checkPtyDependencies, checkPtyTermuxUtils } = require('./postinstall-pty-check');
|
|
7
9
|
const { execSync, spawn } = require('child_process');
|
|
8
10
|
const fs = require('fs');
|
|
9
11
|
const path = require('path');
|
|
10
12
|
const pkg = require('../../package.json');
|
|
11
13
|
|
|
14
|
+
function detectTermux() {
|
|
15
|
+
return (
|
|
16
|
+
process.platform === 'android' ||
|
|
17
|
+
process.env.PREFIX?.includes('com.termux') ||
|
|
18
|
+
process.env.TERMUX_VERSION !== undefined
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
// Check if running in Termux
|
|
13
|
-
const isTermux =
|
|
14
|
-
process.env.PREFIX?.includes('com.termux') ||
|
|
15
|
-
fs.existsSync('/data/data/com.termux') ||
|
|
16
|
-
process.env.TERMUX_VERSION !== undefined;
|
|
23
|
+
const isTermux = detectTermux();
|
|
17
24
|
|
|
18
25
|
// ANSI colors (chalk-free for postinstall)
|
|
19
26
|
const colors = {
|
|
@@ -22,7 +29,7 @@ const colors = {
|
|
|
22
29
|
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
23
30
|
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
24
31
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
25
|
-
gray: (s) => `\x1b[90m${s}\x1b[0m
|
|
32
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
function log(msg) {
|
|
@@ -109,41 +116,40 @@ function installPackage(pkg) {
|
|
|
109
116
|
async function main() {
|
|
110
117
|
console.log('');
|
|
111
118
|
|
|
112
|
-
// ============================================
|
|
113
|
-
// TERMUX CHECK - CRITICAL
|
|
114
|
-
// ============================================
|
|
115
|
-
if (!isTermux) {
|
|
116
|
-
console.log(colors.bold('╔═══════════════════════════════════════════════════╗'));
|
|
117
|
-
console.log(colors.bold('║ ') + colors.red('NOT COMPATIBLE') + colors.bold(' ║'));
|
|
118
|
-
console.log(colors.bold('╚═══════════════════════════════════════════════════╝'));
|
|
119
|
-
console.log('');
|
|
120
|
-
console.log(colors.yellow(' NexusCLI is designed for Termux on Android only.'));
|
|
121
|
-
console.log(colors.yellow(' Linux/macOS/Windows support is not available in this version.'));
|
|
122
|
-
console.log('');
|
|
123
|
-
console.log(colors.gray(' For more information: https://npmjs.com/package/@mmmbuto/nexuscli'));
|
|
124
|
-
console.log('');
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ============================================
|
|
129
|
-
// TERMUX INSTALLATION
|
|
130
|
-
// ============================================
|
|
131
119
|
const versionLabel = `📱 NexusCLI TRI CLI v${pkg.version}`;
|
|
132
120
|
const padding = Math.max(0, 45 - versionLabel.length);
|
|
133
121
|
console.log(colors.bold('╔═══════════════════════════════════════════════════╗'));
|
|
134
122
|
console.log(colors.bold(`║ ${versionLabel}${' '.repeat(padding)}║`));
|
|
135
|
-
console.log(colors.bold('║ Claude • Codex • Gemini
|
|
123
|
+
console.log(colors.bold('║ Claude • Codex • Gemini • Qwen ║'));
|
|
136
124
|
console.log(colors.bold('╚═══════════════════════════════════════════════════╝'));
|
|
137
125
|
console.log('');
|
|
138
126
|
|
|
127
|
+
if (!isTermux) {
|
|
128
|
+
console.log(colors.cyan('Detected desktop/server platform.'));
|
|
129
|
+
console.log(colors.yellow(' NexusCLI will use npm-first desktop mode with child_process fallback.'));
|
|
130
|
+
console.log('');
|
|
131
|
+
}
|
|
132
|
+
|
|
139
133
|
// Required packages
|
|
140
134
|
// - ripgrep is needed by Claude CLI (vendor lookup expects it on Termux)
|
|
141
135
|
const packages = ['termux-api', 'termux-tools', 'ripgrep'];
|
|
142
136
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
if (isTermux) {
|
|
138
|
+
log(colors.cyan('Installing Termux packages:'));
|
|
139
|
+
for (const pkg of packages) {
|
|
140
|
+
installPackage(pkg);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
} else {
|
|
144
|
+
log(colors.cyan('Skipping Termux package install on this platform'));
|
|
145
|
+
console.log('');
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
// Check PTY dependencies
|
|
149
|
+
checkPtyDependencies();
|
|
150
|
+
|
|
151
|
+
// Check pty-termux-utils
|
|
152
|
+
checkPtyTermuxUtils();
|
|
147
153
|
console.log('');
|
|
148
154
|
|
|
149
155
|
// Create directories
|
|
@@ -153,10 +159,13 @@ async function main() {
|
|
|
153
159
|
path.join(HOME, '.nexuscli', 'data'),
|
|
154
160
|
path.join(HOME, '.nexuscli', 'logs'),
|
|
155
161
|
path.join(HOME, '.nexuscli', 'engines'),
|
|
156
|
-
path.join(HOME, '.nexuscli', 'certs')
|
|
157
|
-
path.join(HOME, '.termux', 'boot')
|
|
162
|
+
path.join(HOME, '.nexuscli', 'certs')
|
|
158
163
|
];
|
|
159
164
|
|
|
165
|
+
if (isTermux) {
|
|
166
|
+
dirs.push(path.join(HOME, '.termux', 'boot'));
|
|
167
|
+
}
|
|
168
|
+
|
|
160
169
|
log(colors.cyan('Creating directories:'));
|
|
161
170
|
for (const dir of dirs) {
|
|
162
171
|
try {
|
|
@@ -192,20 +201,22 @@ async function main() {
|
|
|
192
201
|
console.log('');
|
|
193
202
|
|
|
194
203
|
// Check Termux:API app
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
if (isTermux) {
|
|
205
|
+
log(colors.cyan('Checking Termux apps:'));
|
|
206
|
+
try {
|
|
207
|
+
execSync('termux-battery-status', { timeout: 5000, stdio: 'ignore' });
|
|
208
|
+
success('Termux:API app working');
|
|
209
|
+
} catch {
|
|
210
|
+
warn('Termux:API app not detected');
|
|
211
|
+
console.log(' Install from F-Droid for notifications & wake-lock');
|
|
212
|
+
}
|
|
213
|
+
console.log('');
|
|
202
214
|
}
|
|
203
|
-
console.log('');
|
|
204
215
|
|
|
205
216
|
// ============================================
|
|
206
217
|
// AUTO-RUN WIZARD IF NOT CONFIGURED
|
|
207
218
|
// ============================================
|
|
208
|
-
if (!isConfigured()) {
|
|
219
|
+
if (isTermux && !isConfigured()) {
|
|
209
220
|
console.log(colors.bold('╔═══════════════════════════════════════════════════╗'));
|
|
210
221
|
console.log(colors.bold('║ First time setup - Running wizard... ║'));
|
|
211
222
|
console.log(colors.bold('╚═══════════════════════════════════════════════════╝'));
|
|
@@ -223,7 +234,7 @@ async function main() {
|
|
|
223
234
|
console.log(` Run ${colors.cyan('nexuscli init')} to complete setup manually`);
|
|
224
235
|
console.log('');
|
|
225
236
|
}
|
|
226
|
-
} else {
|
|
237
|
+
} else if (isTermux) {
|
|
227
238
|
// Already configured
|
|
228
239
|
console.log(colors.bold('╔═══════════════════════════════════════════════════╗'));
|
|
229
240
|
console.log(colors.bold('║ ✅ NexusCLI updated! ║'));
|
|
@@ -235,6 +246,14 @@ async function main() {
|
|
|
235
246
|
console.log(' • Termux:API - notifications & wake-lock');
|
|
236
247
|
console.log(' • Termux:Boot - auto-start on device boot');
|
|
237
248
|
console.log('');
|
|
249
|
+
} else {
|
|
250
|
+
console.log(colors.bold('╔═══════════════════════════════════════════════════╗'));
|
|
251
|
+
console.log(colors.bold('║ ✅ NexusCLI ready for Linux/macOS ║'));
|
|
252
|
+
console.log(colors.bold('╚═══════════════════════════════════════════════════╝'));
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(` Run ${colors.cyan('nexuscli start')} to launch the server`);
|
|
255
|
+
console.log(` Run ${colors.cyan('nexuscli init')} for interactive setup if needed`);
|
|
256
|
+
console.log('');
|
|
238
257
|
}
|
|
239
258
|
}
|
|
240
259
|
|
|
@@ -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
|
+
};
|
package/lib/utils/termux.js
CHANGED
|
@@ -11,8 +11,8 @@ const { execSync, spawn } = require('child_process');
|
|
|
11
11
|
*/
|
|
12
12
|
function isTermux() {
|
|
13
13
|
return (
|
|
14
|
+
process.platform === 'android' ||
|
|
14
15
|
process.env.PREFIX?.includes('com.termux') ||
|
|
15
|
-
fs.existsSync('/data/data/com.termux') ||
|
|
16
16
|
process.env.TERMUX_VERSION !== undefined
|
|
17
17
|
);
|
|
18
18
|
}
|