@gricha/perry 0.0.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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/router.js +1017 -0
- package/dist/agent/run.js +182 -0
- package/dist/agent/static.js +58 -0
- package/dist/agent/systemd.js +229 -0
- package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
- package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
- package/dist/agent/web/index.html +14 -0
- package/dist/agent/web/vite.svg +1 -0
- package/dist/chat/handler.js +174 -0
- package/dist/chat/host-handler.js +170 -0
- package/dist/chat/host-opencode-handler.js +169 -0
- package/dist/chat/index.js +2 -0
- package/dist/chat/opencode-handler.js +177 -0
- package/dist/chat/opencode-websocket.js +95 -0
- package/dist/chat/websocket.js +100 -0
- package/dist/client/api.js +138 -0
- package/dist/client/config.js +34 -0
- package/dist/client/docker-proxy.js +103 -0
- package/dist/client/index.js +4 -0
- package/dist/client/proxy.js +96 -0
- package/dist/client/shell.js +71 -0
- package/dist/client/ws-shell.js +120 -0
- package/dist/config/loader.js +59 -0
- package/dist/docker/index.js +372 -0
- package/dist/docker/types.js +1 -0
- package/dist/index.js +475 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/metadata.js +55 -0
- package/dist/sessions/parser.js +553 -0
- package/dist/sessions/types.js +1 -0
- package/dist/shared/base-websocket.js +51 -0
- package/dist/shared/client-types.js +1 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/types.js +5 -0
- package/dist/terminal/handler.js +86 -0
- package/dist/terminal/host-handler.js +76 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/types.js +8 -0
- package/dist/terminal/websocket.js +115 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/manager.js +475 -0
- package/dist/workspace/state.js +66 -0
- package/dist/workspace/types.js +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export class TerminalSession {
|
|
2
|
+
process = null;
|
|
3
|
+
terminal = null;
|
|
4
|
+
containerName;
|
|
5
|
+
user;
|
|
6
|
+
shell;
|
|
7
|
+
size;
|
|
8
|
+
onData = null;
|
|
9
|
+
onExit = null;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.containerName = options.containerName;
|
|
12
|
+
this.user = options.user || 'workspace';
|
|
13
|
+
this.shell = options.shell || '/bin/bash';
|
|
14
|
+
this.size = options.size || { cols: 80, rows: 24 };
|
|
15
|
+
}
|
|
16
|
+
start() {
|
|
17
|
+
if (this.process) {
|
|
18
|
+
throw new Error('Terminal session already started');
|
|
19
|
+
}
|
|
20
|
+
const args = [
|
|
21
|
+
'exec',
|
|
22
|
+
'-it',
|
|
23
|
+
'-u',
|
|
24
|
+
this.user,
|
|
25
|
+
'-e',
|
|
26
|
+
`TERM=xterm-256color`,
|
|
27
|
+
this.containerName,
|
|
28
|
+
this.shell,
|
|
29
|
+
'-l',
|
|
30
|
+
];
|
|
31
|
+
this.process = Bun.spawn(['docker', ...args], {
|
|
32
|
+
terminal: {
|
|
33
|
+
cols: this.size.cols,
|
|
34
|
+
rows: this.size.rows,
|
|
35
|
+
data: (_terminal, chunk) => {
|
|
36
|
+
if (this.onData) {
|
|
37
|
+
this.onData(Buffer.from(chunk));
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
this.terminal = this.process.terminal;
|
|
43
|
+
this.process.exited.then((code) => {
|
|
44
|
+
this.process = null;
|
|
45
|
+
this.terminal = null;
|
|
46
|
+
if (this.onExit) {
|
|
47
|
+
this.onExit(code);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
write(data) {
|
|
52
|
+
if (!this.terminal) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.terminal.write(data.toString());
|
|
56
|
+
}
|
|
57
|
+
resize(size) {
|
|
58
|
+
this.size = size;
|
|
59
|
+
console.log('[terminal] Resize request:', size.cols, 'x', size.rows);
|
|
60
|
+
if (!this.terminal) {
|
|
61
|
+
console.log('[terminal] No terminal yet, storing size for later');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.terminal.resize(size.cols, size.rows);
|
|
65
|
+
console.log('[terminal] Resized terminal to', size.cols, 'x', size.rows);
|
|
66
|
+
}
|
|
67
|
+
setOnData(callback) {
|
|
68
|
+
this.onData = callback;
|
|
69
|
+
}
|
|
70
|
+
setOnExit(callback) {
|
|
71
|
+
this.onExit = callback;
|
|
72
|
+
}
|
|
73
|
+
kill() {
|
|
74
|
+
if (this.process) {
|
|
75
|
+
this.process.kill();
|
|
76
|
+
this.process = null;
|
|
77
|
+
this.terminal = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
isRunning() {
|
|
81
|
+
return this.process !== null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function createTerminalSession(options) {
|
|
85
|
+
return new TerminalSession(options);
|
|
86
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
export class HostTerminalSession {
|
|
3
|
+
process = null;
|
|
4
|
+
terminal = null;
|
|
5
|
+
shell;
|
|
6
|
+
size;
|
|
7
|
+
workDir;
|
|
8
|
+
onData = null;
|
|
9
|
+
onExit = null;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.shell = options.shell || process.env.SHELL || '/bin/bash';
|
|
12
|
+
this.size = options.size || { cols: 80, rows: 24 };
|
|
13
|
+
this.workDir = options.workDir || homedir();
|
|
14
|
+
}
|
|
15
|
+
start() {
|
|
16
|
+
if (this.process) {
|
|
17
|
+
throw new Error('Terminal session already started');
|
|
18
|
+
}
|
|
19
|
+
this.process = Bun.spawn([this.shell, '-l'], {
|
|
20
|
+
cwd: this.workDir,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
TERM: 'xterm-256color',
|
|
24
|
+
},
|
|
25
|
+
terminal: {
|
|
26
|
+
cols: this.size.cols,
|
|
27
|
+
rows: this.size.rows,
|
|
28
|
+
data: (_terminal, chunk) => {
|
|
29
|
+
if (this.onData) {
|
|
30
|
+
this.onData(Buffer.from(chunk));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
this.terminal = this.process.terminal;
|
|
36
|
+
this.process.exited.then((code) => {
|
|
37
|
+
this.process = null;
|
|
38
|
+
this.terminal = null;
|
|
39
|
+
if (this.onExit) {
|
|
40
|
+
this.onExit(code);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
write(data) {
|
|
45
|
+
if (!this.terminal) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.terminal.write(data.toString());
|
|
49
|
+
}
|
|
50
|
+
resize(size) {
|
|
51
|
+
this.size = size;
|
|
52
|
+
if (!this.terminal) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.terminal.resize(size.cols, size.rows);
|
|
56
|
+
}
|
|
57
|
+
setOnData(callback) {
|
|
58
|
+
this.onData = callback;
|
|
59
|
+
}
|
|
60
|
+
setOnExit(callback) {
|
|
61
|
+
this.onExit = callback;
|
|
62
|
+
}
|
|
63
|
+
kill() {
|
|
64
|
+
if (this.process) {
|
|
65
|
+
this.process.kill();
|
|
66
|
+
this.process = null;
|
|
67
|
+
this.terminal = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
isRunning() {
|
|
71
|
+
return this.process !== null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function createHostTerminalSession(options) {
|
|
75
|
+
return new HostTerminalSession(options);
|
|
76
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { BaseWebSocketServer } from '../shared/base-websocket';
|
|
3
|
+
import { createTerminalSession } from './handler';
|
|
4
|
+
import { createHostTerminalSession } from './host-handler';
|
|
5
|
+
import { isControlMessage } from './types';
|
|
6
|
+
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
7
|
+
export class TerminalWebSocketServer extends BaseWebSocketServer {
|
|
8
|
+
getContainerName;
|
|
9
|
+
isHostAccessAllowed;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super({ isWorkspaceRunning: options.isWorkspaceRunning });
|
|
12
|
+
this.getContainerName = options.getContainerName;
|
|
13
|
+
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
14
|
+
}
|
|
15
|
+
handleConnection(ws, workspaceName) {
|
|
16
|
+
const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
|
|
17
|
+
if (isHostMode && !this.isHostAccessAllowed()) {
|
|
18
|
+
ws.close(4003, 'Host access is disabled');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let session = null;
|
|
22
|
+
let started = false;
|
|
23
|
+
const startSession = (cols, rows) => {
|
|
24
|
+
if (started)
|
|
25
|
+
return;
|
|
26
|
+
started = true;
|
|
27
|
+
if (isHostMode) {
|
|
28
|
+
session = createHostTerminalSession({
|
|
29
|
+
size: { cols, rows },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const containerName = this.getContainerName(workspaceName);
|
|
34
|
+
session = createTerminalSession({
|
|
35
|
+
containerName,
|
|
36
|
+
user: 'workspace',
|
|
37
|
+
size: { cols, rows },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const connection = {
|
|
41
|
+
ws,
|
|
42
|
+
session,
|
|
43
|
+
workspaceName,
|
|
44
|
+
};
|
|
45
|
+
this.connections.set(ws, connection);
|
|
46
|
+
session.setOnData((data) => {
|
|
47
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
48
|
+
ws.send(data);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
session.setOnExit((code) => {
|
|
52
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
53
|
+
ws.close(1000, `Process exited with code ${code}`);
|
|
54
|
+
}
|
|
55
|
+
this.connections.delete(ws);
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
session.start();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error('Failed to start terminal session:', err);
|
|
62
|
+
ws.close(1011, 'Failed to start terminal');
|
|
63
|
+
this.connections.delete(ws);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
ws.on('message', (data) => {
|
|
67
|
+
const str = typeof data === 'string' ? data : data.toString();
|
|
68
|
+
if (str.startsWith('{')) {
|
|
69
|
+
try {
|
|
70
|
+
const message = JSON.parse(str);
|
|
71
|
+
if (isControlMessage(message)) {
|
|
72
|
+
if (!started) {
|
|
73
|
+
startSession(message.cols, message.rows);
|
|
74
|
+
}
|
|
75
|
+
else if (session) {
|
|
76
|
+
session.resize({ cols: message.cols, rows: message.rows });
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Not valid JSON control message, pass through as input
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (session) {
|
|
86
|
+
session.write(data);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
ws.on('close', () => {
|
|
90
|
+
if (session) {
|
|
91
|
+
session.kill();
|
|
92
|
+
}
|
|
93
|
+
this.connections.delete(ws);
|
|
94
|
+
});
|
|
95
|
+
ws.on('error', (err) => {
|
|
96
|
+
console.error('WebSocket error:', err);
|
|
97
|
+
if (session) {
|
|
98
|
+
session.kill();
|
|
99
|
+
}
|
|
100
|
+
this.connections.delete(ws);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
cleanupConnection(connection) {
|
|
104
|
+
connection.session.kill();
|
|
105
|
+
}
|
|
106
|
+
getConnectionsForWorkspace(workspaceName) {
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const conn of this.connections.values()) {
|
|
109
|
+
if (conn.workspaceName === workspaceName) {
|
|
110
|
+
count++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
}
|