@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/agent/index.js +6 -0
  4. package/dist/agent/router.js +1017 -0
  5. package/dist/agent/run.js +182 -0
  6. package/dist/agent/static.js +58 -0
  7. package/dist/agent/systemd.js +229 -0
  8. package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
  9. package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
  10. package/dist/agent/web/index.html +14 -0
  11. package/dist/agent/web/vite.svg +1 -0
  12. package/dist/chat/handler.js +174 -0
  13. package/dist/chat/host-handler.js +170 -0
  14. package/dist/chat/host-opencode-handler.js +169 -0
  15. package/dist/chat/index.js +2 -0
  16. package/dist/chat/opencode-handler.js +177 -0
  17. package/dist/chat/opencode-websocket.js +95 -0
  18. package/dist/chat/websocket.js +100 -0
  19. package/dist/client/api.js +138 -0
  20. package/dist/client/config.js +34 -0
  21. package/dist/client/docker-proxy.js +103 -0
  22. package/dist/client/index.js +4 -0
  23. package/dist/client/proxy.js +96 -0
  24. package/dist/client/shell.js +71 -0
  25. package/dist/client/ws-shell.js +120 -0
  26. package/dist/config/loader.js +59 -0
  27. package/dist/docker/index.js +372 -0
  28. package/dist/docker/types.js +1 -0
  29. package/dist/index.js +475 -0
  30. package/dist/sessions/index.js +2 -0
  31. package/dist/sessions/metadata.js +55 -0
  32. package/dist/sessions/parser.js +553 -0
  33. package/dist/sessions/types.js +1 -0
  34. package/dist/shared/base-websocket.js +51 -0
  35. package/dist/shared/client-types.js +1 -0
  36. package/dist/shared/constants.js +11 -0
  37. package/dist/shared/types.js +5 -0
  38. package/dist/terminal/handler.js +86 -0
  39. package/dist/terminal/host-handler.js +76 -0
  40. package/dist/terminal/index.js +3 -0
  41. package/dist/terminal/types.js +8 -0
  42. package/dist/terminal/websocket.js +115 -0
  43. package/dist/workspace/index.js +3 -0
  44. package/dist/workspace/manager.js +475 -0
  45. package/dist/workspace/state.js +66 -0
  46. package/dist/workspace/types.js +1 -0
  47. package/package.json +68 -0
@@ -0,0 +1,34 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { DEFAULT_CONFIG_DIR, CLIENT_CONFIG_FILE } from '../shared/types';
4
+ function getClientConfigPath(configDir) {
5
+ return path.join(configDir || DEFAULT_CONFIG_DIR, CLIENT_CONFIG_FILE);
6
+ }
7
+ export async function loadClientConfig(configDir) {
8
+ const configPath = getClientConfigPath(configDir);
9
+ try {
10
+ const content = await fs.readFile(configPath, 'utf-8');
11
+ return JSON.parse(content);
12
+ }
13
+ catch (err) {
14
+ if (err.code === 'ENOENT') {
15
+ return null;
16
+ }
17
+ throw err;
18
+ }
19
+ }
20
+ export async function saveClientConfig(config, configDir) {
21
+ const configPath = getClientConfigPath(configDir);
22
+ const dir = path.dirname(configPath);
23
+ await fs.mkdir(dir, { recursive: true });
24
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
25
+ }
26
+ export async function getWorker(configDir) {
27
+ const config = await loadClientConfig(configDir);
28
+ return config?.worker || null;
29
+ }
30
+ export async function setWorker(worker, configDir) {
31
+ const config = (await loadClientConfig(configDir)) || { worker: '' };
32
+ config.worker = worker;
33
+ await saveClientConfig(config, configDir);
34
+ }
@@ -0,0 +1,103 @@
1
+ export async function startDockerProxy(options) {
2
+ const { containerIp, forwards, onConnect, onError } = options;
3
+ const servers = [];
4
+ for (const fwd of forwards) {
5
+ try {
6
+ const server = Bun.listen({
7
+ hostname: '0.0.0.0',
8
+ port: fwd.localPort,
9
+ socket: {
10
+ open(socket) {
11
+ socket.data = { upstream: null, buffer: [] };
12
+ Bun.connect({
13
+ hostname: containerIp,
14
+ port: fwd.remotePort,
15
+ socket: {
16
+ open(upstream) {
17
+ socket.data.upstream = upstream;
18
+ upstream.data = { downstream: socket };
19
+ for (const chunk of socket.data.buffer) {
20
+ upstream.write(chunk);
21
+ }
22
+ socket.data.buffer = [];
23
+ },
24
+ data(upstream, data) {
25
+ const downstream = upstream.data?.downstream;
26
+ if (downstream) {
27
+ downstream.write(data);
28
+ }
29
+ },
30
+ close(upstream) {
31
+ const downstream = upstream.data?.downstream;
32
+ if (downstream) {
33
+ downstream.end();
34
+ }
35
+ },
36
+ error(upstream, err) {
37
+ const downstream = upstream.data?.downstream;
38
+ if (downstream) {
39
+ downstream.end();
40
+ }
41
+ if (onError)
42
+ onError(err);
43
+ },
44
+ },
45
+ }).catch((err) => {
46
+ socket.end();
47
+ if (onError)
48
+ onError(err);
49
+ });
50
+ },
51
+ data(socket, data) {
52
+ if (socket.data.upstream) {
53
+ socket.data.upstream.write(data);
54
+ }
55
+ else {
56
+ socket.data.buffer.push(Buffer.from(data));
57
+ }
58
+ },
59
+ close(socket) {
60
+ if (socket.data.upstream) {
61
+ socket.data.upstream.end();
62
+ }
63
+ },
64
+ error(socket, err) {
65
+ if (socket.data.upstream) {
66
+ socket.data.upstream.end();
67
+ }
68
+ if (onError)
69
+ onError(err);
70
+ },
71
+ },
72
+ });
73
+ servers.push(server);
74
+ if (onConnect)
75
+ onConnect(fwd.localPort);
76
+ }
77
+ catch (err) {
78
+ if (onError)
79
+ onError(err);
80
+ }
81
+ }
82
+ return () => {
83
+ for (const server of servers) {
84
+ server.stop();
85
+ }
86
+ };
87
+ }
88
+ export function formatPortForwards(forwards) {
89
+ return forwards
90
+ .map((f) => f.localPort === f.remotePort ? String(f.localPort) : `${f.localPort}:${f.remotePort}`)
91
+ .join(', ');
92
+ }
93
+ export function parsePortForward(spec) {
94
+ if (spec.includes(':')) {
95
+ const [local, remote] = spec.split(':');
96
+ return {
97
+ localPort: parseInt(local, 10),
98
+ remotePort: parseInt(remote, 10),
99
+ };
100
+ }
101
+ const port = parseInt(spec, 10);
102
+ return { localPort: port, remotePort: port };
103
+ }
@@ -0,0 +1,4 @@
1
+ export * from './api';
2
+ export * from './config';
3
+ export * from './shell';
4
+ export * from './proxy';
@@ -0,0 +1,96 @@
1
+ import { spawn } from 'child_process';
2
+ export function parsePortForward(spec) {
3
+ if (spec.includes(':')) {
4
+ const [local, remote] = spec.split(':');
5
+ return {
6
+ localPort: parseInt(local, 10),
7
+ remotePort: parseInt(remote, 10),
8
+ };
9
+ }
10
+ const port = parseInt(spec, 10);
11
+ return { localPort: port, remotePort: port };
12
+ }
13
+ export async function startProxy(options) {
14
+ const { worker, sshPort, forwards, user = 'workspace', onConnect, onDisconnect, onError, } = options;
15
+ const workerHost = worker.includes(':') ? worker.split(':')[0] : worker;
16
+ return new Promise((resolve, reject) => {
17
+ const sshArgs = [
18
+ '-N',
19
+ '-o',
20
+ 'StrictHostKeyChecking=no',
21
+ '-o',
22
+ 'UserKnownHostsFile=/dev/null',
23
+ '-o',
24
+ 'LogLevel=ERROR',
25
+ '-o',
26
+ 'ServerAliveInterval=60',
27
+ '-o',
28
+ 'ServerAliveCountMax=3',
29
+ '-p',
30
+ String(sshPort),
31
+ ];
32
+ for (const fwd of forwards) {
33
+ sshArgs.push('-L', `${fwd.localPort}:localhost:${fwd.remotePort}`);
34
+ }
35
+ sshArgs.push(`${user}@${workerHost}`);
36
+ const proc = spawn('ssh', sshArgs, {
37
+ stdio: ['ignore', 'pipe', 'pipe'],
38
+ });
39
+ let connected = false;
40
+ let errorOutput = '';
41
+ proc.stderr?.on('data', (data) => {
42
+ errorOutput += data.toString();
43
+ });
44
+ const connectionTimeout = setTimeout(() => {
45
+ if (!connected) {
46
+ proc.kill();
47
+ reject(new Error('SSH connection timeout'));
48
+ }
49
+ }, 30000);
50
+ setTimeout(() => {
51
+ if (proc.exitCode === null) {
52
+ connected = true;
53
+ clearTimeout(connectionTimeout);
54
+ if (onConnect) {
55
+ onConnect();
56
+ }
57
+ }
58
+ }, 2000);
59
+ proc.on('error', (err) => {
60
+ clearTimeout(connectionTimeout);
61
+ if (!connected) {
62
+ reject(err);
63
+ }
64
+ else if (onError) {
65
+ onError(err);
66
+ }
67
+ });
68
+ const cleanup = () => {
69
+ process.removeListener('SIGINT', handleSignal);
70
+ process.removeListener('SIGTERM', handleSignal);
71
+ };
72
+ const handleSignal = () => {
73
+ proc.kill('SIGTERM');
74
+ };
75
+ process.on('SIGINT', handleSignal);
76
+ process.on('SIGTERM', handleSignal);
77
+ proc.on('close', (code) => {
78
+ clearTimeout(connectionTimeout);
79
+ cleanup();
80
+ if (!connected) {
81
+ reject(new Error(`SSH failed: ${errorOutput || `exit code ${code}`}`));
82
+ }
83
+ else {
84
+ if (onDisconnect) {
85
+ onDisconnect(code || 0);
86
+ }
87
+ resolve();
88
+ }
89
+ });
90
+ });
91
+ }
92
+ export function formatPortForwards(forwards) {
93
+ return forwards
94
+ .map((f) => f.localPort === f.remotePort ? String(f.localPort) : `${f.localPort}:${f.remotePort}`)
95
+ .join(', ');
96
+ }
@@ -0,0 +1,71 @@
1
+ import { spawn } from 'child_process';
2
+ function extractHost(worker) {
3
+ let host = worker;
4
+ if (host.startsWith('http://')) {
5
+ host = host.slice(7);
6
+ }
7
+ else if (host.startsWith('https://')) {
8
+ host = host.slice(8);
9
+ }
10
+ if (host.includes(':')) {
11
+ host = host.split(':')[0];
12
+ }
13
+ return host;
14
+ }
15
+ export async function openSSHShell(options) {
16
+ const { worker, sshPort, user = 'workspace', onConnect, onDisconnect, onError } = options;
17
+ const host = extractHost(worker);
18
+ return new Promise((resolve, reject) => {
19
+ const sshArgs = [
20
+ '-o',
21
+ 'StrictHostKeyChecking=no',
22
+ '-o',
23
+ 'UserKnownHostsFile=/dev/null',
24
+ '-o',
25
+ 'LogLevel=ERROR',
26
+ '-p',
27
+ String(sshPort),
28
+ `${user}@${host}`,
29
+ ];
30
+ const proc = spawn('ssh', sshArgs, {
31
+ stdio: 'inherit',
32
+ });
33
+ let connected = false;
34
+ const connectionTimeout = setTimeout(() => {
35
+ if (!connected) {
36
+ proc.kill();
37
+ reject(new Error('SSH connection timeout'));
38
+ }
39
+ }, 30000);
40
+ setTimeout(() => {
41
+ if (proc.exitCode === null) {
42
+ connected = true;
43
+ clearTimeout(connectionTimeout);
44
+ if (onConnect) {
45
+ onConnect();
46
+ }
47
+ }
48
+ }, 500);
49
+ proc.on('error', (err) => {
50
+ clearTimeout(connectionTimeout);
51
+ if (!connected) {
52
+ reject(err);
53
+ }
54
+ else if (onError) {
55
+ onError(err);
56
+ }
57
+ });
58
+ proc.on('close', (code) => {
59
+ clearTimeout(connectionTimeout);
60
+ if (!connected && code !== 0) {
61
+ reject(new Error(`SSH failed with exit code ${code}`));
62
+ }
63
+ else {
64
+ if (onDisconnect) {
65
+ onDisconnect(code || 0);
66
+ }
67
+ resolve();
68
+ }
69
+ });
70
+ });
71
+ }
@@ -0,0 +1,120 @@
1
+ import WebSocket from 'ws';
2
+ import { spawn } from 'child_process';
3
+ export function isLocalWorker(worker) {
4
+ const host = worker
5
+ .replace(/^https?:\/\//, '')
6
+ .split(':')[0]
7
+ .toLowerCase();
8
+ return host === 'localhost' || host === '127.0.0.1';
9
+ }
10
+ export async function openDockerExec(options) {
11
+ const { containerName, onConnect, onDisconnect, onError } = options;
12
+ return new Promise((resolve, reject) => {
13
+ const args = [
14
+ 'exec',
15
+ '-it',
16
+ '-u',
17
+ 'workspace',
18
+ '-e',
19
+ 'TERM=xterm-256color',
20
+ containerName,
21
+ '/bin/bash',
22
+ '-l',
23
+ ];
24
+ const proc = spawn('docker', args, {
25
+ stdio: 'inherit',
26
+ });
27
+ let connected = false;
28
+ setTimeout(() => {
29
+ if (proc.exitCode === null) {
30
+ connected = true;
31
+ if (onConnect)
32
+ onConnect();
33
+ }
34
+ }, 100);
35
+ proc.on('error', (err) => {
36
+ if (!connected) {
37
+ reject(err);
38
+ }
39
+ else if (onError) {
40
+ onError(err);
41
+ }
42
+ });
43
+ proc.on('close', (code) => {
44
+ if (onDisconnect)
45
+ onDisconnect(code || 0);
46
+ resolve();
47
+ });
48
+ });
49
+ }
50
+ export async function openWSShell(options) {
51
+ const { url, onConnect, onDisconnect, onError } = options;
52
+ return new Promise((resolve, reject) => {
53
+ const ws = new WebSocket(url);
54
+ let connected = false;
55
+ const stdin = process.stdin;
56
+ const stdout = process.stdout;
57
+ const sendResize = () => {
58
+ if (ws.readyState === WebSocket.OPEN && stdout.columns && stdout.rows) {
59
+ ws.send(JSON.stringify({ type: 'resize', cols: stdout.columns, rows: stdout.rows }));
60
+ }
61
+ };
62
+ ws.on('open', () => {
63
+ connected = true;
64
+ if (stdin.isTTY) {
65
+ stdin.setRawMode(true);
66
+ }
67
+ stdin.resume();
68
+ sendResize();
69
+ if (onConnect) {
70
+ onConnect();
71
+ }
72
+ });
73
+ ws.on('message', (data) => {
74
+ const text = typeof data === 'string' ? data : data.toString();
75
+ stdout.write(text);
76
+ });
77
+ ws.on('close', (code) => {
78
+ if (stdin.isTTY) {
79
+ stdin.setRawMode(false);
80
+ }
81
+ stdin.pause();
82
+ if (onDisconnect) {
83
+ onDisconnect(code);
84
+ }
85
+ resolve();
86
+ });
87
+ ws.on('error', (err) => {
88
+ if (!connected) {
89
+ reject(err);
90
+ }
91
+ else if (onError) {
92
+ onError(err);
93
+ }
94
+ });
95
+ stdin.on('data', (data) => {
96
+ if (ws.readyState === WebSocket.OPEN) {
97
+ ws.send(data);
98
+ }
99
+ });
100
+ stdout.on('resize', sendResize);
101
+ const cleanup = () => {
102
+ stdout.removeListener('resize', sendResize);
103
+ if (stdin.isTTY) {
104
+ stdin.setRawMode(false);
105
+ }
106
+ ws.close();
107
+ };
108
+ process.on('SIGINT', cleanup);
109
+ process.on('SIGTERM', cleanup);
110
+ });
111
+ }
112
+ export function getTerminalWSUrl(worker, workspaceName) {
113
+ let base = worker;
114
+ if (!base.startsWith('http://') && !base.startsWith('https://')) {
115
+ base = `http://${base}`;
116
+ }
117
+ const wsProtocol = base.startsWith('https://') ? 'wss://' : 'ws://';
118
+ const host = base.replace(/^https?:\/\//, '');
119
+ return `${wsProtocol}${host}/rpc/terminal/${encodeURIComponent(workspaceName)}`;
120
+ }
@@ -0,0 +1,59 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { DEFAULT_CONFIG_DIR, CONFIG_FILE } from '../shared/types';
4
+ import { DEFAULT_AGENT_PORT } from '../shared/constants';
5
+ export function getConfigDir(configDir) {
6
+ return configDir || process.env.WS_CONFIG_DIR || DEFAULT_CONFIG_DIR;
7
+ }
8
+ export async function ensureConfigDir(configDir) {
9
+ const dir = getConfigDir(configDir);
10
+ await fs.mkdir(dir, { recursive: true });
11
+ }
12
+ export function createDefaultAgentConfig() {
13
+ return {
14
+ port: DEFAULT_AGENT_PORT,
15
+ credentials: {
16
+ env: {},
17
+ files: {},
18
+ },
19
+ scripts: {},
20
+ agents: {},
21
+ allowHostAccess: true,
22
+ };
23
+ }
24
+ export async function loadAgentConfig(configDir) {
25
+ const dir = getConfigDir(configDir);
26
+ const configPath = path.join(dir, CONFIG_FILE);
27
+ try {
28
+ const content = await fs.readFile(configPath, 'utf-8');
29
+ const config = JSON.parse(content);
30
+ return {
31
+ port: config.port || DEFAULT_AGENT_PORT,
32
+ credentials: {
33
+ env: config.credentials?.env || {},
34
+ files: config.credentials?.files || {},
35
+ },
36
+ scripts: config.scripts || {},
37
+ agents: config.agents || {},
38
+ allowHostAccess: config.allowHostAccess ?? true,
39
+ };
40
+ }
41
+ catch (err) {
42
+ if (err.code === 'ENOENT') {
43
+ return createDefaultAgentConfig();
44
+ }
45
+ throw err;
46
+ }
47
+ }
48
+ export async function saveAgentConfig(config, configDir) {
49
+ const dir = getConfigDir(configDir);
50
+ await ensureConfigDir(dir);
51
+ const configPath = path.join(dir, CONFIG_FILE);
52
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
53
+ }
54
+ export function expandPath(filePath) {
55
+ if (filePath.startsWith('~/')) {
56
+ return path.join(process.env.HOME || '', filePath.slice(2));
57
+ }
58
+ return filePath;
59
+ }