@mmmbuto/nexuscrew 0.2.5 → 0.4.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.
Files changed (94) hide show
  1. package/README.md +63 -163
  2. package/bin/nexuscrew.js +1 -296
  3. package/frontend/dist/assets/index-B41xYEDb.js +81 -0
  4. package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
  5. package/frontend/dist/icon-maskable.svg +4 -0
  6. package/frontend/dist/icon.svg +4 -0
  7. package/frontend/dist/index.html +11 -3
  8. package/frontend/dist/manifest.json +24 -0
  9. package/frontend/dist/sw.js +3 -0
  10. package/frontend/index.html +20 -0
  11. package/lib/auth/token.js +26 -0
  12. package/lib/config.js +25 -0
  13. package/lib/pty/attach.js +30 -0
  14. package/lib/pty/provider.js +33 -0
  15. package/lib/server.js +62 -0
  16. package/lib/tmux/actions.js +27 -0
  17. package/lib/tmux/list.js +33 -0
  18. package/lib/ws/bridge.js +66 -0
  19. package/package.json +20 -24
  20. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  24. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  25. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  26. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  27. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  28. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  29. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  30. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  32. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  36. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  37. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  38. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  39. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  40. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  42. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  47. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  58. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  59. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  61. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  62. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  63. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  64. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  65. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  66. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  67. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  68. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  69. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  70. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  71. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  72. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  73. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  74. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  75. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  76. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  77. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  78. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  79. package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
  80. package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
  81. package/lib/config/paths.js +0 -30
  82. package/lib/server/routes/hosts.js +0 -82
  83. package/lib/server/routes/launchers.js +0 -17
  84. package/lib/server/routes/models.js +0 -26
  85. package/lib/server/routes/runtime.js +0 -14
  86. package/lib/server/routes/send.js +0 -236
  87. package/lib/server/routes/sessions.js +0 -104
  88. package/lib/server/routes/status.js +0 -62
  89. package/lib/server/routes/tmux.js +0 -91
  90. package/lib/server/server.js +0 -189
  91. package/lib/services/engine-discovery.js +0 -317
  92. package/lib/services/log-watcher.js +0 -279
  93. package/lib/services/session-store.js +0 -198
  94. package/lib/services/tmux-manager.js +0 -288
@@ -1,198 +0,0 @@
1
- /**
2
- * SessionStore — SQLite persistence for nexuscrew
3
- *
4
- * Schema:
5
- * - conversations: id, title, engine, model, tmux_session, log_path, workspace, host, status, launcher_id, created_at, updated_at
6
- * - messages: id, conversation_id, role, content, engine, model, tokens_in, tokens_out, created_at
7
- */
8
-
9
- const Database = require('better-sqlite3');
10
- const path = require('path');
11
- const fs = require('fs');
12
- const { v4: uuidv4 } = require('uuid');
13
-
14
- class SessionStore {
15
- constructor(dbPath) {
16
- const dir = path.dirname(dbPath);
17
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
-
19
- this.db = new Database(dbPath);
20
- this.db.pragma('journal_mode = WAL');
21
- this.db.pragma('foreign_keys = ON');
22
- this._migrate();
23
- }
24
-
25
- _migrate() {
26
- this.db.exec(`
27
- CREATE TABLE IF NOT EXISTS conversations (
28
- id TEXT PRIMARY KEY,
29
- title TEXT DEFAULT 'New Chat',
30
- engine TEXT,
31
- model TEXT,
32
- tmux_window TEXT,
33
- tmux_session TEXT,
34
- log_path TEXT,
35
- workspace TEXT DEFAULT '',
36
- host TEXT DEFAULT 'local',
37
- launcher_id TEXT,
38
- status TEXT DEFAULT 'active',
39
- created_at INTEGER DEFAULT (unixepoch()),
40
- updated_at INTEGER DEFAULT (unixepoch())
41
- );
42
-
43
- CREATE TABLE IF NOT EXISTS messages (
44
- id TEXT PRIMARY KEY,
45
- conversation_id TEXT NOT NULL,
46
- role TEXT NOT NULL,
47
- content TEXT DEFAULT '',
48
- engine TEXT,
49
- model TEXT,
50
- tokens_in INTEGER DEFAULT 0,
51
- tokens_out INTEGER DEFAULT 0,
52
- created_at INTEGER DEFAULT (unixepoch()),
53
- FOREIGN KEY (conversation_id) REFERENCES conversations(id)
54
- );
55
-
56
- CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at);
57
- CREATE INDEX IF NOT EXISTS idx_conversations_status ON conversations(status, updated_at);
58
- `);
59
-
60
- this._ensureColumn('conversations', 'tmux_session', 'TEXT');
61
- this._ensureColumn('conversations', 'launcher_id', 'TEXT');
62
- this.db.exec('CREATE INDEX IF NOT EXISTS idx_conversations_tmux_session ON conversations(tmux_session)');
63
- }
64
-
65
- _ensureColumn(tableName, columnName, definition) {
66
- const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
67
- if (!columns.find((column) => column.name === columnName)) {
68
- this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
69
- }
70
- }
71
-
72
- // --- Conversations ---
73
-
74
- createConversation({ engine, model, workspace, host, tmuxWindow, tmuxSession, logPath, launcherId } = {}) {
75
- const id = uuidv4();
76
- this.db.prepare(`
77
- INSERT INTO conversations (id, engine, model, workspace, host, tmux_window, tmux_session, log_path, launcher_id)
78
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
79
- `).run(id, engine || null, model || null, workspace || '', host || 'local', tmuxWindow || null, tmuxSession || null, logPath || null, launcherId || null);
80
- return this.getConversation(id);
81
- }
82
-
83
- getConversation(id) {
84
- return this.db.prepare('SELECT * FROM conversations WHERE id = ?').get(id);
85
- }
86
-
87
- updateConversation(id, updates) {
88
- const fields = [];
89
- const values = [];
90
- for (const [key, value] of Object.entries(updates)) {
91
- if (['title', 'engine', 'model', 'tmux_window', 'tmux_session', 'log_path', 'workspace', 'host', 'status', 'launcher_id'].includes(key)) {
92
- fields.push(`${key} = ?`);
93
- values.push(value);
94
- }
95
- }
96
- if (fields.length === 0) return;
97
- fields.push('updated_at = unixepoch()');
98
- values.push(id);
99
- this.db.prepare(`UPDATE conversations SET ${fields.join(', ')} WHERE id = ?`).run(...values);
100
- }
101
-
102
- listConversations({ workspace, host, status, limit = 50 } = {}) {
103
- let sql = 'SELECT * FROM conversations WHERE 1=1';
104
- const params = [];
105
- if (workspace) { sql += ' AND workspace = ?'; params.push(workspace); }
106
- if (host) { sql += ' AND host = ?'; params.push(host); }
107
- if (status) { sql += ' AND status = ?'; params.push(status); }
108
- sql += ' ORDER BY updated_at DESC LIMIT ?';
109
- params.push(limit);
110
- return this.db.prepare(sql).all(...params);
111
- }
112
-
113
- deleteConversation(id) {
114
- this.db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(id);
115
- this.db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
116
- }
117
-
118
- /** Find conversation by tmux window name */
119
- findByWindow(windowName) {
120
- return this.db.prepare('SELECT * FROM conversations WHERE tmux_window = ?').get(windowName);
121
- }
122
-
123
- findByTmuxSession(sessionName) {
124
- return this.db.prepare('SELECT * FROM conversations WHERE tmux_session = ?').get(sessionName);
125
- }
126
-
127
- /** Get conversations grouped by date */
128
- listGroupedByDate({ workspace, limit = 100 } = {}) {
129
- const convs = this.listConversations({ workspace, limit });
130
- const grouped = {};
131
- for (const c of convs) {
132
- const date = new Date(c.updated_at * 1000).toISOString().split('T')[0];
133
- if (!grouped[date]) grouped[date] = [];
134
- grouped[date].push(c);
135
- }
136
- return grouped;
137
- }
138
-
139
- // --- Messages ---
140
-
141
- addMessage({ conversationId, role, content, engine, model, tokensIn, tokensOut } = {}) {
142
- const id = uuidv4();
143
- this.db.prepare(`
144
- INSERT INTO messages (id, conversation_id, role, content, engine, model, tokens_in, tokens_out)
145
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
146
- `).run(id, conversationId, role, content || '', engine || null, model || null, tokensIn || 0, tokensOut || 0);
147
-
148
- // Update conversation timestamp
149
- this.db.prepare('UPDATE conversations SET updated_at = unixepoch() WHERE id = ?').run(conversationId);
150
- return id;
151
- }
152
-
153
- getMessages(conversationId, { limit = 50, before } = {}) {
154
- let sql = 'SELECT * FROM messages WHERE conversation_id = ?';
155
- const params = [conversationId];
156
- if (before) {
157
- sql += ' AND created_at < ?';
158
- params.push(before);
159
- }
160
- sql += ' ORDER BY created_at ASC LIMIT ?';
161
- params.push(limit);
162
- return this.db.prepare(sql).all(...params);
163
- }
164
-
165
- getMessageCount(conversationId) {
166
- return this.db.prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?').get(conversationId).count;
167
- }
168
-
169
- // --- Recovery ---
170
-
171
- /**
172
- * Sync DB with actual tmux state
173
- * Mark conversations as 'dead' if their tmux window no longer exists
174
- */
175
- syncWithTmux(tmuxManager) {
176
- const active = this.db.prepare("SELECT * FROM conversations WHERE status = 'active'").all();
177
- for (const conv of active) {
178
- if (conv.tmux_session) {
179
- const exists = tmuxManager.hasSession(conv.tmux_session, conv.host !== 'local' ? conv.host : null);
180
- if (!exists) {
181
- this.updateConversation(conv.id, { status: 'dead' });
182
- }
183
- } else if (conv.tmux_window) {
184
- const exists = tmuxManager.hasSession(conv.tmux_window, conv.host !== 'local' ? conv.host : null);
185
- if (!exists) {
186
- this.updateConversation(conv.id, { status: 'dead' });
187
- }
188
- }
189
- }
190
- }
191
-
192
- /** Close DB connection */
193
- close() {
194
- this.db.close();
195
- }
196
- }
197
-
198
- module.exports = SessionStore;
@@ -1,288 +0,0 @@
1
- /**
2
- * TmuxManager — Core tmux operations for nexuscrew
3
- *
4
- * The primary unit is a standalone tmux session. nexuscrew never creates
5
- * implicit "master" sessions and never assumes ownership of all tmux state.
6
- */
7
-
8
- const { execSync } = require('child_process');
9
- const path = require('path');
10
- const fs = require('fs');
11
- const os = require('os');
12
-
13
- class TmuxManager {
14
- constructor(config = {}) {
15
- this.logDir = config.logDir || path.join(os.homedir(), '.nexuscrew', 'logs');
16
- this.configDir = config.configDir || path.dirname(this.logDir);
17
- this.hosts = config.hosts || [{ name: 'local', type: 'local' }];
18
- this.shell = config.preferredShell || process.env.SHELL || '/bin/sh';
19
-
20
- if (!fs.existsSync(this.logDir)) {
21
- fs.mkdirSync(this.logDir, { recursive: true });
22
- }
23
- }
24
-
25
- listSessions(host = null) {
26
- try {
27
- const cmd = this._cmd(
28
- "tmux list-sessions -F '#{session_name}\t#{session_attached}\t#{session_windows}\t#{session_created}' 2>/dev/null",
29
- host
30
- );
31
- const output = this._exec(cmd);
32
- return output.trim().split('\n').filter(Boolean).map((line) => {
33
- const [sessionName, attached, windows, created] = line.split('\t');
34
- return {
35
- sessionName,
36
- attached: attached === '1',
37
- windows: Number(windows || 0),
38
- createdAt: created ? Number(created) : null
39
- };
40
- });
41
- } catch {
42
- return [];
43
- }
44
- }
45
-
46
- hasSession(sessionName, host = null) {
47
- try {
48
- const cmd = this._cmd(`tmux has-session -t ${this._quote(sessionName)} 2>/dev/null`, host);
49
- this._exec(cmd);
50
- return true;
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- createSession({ sessionName, cwd, host = null, launcherCommand = '', shell = null }) {
57
- const effectiveHost = this._resolveHost(host);
58
- const runtime = this.getRuntimeInfo(effectiveHost);
59
- const effectiveShell = shell || runtime.shell || this.shell;
60
- const workingDir = cwd || runtime.homeDir || os.homedir();
61
-
62
- if (this.hasSession(sessionName, effectiveHost)) {
63
- throw new Error(`tmux session "${sessionName}" already exists`);
64
- }
65
-
66
- const shellCmd = this._interactiveShellCommand(effectiveShell);
67
- const createCmd = this._cmd(
68
- `tmux new-session -d -s ${this._quote(sessionName)} -c ${this._quote(workingDir)} ${this._quote(shellCmd)}`,
69
- effectiveHost
70
- );
71
- this._exec(createCmd);
72
-
73
- const logPath = this.ensureSessionLogging(sessionName, effectiveHost);
74
- if (launcherCommand && launcherCommand.trim()) {
75
- this.sendKeys(sessionName, launcherCommand.trim(), { enter: true, host: effectiveHost });
76
- }
77
-
78
- return { sessionName, logPath };
79
- }
80
-
81
- ensureSessionLogging(sessionName, host = null) {
82
- const effectiveHost = this._resolveHost(host);
83
- if (effectiveHost && effectiveHost.type !== 'local') {
84
- return null;
85
- }
86
- const logPath = path.join(this.logDir, `${sessionName}.log`);
87
- const pipeCmd = this._cmd(
88
- `tmux pipe-pane -o -t ${this._quote(sessionName)} ${this._quote(`cat >> ${logPath}`)}`,
89
- effectiveHost
90
- );
91
- this._exec(pipeCmd);
92
- return logPath;
93
- }
94
-
95
- sendKeys(sessionName, text, opts = {}) {
96
- const effectiveHost = this._resolveHost(opts.host);
97
- const enterSuffix = opts.enter ? ' Enter' : '';
98
- const escapedText = text.replace(/'/g, "'\\''");
99
- const cmd = this._cmd(
100
- `tmux send-keys -t ${this._quote(sessionName)} '${escapedText}'${enterSuffix}`,
101
- effectiveHost
102
- );
103
- this._exec(cmd);
104
- }
105
-
106
- sendSpecialKey(sessionName, key, host = null) {
107
- const effectiveHost = this._resolveHost(host);
108
- const cmd = this._cmd(
109
- `tmux send-keys -t ${this._quote(sessionName)} ${key}`,
110
- effectiveHost
111
- );
112
- this._exec(cmd);
113
- }
114
-
115
- interrupt(sessionName, host = null) {
116
- const effectiveHost = this._resolveHost(host);
117
- this.sendSpecialKey(sessionName, 'Escape', effectiveHost);
118
- setTimeout(() => {
119
- this.sendSpecialKey(sessionName, 'C-c', effectiveHost);
120
- }, 100);
121
- }
122
-
123
- capturePane(sessionName, host = null) {
124
- try {
125
- const cmd = this._cmd(
126
- `tmux capture-pane -t ${this._quote(sessionName)} -p -S -100`,
127
- this._resolveHost(host)
128
- );
129
- return this._exec(cmd);
130
- } catch {
131
- return '';
132
- }
133
- }
134
-
135
- killSession(sessionName, host = null) {
136
- try {
137
- const cmd = this._cmd(
138
- `tmux kill-session -t ${this._quote(sessionName)} 2>/dev/null`,
139
- this._resolveHost(host)
140
- );
141
- this._exec(cmd);
142
- } catch {}
143
- }
144
-
145
- getHost(name) {
146
- if (!name || name === 'local') return null;
147
- return this.hosts.find((h) => h.name === name) || null;
148
- }
149
-
150
- getAllHosts() {
151
- return this.hosts;
152
- }
153
-
154
- testHost(host) {
155
- const effectiveHost = this._resolveHost(host);
156
- if (!effectiveHost || effectiveHost.type === 'local') return true;
157
- try {
158
- this._exec(this._sshCmd(effectiveHost, 'echo ok'), 5000);
159
- return true;
160
- } catch {
161
- return false;
162
- }
163
- }
164
-
165
- _resolveHost(host) {
166
- if (!host || host === 'local') return null;
167
- if (typeof host === 'string') return this.getHost(host);
168
- return host;
169
- }
170
-
171
- getRuntimeInfo(host = null) {
172
- const effectiveHost = this._resolveHost(host);
173
- if (!effectiveHost || effectiveHost.type === 'local') {
174
- return {
175
- homeDir: os.homedir(),
176
- shell: this.shell,
177
- platform: process.platform,
178
- host: 'local'
179
- };
180
- }
181
-
182
- try {
183
- const output = this.execCommand(
184
- "printf '__NEXUSCREW_HOME__%s\\n' \"$HOME\"; printf '__NEXUSCREW_SHELL__%s\\n' \"${SHELL:-/bin/sh}\"; uname -s",
185
- effectiveHost,
186
- 5000
187
- );
188
- const lines = output.trim().split('\n');
189
- const homeDir = lines.find((line) => line.startsWith('__NEXUSCREW_HOME__'))?.replace('__NEXUSCREW_HOME__', '') || '';
190
- const shell = lines.find((line) => line.startsWith('__NEXUSCREW_SHELL__'))?.replace('__NEXUSCREW_SHELL__', '') || '/bin/sh';
191
- const uname = lines[lines.length - 1] || '';
192
- return {
193
- homeDir: homeDir || '',
194
- shell: shell || '/bin/sh',
195
- platform: this._mapPlatform(uname),
196
- host: effectiveHost.name || 'remote'
197
- };
198
- } catch {
199
- return {
200
- homeDir: '',
201
- shell: '/bin/sh',
202
- platform: 'unknown',
203
- host: effectiveHost.name || 'remote'
204
- };
205
- }
206
- }
207
-
208
- execCommand(cmd, host = null, timeout = 10000) {
209
- return this._exec(this._cmd(cmd, host), timeout);
210
- }
211
-
212
- resolveCommand(command, host = null) {
213
- try {
214
- const output = this.execCommand(`command -v ${command} 2>/dev/null`, host, 5000).trim();
215
- return output || null;
216
- } catch {
217
- return null;
218
- }
219
- }
220
-
221
- fileExists(filePath, host = null) {
222
- try {
223
- this.execCommand(`test -f ${this._quote(filePath)}`, host, 5000);
224
- return true;
225
- } catch {
226
- return false;
227
- }
228
- }
229
-
230
- readFile(filePath, host = null) {
231
- return this.execCommand(`cat ${this._quote(filePath)}`, host, 5000);
232
- }
233
-
234
- runInteractiveShell(shellPath, script, host = null) {
235
- const shellName = path.basename(shellPath || '/bin/sh');
236
- const shellArgs = this._shellArgs(shellName, script)
237
- .map((arg) => this._quote(arg))
238
- .join(' ');
239
- return this.execCommand(`${this._quote(shellPath)} ${shellArgs}`, host, 10000);
240
- }
241
-
242
- _interactiveShellCommand(shellPath) {
243
- const shellName = path.basename(shellPath || '/bin/sh');
244
- if (shellName === 'bash' || shellName === 'zsh') {
245
- return `${shellPath} -il`;
246
- }
247
- if (shellName === 'fish') {
248
- return `${shellPath} -i`;
249
- }
250
- return `${shellPath} -l`;
251
- }
252
-
253
- _cmd(cmd, host) {
254
- const effectiveHost = this._resolveHost(host);
255
- if (!effectiveHost || effectiveHost.type === 'local') return cmd;
256
- return this._sshCmd(effectiveHost, cmd);
257
- }
258
-
259
- _sshCmd(host, cmd) {
260
- const parts = ['ssh', '-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes'];
261
- if (host.port && host.port !== '22') parts.push('-p', host.port);
262
- if (host.key) parts.push('-i', host.key);
263
- parts.push(`${host.user}@${host.host}`);
264
- parts.push(this._quote(cmd));
265
- return parts.join(' ');
266
- }
267
-
268
- _quote(value) {
269
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
270
- }
271
-
272
- _mapPlatform(uname) {
273
- const value = String(uname || '').trim().toLowerCase();
274
- if (value === 'darwin') return 'darwin';
275
- if (value === 'linux') return 'linux';
276
- return value || 'unknown';
277
- }
278
-
279
- _exec(cmd, timeout = 10000) {
280
- return execSync(cmd, {
281
- encoding: 'utf8',
282
- timeout,
283
- stdio: ['pipe', 'pipe', 'pipe']
284
- });
285
- }
286
- }
287
-
288
- module.exports = TmuxManager;