@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
|
@@ -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
|
}
|
|
@@ -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.
|
|
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
|
}
|