@gricha/perry 0.0.1 → 0.1.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/README.md +25 -33
- package/dist/agent/router.js +39 -404
- package/dist/agent/web/assets/index-BGbqUzMS.js +104 -0
- package/dist/agent/web/assets/index-CHEQQv1U.css +1 -0
- package/dist/agent/web/favicon.ico +0 -0
- package/dist/agent/web/index.html +3 -3
- package/dist/agent/web/logo-192.png +0 -0
- package/dist/agent/web/logo-512.png +0 -0
- package/dist/agent/web/logo.png +0 -0
- package/dist/agent/web/logo.webp +0 -0
- package/dist/chat/base-chat-websocket.js +83 -0
- package/dist/chat/host-opencode-handler.js +115 -3
- package/dist/chat/opencode-handler.js +60 -0
- package/dist/chat/opencode-server.js +252 -0
- package/dist/chat/opencode-websocket.js +15 -87
- package/dist/chat/websocket.js +19 -86
- package/dist/client/ws-shell.js +15 -5
- package/dist/docker/index.js +41 -1
- package/dist/index.js +3 -3
- package/dist/sessions/agents/claude.js +86 -0
- package/dist/sessions/agents/codex.js +110 -0
- package/dist/sessions/agents/index.js +44 -0
- package/dist/sessions/agents/opencode.js +168 -0
- package/dist/sessions/agents/types.js +1 -0
- package/dist/sessions/agents/utils.js +31 -0
- package/dist/shared/base-websocket.js +13 -1
- package/dist/shared/constants.js +2 -1
- package/dist/terminal/base-handler.js +68 -0
- package/dist/terminal/handler.js +18 -75
- package/dist/terminal/host-handler.js +7 -61
- package/dist/terminal/websocket.js +2 -4
- package/dist/workspace/manager.js +33 -22
- package/dist/workspace/state.js +33 -2
- package/package.json +1 -1
- package/dist/agent/web/assets/index-9t2sFIJM.js +0 -101
- package/dist/agent/web/assets/index-CCFpTruF.css +0 -1
- package/dist/agent/web/vite.svg +0 -1
|
@@ -2,11 +2,12 @@ import { createServer } from 'net';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import pkg from '../../package.json';
|
|
5
6
|
import { StateManager } from './state';
|
|
6
7
|
import { expandPath } from '../config/loader';
|
|
7
8
|
import * as docker from '../docker';
|
|
8
9
|
import { getContainerName } from '../docker';
|
|
9
|
-
import { VOLUME_PREFIX,
|
|
10
|
+
import { VOLUME_PREFIX, WORKSPACE_IMAGE_LOCAL, WORKSPACE_IMAGE_REGISTRY, SSH_PORT_RANGE_START, SSH_PORT_RANGE_END, } from '../shared/constants';
|
|
10
11
|
async function findAvailablePort(start, end) {
|
|
11
12
|
for (let port = start; port <= end; port++) {
|
|
12
13
|
const available = await new Promise((resolve) => {
|
|
@@ -23,6 +24,21 @@ async function findAvailablePort(start, end) {
|
|
|
23
24
|
}
|
|
24
25
|
throw new Error(`No available port in range ${start}-${end}`);
|
|
25
26
|
}
|
|
27
|
+
async function ensureWorkspaceImage() {
|
|
28
|
+
const registryImage = `${WORKSPACE_IMAGE_REGISTRY}:${pkg.version}`;
|
|
29
|
+
const localExists = await docker.imageExists(WORKSPACE_IMAGE_LOCAL);
|
|
30
|
+
if (localExists) {
|
|
31
|
+
return WORKSPACE_IMAGE_LOCAL;
|
|
32
|
+
}
|
|
33
|
+
console.log(`Pulling workspace image ${registryImage}...`);
|
|
34
|
+
const pulled = await docker.tryPullImage(registryImage);
|
|
35
|
+
if (pulled) {
|
|
36
|
+
return registryImage;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Workspace image not found. Either:\n` +
|
|
39
|
+
` 1. Run 'perry build' to build locally, or\n` +
|
|
40
|
+
` 2. Check your network connection to pull from registry`);
|
|
41
|
+
}
|
|
26
42
|
async function copyCredentialToContainer(options) {
|
|
27
43
|
const { source, dest, containerName, dirPermissions = '700', filePermissions = '600', tempPrefix = 'ws-cred', } = options;
|
|
28
44
|
const expandedSource = expandPath(source);
|
|
@@ -210,6 +226,13 @@ export class WorkspaceManager {
|
|
|
210
226
|
tempPrefix: 'ws-gitconfig',
|
|
211
227
|
});
|
|
212
228
|
}
|
|
229
|
+
async setupWorkspaceCredentials(containerName) {
|
|
230
|
+
await this.copyGitConfig(containerName);
|
|
231
|
+
await this.copyCredentialFiles(containerName);
|
|
232
|
+
await this.setupClaudeCodeConfig(containerName);
|
|
233
|
+
await this.copyCodexCredentials(containerName);
|
|
234
|
+
await this.setupOpencodeConfig(containerName);
|
|
235
|
+
}
|
|
213
236
|
async runPostStartScript(containerName) {
|
|
214
237
|
const scriptPath = this.config.scripts.post_start;
|
|
215
238
|
if (!scriptPath) {
|
|
@@ -287,10 +310,7 @@ export class WorkspaceManager {
|
|
|
287
310
|
};
|
|
288
311
|
await this.state.setWorkspace(workspace);
|
|
289
312
|
try {
|
|
290
|
-
const
|
|
291
|
-
if (!imageReady) {
|
|
292
|
-
throw new Error(`Workspace image '${WORKSPACE_IMAGE}' not found. Run 'workspace build' first.`);
|
|
293
|
-
}
|
|
313
|
+
const workspaceImage = await ensureWorkspaceImage();
|
|
294
314
|
if (!(await docker.volumeExists(volumeName))) {
|
|
295
315
|
await docker.createVolume(volumeName);
|
|
296
316
|
}
|
|
@@ -310,7 +330,7 @@ export class WorkspaceManager {
|
|
|
310
330
|
}
|
|
311
331
|
const containerId = await docker.createContainer({
|
|
312
332
|
name: containerName,
|
|
313
|
-
image:
|
|
333
|
+
image: workspaceImage,
|
|
314
334
|
hostname: name,
|
|
315
335
|
privileged: true,
|
|
316
336
|
restartPolicy: 'unless-stopped',
|
|
@@ -326,11 +346,8 @@ export class WorkspaceManager {
|
|
|
326
346
|
workspace.ports.ssh = sshPort;
|
|
327
347
|
await this.state.setWorkspace(workspace);
|
|
328
348
|
await docker.startContainer(containerName);
|
|
329
|
-
await
|
|
330
|
-
await this.
|
|
331
|
-
await this.setupClaudeCodeConfig(containerName);
|
|
332
|
-
await this.copyCodexCredentials(containerName);
|
|
333
|
-
await this.setupOpencodeConfig(containerName);
|
|
349
|
+
await docker.waitForContainerReady(containerName);
|
|
350
|
+
await this.setupWorkspaceCredentials(containerName);
|
|
334
351
|
workspace.status = 'running';
|
|
335
352
|
await this.state.setWorkspace(workspace);
|
|
336
353
|
await this.runPostStartScript(containerName);
|
|
@@ -356,6 +373,7 @@ export class WorkspaceManager {
|
|
|
356
373
|
throw new Error(`Container and volume for workspace '${name}' were deleted. ` +
|
|
357
374
|
`Please delete this workspace and create a new one.`);
|
|
358
375
|
}
|
|
376
|
+
const workspaceImage = await ensureWorkspaceImage();
|
|
359
377
|
const sshPort = await findAvailablePort(SSH_PORT_RANGE_START, SSH_PORT_RANGE_END);
|
|
360
378
|
const containerEnv = {
|
|
361
379
|
...this.config.credentials.env,
|
|
@@ -371,7 +389,7 @@ export class WorkspaceManager {
|
|
|
371
389
|
}
|
|
372
390
|
const containerId = await docker.createContainer({
|
|
373
391
|
name: containerName,
|
|
374
|
-
image:
|
|
392
|
+
image: workspaceImage,
|
|
375
393
|
hostname: name,
|
|
376
394
|
privileged: true,
|
|
377
395
|
restartPolicy: 'unless-stopped',
|
|
@@ -394,11 +412,8 @@ export class WorkspaceManager {
|
|
|
394
412
|
return workspace;
|
|
395
413
|
}
|
|
396
414
|
await docker.startContainer(containerName);
|
|
397
|
-
await
|
|
398
|
-
await this.
|
|
399
|
-
await this.setupClaudeCodeConfig(containerName);
|
|
400
|
-
await this.copyCodexCredentials(containerName);
|
|
401
|
-
await this.setupOpencodeConfig(containerName);
|
|
415
|
+
await docker.waitForContainerReady(containerName);
|
|
416
|
+
await this.setupWorkspaceCredentials(containerName);
|
|
402
417
|
workspace.status = 'running';
|
|
403
418
|
await this.state.setWorkspace(workspace);
|
|
404
419
|
await this.runPostStartScript(containerName);
|
|
@@ -466,10 +481,6 @@ export class WorkspaceManager {
|
|
|
466
481
|
if (!running) {
|
|
467
482
|
throw new Error(`Workspace '${name}' is not running`);
|
|
468
483
|
}
|
|
469
|
-
await this.
|
|
470
|
-
await this.copyCredentialFiles(containerName);
|
|
471
|
-
await this.setupClaudeCodeConfig(containerName);
|
|
472
|
-
await this.copyCodexCredentials(containerName);
|
|
473
|
-
await this.setupOpencodeConfig(containerName);
|
|
484
|
+
await this.setupWorkspaceCredentials(containerName);
|
|
474
485
|
}
|
|
475
486
|
}
|
package/dist/workspace/state.js
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import lockfile from 'proper-lockfile';
|
|
3
4
|
import { STATE_FILE } from '../shared/types';
|
|
4
5
|
export class StateManager {
|
|
5
6
|
statePath;
|
|
6
7
|
state = null;
|
|
8
|
+
lockfilePath;
|
|
7
9
|
constructor(configDir) {
|
|
8
10
|
this.statePath = path.join(configDir, STATE_FILE);
|
|
11
|
+
this.lockfilePath = path.join(configDir, '.state.lock');
|
|
12
|
+
}
|
|
13
|
+
async ensureLockfile() {
|
|
14
|
+
try {
|
|
15
|
+
await fs.mkdir(path.dirname(this.lockfilePath), { recursive: true });
|
|
16
|
+
await fs.writeFile(this.lockfilePath, '', { flag: 'wx' });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code !== 'EEXIST') {
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async withLock(fn) {
|
|
25
|
+
await this.ensureLockfile();
|
|
26
|
+
let release;
|
|
27
|
+
try {
|
|
28
|
+
release = await lockfile.lock(this.lockfilePath, {
|
|
29
|
+
retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
|
|
30
|
+
});
|
|
31
|
+
return await fn();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (release) {
|
|
35
|
+
await release();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
9
38
|
}
|
|
10
39
|
async load() {
|
|
11
40
|
if (this.state) {
|
|
@@ -29,8 +58,10 @@ export class StateManager {
|
|
|
29
58
|
if (!this.state) {
|
|
30
59
|
return;
|
|
31
60
|
}
|
|
32
|
-
await
|
|
33
|
-
|
|
61
|
+
await this.withLock(async () => {
|
|
62
|
+
await fs.mkdir(path.dirname(this.statePath), { recursive: true });
|
|
63
|
+
await fs.writeFile(this.statePath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
64
|
+
});
|
|
34
65
|
}
|
|
35
66
|
async getWorkspace(name) {
|
|
36
67
|
const state = await this.load();
|