groove-dev 0.16.2 → 0.16.4
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/node_modules/@groove-dev/daemon/src/index.js +4 -1
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +110 -33
- package/node_modules/@groove-dev/gui/dist/assets/{index-DeXW9EFU.js → index-B_VHpncx.js} +19 -19
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/components/EditorTabs.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/FileTree.jsx +18 -8
- package/node_modules/@groove-dev/gui/src/components/Terminal.jsx +29 -12
- package/package.json +1 -1
- package/packages/daemon/src/index.js +4 -1
- package/packages/daemon/src/terminal-pty.js +110 -33
- package/packages/gui/dist/assets/{index-DeXW9EFU.js → index-B_VHpncx.js} +19 -19
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/components/EditorTabs.jsx +2 -2
- package/packages/gui/src/components/FileTree.jsx +18 -8
- package/packages/gui/src/components/Terminal.jsx +29 -12
|
@@ -174,13 +174,16 @@ export class Daemon {
|
|
|
174
174
|
break;
|
|
175
175
|
// Terminal
|
|
176
176
|
case 'terminal:spawn': {
|
|
177
|
-
const id = this.terminalManager.spawn(ws, { cwd: msg.cwd });
|
|
177
|
+
const id = this.terminalManager.spawn(ws, { cwd: msg.cwd, cols: msg.cols, rows: msg.rows });
|
|
178
178
|
ws.send(JSON.stringify({ type: 'terminal:spawned', id }));
|
|
179
179
|
break;
|
|
180
180
|
}
|
|
181
181
|
case 'terminal:input':
|
|
182
182
|
if (msg.id && msg.data) this.terminalManager.write(msg.id, msg.data);
|
|
183
183
|
break;
|
|
184
|
+
case 'terminal:resize':
|
|
185
|
+
if (msg.id && msg.rows && msg.cols) this.terminalManager.resize(msg.id, msg.rows, msg.cols);
|
|
186
|
+
break;
|
|
184
187
|
case 'terminal:kill':
|
|
185
188
|
if (msg.id) this.terminalManager.kill(msg.id);
|
|
186
189
|
break;
|
|
@@ -1,38 +1,116 @@
|
|
|
1
1
|
// GROOVE — Terminal PTY Manager (shell sessions over WebSocket)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { spawn } from 'child_process';
|
|
4
|
+
import { spawn, execFileSync } from 'child_process';
|
|
5
5
|
import { existsSync } from 'fs';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// Python helper that creates a real PTY and relays I/O through stdin/stdout pipes.
|
|
8
|
+
// The shell sees a genuine TTY — prompts, colors, line editing, tab completion all work.
|
|
9
|
+
const PTY_HELPER = `
|
|
10
|
+
import pty, os, sys, select, signal, struct, fcntl, termios, errno
|
|
11
|
+
|
|
12
|
+
master, slave = pty.openpty()
|
|
13
|
+
|
|
14
|
+
cols = int(os.environ.get('COLS', '120'))
|
|
15
|
+
rows = int(os.environ.get('ROWS', '30'))
|
|
16
|
+
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', rows, cols, 0, 0))
|
|
17
|
+
|
|
18
|
+
pid = os.fork()
|
|
19
|
+
if pid == 0:
|
|
20
|
+
os.setsid()
|
|
21
|
+
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
|
|
22
|
+
os.dup2(slave, 0)
|
|
23
|
+
os.dup2(slave, 1)
|
|
24
|
+
os.dup2(slave, 2)
|
|
25
|
+
os.close(master)
|
|
26
|
+
os.close(slave)
|
|
27
|
+
shell = os.environ.get('GROOVE_SHELL', os.environ.get('SHELL', '/bin/bash'))
|
|
28
|
+
os.execvp(shell, [shell, '-l'])
|
|
29
|
+
|
|
30
|
+
os.close(slave)
|
|
31
|
+
|
|
32
|
+
def resize(sig, frame):
|
|
33
|
+
pass
|
|
34
|
+
signal.signal(signal.SIGWINCH, resize)
|
|
35
|
+
|
|
36
|
+
flags = fcntl.fcntl(0, fcntl.F_GETFL)
|
|
37
|
+
fcntl.fcntl(0, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
while True:
|
|
41
|
+
rlist = select.select([0, master], [], [], 0.05)[0]
|
|
42
|
+
if 0 in rlist:
|
|
43
|
+
try:
|
|
44
|
+
data = os.read(0, 4096)
|
|
45
|
+
if not data: break
|
|
46
|
+
# Resize command: ESC ] 7 ; <rows> ; <cols> BEL
|
|
47
|
+
if b'\\x1b]7;' in data:
|
|
48
|
+
idx = data.index(b'\\x1b]7;')
|
|
49
|
+
end = data.index(b'\\x07', idx)
|
|
50
|
+
params = data[idx+4:end].decode().split(';')
|
|
51
|
+
if len(params) == 2:
|
|
52
|
+
r, c = int(params[0]), int(params[1])
|
|
53
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', r, c, 0, 0))
|
|
54
|
+
os.kill(pid, signal.SIGWINCH)
|
|
55
|
+
rest = data[:idx] + data[end+1:]
|
|
56
|
+
if rest:
|
|
57
|
+
os.write(master, rest)
|
|
58
|
+
else:
|
|
59
|
+
os.write(master, data)
|
|
60
|
+
except OSError as e:
|
|
61
|
+
if e.errno != errno.EAGAIN: break
|
|
62
|
+
if master in rlist:
|
|
63
|
+
try:
|
|
64
|
+
data = os.read(master, 4096)
|
|
65
|
+
if not data: break
|
|
66
|
+
sys.stdout.buffer.write(data)
|
|
67
|
+
sys.stdout.buffer.flush()
|
|
68
|
+
except OSError: break
|
|
69
|
+
# Check child
|
|
70
|
+
try:
|
|
71
|
+
p, status = os.waitpid(pid, os.WNOHANG)
|
|
72
|
+
if p != 0: break
|
|
73
|
+
except ChildProcessError: break
|
|
74
|
+
except: pass
|
|
75
|
+
finally:
|
|
76
|
+
try: os.kill(pid, signal.SIGTERM)
|
|
77
|
+
except: pass
|
|
78
|
+
`.trim();
|
|
79
|
+
|
|
12
80
|
export class TerminalManager {
|
|
13
81
|
constructor(daemon) {
|
|
14
82
|
this.daemon = daemon;
|
|
15
|
-
this.sessions = new Map();
|
|
83
|
+
this.sessions = new Map();
|
|
16
84
|
this.counter = 0;
|
|
85
|
+
this._python = this._findPython();
|
|
17
86
|
}
|
|
18
87
|
|
|
19
|
-
/**
|
|
20
|
-
* Spawn a new shell session connected to a specific WebSocket client.
|
|
21
|
-
*/
|
|
22
88
|
spawn(ws, options = {}) {
|
|
23
89
|
const id = `term-${++this.counter}`;
|
|
24
90
|
const shell = this._detectShell();
|
|
25
91
|
const cwd = options.cwd || this.daemon.projectDir;
|
|
92
|
+
const cols = options.cols || 120;
|
|
93
|
+
const rows = options.rows || 30;
|
|
94
|
+
|
|
95
|
+
if (!this._python) {
|
|
96
|
+
ws.send(JSON.stringify({
|
|
97
|
+
type: 'terminal:output', id,
|
|
98
|
+
data: '\r\n\x1b[31mTerminal requires Python 3 (python3 not found in PATH)\x1b[0m\r\n',
|
|
99
|
+
}));
|
|
100
|
+
ws.send(JSON.stringify({ type: 'terminal:exit', id, code: 1 }));
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
26
103
|
|
|
27
|
-
const proc = spawn(
|
|
104
|
+
const proc = spawn(this._python, ['-u', '-c', PTY_HELPER], {
|
|
28
105
|
cwd,
|
|
29
106
|
env: {
|
|
30
107
|
...process.env,
|
|
31
108
|
TERM: 'xterm-256color',
|
|
32
109
|
COLORTERM: 'truecolor',
|
|
33
110
|
LANG: process.env.LANG || 'en_US.UTF-8',
|
|
34
|
-
|
|
35
|
-
|
|
111
|
+
GROOVE_SHELL: shell,
|
|
112
|
+
COLS: String(cols),
|
|
113
|
+
ROWS: String(rows),
|
|
36
114
|
},
|
|
37
115
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
116
|
});
|
|
@@ -40,14 +118,12 @@ export class TerminalManager {
|
|
|
40
118
|
const session = { proc, ws, id };
|
|
41
119
|
this.sessions.set(id, session);
|
|
42
120
|
|
|
43
|
-
// Relay stdout → WS
|
|
44
121
|
proc.stdout.on('data', (data) => {
|
|
45
122
|
if (ws.readyState === 1) {
|
|
46
123
|
ws.send(JSON.stringify({ type: 'terminal:output', id, data: data.toString('utf8') }));
|
|
47
124
|
}
|
|
48
125
|
});
|
|
49
126
|
|
|
50
|
-
// Relay stderr → WS (merge with stdout)
|
|
51
127
|
proc.stderr.on('data', (data) => {
|
|
52
128
|
if (ws.readyState === 1) {
|
|
53
129
|
ws.send(JSON.stringify({ type: 'terminal:output', id, data: data.toString('utf8') }));
|
|
@@ -71,18 +147,19 @@ export class TerminalManager {
|
|
|
71
147
|
return id;
|
|
72
148
|
}
|
|
73
149
|
|
|
74
|
-
/**
|
|
75
|
-
* Write input to a terminal session.
|
|
76
|
-
*/
|
|
77
150
|
write(id, data) {
|
|
78
151
|
const session = this.sessions.get(id);
|
|
79
152
|
if (!session || !session.proc.stdin.writable) return;
|
|
80
153
|
session.proc.stdin.write(data);
|
|
81
154
|
}
|
|
82
155
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
156
|
+
resize(id, rows, cols) {
|
|
157
|
+
const session = this.sessions.get(id);
|
|
158
|
+
if (!session || !session.proc.stdin.writable) return;
|
|
159
|
+
// Send resize command via the custom escape sequence
|
|
160
|
+
session.proc.stdin.write(`\x1b]7;${rows};${cols}\x07`);
|
|
161
|
+
}
|
|
162
|
+
|
|
86
163
|
kill(id) {
|
|
87
164
|
const session = this.sessions.get(id);
|
|
88
165
|
if (!session) return;
|
|
@@ -95,18 +172,12 @@ export class TerminalManager {
|
|
|
95
172
|
this.sessions.delete(id);
|
|
96
173
|
}
|
|
97
174
|
|
|
98
|
-
/**
|
|
99
|
-
* Kill all sessions (daemon shutdown).
|
|
100
|
-
*/
|
|
101
175
|
killAll() {
|
|
102
176
|
for (const [id] of this.sessions) {
|
|
103
177
|
this.kill(id);
|
|
104
178
|
}
|
|
105
179
|
}
|
|
106
180
|
|
|
107
|
-
/**
|
|
108
|
-
* Clean up sessions for a disconnected WS client.
|
|
109
|
-
*/
|
|
110
181
|
cleanupClient(ws) {
|
|
111
182
|
for (const [id, session] of this.sessions) {
|
|
112
183
|
if (session.ws === ws) {
|
|
@@ -116,14 +187,20 @@ export class TerminalManager {
|
|
|
116
187
|
}
|
|
117
188
|
|
|
118
189
|
_detectShell() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return process.env.SHELL;
|
|
122
|
-
}
|
|
123
|
-
const candidates = ['/bin/zsh', '/bin/bash', '/bin/sh'];
|
|
124
|
-
for (const sh of candidates) {
|
|
190
|
+
if (process.env.SHELL && existsSync(process.env.SHELL)) return process.env.SHELL;
|
|
191
|
+
for (const sh of ['/bin/zsh', '/bin/bash', '/bin/sh']) {
|
|
125
192
|
if (existsSync(sh)) return sh;
|
|
126
193
|
}
|
|
127
194
|
return 'sh';
|
|
128
195
|
}
|
|
196
|
+
|
|
197
|
+
_findPython() {
|
|
198
|
+
for (const cmd of ['python3', 'python']) {
|
|
199
|
+
try {
|
|
200
|
+
const v = execFileSync(cmd, ['--version'], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
201
|
+
if (v.startsWith('Python 3')) return cmd;
|
|
202
|
+
} catch { /* not found */ }
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
129
206
|
}
|