@gricha/perry 0.2.5 → 0.3.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/dist/agent/router.js +156 -0
- package/dist/agent/run.js +161 -99
- package/dist/agent/static.js +32 -0
- package/dist/agent/web/assets/index-CYo-1I5o.css +1 -0
- package/dist/agent/web/assets/index-CZjSxNrg.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/chat/base-claude-session.js +48 -2
- package/dist/chat/opencode-server.js +241 -24
- package/dist/chat/session-monitor.js +186 -0
- package/dist/chat/session-utils.js +155 -0
- package/dist/client/api.js +19 -0
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/claude.js +256 -0
- package/dist/session-manager/adapters/index.js +2 -0
- package/dist/session-manager/adapters/opencode.js +317 -0
- package/dist/session-manager/bun-handler.js +175 -0
- package/dist/session-manager/index.js +3 -0
- package/dist/session-manager/manager.js +302 -0
- package/dist/session-manager/ring-buffer.js +66 -0
- package/dist/session-manager/types.js +1 -0
- package/dist/session-manager/websocket.js +153 -0
- package/dist/shared/base-websocket.js +39 -7
- package/dist/tailscale/index.js +20 -6
- package/dist/terminal/bun-handler.js +151 -0
- package/dist/terminal/handler.js +17 -1
- package/dist/terminal/host-handler.js +19 -1
- package/dist/terminal/websocket.js +5 -0
- package/package.json +3 -3
- package/dist/agent/web/assets/index-0UMxrAK_.js +0 -104
- package/dist/agent/web/assets/index-BwItLEFi.css +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createTerminalSession } from './handler';
|
|
2
|
+
import { createHostTerminalSession } from './host-handler';
|
|
3
|
+
import { isControlMessage } from './types';
|
|
4
|
+
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
5
|
+
export class TerminalHandler {
|
|
6
|
+
connections = new Map();
|
|
7
|
+
getContainerName;
|
|
8
|
+
isHostAccessAllowed;
|
|
9
|
+
getPreferredShell;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.getContainerName = options.getContainerName;
|
|
12
|
+
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
13
|
+
this.getPreferredShell = options.getPreferredShell || (() => undefined);
|
|
14
|
+
}
|
|
15
|
+
handleOpen(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
|
+
const connection = {
|
|
22
|
+
ws,
|
|
23
|
+
session: null,
|
|
24
|
+
workspaceName,
|
|
25
|
+
started: false,
|
|
26
|
+
};
|
|
27
|
+
this.connections.set(ws, connection);
|
|
28
|
+
}
|
|
29
|
+
handleMessage(ws, data) {
|
|
30
|
+
const connection = this.connections.get(ws);
|
|
31
|
+
if (!connection)
|
|
32
|
+
return;
|
|
33
|
+
if (data.startsWith('{')) {
|
|
34
|
+
try {
|
|
35
|
+
const message = JSON.parse(data);
|
|
36
|
+
if (isControlMessage(message)) {
|
|
37
|
+
if (!connection.started) {
|
|
38
|
+
this.startSession(connection, message.cols, message.rows);
|
|
39
|
+
}
|
|
40
|
+
else if (connection.session) {
|
|
41
|
+
connection.session.resize({ cols: message.cols, rows: message.rows });
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Not valid JSON control message, pass through as input
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (connection.session) {
|
|
51
|
+
connection.session.write(data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
handleClose(ws, _code, _reason) {
|
|
55
|
+
const connection = this.connections.get(ws);
|
|
56
|
+
if (connection?.session) {
|
|
57
|
+
connection.session.kill();
|
|
58
|
+
}
|
|
59
|
+
this.connections.delete(ws);
|
|
60
|
+
}
|
|
61
|
+
handleError(ws, error) {
|
|
62
|
+
console.error('WebSocket error:', error);
|
|
63
|
+
const connection = this.connections.get(ws);
|
|
64
|
+
if (connection?.session) {
|
|
65
|
+
connection.session.kill();
|
|
66
|
+
}
|
|
67
|
+
this.connections.delete(ws);
|
|
68
|
+
}
|
|
69
|
+
startSession(connection, cols, rows) {
|
|
70
|
+
if (connection.started)
|
|
71
|
+
return;
|
|
72
|
+
connection.started = true;
|
|
73
|
+
const { ws, workspaceName } = connection;
|
|
74
|
+
const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
|
|
75
|
+
const preferredShell = this.getPreferredShell();
|
|
76
|
+
let session;
|
|
77
|
+
if (isHostMode) {
|
|
78
|
+
session = createHostTerminalSession({
|
|
79
|
+
size: { cols, rows },
|
|
80
|
+
shell: preferredShell,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const containerName = this.getContainerName(workspaceName);
|
|
85
|
+
session = createTerminalSession({
|
|
86
|
+
containerName,
|
|
87
|
+
user: 'workspace',
|
|
88
|
+
size: { cols, rows },
|
|
89
|
+
shell: preferredShell,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
connection.session = session;
|
|
93
|
+
session.setOnData((data) => {
|
|
94
|
+
try {
|
|
95
|
+
ws.send(data);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// WebSocket might be closed
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
session.setOnExit((code) => {
|
|
102
|
+
try {
|
|
103
|
+
ws.close(1000, `Process exited with code ${code}`);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// WebSocket might be closed
|
|
107
|
+
}
|
|
108
|
+
this.connections.delete(ws);
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
session.start();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.error('Failed to start terminal session:', err);
|
|
115
|
+
ws.close(1011, 'Failed to start terminal');
|
|
116
|
+
this.connections.delete(ws);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
getConnectionCount() {
|
|
120
|
+
return this.connections.size;
|
|
121
|
+
}
|
|
122
|
+
getConnectionsForWorkspace(workspaceName) {
|
|
123
|
+
let count = 0;
|
|
124
|
+
for (const conn of this.connections.values()) {
|
|
125
|
+
if (conn.workspaceName === workspaceName) {
|
|
126
|
+
count++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
closeConnectionsForWorkspace(workspaceName) {
|
|
132
|
+
for (const [ws, conn] of this.connections.entries()) {
|
|
133
|
+
if (conn.workspaceName === workspaceName) {
|
|
134
|
+
if (conn.session) {
|
|
135
|
+
conn.session.kill();
|
|
136
|
+
}
|
|
137
|
+
ws.close(1001, 'Workspace stopped');
|
|
138
|
+
this.connections.delete(ws);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
close() {
|
|
143
|
+
for (const [ws, conn] of this.connections.entries()) {
|
|
144
|
+
if (conn.session) {
|
|
145
|
+
conn.session.kill();
|
|
146
|
+
}
|
|
147
|
+
ws.close(1001, 'Server shutting down');
|
|
148
|
+
}
|
|
149
|
+
this.connections.clear();
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/terminal/handler.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
1
2
|
import { BaseTerminalSession } from './base-handler';
|
|
3
|
+
function shellExistsInContainer(containerName, shell) {
|
|
4
|
+
const result = spawnSync('docker', ['exec', containerName, 'test', '-x', shell], {
|
|
5
|
+
timeout: 5000,
|
|
6
|
+
});
|
|
7
|
+
return result.status === 0;
|
|
8
|
+
}
|
|
9
|
+
function resolveShell(containerName, preferred) {
|
|
10
|
+
const fallback = '/bin/bash';
|
|
11
|
+
if (!preferred)
|
|
12
|
+
return fallback;
|
|
13
|
+
if (shellExistsInContainer(containerName, preferred))
|
|
14
|
+
return preferred;
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
2
17
|
export class TerminalSession extends BaseTerminalSession {
|
|
3
18
|
containerName;
|
|
4
19
|
user;
|
|
5
20
|
constructor(options) {
|
|
6
|
-
|
|
21
|
+
const shell = resolveShell(options.containerName, options.shell);
|
|
22
|
+
super(shell, options.size);
|
|
7
23
|
this.containerName = options.containerName;
|
|
8
24
|
this.user = options.user || 'workspace';
|
|
9
25
|
}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import { accessSync, constants } from 'fs';
|
|
1
2
|
import { BaseTerminalSession } from './base-handler';
|
|
2
3
|
import { homedir } from 'os';
|
|
4
|
+
function shellExistsOnHost(shell) {
|
|
5
|
+
try {
|
|
6
|
+
accessSync(shell, constants.X_OK);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function resolveHostShell(preferred) {
|
|
14
|
+
const fallback = '/bin/bash';
|
|
15
|
+
if (!preferred)
|
|
16
|
+
return process.env.SHELL || fallback;
|
|
17
|
+
if (shellExistsOnHost(preferred))
|
|
18
|
+
return preferred;
|
|
19
|
+
return process.env.SHELL || fallback;
|
|
20
|
+
}
|
|
3
21
|
export class HostTerminalSession extends BaseTerminalSession {
|
|
4
22
|
workDir;
|
|
5
23
|
constructor(options = {}) {
|
|
6
|
-
super(options.shell
|
|
24
|
+
super(resolveHostShell(options.shell), options.size);
|
|
7
25
|
this.workDir = options.workDir || homedir();
|
|
8
26
|
}
|
|
9
27
|
getSpawnConfig() {
|
|
@@ -7,10 +7,12 @@ import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
|
7
7
|
export class TerminalWebSocketServer extends BaseWebSocketServer {
|
|
8
8
|
getContainerName;
|
|
9
9
|
isHostAccessAllowed;
|
|
10
|
+
getPreferredShell;
|
|
10
11
|
constructor(options) {
|
|
11
12
|
super({ isWorkspaceRunning: options.isWorkspaceRunning });
|
|
12
13
|
this.getContainerName = options.getContainerName;
|
|
13
14
|
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
15
|
+
this.getPreferredShell = options.getPreferredShell || (() => undefined);
|
|
14
16
|
}
|
|
15
17
|
handleConnection(ws, workspaceName) {
|
|
16
18
|
const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
|
|
@@ -24,9 +26,11 @@ export class TerminalWebSocketServer extends BaseWebSocketServer {
|
|
|
24
26
|
if (started)
|
|
25
27
|
return;
|
|
26
28
|
started = true;
|
|
29
|
+
const preferredShell = this.getPreferredShell();
|
|
27
30
|
if (isHostMode) {
|
|
28
31
|
session = createHostTerminalSession({
|
|
29
32
|
size: { cols, rows },
|
|
33
|
+
shell: preferredShell,
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
36
|
else {
|
|
@@ -35,6 +39,7 @@ export class TerminalWebSocketServer extends BaseWebSocketServer {
|
|
|
35
39
|
containerName,
|
|
36
40
|
user: 'workspace',
|
|
37
41
|
size: { cols, rows },
|
|
42
|
+
shell: preferredShell,
|
|
38
43
|
});
|
|
39
44
|
}
|
|
40
45
|
const connection = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gricha/perry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Self-contained CLI for spinning up Docker-in-Docker development environments with SSH and proxy helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"test:web": "playwright test",
|
|
19
19
|
"test:tui": "vitest run --config vitest.tui.config.js",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
|
-
"lint": "oxlint --deny-warnings src/",
|
|
22
|
-
"lint:fix": "oxlint --fix src/",
|
|
21
|
+
"lint": "oxlint --deny-warnings src/ mobile/src/",
|
|
22
|
+
"lint:fix": "oxlint --fix src/ mobile/src/",
|
|
23
23
|
"format": "oxfmt --write src/ test/",
|
|
24
24
|
"format:check": "oxfmt --check src/ test/",
|
|
25
25
|
"check": "bun run lint && bun run format:check && bun x tsc --noEmit",
|