@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +89 -152
  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/codex-wrapper.js +79 -10
  39. package/lib/server/services/gemini-wrapper.js +9 -4
  40. package/lib/server/services/job-runner.js +156 -0
  41. package/lib/server/services/qwen-wrapper.js +26 -11
  42. package/lib/server/services/runtime-manager.js +467 -0
  43. package/lib/server/services/session-manager.js +56 -14
  44. package/lib/server/tests/integration.test.js +12 -0
  45. package/lib/server/tests/runtime-manager.test.js +46 -0
  46. package/lib/server/tests/runtime-persistence.test.js +97 -0
  47. package/lib/setup/postinstall-pty-check.js +183 -0
  48. package/lib/setup/postinstall.js +60 -41
  49. package/lib/utils/restart-warning.js +18 -0
  50. package/lib/utils/server.js +88 -0
  51. package/lib/utils/termux.js +1 -1
  52. package/lib/utils/update-check.js +153 -0
  53. package/lib/utils/update-runner.js +62 -0
  54. 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
+ };
@@ -1,19 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * NexusCLI Post-Install Script
4
- * Termux-only: auto-run wizard if not configured
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
- log(colors.cyan('Installing Termux packages:'));
144
- for (const pkg of packages) {
145
- installPackage(pkg);
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
- log(colors.cyan('Checking Termux apps:'));
196
- try {
197
- execSync('termux-battery-status', { timeout: 5000, stdio: 'ignore' });
198
- success('Termux:API app working');
199
- } catch {
200
- warn('Termux:API app not detected');
201
- console.log(' Install from F-Droid for notifications & wake-lock');
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
+ };
@@ -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
  }