@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,372 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export * from './types';
|
|
3
|
+
const CONTAINER_PREFIX = 'workspace-';
|
|
4
|
+
export function getContainerName(name) {
|
|
5
|
+
return `${CONTAINER_PREFIX}${name}`;
|
|
6
|
+
}
|
|
7
|
+
async function runCommand(command, args, options = {}) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
cwd: options.cwd,
|
|
11
|
+
env: { ...process.env, ...options.env },
|
|
12
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
13
|
+
});
|
|
14
|
+
let stdout = '';
|
|
15
|
+
let stderr = '';
|
|
16
|
+
child.stdout.on('data', (chunk) => {
|
|
17
|
+
stdout += chunk;
|
|
18
|
+
});
|
|
19
|
+
child.stderr.on('data', (chunk) => {
|
|
20
|
+
stderr += chunk;
|
|
21
|
+
});
|
|
22
|
+
child.on('error', reject);
|
|
23
|
+
child.on('close', (code) => {
|
|
24
|
+
const result = { stdout: stdout.trim(), stderr: stderr.trim(), code: code ?? 1 };
|
|
25
|
+
if (code === 0) {
|
|
26
|
+
resolve(result);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const err = new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
30
|
+
err.code = code ?? undefined;
|
|
31
|
+
err.stdout = stdout;
|
|
32
|
+
err.stderr = stderr;
|
|
33
|
+
reject(err);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function docker(args) {
|
|
39
|
+
return runCommand('docker', args);
|
|
40
|
+
}
|
|
41
|
+
export async function getDockerVersion() {
|
|
42
|
+
const { stdout } = await docker(['version', '--format', '{{.Server.Version}}']);
|
|
43
|
+
return stdout;
|
|
44
|
+
}
|
|
45
|
+
export async function containerExists(name) {
|
|
46
|
+
const { stdout } = await docker(['ps', '-a', '-q', '--filter', `name=^${name}$`]);
|
|
47
|
+
return stdout.length > 0;
|
|
48
|
+
}
|
|
49
|
+
export async function containerRunning(name) {
|
|
50
|
+
const { stdout } = await docker([
|
|
51
|
+
'ps',
|
|
52
|
+
'-q',
|
|
53
|
+
'--filter',
|
|
54
|
+
`name=^${name}$`,
|
|
55
|
+
'--filter',
|
|
56
|
+
'status=running',
|
|
57
|
+
]);
|
|
58
|
+
return stdout.length > 0;
|
|
59
|
+
}
|
|
60
|
+
export async function getContainer(name) {
|
|
61
|
+
try {
|
|
62
|
+
const { stdout } = await docker(['inspect', '--format', '{{json .}}', name]);
|
|
63
|
+
const data = JSON.parse(stdout);
|
|
64
|
+
const portMappings = [];
|
|
65
|
+
const networkSettings = data.NetworkSettings?.Ports || {};
|
|
66
|
+
for (const [containerPort, hostBindings] of Object.entries(networkSettings)) {
|
|
67
|
+
if (!hostBindings)
|
|
68
|
+
continue;
|
|
69
|
+
const [port, protocol] = containerPort.split('/');
|
|
70
|
+
for (const binding of hostBindings) {
|
|
71
|
+
portMappings.push({
|
|
72
|
+
containerPort: parseInt(port, 10),
|
|
73
|
+
hostPort: parseInt(binding.HostPort, 10),
|
|
74
|
+
protocol: protocol,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
id: data.Id,
|
|
80
|
+
name: data.Name.replace(/^\//, ''),
|
|
81
|
+
image: data.Config.Image,
|
|
82
|
+
status: data.State.Status,
|
|
83
|
+
state: data.State.Running ? 'running' : data.State.Status,
|
|
84
|
+
ports: portMappings,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
89
|
+
if (!stderr.includes('no such object') && !stderr.includes('no such container')) {
|
|
90
|
+
console.error(`[docker] Error getting container '${name}':`, stderr);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function getContainerIp(name) {
|
|
96
|
+
try {
|
|
97
|
+
const { stdout } = await docker([
|
|
98
|
+
'inspect',
|
|
99
|
+
'--format',
|
|
100
|
+
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}',
|
|
101
|
+
name,
|
|
102
|
+
]);
|
|
103
|
+
const ip = stdout.trim();
|
|
104
|
+
return ip || null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function listContainers(prefix) {
|
|
111
|
+
const args = ['ps', '-a', '--format', '{{json .}}'];
|
|
112
|
+
if (prefix) {
|
|
113
|
+
args.push('--filter', `name=^${prefix}`);
|
|
114
|
+
}
|
|
115
|
+
const { stdout } = await docker(args);
|
|
116
|
+
if (!stdout)
|
|
117
|
+
return [];
|
|
118
|
+
const containers = [];
|
|
119
|
+
for (const line of stdout.split('\n')) {
|
|
120
|
+
if (!line.trim())
|
|
121
|
+
continue;
|
|
122
|
+
const data = JSON.parse(line);
|
|
123
|
+
containers.push({
|
|
124
|
+
id: data.ID,
|
|
125
|
+
name: data.Names,
|
|
126
|
+
image: data.Image,
|
|
127
|
+
status: data.Status,
|
|
128
|
+
state: data.State.toLowerCase(),
|
|
129
|
+
ports: [],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return containers;
|
|
133
|
+
}
|
|
134
|
+
export async function createContainer(options) {
|
|
135
|
+
const args = ['create'];
|
|
136
|
+
if (options.name) {
|
|
137
|
+
args.push('--name', options.name);
|
|
138
|
+
}
|
|
139
|
+
if (options.hostname) {
|
|
140
|
+
args.push('--hostname', options.hostname);
|
|
141
|
+
}
|
|
142
|
+
if (options.privileged) {
|
|
143
|
+
args.push('--privileged');
|
|
144
|
+
}
|
|
145
|
+
if (options.network) {
|
|
146
|
+
args.push('--network', options.network);
|
|
147
|
+
}
|
|
148
|
+
if (options.workdir) {
|
|
149
|
+
args.push('--workdir', options.workdir);
|
|
150
|
+
}
|
|
151
|
+
if (options.user) {
|
|
152
|
+
args.push('--user', options.user);
|
|
153
|
+
}
|
|
154
|
+
if (options.restartPolicy) {
|
|
155
|
+
args.push('--restart', options.restartPolicy);
|
|
156
|
+
}
|
|
157
|
+
if (options.env) {
|
|
158
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
159
|
+
args.push('-e', `${key}=${value}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.volumes) {
|
|
163
|
+
for (const vol of options.volumes) {
|
|
164
|
+
const mode = vol.readonly ? 'ro' : 'rw';
|
|
165
|
+
args.push('-v', `${vol.source}:${vol.target}:${mode}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (options.ports) {
|
|
169
|
+
for (const port of options.ports) {
|
|
170
|
+
args.push('-p', `${port.hostPort}:${port.containerPort}/${port.protocol}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (options.labels) {
|
|
174
|
+
for (const [key, value] of Object.entries(options.labels)) {
|
|
175
|
+
args.push('--label', `${key}=${value}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (options.entrypoint) {
|
|
179
|
+
args.push('--entrypoint', options.entrypoint.join(' '));
|
|
180
|
+
}
|
|
181
|
+
args.push(options.image);
|
|
182
|
+
if (options.command) {
|
|
183
|
+
args.push(...options.command);
|
|
184
|
+
}
|
|
185
|
+
const { stdout } = await docker(args);
|
|
186
|
+
return stdout.trim();
|
|
187
|
+
}
|
|
188
|
+
export async function startContainer(name) {
|
|
189
|
+
await docker(['start', name]);
|
|
190
|
+
}
|
|
191
|
+
export async function stopContainer(name, timeout = 10) {
|
|
192
|
+
await docker(['stop', '-t', String(timeout), name]);
|
|
193
|
+
}
|
|
194
|
+
export async function removeContainer(name, force = false) {
|
|
195
|
+
const args = ['rm'];
|
|
196
|
+
if (force)
|
|
197
|
+
args.push('-f');
|
|
198
|
+
args.push(name);
|
|
199
|
+
await docker(args);
|
|
200
|
+
}
|
|
201
|
+
export async function execInContainer(name, command, options = {}) {
|
|
202
|
+
const args = ['exec'];
|
|
203
|
+
if (options.user) {
|
|
204
|
+
args.push('-u', options.user);
|
|
205
|
+
}
|
|
206
|
+
if (options.workdir) {
|
|
207
|
+
args.push('-w', options.workdir);
|
|
208
|
+
}
|
|
209
|
+
if (options.env) {
|
|
210
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
211
|
+
args.push('-e', `${key}=${value}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
args.push(name, ...command);
|
|
215
|
+
try {
|
|
216
|
+
const result = await docker(args);
|
|
217
|
+
return { ...result, exitCode: 0 };
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const cmdErr = err;
|
|
221
|
+
return {
|
|
222
|
+
stdout: cmdErr.stdout || '',
|
|
223
|
+
stderr: cmdErr.stderr || '',
|
|
224
|
+
code: cmdErr.code || 1,
|
|
225
|
+
exitCode: cmdErr.code || 1,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export async function copyToContainer(containerName, sourcePath, destPath) {
|
|
230
|
+
await docker(['cp', sourcePath, `${containerName}:${destPath}`]);
|
|
231
|
+
}
|
|
232
|
+
export async function copyFromContainer(containerName, sourcePath, destPath) {
|
|
233
|
+
await docker(['cp', `${containerName}:${sourcePath}`, destPath]);
|
|
234
|
+
}
|
|
235
|
+
export async function volumeExists(name) {
|
|
236
|
+
try {
|
|
237
|
+
await docker(['volume', 'inspect', name]);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
242
|
+
if (!stderr.includes('no such volume')) {
|
|
243
|
+
console.error(`[docker] Error checking volume '${name}':`, stderr);
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function createVolume(name) {
|
|
249
|
+
await docker(['volume', 'create', name]);
|
|
250
|
+
}
|
|
251
|
+
export async function removeVolume(name, force = false) {
|
|
252
|
+
const args = ['volume', 'rm'];
|
|
253
|
+
if (force)
|
|
254
|
+
args.push('-f');
|
|
255
|
+
args.push(name);
|
|
256
|
+
await docker(args);
|
|
257
|
+
}
|
|
258
|
+
export async function getVolume(name) {
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await docker(['volume', 'inspect', '--format', '{{json .}}', name]);
|
|
261
|
+
const data = JSON.parse(stdout);
|
|
262
|
+
return {
|
|
263
|
+
name: data.Name,
|
|
264
|
+
driver: data.Driver,
|
|
265
|
+
mountpoint: data.Mountpoint,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
270
|
+
if (!stderr.includes('no such volume')) {
|
|
271
|
+
console.error(`[docker] Error getting volume '${name}':`, stderr);
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export async function networkExists(name) {
|
|
277
|
+
try {
|
|
278
|
+
await docker(['network', 'inspect', name]);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
283
|
+
if (!stderr.includes('no such network')) {
|
|
284
|
+
console.error(`[docker] Error checking network '${name}':`, stderr);
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export async function createNetwork(name) {
|
|
290
|
+
await docker(['network', 'create', name]);
|
|
291
|
+
}
|
|
292
|
+
export async function removeNetwork(name) {
|
|
293
|
+
await docker(['network', 'rm', name]);
|
|
294
|
+
}
|
|
295
|
+
export async function getNetwork(name) {
|
|
296
|
+
try {
|
|
297
|
+
const { stdout } = await docker(['network', 'inspect', '--format', '{{json .}}', name]);
|
|
298
|
+
const data = JSON.parse(stdout);
|
|
299
|
+
return {
|
|
300
|
+
name: data.Name,
|
|
301
|
+
id: data.Id,
|
|
302
|
+
driver: data.Driver,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
307
|
+
if (!stderr.includes('no such network')) {
|
|
308
|
+
console.error(`[docker] Error getting network '${name}':`, stderr);
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export async function connectToNetwork(containerName, networkName) {
|
|
314
|
+
try {
|
|
315
|
+
await docker(['network', 'connect', networkName, containerName]);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
const message = err.stderr || '';
|
|
319
|
+
if (!message.includes('already exists in network')) {
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export async function imageExists(tag) {
|
|
325
|
+
try {
|
|
326
|
+
await docker(['image', 'inspect', tag]);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
const stderr = err.stderr?.toLowerCase() || '';
|
|
331
|
+
if (!stderr.includes('no such image')) {
|
|
332
|
+
console.error(`[docker] Error checking image '${tag}':`, stderr);
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
export async function pullImage(tag) {
|
|
338
|
+
await docker(['pull', tag]);
|
|
339
|
+
}
|
|
340
|
+
export async function buildImage(tag, context, options = {}) {
|
|
341
|
+
const args = ['build', '-t', tag];
|
|
342
|
+
if (options.noCache) {
|
|
343
|
+
args.push('--no-cache');
|
|
344
|
+
}
|
|
345
|
+
args.push(context);
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const child = spawn('docker', args, {
|
|
348
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
349
|
+
});
|
|
350
|
+
child.on('error', reject);
|
|
351
|
+
child.on('close', (code) => {
|
|
352
|
+
if (code === 0) {
|
|
353
|
+
resolve();
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
reject(new Error(`Docker build failed with exit code ${code}`));
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
export async function getLogs(containerName, options = {}) {
|
|
362
|
+
const args = ['logs'];
|
|
363
|
+
if (options.tail) {
|
|
364
|
+
args.push('--tail', String(options.tail));
|
|
365
|
+
}
|
|
366
|
+
if (options.since) {
|
|
367
|
+
args.push('--since', options.since);
|
|
368
|
+
}
|
|
369
|
+
args.push(containerName);
|
|
370
|
+
const { stdout, stderr } = await docker(args);
|
|
371
|
+
return stdout + stderr;
|
|
372
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|