@mmmbuto/nexuscli 0.9.7004-termux → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +89 -158
  3. package/bin/nexuscli.js +12 -0
  4. package/frontend/dist/assets/{index-D8XkscmI.js → index-Bztt9hew.js} +1704 -1704
  5. package/frontend/dist/assets/{index-CoLEGBO4.css → index-Dj7jz2fy.css} +1 -1
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/dist/sw.js +1 -1
  8. package/lib/cli/api.js +19 -1
  9. package/lib/cli/config.js +27 -5
  10. package/lib/cli/engines.js +84 -202
  11. package/lib/cli/init.js +56 -2
  12. package/lib/cli/model.js +17 -7
  13. package/lib/cli/start.js +37 -24
  14. package/lib/cli/stop.js +12 -41
  15. package/lib/cli/update.js +28 -0
  16. package/lib/cli/workspaces.js +4 -0
  17. package/lib/config/manager.js +112 -8
  18. package/lib/config/models.js +388 -192
  19. package/lib/server/db/migrations/001_ultra_light_schema.sql +1 -1
  20. package/lib/server/db/migrations/006_runtime_lane_tracking.sql +79 -0
  21. package/lib/server/lib/getPty.js +51 -0
  22. package/lib/server/lib/pty-adapter.js +101 -57
  23. package/lib/server/lib/pty-provider.js +63 -0
  24. package/lib/server/lib/pty-utils-loader.js +136 -0
  25. package/lib/server/middleware/auth.js +27 -4
  26. package/lib/server/models/Conversation.js +7 -3
  27. package/lib/server/models/Message.js +29 -5
  28. package/lib/server/routes/chat.js +27 -4
  29. package/lib/server/routes/codex.js +35 -8
  30. package/lib/server/routes/config.js +9 -1
  31. package/lib/server/routes/gemini.js +24 -5
  32. package/lib/server/routes/jobs.js +15 -156
  33. package/lib/server/routes/models.js +12 -10
  34. package/lib/server/routes/qwen.js +26 -7
  35. package/lib/server/routes/runtimes.js +68 -0
  36. package/lib/server/server.js +3 -0
  37. package/lib/server/services/claude-wrapper.js +60 -62
  38. package/lib/server/services/cli-loader.js +1 -1
  39. package/lib/server/services/codex-wrapper.js +79 -10
  40. package/lib/server/services/gemini-wrapper.js +9 -4
  41. package/lib/server/services/job-runner.js +156 -0
  42. package/lib/server/services/qwen-wrapper.js +26 -11
  43. package/lib/server/services/runtime-manager.js +467 -0
  44. package/lib/server/services/session-importer.js +6 -1
  45. package/lib/server/services/session-manager.js +56 -14
  46. package/lib/server/services/workspace-manager.js +121 -0
  47. package/lib/server/tests/integration.test.js +12 -0
  48. package/lib/server/tests/runtime-manager.test.js +46 -0
  49. package/lib/server/tests/runtime-persistence.test.js +97 -0
  50. package/lib/setup/postinstall-pty-check.js +183 -0
  51. package/lib/setup/postinstall.js +60 -41
  52. package/lib/utils/restart-warning.js +18 -0
  53. package/lib/utils/server.js +88 -0
  54. package/lib/utils/termux.js +1 -1
  55. package/lib/utils/update-check.js +153 -0
  56. package/lib/utils/update-runner.js +62 -0
  57. package/package.json +6 -5
@@ -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
  }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Update check utilities (npm registry) with simple caching
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { PATHS, ensureDirectories } = require('./paths');
8
+ const pkg = require('../../package.json');
9
+
10
+ const CACHE_FILE = path.join(PATHS.DATA_DIR, 'version.json');
11
+ const CHECK_INTERVAL_MS = 20 * 60 * 60 * 1000;
12
+ const DEFAULT_TIMEOUT_MS = 4000;
13
+
14
+ function normalizeVersion(raw) {
15
+ if (!raw || typeof raw !== 'string') return null;
16
+ const match = raw.trim().match(/(\d+\.\d+\.\d+)/);
17
+ return match ? match[1] : null;
18
+ }
19
+
20
+ function parseSemver(v) {
21
+ if (!v) return null;
22
+ const parts = v.trim().split('.');
23
+ if (parts.length < 3) return null;
24
+ const major = Number(parts[0]);
25
+ const minor = Number(parts[1]);
26
+ const patch = Number(parts[2]);
27
+ if (![major, minor, patch].every(Number.isFinite)) return null;
28
+ return [major, minor, patch];
29
+ }
30
+
31
+ function compareSemver(a, b) {
32
+ const av = parseSemver(a);
33
+ const bv = parseSemver(b);
34
+ if (!av || !bv) return null;
35
+ for (let i = 0; i < 3; i++) {
36
+ if (av[i] > bv[i]) return 1;
37
+ if (av[i] < bv[i]) return -1;
38
+ }
39
+ return 0;
40
+ }
41
+
42
+ function isNewer(latest, current) {
43
+ const cmp = compareSemver(latest, current);
44
+ if (cmp === null) return null;
45
+ return cmp === 1;
46
+ }
47
+
48
+ function readCache() {
49
+ if (!fs.existsSync(CACHE_FILE)) return null;
50
+ try {
51
+ const content = fs.readFileSync(CACHE_FILE, 'utf8');
52
+ return JSON.parse(content);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function writeCache(info) {
59
+ ensureDirectories();
60
+ try {
61
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(info, null, 2), 'utf8');
62
+ } catch {}
63
+ }
64
+
65
+ async function fetchJson(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
66
+ const controller = new AbortController();
67
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
68
+ const res = await fetch(url, { ...options, signal: controller.signal });
69
+ clearTimeout(timer);
70
+ if (!res.ok) {
71
+ throw new Error(`HTTP ${res.status}`);
72
+ }
73
+ return res.json();
74
+ }
75
+
76
+ async function fetchNpmLatest(timeoutMs) {
77
+ const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`;
78
+ const data = await fetchJson(registryUrl, { headers: { 'Accept': 'application/json' } }, timeoutMs);
79
+ const distTags = data['dist-tags'] || {};
80
+ return normalizeVersion(distTags.latest || distTags.stable);
81
+ }
82
+
83
+ function pickLatest(a, b) {
84
+ if (a && !b) return a;
85
+ if (b && !a) return b;
86
+ if (!a && !b) return null;
87
+ const cmp = compareSemver(a, b);
88
+ if (cmp === null) return a || b;
89
+ return cmp >= 0 ? a : b;
90
+ }
91
+
92
+ function buildInfo(info, { usedCache = false, error = null } = {}) {
93
+ const currentVersion = pkg.version;
94
+ const npmVersion = info?.npm_version || null;
95
+ const githubVersion = null;
96
+ const latestVersion = info?.latest_version || npmVersion;
97
+ const npmNewer = isNewer(npmVersion, currentVersion);
98
+ const githubNewer = null;
99
+
100
+ return {
101
+ currentVersion,
102
+ npmVersion,
103
+ githubVersion,
104
+ latestVersion,
105
+ npmNewer,
106
+ githubNewer,
107
+ updateAvailable: npmNewer === true,
108
+ usedCache,
109
+ error
110
+ };
111
+ }
112
+
113
+ async function getUpdateInfo({ force = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
114
+ const cache = readCache();
115
+ const lastCheckedAt = cache?.last_checked_at ? Date.parse(cache.last_checked_at) : null;
116
+ const cacheFresh = lastCheckedAt && (Date.now() - lastCheckedAt) < CHECK_INTERVAL_MS;
117
+
118
+ if (!force && cache && cacheFresh) {
119
+ return buildInfo(cache, { usedCache: true });
120
+ }
121
+
122
+ let npmVersion = null;
123
+ const errors = [];
124
+
125
+ try {
126
+ npmVersion = await fetchNpmLatest(timeoutMs);
127
+ } catch (err) {
128
+ errors.push(`npm: ${err.message}`);
129
+ }
130
+
131
+ if (!npmVersion) {
132
+ if (cache) {
133
+ return buildInfo(cache, { usedCache: true, error: errors.join('; ') });
134
+ }
135
+ return buildInfo(null, { usedCache: false, error: errors.join('; ') });
136
+ }
137
+
138
+ const info = {
139
+ latest_version: npmVersion,
140
+ npm_version: npmVersion,
141
+ last_checked_at: new Date().toISOString()
142
+ };
143
+
144
+ writeCache(info);
145
+ return buildInfo(info, { usedCache: false });
146
+ }
147
+
148
+ module.exports = {
149
+ getUpdateInfo,
150
+ normalizeVersion,
151
+ compareSemver,
152
+ isNewer
153
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Update + restart helpers
3
+ */
4
+
5
+ const { spawn, spawnSync } = require('child_process');
6
+ const chalk = require('chalk');
7
+ const pkg = require('../../package.json');
8
+ const { getServerPid, stopServer } = require('./server');
9
+
10
+ function runUpdateCommand() {
11
+ console.log(chalk.cyan(`Running npm update for ${pkg.name}...`));
12
+ const result = spawnSync('npm', ['update', '-g', pkg.name], { stdio: 'inherit' });
13
+
14
+ if (result.error) {
15
+ console.log(chalk.red(`Update failed: ${result.error.message}`));
16
+ return false;
17
+ }
18
+
19
+ if (typeof result.status === 'number' && result.status !== 0) {
20
+ console.log(chalk.red(`Update exited with code ${result.status}`));
21
+ return false;
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ function restartCli(restartArgs = []) {
28
+ const args = restartArgs.length > 0 ? restartArgs : ['start'];
29
+ const env = { ...process.env, NEXUSCLI_SKIP_UPDATE_CHECK: '1' };
30
+
31
+ return new Promise((resolve) => {
32
+ const child = spawn('nexuscli', args, { stdio: 'inherit', env });
33
+ child.on('exit', (code) => resolve(code ?? 0));
34
+ });
35
+ }
36
+
37
+ async function runUpdateAndRestart({ restartArgs = [] } = {}) {
38
+ const runningPid = getServerPid();
39
+ if (runningPid) {
40
+ console.log(chalk.yellow(`Stopping NexusCLI (PID: ${runningPid})...`));
41
+ const stopResult = stopServer();
42
+ if (stopResult.error) {
43
+ console.log(chalk.red(`Failed to stop server: ${stopResult.error.message}`));
44
+ return { ok: false, error: stopResult.error };
45
+ }
46
+ }
47
+
48
+ const updated = runUpdateCommand();
49
+ if (!updated) {
50
+ return { ok: false, error: new Error('Update failed') };
51
+ }
52
+
53
+ console.log(chalk.green('✓ Update complete. Restarting NexusCLI...'));
54
+ const restartExitCode = await restartCli(restartArgs);
55
+ return { ok: true, restartExitCode };
56
+ }
57
+
58
+ module.exports = {
59
+ runUpdateCommand,
60
+ restartCli,
61
+ runUpdateAndRestart
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.7004-termux",
3
+ "version": "0.10.0",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini/Qwen)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {
@@ -44,20 +44,18 @@
44
44
  "frontend/dist/",
45
45
  "package.json",
46
46
  "README.md",
47
+ "CHANGELOG.md",
47
48
  "LICENSE"
48
49
  ],
49
50
  "engines": {
50
51
  "node": ">=18.0.0"
51
52
  },
52
- "os": [
53
- "linux",
54
- "android"
55
- ],
56
53
  "cpu": [
57
54
  "arm64",
58
55
  "x64"
59
56
  ],
60
57
  "dependencies": {
58
+ "@mmmbuto/pty-termux-utils": "^1.1.4",
61
59
  "bcryptjs": "^3.0.3",
62
60
  "chalk": "^4.1.2",
63
61
  "commander": "^12.1.0",
@@ -75,5 +73,8 @@
75
73
  },
76
74
  "devDependencies": {
77
75
  "jest": "^30.2.0"
76
+ },
77
+ "optionalDependencies": {
78
+ "@mmmbuto/node-pty-android-arm64": "~1.1.0"
78
79
  }
79
80
  }