@mmmbuto/nexuscli 0.5.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * nexuscli logs - View server logs
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const fs = require('fs');
7
+ const { spawn } = require('child_process');
8
+
9
+ const { PATHS } = require('../utils/paths');
10
+
11
+ /**
12
+ * Main logs command
13
+ */
14
+ async function logs(options) {
15
+ console.log('');
16
+
17
+ // Check if log file exists
18
+ if (!fs.existsSync(PATHS.SERVER_LOG)) {
19
+ console.log(chalk.yellow('No log file found.'));
20
+ console.log(chalk.gray(`Expected: ${PATHS.SERVER_LOG}`));
21
+ console.log('');
22
+ console.log('Server may not have been started as daemon yet.');
23
+ console.log(`Run ${chalk.cyan('nexuscli start --daemon')} first.`);
24
+ console.log('');
25
+ return;
26
+ }
27
+
28
+ const lines = parseInt(options.lines) || 50;
29
+
30
+ if (options.follow) {
31
+ // Follow mode (tail -f)
32
+ console.log(chalk.cyan(`Following logs (Ctrl+C to exit)...`));
33
+ console.log(chalk.gray(`File: ${PATHS.SERVER_LOG}`));
34
+ console.log('');
35
+
36
+ const tail = spawn('tail', ['-f', '-n', String(lines), PATHS.SERVER_LOG], {
37
+ stdio: 'inherit'
38
+ });
39
+
40
+ // Handle Ctrl+C
41
+ process.on('SIGINT', () => {
42
+ tail.kill();
43
+ console.log('');
44
+ process.exit(0);
45
+ });
46
+
47
+ // Wait for tail to exit
48
+ await new Promise((resolve) => {
49
+ tail.on('exit', resolve);
50
+ });
51
+ } else {
52
+ // Static mode (show last N lines)
53
+ console.log(chalk.cyan(`Last ${lines} lines:`));
54
+ console.log(chalk.gray(`File: ${PATHS.SERVER_LOG}`));
55
+ console.log('');
56
+
57
+ try {
58
+ const content = fs.readFileSync(PATHS.SERVER_LOG, 'utf8');
59
+ const allLines = content.split('\n');
60
+ const lastLines = allLines.slice(-lines).join('\n');
61
+ console.log(lastLines);
62
+ } catch (err) {
63
+ console.log(chalk.red(`Error reading logs: ${err.message}`));
64
+ }
65
+
66
+ console.log('');
67
+ console.log(chalk.gray(`Use ${chalk.white('nexuscli logs -f')} to follow live`));
68
+ console.log('');
69
+ }
70
+ }
71
+
72
+ module.exports = logs;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * nexuscli start - Start the server
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawn, execSync } = require('child_process');
9
+ const readline = require('readline');
10
+
11
+ const { isInitialized, getConfig } = require('../config/manager');
12
+ const { PATHS } = require('../utils/paths');
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
+ }
37
+
38
+ /**
39
+ * Write PID file
40
+ */
41
+ function writePidFile(pid) {
42
+ fs.writeFileSync(PATHS.PID_FILE, String(pid));
43
+ }
44
+
45
+ /**
46
+ * Open URL in browser (Termux-only)
47
+ */
48
+ function openBrowser(url) {
49
+ try {
50
+ // Termux: use am start (Android intent) - most reliable
51
+ try {
52
+ execSync(`am start -a android.intent.action.VIEW -d "${url}"`, { stdio: 'ignore' });
53
+ } catch {
54
+ // Fallback to termux-open-url if am fails
55
+ const child = spawn('termux-open-url', [url], {
56
+ detached: true,
57
+ stdio: 'ignore'
58
+ });
59
+ child.unref();
60
+ }
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Ask yes/no question
69
+ */
70
+ function askYesNo(question) {
71
+ return new Promise((resolve) => {
72
+ const rl = readline.createInterface({
73
+ input: process.stdin,
74
+ output: process.stdout
75
+ });
76
+
77
+ rl.question(question, (answer) => {
78
+ rl.close();
79
+ const normalized = answer.trim().toLowerCase();
80
+ resolve(normalized === 'y' || normalized === 'yes' || normalized === 's' || normalized === 'si');
81
+ });
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Start server in foreground
87
+ */
88
+ function startForeground(config) {
89
+ const serverPath = path.join(__dirname, '..', 'server', 'server.js');
90
+
91
+ // Set environment variables from config
92
+ process.env.PORT = config.server.port;
93
+ process.env.JWT_SECRET = config.auth.jwt_secret;
94
+ process.env.NEXUSCLI_AUTO_SEED = '0'; // Don't auto-seed, we have config
95
+
96
+ // Acquire wake lock on Termux
97
+ if (isTermux() && config.termux.wake_lock) {
98
+ acquireWakeLock();
99
+ console.log(chalk.green(' ✓ Wake-lock acquired'));
100
+ }
101
+
102
+ // Start server
103
+ console.log('');
104
+ console.log(chalk.cyan(`Starting NexusCLI on port ${config.server.port}...`));
105
+ console.log('');
106
+
107
+ require(serverPath);
108
+ }
109
+
110
+ /**
111
+ * Start server as daemon
112
+ */
113
+ function startDaemon(config) {
114
+ const serverPath = path.join(__dirname, '..', 'server', 'server.js');
115
+
116
+ // Ensure log directory exists
117
+ if (!fs.existsSync(PATHS.LOGS_DIR)) {
118
+ fs.mkdirSync(PATHS.LOGS_DIR, { recursive: true });
119
+ }
120
+
121
+ // Open log file
122
+ const logFile = fs.openSync(PATHS.SERVER_LOG, 'a');
123
+
124
+ // Spawn detached process
125
+ const child = spawn('node', [serverPath], {
126
+ detached: true,
127
+ stdio: ['ignore', logFile, logFile],
128
+ env: {
129
+ ...process.env,
130
+ PORT: config.server.port,
131
+ JWT_SECRET: config.auth.jwt_secret,
132
+ NEXUSCLI_AUTO_SEED: '0',
133
+ NEXUSCLI_DAEMON: '1'
134
+ }
135
+ });
136
+
137
+ // Write PID file
138
+ writePidFile(child.pid);
139
+
140
+ // Unref to allow parent to exit
141
+ child.unref();
142
+
143
+ // Acquire wake lock on Termux
144
+ if (isTermux() && config.termux.wake_lock) {
145
+ acquireWakeLock();
146
+ }
147
+
148
+ // Send notification on Termux
149
+ if (isTermux() && config.termux.notifications) {
150
+ sendNotification('NexusCLI', `Server running on port ${config.server.port}`);
151
+ }
152
+
153
+ return child.pid;
154
+ }
155
+
156
+ /**
157
+ * Main start command
158
+ */
159
+ async function start(options) {
160
+ console.log('');
161
+
162
+ // Check if initialized
163
+ if (!isInitialized()) {
164
+ console.log(chalk.yellow('NexusCLI is not configured.'));
165
+ console.log(`Run ${chalk.cyan('nexuscli init')} first.`);
166
+ console.log('');
167
+ process.exit(1);
168
+ }
169
+
170
+ // Check if already running
171
+ const runningPid = isServerRunning();
172
+ if (runningPid) {
173
+ console.log(chalk.yellow(`Server already running (PID: ${runningPid})`));
174
+ console.log(`Run ${chalk.cyan('nexuscli stop')} to stop it.`);
175
+ console.log('');
176
+ process.exit(1);
177
+ }
178
+
179
+ const config = getConfig();
180
+ const port = options.port || config.server.port;
181
+ config.server.port = port;
182
+
183
+ const url = `http://localhost:${port}`;
184
+
185
+ // Foreground mode (--foreground flag)
186
+ if (options.foreground) {
187
+ startForeground(config);
188
+ return;
189
+ }
190
+
191
+ // Default: Daemon mode
192
+ console.log(chalk.bold('Starting NexusCLI...'));
193
+ console.log('');
194
+
195
+ const pid = startDaemon(config);
196
+
197
+ console.log(chalk.green(` ✓ Server started (PID: ${pid})`));
198
+ console.log(chalk.gray(` Logs: ${PATHS.SERVER_LOG}`));
199
+ console.log('');
200
+ console.log(chalk.bold(` ${chalk.cyan(url)}`));
201
+ console.log('');
202
+
203
+ // Ask to open browser (unless --no-browser)
204
+ if (options.browser !== false) {
205
+ const shouldOpen = await askYesNo(` Open browser? ${chalk.gray('(Y/n)')} `);
206
+
207
+ if (shouldOpen) {
208
+ const opened = openBrowser(url);
209
+ if (opened) {
210
+ console.log(chalk.green(' ✓ Browser opened'));
211
+ } else {
212
+ console.log(chalk.yellow(' Could not open browser'));
213
+ }
214
+ }
215
+ }
216
+
217
+ console.log('');
218
+ }
219
+
220
+ module.exports = start;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * nexuscli status - Show server and engine status
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { execSync } = require('child_process');
9
+
10
+ const { isInitialized, getConfig } = require('../config/manager');
11
+ const { PATHS, HOME } = require('../utils/paths');
12
+ const { isTermux, isTermuxApiWorking } = require('../utils/termux');
13
+
14
+ /**
15
+ * Check if server is running
16
+ */
17
+ function getServerStatus() {
18
+ if (!fs.existsSync(PATHS.PID_FILE)) {
19
+ return { running: false };
20
+ }
21
+
22
+ try {
23
+ const pid = parseInt(fs.readFileSync(PATHS.PID_FILE, 'utf8').trim());
24
+ process.kill(pid, 0);
25
+
26
+ // Try to get uptime from /proc
27
+ let uptime = null;
28
+ try {
29
+ const stat = fs.statSync(`/proc/${pid}`);
30
+ const uptimeMs = Date.now() - stat.ctimeMs;
31
+ uptime = formatUptime(uptimeMs);
32
+ } catch {}
33
+
34
+ return { running: true, pid, uptime };
35
+ } catch {
36
+ return { running: false };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Format uptime in human readable format
42
+ */
43
+ function formatUptime(ms) {
44
+ const seconds = Math.floor(ms / 1000);
45
+ const minutes = Math.floor(seconds / 60);
46
+ const hours = Math.floor(minutes / 60);
47
+ const days = Math.floor(hours / 24);
48
+
49
+ if (days > 0) return `${days}d ${hours % 24}h`;
50
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
51
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
52
+ return `${seconds}s`;
53
+ }
54
+
55
+ /**
56
+ * Check CLI status for an engine
57
+ */
58
+ function getEngineStatus(engine) {
59
+ try {
60
+ const output = execSync(`${engine} --version 2>/dev/null`, { encoding: 'utf8' });
61
+ const version = output.trim().split('\n')[0];
62
+ return { available: true, version };
63
+ } catch {
64
+ return { available: false };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Count workspaces with sessions
70
+ */
71
+ function countWorkspaces() {
72
+ const projectsDir = path.join(HOME, '.claude', 'projects');
73
+ if (!fs.existsSync(projectsDir)) {
74
+ return 0;
75
+ }
76
+
77
+ try {
78
+ const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
79
+ return entries.filter(e => e.isDirectory()).length;
80
+ } catch {
81
+ return 0;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Main status command (TRI CLI v0.4.0)
87
+ */
88
+ async function status() {
89
+ console.log('');
90
+
91
+ if (!isInitialized()) {
92
+ console.log(chalk.yellow('NexusCLI is not configured.'));
93
+ console.log(`Run ${chalk.cyan('nexuscli init')} first.`);
94
+ console.log('');
95
+ return;
96
+ }
97
+
98
+ const config = getConfig();
99
+ const serverStatus = getServerStatus();
100
+ const claudeStatus = getEngineStatus('claude');
101
+ const codexStatus = getEngineStatus('codex');
102
+ const geminiStatus = getEngineStatus('gemini');
103
+ const workspaceCount = countWorkspaces();
104
+
105
+ // Header
106
+ console.log(chalk.bold('╔═══════════════════════════════════════════╗'));
107
+ console.log(chalk.bold('║ NexusCLI Status (TRI CLI v0.4.0) ║'));
108
+ console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
109
+
110
+ // Server
111
+ if (serverStatus.running) {
112
+ const uptimeStr = serverStatus.uptime ? ` (${serverStatus.uptime})` : '';
113
+ console.log(chalk.bold(`║ Server: ${chalk.green('✓ Running')} ${chalk.gray(`PID ${serverStatus.pid}`)}${uptimeStr}`));
114
+ } else {
115
+ console.log(chalk.bold(`║ Server: ${chalk.red('✗ Stopped')}`));
116
+ }
117
+
118
+ console.log(chalk.bold(`║ Port: ${chalk.white(config.server.port)}`));
119
+ console.log(chalk.bold('║'));
120
+
121
+ // Engines
122
+ console.log(chalk.bold('║ Engines:'));
123
+
124
+ // Claude
125
+ if (claudeStatus.available) {
126
+ const model = config.engines?.claude?.model || 'sonnet';
127
+ console.log(chalk.bold(`║ Claude: ${chalk.green('✓')} ${chalk.gray(model)}`));
128
+ } else {
129
+ console.log(chalk.bold(`║ Claude: ${chalk.red('✗')} ${chalk.gray('not found')}`));
130
+ }
131
+
132
+ // Codex
133
+ if (codexStatus.available) {
134
+ const enabled = config.engines?.codex?.enabled;
135
+ const model = config.engines?.codex?.model || 'gpt-5.1-codex-max';
136
+ if (enabled) {
137
+ console.log(chalk.bold(`║ Codex: ${chalk.green('✓')} ${chalk.gray(model)}`));
138
+ } else {
139
+ console.log(chalk.bold(`║ Codex: ${chalk.gray('○')} ${chalk.gray('disabled')}`));
140
+ }
141
+ } else {
142
+ console.log(chalk.bold(`║ Codex: ${chalk.gray('○')} ${chalk.gray('not found')}`));
143
+ }
144
+
145
+ // Gemini
146
+ if (geminiStatus.available) {
147
+ const enabled = config.engines?.gemini?.enabled;
148
+ const model = config.engines?.gemini?.model || 'gemini-3-pro-preview';
149
+ if (enabled) {
150
+ console.log(chalk.bold(`║ Gemini: ${chalk.green('✓')} ${chalk.gray(model)}`));
151
+ } else {
152
+ console.log(chalk.bold(`║ Gemini: ${chalk.gray('○')} ${chalk.gray('disabled')}`));
153
+ }
154
+ } else {
155
+ console.log(chalk.bold(`║ Gemini: ${chalk.gray('○')} ${chalk.gray('not found')}`));
156
+ }
157
+
158
+ console.log(chalk.bold('║'));
159
+
160
+ // Workspaces
161
+ console.log(chalk.bold(`║ Workspaces: ${chalk.white(workspaceCount)} indexed`));
162
+
163
+ // Termux status
164
+ if (isTermux()) {
165
+ console.log(chalk.bold('║'));
166
+ console.log(chalk.bold('║ Termux:'));
167
+ console.log(chalk.bold(`║ Wake-lock: ${config.termux?.wake_lock ? chalk.green('enabled') : chalk.gray('disabled')}`));
168
+ console.log(chalk.bold(`║ Boot-start: ${config.termux?.boot_start ? chalk.green('enabled') : chalk.gray('disabled')}`));
169
+
170
+ if (isTermuxApiWorking()) {
171
+ console.log(chalk.bold(`║ API app: ${chalk.green('working')}`));
172
+ } else {
173
+ console.log(chalk.bold(`║ API app: ${chalk.yellow('not detected')}`));
174
+ }
175
+ }
176
+
177
+ console.log(chalk.bold('╚═══════════════════════════════════════════╝'));
178
+ console.log('');
179
+
180
+ // URL if running
181
+ if (serverStatus.running) {
182
+ console.log(chalk.cyan(` http://localhost:${config.server.port}`));
183
+ console.log('');
184
+ }
185
+ }
186
+
187
+ module.exports = status;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * nexuscli stop - Stop the daemon
3
+ */
4
+
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');
11
+
12
+ /**
13
+ * Main stop command
14
+ */
15
+ async function stop() {
16
+ console.log('');
17
+
18
+ // Check PID file
19
+ if (!fs.existsSync(PATHS.PID_FILE)) {
20
+ console.log(chalk.yellow('No running daemon found.'));
21
+ console.log('');
22
+ return;
23
+ }
24
+
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}`));
59
+ console.log('');
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ module.exports = stop;