@gricha/perry 0.0.1 → 0.1.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 (37) hide show
  1. package/README.md +25 -33
  2. package/dist/agent/router.js +39 -404
  3. package/dist/agent/web/assets/index-BGbqUzMS.js +104 -0
  4. package/dist/agent/web/assets/index-CHEQQv1U.css +1 -0
  5. package/dist/agent/web/favicon.ico +0 -0
  6. package/dist/agent/web/index.html +3 -3
  7. package/dist/agent/web/logo-192.png +0 -0
  8. package/dist/agent/web/logo-512.png +0 -0
  9. package/dist/agent/web/logo.png +0 -0
  10. package/dist/agent/web/logo.webp +0 -0
  11. package/dist/chat/base-chat-websocket.js +83 -0
  12. package/dist/chat/host-opencode-handler.js +115 -3
  13. package/dist/chat/opencode-handler.js +60 -0
  14. package/dist/chat/opencode-server.js +252 -0
  15. package/dist/chat/opencode-websocket.js +15 -87
  16. package/dist/chat/websocket.js +19 -86
  17. package/dist/client/ws-shell.js +15 -5
  18. package/dist/docker/index.js +41 -1
  19. package/dist/index.js +18 -3
  20. package/dist/sessions/agents/claude.js +86 -0
  21. package/dist/sessions/agents/codex.js +110 -0
  22. package/dist/sessions/agents/index.js +44 -0
  23. package/dist/sessions/agents/opencode.js +168 -0
  24. package/dist/sessions/agents/types.js +1 -0
  25. package/dist/sessions/agents/utils.js +31 -0
  26. package/dist/shared/base-websocket.js +13 -1
  27. package/dist/shared/constants.js +2 -1
  28. package/dist/terminal/base-handler.js +68 -0
  29. package/dist/terminal/handler.js +18 -75
  30. package/dist/terminal/host-handler.js +7 -61
  31. package/dist/terminal/websocket.js +2 -4
  32. package/dist/workspace/manager.js +33 -22
  33. package/dist/workspace/state.js +33 -2
  34. package/package.json +1 -1
  35. package/dist/agent/web/assets/index-9t2sFIJM.js +0 -101
  36. package/dist/agent/web/assets/index-CCFpTruF.css +0 -1
  37. package/dist/agent/web/vite.svg +0 -1
@@ -0,0 +1,86 @@
1
+ import { parseClaudeSessionContent } from '../parser';
2
+ import { decodeClaudeProjectPath, extractFirstUserPrompt, extractClaudeSessionName } from './utils';
3
+ export const claudeProvider = {
4
+ async discoverSessions(containerName, exec) {
5
+ const result = await exec(containerName, [
6
+ 'bash',
7
+ '-c',
8
+ 'find /home/workspace/.claude/projects -name "*.jsonl" -type f ! -name "agent-*.jsonl" -printf "%p\\t%T@\\t%s\\n" 2>/dev/null || true',
9
+ ], { user: 'workspace' });
10
+ const sessions = [];
11
+ if (result.exitCode === 0 && result.stdout.trim()) {
12
+ const lines = result.stdout.trim().split('\n').filter(Boolean);
13
+ for (const line of lines) {
14
+ const parts = line.split('\t');
15
+ if (parts.length >= 3) {
16
+ const file = parts[0];
17
+ const mtime = Math.floor(parseFloat(parts[1]) || 0);
18
+ const size = parseInt(parts[2], 10) || 0;
19
+ if (size === 0)
20
+ continue;
21
+ const id = file.split('/').pop()?.replace('.jsonl', '') || '';
22
+ const projDir = file.split('/').slice(-2, -1)[0] || '';
23
+ const projectPath = decodeClaudeProjectPath(projDir);
24
+ if (!projectPath.startsWith('/workspace') && !projectPath.startsWith('/home/workspace')) {
25
+ continue;
26
+ }
27
+ sessions.push({
28
+ id,
29
+ agentType: 'claude-code',
30
+ mtime,
31
+ projectPath,
32
+ filePath: file,
33
+ });
34
+ }
35
+ }
36
+ }
37
+ return sessions;
38
+ },
39
+ async getSessionDetails(containerName, rawSession, exec) {
40
+ const catResult = await exec(containerName, ['cat', rawSession.filePath], {
41
+ user: 'workspace',
42
+ });
43
+ if (catResult.exitCode !== 0) {
44
+ return null;
45
+ }
46
+ const messages = parseClaudeSessionContent(catResult.stdout).filter((msg) => msg.type !== 'system');
47
+ const firstPrompt = extractFirstUserPrompt(messages);
48
+ const name = extractClaudeSessionName(catResult.stdout);
49
+ if (messages.length === 0) {
50
+ return null;
51
+ }
52
+ return {
53
+ id: rawSession.id,
54
+ name: name || null,
55
+ agentType: rawSession.agentType,
56
+ projectPath: rawSession.projectPath,
57
+ messageCount: messages.length,
58
+ lastActivity: new Date(rawSession.mtime * 1000).toISOString(),
59
+ firstPrompt,
60
+ };
61
+ },
62
+ async getSessionMessages(containerName, sessionId, exec) {
63
+ const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
64
+ const findResult = await exec(containerName, [
65
+ 'bash',
66
+ '-c',
67
+ `find /home/workspace/.claude/projects -name "${safeSessionId}.jsonl" -type f 2>/dev/null | head -1`,
68
+ ], { user: 'workspace' });
69
+ if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
70
+ return null;
71
+ }
72
+ const filePath = findResult.stdout.trim();
73
+ const catResult = await exec(containerName, ['cat', filePath], {
74
+ user: 'workspace',
75
+ });
76
+ if (catResult.exitCode !== 0) {
77
+ return null;
78
+ }
79
+ const messages = parseClaudeSessionContent(catResult.stdout)
80
+ .filter((msg) => msg.type !== 'system')
81
+ .filter((msg) => msg.type === 'tool_use' ||
82
+ msg.type === 'tool_result' ||
83
+ (msg.content && msg.content.trim().length > 0));
84
+ return { id: sessionId, messages };
85
+ },
86
+ };
@@ -0,0 +1,110 @@
1
+ import { extractContent } from './utils';
2
+ function parseCodexMessages(content) {
3
+ const lines = content.split('\n').filter(Boolean);
4
+ let sessionId = null;
5
+ const messages = [];
6
+ if (lines.length > 0) {
7
+ try {
8
+ const meta = JSON.parse(lines[0]);
9
+ if (meta.session_id) {
10
+ sessionId = meta.session_id;
11
+ }
12
+ }
13
+ catch {
14
+ // ignore
15
+ }
16
+ }
17
+ for (let i = 1; i < lines.length; i++) {
18
+ try {
19
+ const event = JSON.parse(lines[i]);
20
+ const role = event.payload?.role || event.payload?.message?.role;
21
+ const content = event.payload?.content || event.payload?.message?.content;
22
+ if (role === 'user' || role === 'assistant') {
23
+ const textContent = extractContent(content);
24
+ messages.push({
25
+ type: role,
26
+ content: textContent || undefined,
27
+ timestamp: event.timestamp ? new Date(event.timestamp).toISOString() : undefined,
28
+ });
29
+ }
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ }
35
+ return { sessionId, messages };
36
+ }
37
+ export const codexProvider = {
38
+ async discoverSessions(containerName, exec) {
39
+ const result = await exec(containerName, [
40
+ 'sh',
41
+ '-c',
42
+ 'find /home/workspace/.codex/sessions -name "rollout-*.jsonl" -type f -printf "%p\\t%T@\\t" -exec wc -l {} \\; 2>/dev/null || true',
43
+ ], { user: 'workspace' });
44
+ const sessions = [];
45
+ if (result.exitCode === 0 && result.stdout.trim()) {
46
+ const lines = result.stdout.trim().split('\n').filter(Boolean);
47
+ for (const line of lines) {
48
+ const parts = line.split('\t');
49
+ if (parts.length >= 2) {
50
+ const file = parts[0];
51
+ const mtime = Math.floor(parseFloat(parts[1]) || 0);
52
+ const id = file.split('/').pop()?.replace('.jsonl', '') || '';
53
+ const projPath = file
54
+ .replace('/home/workspace/.codex/sessions/', '')
55
+ .replace(/\/[^/]+$/, '');
56
+ sessions.push({
57
+ id,
58
+ agentType: 'codex',
59
+ projectPath: projPath,
60
+ mtime,
61
+ filePath: file,
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return sessions;
67
+ },
68
+ async getSessionDetails(containerName, rawSession, exec) {
69
+ const catResult = await exec(containerName, ['cat', rawSession.filePath], {
70
+ user: 'workspace',
71
+ });
72
+ if (catResult.exitCode !== 0) {
73
+ return null;
74
+ }
75
+ const { sessionId, messages } = parseCodexMessages(catResult.stdout);
76
+ const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0)?.content;
77
+ if (messages.length === 0) {
78
+ return null;
79
+ }
80
+ return {
81
+ id: sessionId || rawSession.id,
82
+ name: null,
83
+ agentType: rawSession.agentType,
84
+ projectPath: rawSession.projectPath,
85
+ messageCount: messages.length,
86
+ lastActivity: new Date(rawSession.mtime * 1000).toISOString(),
87
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
88
+ };
89
+ },
90
+ async getSessionMessages(containerName, sessionId, exec) {
91
+ const findResult = await exec(containerName, ['bash', '-c', `find /home/workspace/.codex/sessions -name "*.jsonl" -type f 2>/dev/null`], { user: 'workspace' });
92
+ if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
93
+ return null;
94
+ }
95
+ const files = findResult.stdout.trim().split('\n').filter(Boolean);
96
+ for (const file of files) {
97
+ const catResult = await exec(containerName, ['cat', file], {
98
+ user: 'workspace',
99
+ });
100
+ if (catResult.exitCode !== 0)
101
+ continue;
102
+ const { sessionId: parsedId, messages } = parseCodexMessages(catResult.stdout);
103
+ const fileId = file.split('/').pop()?.replace('.jsonl', '') || '';
104
+ if (parsedId === sessionId || fileId === sessionId) {
105
+ return { id: parsedId || fileId, messages };
106
+ }
107
+ }
108
+ return null;
109
+ },
110
+ };
@@ -0,0 +1,44 @@
1
+ import { claudeProvider } from './claude';
2
+ import { opencodeProvider } from './opencode';
3
+ import { codexProvider } from './codex';
4
+ export { claudeProvider } from './claude';
5
+ export { opencodeProvider } from './opencode';
6
+ export { codexProvider } from './codex';
7
+ const providers = {
8
+ 'claude-code': claudeProvider,
9
+ opencode: opencodeProvider,
10
+ codex: codexProvider,
11
+ };
12
+ export async function discoverAllSessions(containerName, exec) {
13
+ const results = await Promise.all([
14
+ claudeProvider.discoverSessions(containerName, exec),
15
+ opencodeProvider.discoverSessions(containerName, exec),
16
+ codexProvider.discoverSessions(containerName, exec),
17
+ ]);
18
+ return results.flat();
19
+ }
20
+ export async function getSessionDetails(containerName, rawSession, exec) {
21
+ const provider = providers[rawSession.agentType];
22
+ if (!provider)
23
+ return null;
24
+ return provider.getSessionDetails(containerName, rawSession, exec);
25
+ }
26
+ export async function getSessionMessages(containerName, sessionId, agentType, exec) {
27
+ const provider = providers[agentType];
28
+ if (!provider)
29
+ return null;
30
+ const result = await provider.getSessionMessages(containerName, sessionId, exec);
31
+ if (!result)
32
+ return null;
33
+ return { ...result, agentType };
34
+ }
35
+ export async function findSessionMessages(containerName, sessionId, exec) {
36
+ const agentTypes = ['claude-code', 'opencode', 'codex'];
37
+ for (const agentType of agentTypes) {
38
+ const result = await getSessionMessages(containerName, sessionId, agentType, exec);
39
+ if (result && result.messages.length > 0) {
40
+ return result;
41
+ }
42
+ }
43
+ return null;
44
+ }
@@ -0,0 +1,168 @@
1
+ import { extractContent } from './utils';
2
+ export const opencodeProvider = {
3
+ async discoverSessions(containerName, exec) {
4
+ const result = await exec(containerName, [
5
+ 'sh',
6
+ '-c',
7
+ 'find /home/workspace/.local/share/opencode/storage/session -name "ses_*.json" -type f 2>/dev/null || true',
8
+ ], { user: 'workspace' });
9
+ const sessions = [];
10
+ if (result.exitCode === 0 && result.stdout.trim()) {
11
+ const files = result.stdout.trim().split('\n').filter(Boolean);
12
+ const catAll = await exec(containerName, ['sh', '-c', `cat ${files.map((f) => `"${f}"`).join(' ')} 2>/dev/null | jq -s '.'`], { user: 'workspace' });
13
+ if (catAll.exitCode === 0) {
14
+ try {
15
+ const sessionData = JSON.parse(catAll.stdout);
16
+ for (let i = 0; i < sessionData.length; i++) {
17
+ const data = sessionData[i];
18
+ const file = files[i];
19
+ const id = data.id || file.split('/').pop()?.replace('.json', '') || '';
20
+ const mtime = Math.floor((data.time?.updated || 0) / 1000);
21
+ sessions.push({
22
+ id,
23
+ agentType: 'opencode',
24
+ projectPath: data.directory || '',
25
+ mtime,
26
+ name: data.title || undefined,
27
+ filePath: file,
28
+ });
29
+ }
30
+ }
31
+ catch {
32
+ // Skip on parse error
33
+ }
34
+ }
35
+ }
36
+ return sessions;
37
+ },
38
+ async getSessionDetails(containerName, rawSession, exec) {
39
+ const msgDir = `/home/workspace/.local/share/opencode/storage/message/${rawSession.id}`;
40
+ const listMsgsResult = await exec(containerName, ['bash', '-c', `ls -1 "${msgDir}"/msg_*.json 2>/dev/null | sort`], { user: 'workspace' });
41
+ const messages = [];
42
+ if (listMsgsResult.exitCode === 0 && listMsgsResult.stdout.trim()) {
43
+ const msgFiles = listMsgsResult.stdout.trim().split('\n').filter(Boolean);
44
+ for (const msgFile of msgFiles) {
45
+ const msgResult = await exec(containerName, ['cat', msgFile], {
46
+ user: 'workspace',
47
+ });
48
+ if (msgResult.exitCode !== 0)
49
+ continue;
50
+ try {
51
+ const msg = JSON.parse(msgResult.stdout);
52
+ if (msg.role === 'user' || msg.role === 'assistant') {
53
+ const content = extractContent(msg.content);
54
+ messages.push({ type: msg.role, content: content || undefined });
55
+ }
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ }
61
+ }
62
+ const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0)?.content;
63
+ if (messages.length === 0) {
64
+ return null;
65
+ }
66
+ return {
67
+ id: rawSession.id,
68
+ name: rawSession.name || null,
69
+ agentType: rawSession.agentType,
70
+ projectPath: rawSession.projectPath,
71
+ messageCount: messages.length,
72
+ lastActivity: new Date(rawSession.mtime * 1000).toISOString(),
73
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
74
+ };
75
+ },
76
+ async getSessionMessages(containerName, sessionId, exec) {
77
+ const findResult = await exec(containerName, [
78
+ 'bash',
79
+ '-c',
80
+ `find /home/workspace/.local/share/opencode/storage/session -name "${sessionId}.json" -type f 2>/dev/null | head -1`,
81
+ ], { user: 'workspace' });
82
+ if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
83
+ return null;
84
+ }
85
+ const filePath = findResult.stdout.trim();
86
+ const catResult = await exec(containerName, ['cat', filePath], {
87
+ user: 'workspace',
88
+ });
89
+ if (catResult.exitCode !== 0) {
90
+ return null;
91
+ }
92
+ let internalId;
93
+ try {
94
+ const session = JSON.parse(catResult.stdout);
95
+ internalId = session.id;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ const msgDir = `/home/workspace/.local/share/opencode/storage/message/${internalId}`;
101
+ const partDir = `/home/workspace/.local/share/opencode/storage/part`;
102
+ const listMsgsResult = await exec(containerName, ['bash', '-c', `ls -1 "${msgDir}"/msg_*.json 2>/dev/null | sort`], { user: 'workspace' });
103
+ if (listMsgsResult.exitCode !== 0 || !listMsgsResult.stdout.trim()) {
104
+ return { id: sessionId, messages: [] };
105
+ }
106
+ const messages = [];
107
+ const msgFiles = listMsgsResult.stdout.trim().split('\n').filter(Boolean);
108
+ for (const msgFile of msgFiles) {
109
+ const msgResult = await exec(containerName, ['cat', msgFile], {
110
+ user: 'workspace',
111
+ });
112
+ if (msgResult.exitCode !== 0)
113
+ continue;
114
+ try {
115
+ const msg = JSON.parse(msgResult.stdout);
116
+ if (!msg.id || (msg.role !== 'user' && msg.role !== 'assistant'))
117
+ continue;
118
+ const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
119
+ const listPartsResult = await exec(containerName, ['bash', '-c', `ls -1 "${partDir}/${msg.id}"/prt_*.json 2>/dev/null | sort`], { user: 'workspace' });
120
+ if (listPartsResult.exitCode === 0 && listPartsResult.stdout.trim()) {
121
+ const partFiles = listPartsResult.stdout.trim().split('\n').filter(Boolean);
122
+ for (const partFile of partFiles) {
123
+ const partResult = await exec(containerName, ['cat', partFile], {
124
+ user: 'workspace',
125
+ });
126
+ if (partResult.exitCode !== 0)
127
+ continue;
128
+ try {
129
+ const part = JSON.parse(partResult.stdout);
130
+ if (part.type === 'text' && part.text) {
131
+ messages.push({
132
+ type: msg.role,
133
+ content: part.text,
134
+ timestamp,
135
+ });
136
+ }
137
+ else if (part.type === 'tool' && part.tool) {
138
+ messages.push({
139
+ type: 'tool_use',
140
+ content: undefined,
141
+ toolName: part.state?.title || part.tool,
142
+ toolId: part.callID || part.id,
143
+ toolInput: JSON.stringify(part.state?.input, null, 2),
144
+ timestamp,
145
+ });
146
+ if (part.state?.output) {
147
+ messages.push({
148
+ type: 'tool_result',
149
+ content: part.state.output,
150
+ toolId: part.callID || part.id,
151
+ timestamp,
152
+ });
153
+ }
154
+ }
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ }
166
+ return { id: sessionId, messages };
167
+ },
168
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ export function decodeClaudeProjectPath(encoded) {
2
+ return encoded.replace(/-/g, '/');
3
+ }
4
+ export function extractFirstUserPrompt(messages) {
5
+ const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0);
6
+ return firstPrompt?.content ? firstPrompt.content.slice(0, 200) : null;
7
+ }
8
+ export function extractClaudeSessionName(content) {
9
+ const lines = content.split('\n').filter((line) => line.trim());
10
+ for (const line of lines) {
11
+ try {
12
+ const obj = JSON.parse(line);
13
+ if (obj.type === 'system' && obj.subtype === 'session_name') {
14
+ return obj.name || null;
15
+ }
16
+ }
17
+ catch {
18
+ continue;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export function extractContent(content) {
24
+ if (typeof content === 'string')
25
+ return content;
26
+ if (Array.isArray(content)) {
27
+ const text = content.find((c) => c.type === 'text')?.text;
28
+ return typeof text === 'string' ? text : null;
29
+ }
30
+ return null;
31
+ }
@@ -1,4 +1,16 @@
1
- import { WebSocketServer } from 'ws';
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
+ export function safeSend(ws, data) {
3
+ if (ws.readyState !== WebSocket.OPEN) {
4
+ return false;
5
+ }
6
+ try {
7
+ ws.send(data);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
2
14
  export class BaseWebSocketServer {
3
15
  wss;
4
16
  connections = new Map();
@@ -1,7 +1,8 @@
1
1
  export const DEFAULT_AGENT_PORT = 7391;
2
2
  export const SSH_PORT_RANGE_START = 2200;
3
3
  export const SSH_PORT_RANGE_END = 2400;
4
- export const WORKSPACE_IMAGE = 'workspace:latest';
4
+ export const WORKSPACE_IMAGE_LOCAL = 'workspace:latest';
5
+ export const WORKSPACE_IMAGE_REGISTRY = 'ghcr.io/gricha/perry';
5
6
  export const VOLUME_PREFIX = 'workspace-';
6
7
  export const CONTAINER_PREFIX = 'workspace-';
7
8
  export const AGENT_SESSION_PATHS = {
@@ -0,0 +1,68 @@
1
+ export class BaseTerminalSession {
2
+ process = null;
3
+ terminal = null;
4
+ shell;
5
+ size;
6
+ onData = null;
7
+ onExit = null;
8
+ constructor(shell, size) {
9
+ this.shell = shell;
10
+ this.size = size || { cols: 80, rows: 24 };
11
+ }
12
+ start() {
13
+ if (this.process) {
14
+ throw new Error('Terminal session already started');
15
+ }
16
+ const config = this.getSpawnConfig();
17
+ this.process = Bun.spawn(config.command, {
18
+ cwd: config.cwd,
19
+ env: config.env,
20
+ terminal: {
21
+ cols: this.size.cols,
22
+ rows: this.size.rows,
23
+ data: (_terminal, chunk) => {
24
+ if (this.onData) {
25
+ this.onData(Buffer.from(chunk));
26
+ }
27
+ },
28
+ },
29
+ });
30
+ this.terminal = this.process.terminal;
31
+ this.process.exited.then((code) => {
32
+ this.process = null;
33
+ this.terminal = null;
34
+ if (this.onExit) {
35
+ this.onExit(code);
36
+ }
37
+ });
38
+ }
39
+ write(data) {
40
+ if (!this.terminal) {
41
+ return;
42
+ }
43
+ this.terminal.write(data.toString());
44
+ }
45
+ resize(size) {
46
+ this.size = size;
47
+ if (!this.terminal) {
48
+ return;
49
+ }
50
+ this.terminal.resize(size.cols, size.rows);
51
+ }
52
+ setOnData(callback) {
53
+ this.onData = callback;
54
+ }
55
+ setOnExit(callback) {
56
+ this.onExit = callback;
57
+ }
58
+ kill() {
59
+ if (this.process) {
60
+ this.process.kill();
61
+ this.process = null;
62
+ this.terminal = null;
63
+ }
64
+ }
65
+ isRunning() {
66
+ return this.process !== null;
67
+ }
68
+ }
@@ -1,84 +1,27 @@
1
- export class TerminalSession {
2
- process = null;
3
- terminal = null;
1
+ import { BaseTerminalSession } from './base-handler';
2
+ export class TerminalSession extends BaseTerminalSession {
4
3
  containerName;
5
4
  user;
6
- shell;
7
- size;
8
- onData = null;
9
- onExit = null;
10
5
  constructor(options) {
6
+ super(options.shell || '/bin/bash', options.size);
11
7
  this.containerName = options.containerName;
12
8
  this.user = options.user || 'workspace';
13
- this.shell = options.shell || '/bin/bash';
14
- this.size = options.size || { cols: 80, rows: 24 };
15
9
  }
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;
10
+ getSpawnConfig() {
11
+ return {
12
+ command: [
13
+ 'docker',
14
+ 'exec',
15
+ '-it',
16
+ '-u',
17
+ this.user,
18
+ '-e',
19
+ 'TERM=xterm-256color',
20
+ this.containerName,
21
+ this.shell,
22
+ '-l',
23
+ ],
24
+ };
82
25
  }
83
26
  }
84
27
  export function createTerminalSession(options) {