@mmmbuto/nexuscrew 0.1.0-beta.1 → 0.2.1
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/README.md +67 -120
- package/bin/nexuscrew.js +468 -264
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/{index-C7bAndew.js → index-BG1N7bSL.js} +1710 -1702
- package/frontend/dist/assets/index-CiAtinNP.css +1 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/site.webmanifest +29 -0
- package/lib/config/hosts.js +67 -0
- package/lib/config/manager.js +379 -0
- package/lib/config/models.js +408 -0
- package/lib/server/db/adapter.js +274 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_base_schema.sql +70 -0
- package/lib/server/middleware/auth.js +134 -0
- package/lib/server/middleware/rate-limit.js +63 -0
- package/lib/server/models/User.js +128 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/hosts.js +11 -20
- package/lib/server/routes/keys.js +28 -0
- package/lib/server/routes/models.js +59 -4
- package/lib/server/routes/runtimes.js +34 -0
- package/lib/server/routes/send.js +76 -12
- package/lib/server/routes/sessions.js +39 -10
- package/lib/server/routes/speech.js +46 -0
- package/lib/server/routes/status.js +8 -6
- package/lib/server/routes/upload.js +135 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspaces.js +101 -0
- package/lib/server/server.js +66 -33
- package/lib/server/services/attachment-manager.js +57 -0
- package/lib/server/services/context-bridge.js +425 -0
- package/lib/server/services/runtime-manager.js +462 -0
- package/lib/server/services/speech-manager.js +76 -0
- package/lib/server/services/summary-generator.js +309 -0
- package/lib/server/services/workspace-manager.js +79 -0
- package/lib/services/engine-discovery.js +198 -13
- package/lib/services/log-watcher.js +40 -22
- package/lib/services/remote-pane-watcher.js +155 -0
- package/lib/services/session-store.js +60 -64
- package/lib/services/tmux-manager.js +127 -116
- package/lib/setup/postinstall.js +38 -0
- package/lib/utils/paths.js +124 -0
- package/lib/utils/termux.js +182 -0
- package/package.json +8 -4
- package/frontend/dist/assets/index-OENqI1_9.css +0 -1
|
@@ -5,35 +5,67 @@
|
|
|
5
5
|
* Every AI session = one tmux window inside the master session.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { execSync
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs');
|
|
11
|
+
const { readHosts, getHostByName } = require('../config/hosts');
|
|
12
|
+
const { PATHS } = require('../utils/paths');
|
|
11
13
|
|
|
12
14
|
const TMUX_SESSION = 'nexuscrew';
|
|
13
15
|
|
|
16
|
+
function shellEscape(value) {
|
|
17
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
class TmuxManager {
|
|
15
21
|
constructor(config = {}) {
|
|
16
22
|
this.sessionName = config.tmuxSession || TMUX_SESSION;
|
|
17
|
-
this.logDir = config.logDir ||
|
|
18
|
-
this.hosts = config.hosts || [{ name: 'local', type: 'local' }];
|
|
23
|
+
this.logDir = config.logDir || PATHS.LOGS_DIR;
|
|
19
24
|
|
|
20
|
-
// Ensure log dir
|
|
21
25
|
if (!fs.existsSync(this.logDir)) {
|
|
22
26
|
fs.mkdirSync(this.logDir, { recursive: true });
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
getAllHosts() {
|
|
31
|
+
return readHosts();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getHost(name) {
|
|
35
|
+
return this.resolveHost(name);
|
|
36
|
+
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
resolveHost(hostRef) {
|
|
39
|
+
if (!hostRef || hostRef === 'local') {
|
|
40
|
+
return getHostByName('local');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof hostRef === 'string') {
|
|
44
|
+
return getHostByName(hostRef);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof hostRef === 'object') {
|
|
48
|
+
if (hostRef.type === 'local' || hostRef.name === 'local') {
|
|
49
|
+
return getHostByName('local');
|
|
50
|
+
}
|
|
51
|
+
if (hostRef.name) {
|
|
52
|
+
return { ...getHostByName(hostRef.name), ...hostRef };
|
|
53
|
+
}
|
|
54
|
+
return hostRef;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ensureSession(hostRef = null) {
|
|
61
|
+
const host = this.resolveHost(hostRef);
|
|
30
62
|
const cmd = this._cmd('tmux new-session -d -s ${SESSION} -x 200 -y 50 2>/dev/null || true', host);
|
|
31
63
|
this._exec(cmd);
|
|
32
64
|
}
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
hasSession(host = null) {
|
|
66
|
+
hasSession(hostRef = null) {
|
|
36
67
|
try {
|
|
68
|
+
const host = this.resolveHost(hostRef);
|
|
37
69
|
this._exec(this._cmd('tmux has-session -t ${SESSION} 2>/dev/null', host));
|
|
38
70
|
return true;
|
|
39
71
|
} catch {
|
|
@@ -41,98 +73,68 @@ class TmuxManager {
|
|
|
41
73
|
}
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create a new tmux window running a CLI command
|
|
48
|
-
* @param {Object} opts
|
|
49
|
-
* @param {string} opts.windowName - Unique window name (e.g. "claude-abc123")
|
|
50
|
-
* @param {string} opts.command - Full command to run
|
|
51
|
-
* @param {string} opts.cwd - Working directory
|
|
52
|
-
* @param {string} opts.host - Host name or null for local
|
|
53
|
-
* @returns {{ windowName, logPath }}
|
|
54
|
-
*/
|
|
55
|
-
createWindow({ windowName, command, cwd, host = null }) {
|
|
76
|
+
createWindow({ windowName, command, cwd, host: hostRef = null }) {
|
|
77
|
+
const host = this.resolveHost(hostRef);
|
|
56
78
|
this.ensureSession(host);
|
|
57
79
|
|
|
58
|
-
const logPath =
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
// For remote hosts, logs go to a local path
|
|
62
|
-
const fullCommand = cwd
|
|
63
|
-
? `cd ${cwd} && ${command} 2>&1 | tee ${logPath}`
|
|
64
|
-
: `${command} 2>&1 | tee ${logPath}`;
|
|
80
|
+
const logPath = this.getLogPath(windowName);
|
|
81
|
+
const targetCwd = cwd || process.env.HOME || '.';
|
|
82
|
+
const fullCommand = `cd ${shellEscape(targetCwd)} && ${command} 2>&1 | tee ${shellEscape(logPath)}`;
|
|
65
83
|
|
|
66
84
|
const cmd = this._cmd(
|
|
67
|
-
`tmux new-window -t \${SESSION} -n ${windowName} -c
|
|
85
|
+
`tmux new-window -t \${SESSION} -n ${shellEscape(windowName)} -c ${shellEscape(targetCwd)} ${shellEscape(fullCommand)}`,
|
|
68
86
|
host
|
|
69
87
|
);
|
|
70
88
|
|
|
71
|
-
this._exec(cmd);
|
|
72
|
-
|
|
89
|
+
this._exec(cmd, 20000);
|
|
73
90
|
return { windowName, logPath };
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
/**
|
|
77
|
-
* Send text to a tmux window (like typing)
|
|
78
|
-
* @param {string} windowName
|
|
79
|
-
* @param {string} text - Text to send
|
|
80
|
-
* @param {Object} opts
|
|
81
|
-
* @param {boolean} opts.enter - Press Enter after text
|
|
82
|
-
* @param {string} opts.host - Host name or null
|
|
83
|
-
*/
|
|
84
93
|
sendKeys(windowName, text, opts = {}) {
|
|
85
|
-
const
|
|
86
|
-
const escapedText = text.replace(/'/g, "'\\''");
|
|
94
|
+
const host = this.resolveHost(opts.host);
|
|
87
95
|
const cmd = this._cmd(
|
|
88
|
-
`tmux send-keys -t \${SESSION}:${windowName}
|
|
89
|
-
|
|
96
|
+
`tmux send-keys -t \${SESSION}:${shellEscape(windowName)} -l ${shellEscape(text)}`,
|
|
97
|
+
host
|
|
90
98
|
);
|
|
91
99
|
this._exec(cmd);
|
|
100
|
+
if (opts.enter) {
|
|
101
|
+
this.sendSpecialKey(windowName, 'Enter', host);
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*/
|
|
97
|
-
sendSpecialKey(windowName, key, host = null) {
|
|
105
|
+
sendSpecialKey(windowName, key, hostRef = null) {
|
|
106
|
+
const host = this.resolveHost(hostRef);
|
|
98
107
|
const cmd = this._cmd(
|
|
99
|
-
`tmux send-keys -t \${SESSION}:${windowName} ${key}`,
|
|
108
|
+
`tmux send-keys -t \${SESSION}:${shellEscape(windowName)} ${shellEscape(key)}`,
|
|
100
109
|
host
|
|
101
110
|
);
|
|
102
111
|
this._exec(cmd);
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
interrupt(windowName, hostRef = null) {
|
|
115
|
+
const host = this.resolveHost(hostRef);
|
|
107
116
|
this.sendSpecialKey(windowName, 'Escape', host);
|
|
108
|
-
|
|
109
|
-
this.sendSpecialKey(windowName, 'C-c', host);
|
|
110
|
-
}, 100);
|
|
117
|
+
this.sendSpecialKey(windowName, 'C-c', host);
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
|
|
114
|
-
* Capture current pane content
|
|
115
|
-
* @returns {string} visible text in the pane
|
|
116
|
-
*/
|
|
117
|
-
capturePane(windowName, host = null) {
|
|
120
|
+
capturePane(windowName, hostRef = null, lines = 200) {
|
|
118
121
|
try {
|
|
122
|
+
const host = this.resolveHost(hostRef);
|
|
119
123
|
const cmd = this._cmd(
|
|
120
|
-
`tmux capture-pane -t \${SESSION}:${windowName} -p -S
|
|
124
|
+
`tmux capture-pane -t \${SESSION}:${shellEscape(windowName)} -p -S -${Math.max(50, lines)}`,
|
|
121
125
|
host
|
|
122
126
|
);
|
|
123
|
-
return this._exec(cmd);
|
|
127
|
+
return this._exec(cmd, 15000);
|
|
124
128
|
} catch {
|
|
125
129
|
return '';
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
* Check if a window exists
|
|
131
|
-
*/
|
|
132
|
-
hasWindow(windowName, host = null) {
|
|
133
|
+
hasWindow(windowName, hostRef = null) {
|
|
133
134
|
try {
|
|
135
|
+
const host = this.resolveHost(hostRef);
|
|
134
136
|
const cmd = this._cmd(
|
|
135
|
-
`tmux list-windows -t \${SESSION} -F '#{
|
|
137
|
+
`tmux list-windows -t \${SESSION} -F '#{window_name}' 2>/dev/null | grep -Fx -- ${shellEscape(windowName)}`,
|
|
136
138
|
host
|
|
137
139
|
);
|
|
138
140
|
this._exec(cmd);
|
|
@@ -142,38 +144,35 @@ class TmuxManager {
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
* Kill a window
|
|
147
|
-
*/
|
|
148
|
-
killWindow(windowName, host = null) {
|
|
147
|
+
killWindow(windowName, hostRef = null) {
|
|
149
148
|
try {
|
|
149
|
+
const host = this.resolveHost(hostRef);
|
|
150
150
|
const cmd = this._cmd(
|
|
151
|
-
`tmux kill-window -t \${SESSION}:${windowName} 2>/dev/null`,
|
|
151
|
+
`tmux kill-window -t \${SESSION}:${shellEscape(windowName)} 2>/dev/null`,
|
|
152
152
|
host
|
|
153
153
|
);
|
|
154
154
|
this._exec(cmd);
|
|
155
155
|
} catch {}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
* List all windows in the master session
|
|
160
|
-
* @returns {Array<{index, name, active, size}>}
|
|
161
|
-
*/
|
|
162
|
-
listWindows(host = null) {
|
|
158
|
+
listWindows(hostRef = null) {
|
|
163
159
|
try {
|
|
160
|
+
const host = this.resolveHost(hostRef);
|
|
164
161
|
const cmd = this._cmd(
|
|
165
|
-
`tmux list-windows -t \${SESSION} -F '#{
|
|
162
|
+
`tmux list-windows -t \${SESSION} -F '#{window_index}\t#{window_name}\t#{window_width}x#{window_height}\t#{?window_active,1,0}' 2>/dev/null`,
|
|
166
163
|
host
|
|
167
164
|
);
|
|
168
|
-
const output = this._exec(cmd);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
const output = this._exec(cmd, 15000).trim();
|
|
166
|
+
if (!output) return [];
|
|
167
|
+
|
|
168
|
+
return output.split('\n').filter(Boolean).map((line) => {
|
|
169
|
+
const [index, name, size, active] = line.split('\t');
|
|
172
170
|
return {
|
|
173
|
-
index:
|
|
174
|
-
name
|
|
171
|
+
index: Number(index),
|
|
172
|
+
name,
|
|
175
173
|
size,
|
|
176
|
-
active: active === '1'
|
|
174
|
+
active: active === '1',
|
|
175
|
+
host: host?.name || 'local'
|
|
177
176
|
};
|
|
178
177
|
});
|
|
179
178
|
} catch {
|
|
@@ -181,13 +180,11 @@ class TmuxManager {
|
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
* Get process PID running in a window
|
|
186
|
-
*/
|
|
187
|
-
getWindowPid(windowName, host = null) {
|
|
183
|
+
getWindowPid(windowName, hostRef = null) {
|
|
188
184
|
try {
|
|
185
|
+
const host = this.resolveHost(hostRef);
|
|
189
186
|
const cmd = this._cmd(
|
|
190
|
-
`tmux list-panes -t \${SESSION}:${windowName} -F '#{
|
|
187
|
+
`tmux list-panes -t \${SESSION}:${shellEscape(windowName)} -F '#{pane_pid}' 2>/dev/null`,
|
|
191
188
|
host
|
|
192
189
|
);
|
|
193
190
|
return this._exec(cmd).trim();
|
|
@@ -196,40 +193,51 @@ class TmuxManager {
|
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/** Get host config by name */
|
|
202
|
-
getHost(name) {
|
|
203
|
-
if (!name || name === 'local') return null;
|
|
204
|
-
return this.hosts.find(h => h.name === name) || null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Get all configured hosts */
|
|
208
|
-
getAllHosts() {
|
|
209
|
-
return this.hosts;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Test SSH connectivity */
|
|
213
|
-
testHost(host) {
|
|
196
|
+
testHost(hostRef) {
|
|
197
|
+
const host = this.resolveHost(hostRef);
|
|
214
198
|
if (!host || host.type === 'local') return true;
|
|
215
199
|
try {
|
|
216
|
-
|
|
217
|
-
this._exec(sshCmd, 5000);
|
|
200
|
+
this._exec(this._sshCmd(host, 'echo ok'), 5000);
|
|
218
201
|
return true;
|
|
219
202
|
} catch {
|
|
220
203
|
return false;
|
|
221
204
|
}
|
|
222
205
|
}
|
|
223
206
|
|
|
224
|
-
|
|
207
|
+
ensureRemoteDirectory(remotePath, hostRef) {
|
|
208
|
+
const host = this.resolveHost(hostRef);
|
|
209
|
+
if (!host || host.type === 'local') {
|
|
210
|
+
fs.mkdirSync(remotePath, { recursive: true });
|
|
211
|
+
return remotePath;
|
|
212
|
+
}
|
|
213
|
+
this._exec(this._sshCmd(host, `mkdir -p ${shellEscape(remotePath)}`), 10000);
|
|
214
|
+
return remotePath;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
copyToHost(localPath, remotePath, hostRef) {
|
|
218
|
+
const host = this.resolveHost(hostRef);
|
|
219
|
+
if (!host || host.type === 'local') {
|
|
220
|
+
const targetDir = path.dirname(remotePath);
|
|
221
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
222
|
+
fs.copyFileSync(localPath, remotePath);
|
|
223
|
+
return remotePath;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.ensureRemoteDirectory(path.posix.dirname(remotePath), host);
|
|
227
|
+
const parts = ['scp', '-q', '-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes'];
|
|
228
|
+
if (host.port && host.port !== '22') parts.push('-P', String(host.port));
|
|
229
|
+
if (host.key) parts.push('-i', host.key);
|
|
230
|
+
parts.push(localPath, `${host.user}@${host.host}:${remotePath}`);
|
|
231
|
+
this._exec(parts.map(shellEscape).join(' '), 30000);
|
|
232
|
+
return remotePath;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getLogPath(windowName) {
|
|
236
|
+
return path.join(this.logDir, `${windowName}.log`);
|
|
237
|
+
}
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* @param {string} tmpl - Command template with ${SESSION}
|
|
229
|
-
* @param {Object|null} host - Host config
|
|
230
|
-
* @returns {string} final command
|
|
231
|
-
*/
|
|
232
|
-
_cmd(tmpl, host) {
|
|
239
|
+
_cmd(tmpl, hostRef) {
|
|
240
|
+
const host = this.resolveHost(hostRef);
|
|
233
241
|
const cmd = tmpl.replace(/\$\{SESSION\}/g, this.sessionName);
|
|
234
242
|
if (!host || host.type === 'local') return cmd;
|
|
235
243
|
return this._sshCmd(host, cmd);
|
|
@@ -237,11 +245,14 @@ class TmuxManager {
|
|
|
237
245
|
|
|
238
246
|
_sshCmd(host, cmd) {
|
|
239
247
|
const parts = ['ssh', '-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes'];
|
|
240
|
-
if (host.port && host.port !== '22') parts.push('-p', host.port);
|
|
248
|
+
if (host.port && host.port !== '22') parts.push('-p', String(host.port));
|
|
241
249
|
if (host.key) parts.push('-i', host.key);
|
|
242
250
|
parts.push(`${host.user}@${host.host}`);
|
|
243
|
-
parts.push(cmd);
|
|
244
|
-
return parts.
|
|
251
|
+
parts.push(shellEscape(cmd));
|
|
252
|
+
return parts.map((part, index) => {
|
|
253
|
+
if (index === parts.length - 1) return part;
|
|
254
|
+
return /\s/.test(part) ? shellEscape(part) : part;
|
|
255
|
+
}).join(' ');
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
_exec(cmd, timeout = 10000) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { ensureDirectories, PATHS } = require('../utils/paths');
|
|
5
|
+
const { initializeConfig, isInitialized } = require('../config/manager');
|
|
6
|
+
const { ensureHostsFile } = require('../config/hosts');
|
|
7
|
+
const { isTermux } = require('../utils/termux');
|
|
8
|
+
|
|
9
|
+
function has(command) {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ensureDirectories();
|
|
19
|
+
ensureHostsFile();
|
|
20
|
+
|
|
21
|
+
if (!isInitialized()) {
|
|
22
|
+
initializeConfig({ workspaces: [`${process.env.HOME || ''}/Dev`] });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log('NexusCrew postinstall');
|
|
27
|
+
console.log(` config: ${PATHS.CONFIG_FILE}`);
|
|
28
|
+
console.log(` hosts: ${PATHS.HOSTS_FILE}`);
|
|
29
|
+
console.log(` tmux: ${has('tmux') ? 'OK' : 'missing'}`);
|
|
30
|
+
console.log(` ssh: ${has('ssh') ? 'OK' : 'missing'}`);
|
|
31
|
+
console.log(` scp: ${has('scp') ? 'OK' : 'missing'}`);
|
|
32
|
+
console.log(` whisper-cli: ${has('whisper-cli') ? 'OK' : 'missing'}`);
|
|
33
|
+
if (isTermux()) {
|
|
34
|
+
console.log(' platform: Termux');
|
|
35
|
+
console.log(` termux-services: ${has('sv') ? 'OK' : 'missing'}`);
|
|
36
|
+
}
|
|
37
|
+
console.log(' next: run `nexuscrew setup` to configure workspace, SSH hosts, local STT, and optional Termux boot/runit');
|
|
38
|
+
console.log('');
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Paths for NexusCrew
|
|
3
|
+
* Termux Mobile First - all paths relative to $HOME
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const HOME = process.env.HOME || os.homedir();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* NexusCrew standard paths
|
|
14
|
+
*/
|
|
15
|
+
const PATHS = {
|
|
16
|
+
// Main config directory
|
|
17
|
+
CONFIG_DIR: path.join(HOME, '.nexuscrew'),
|
|
18
|
+
|
|
19
|
+
// Config file
|
|
20
|
+
CONFIG_FILE: path.join(HOME, '.nexuscrew', 'config.json'),
|
|
21
|
+
|
|
22
|
+
// Data directory (database, cache)
|
|
23
|
+
DATA_DIR: path.join(HOME, '.nexuscrew', 'data'),
|
|
24
|
+
|
|
25
|
+
// Database file
|
|
26
|
+
DATABASE: path.join(HOME, '.nexuscrew', 'data', 'nexuscrew.db'),
|
|
27
|
+
|
|
28
|
+
// Logs directory
|
|
29
|
+
LOGS_DIR: path.join(HOME, '.nexuscrew', 'logs'),
|
|
30
|
+
|
|
31
|
+
// Server log
|
|
32
|
+
SERVER_LOG: path.join(HOME, '.nexuscrew', 'logs', 'server.log'),
|
|
33
|
+
|
|
34
|
+
// PID file for daemon
|
|
35
|
+
PID_FILE: path.join(HOME, '.nexuscrew', 'nexuscrew.pid'),
|
|
36
|
+
|
|
37
|
+
// Engines config directory
|
|
38
|
+
ENGINES_DIR: path.join(HOME, '.nexuscrew', 'engines'),
|
|
39
|
+
|
|
40
|
+
// Attachments directory
|
|
41
|
+
ATTACHMENTS_DIR: path.join(HOME, '.nexuscrew', 'attachments'),
|
|
42
|
+
|
|
43
|
+
// Local helper binaries
|
|
44
|
+
BIN_DIR: path.join(HOME, '.nexuscrew', 'bin'),
|
|
45
|
+
|
|
46
|
+
// Hosts registry
|
|
47
|
+
HOSTS_FILE: path.join(HOME, '.nexuscrew', 'hosts.json'),
|
|
48
|
+
|
|
49
|
+
// Termux boot script
|
|
50
|
+
BOOT_SCRIPT: path.join(HOME, '.termux', 'boot', 'nexuscrew-start.sh'),
|
|
51
|
+
|
|
52
|
+
// Termux runit service directory
|
|
53
|
+
TERMUX_SERVICE_DIR: path.join(process.env.PREFIX || '/data/data/com.termux/files/usr', 'var', 'service', 'nexuscrew'),
|
|
54
|
+
|
|
55
|
+
// Termux runit logs
|
|
56
|
+
TERMUX_SERVICE_LOG_DIR: path.join(process.env.PREFIX || '/data/data/com.termux/files/usr', 'var', 'log', 'sv', 'nexuscrew'),
|
|
57
|
+
|
|
58
|
+
// Claude CLI history (read-only)
|
|
59
|
+
CLAUDE_HISTORY: path.join(HOME, '.claude', 'history.jsonl'),
|
|
60
|
+
CLAUDE_PROJECTS: path.join(HOME, '.claude', 'projects'),
|
|
61
|
+
|
|
62
|
+
// Default workspace
|
|
63
|
+
DEFAULT_WORKSPACE: path.join(HOME, 'Dev')
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ensure all required directories exist
|
|
68
|
+
*/
|
|
69
|
+
function ensureDirectories() {
|
|
70
|
+
const dirs = [
|
|
71
|
+
PATHS.CONFIG_DIR,
|
|
72
|
+
PATHS.DATA_DIR,
|
|
73
|
+
PATHS.LOGS_DIR,
|
|
74
|
+
PATHS.ENGINES_DIR,
|
|
75
|
+
PATHS.ATTACHMENTS_DIR,
|
|
76
|
+
PATHS.BIN_DIR
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const dir of dirs) {
|
|
80
|
+
if (!fs.existsSync(dir)) {
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensure Termux boot directory exists
|
|
88
|
+
*/
|
|
89
|
+
function ensureBootDirectory() {
|
|
90
|
+
const bootDir = path.join(HOME, '.termux', 'boot');
|
|
91
|
+
if (!fs.existsSync(bootDir)) {
|
|
92
|
+
fs.mkdirSync(bootDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
return bootDir;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get path resolving ~ to HOME
|
|
99
|
+
*/
|
|
100
|
+
function resolvePath(p) {
|
|
101
|
+
if (p.startsWith('~')) {
|
|
102
|
+
return path.join(HOME, p.slice(1));
|
|
103
|
+
}
|
|
104
|
+
return path.resolve(p);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get relative path from HOME (for display)
|
|
109
|
+
*/
|
|
110
|
+
function toDisplayPath(p) {
|
|
111
|
+
if (p.startsWith(HOME)) {
|
|
112
|
+
return '~' + p.slice(HOME.length);
|
|
113
|
+
}
|
|
114
|
+
return p;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
PATHS,
|
|
119
|
+
HOME,
|
|
120
|
+
ensureDirectories,
|
|
121
|
+
ensureBootDirectory,
|
|
122
|
+
resolvePath,
|
|
123
|
+
toDisplayPath
|
|
124
|
+
};
|