@gricha/perry 0.1.4 → 0.1.6
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 +44 -0
- package/dist/agent/web/assets/index-BHtbHpxa.js +104 -0
- package/dist/agent/web/assets/index-C_2MULGg.css +1 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/config/loader.js +16 -0
- package/dist/index.js +187 -1
- package/dist/ssh/discovery.js +96 -0
- package/dist/ssh/index.js +2 -0
- package/dist/ssh/sync.js +80 -0
- package/dist/update-checker.js +81 -0
- package/dist/workspace/manager.js +57 -4
- package/package.json +1 -1
- package/dist/agent/web/assets/index-BGbqUzMS.js +0 -104
- package/dist/agent/web/assets/index-CHEQQv1U.css +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
const PACKAGE_NAME = '@gricha/perry';
|
|
5
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
async function getCacheDir() {
|
|
7
|
+
const dir = join(homedir(), '.config', 'perry');
|
|
8
|
+
await mkdir(dir, { recursive: true });
|
|
9
|
+
return dir;
|
|
10
|
+
}
|
|
11
|
+
async function readCache() {
|
|
12
|
+
try {
|
|
13
|
+
const cacheFile = join(await getCacheDir(), 'update-cache.json');
|
|
14
|
+
const content = await readFile(cacheFile, 'utf-8');
|
|
15
|
+
return JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function writeCache(cache) {
|
|
22
|
+
try {
|
|
23
|
+
const cacheFile = join(await getCacheDir(), 'update-cache.json');
|
|
24
|
+
await writeFile(cacheFile, JSON.stringify(cache));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Ignore cache write errors
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function fetchLatestVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
33
|
+
signal: AbortSignal.timeout(3000),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
return null;
|
|
37
|
+
const data = (await response.json());
|
|
38
|
+
return data.version || null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function compareVersions(current, latest) {
|
|
45
|
+
const currentParts = current.split('.').map(Number);
|
|
46
|
+
const latestParts = latest.split('.').map(Number);
|
|
47
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
48
|
+
const c = currentParts[i] || 0;
|
|
49
|
+
const l = latestParts[i] || 0;
|
|
50
|
+
if (l > c)
|
|
51
|
+
return 1;
|
|
52
|
+
if (l < c)
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
export async function checkForUpdates(currentVersion) {
|
|
58
|
+
try {
|
|
59
|
+
const cache = await readCache();
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
let latestVersion = null;
|
|
62
|
+
if (cache && now - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
63
|
+
latestVersion = cache.latestVersion;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
latestVersion = await fetchLatestVersion();
|
|
67
|
+
await writeCache({ lastCheck: now, latestVersion });
|
|
68
|
+
}
|
|
69
|
+
if (latestVersion && compareVersions(currentVersion, latestVersion) > 0) {
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(`\x1b[33m╭─────────────────────────────────────────────────────────╮\x1b[0m`);
|
|
72
|
+
console.log(`\x1b[33m│\x1b[0m Update available: \x1b[90m${currentVersion}\x1b[0m → \x1b[32m${latestVersion}\x1b[0m \x1b[33m│\x1b[0m`);
|
|
73
|
+
console.log(`\x1b[33m│\x1b[0m Run \x1b[36mnpm install -g ${PACKAGE_NAME}\x1b[0m to update \x1b[33m│\x1b[0m`);
|
|
74
|
+
console.log(`\x1b[33m╰─────────────────────────────────────────────────────────╯\x1b[0m`);
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Silently ignore update check errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -8,6 +8,7 @@ import { expandPath } from '../config/loader';
|
|
|
8
8
|
import * as docker from '../docker';
|
|
9
9
|
import { getContainerName } from '../docker';
|
|
10
10
|
import { VOLUME_PREFIX, WORKSPACE_IMAGE_LOCAL, WORKSPACE_IMAGE_REGISTRY, SSH_PORT_RANGE_START, SSH_PORT_RANGE_END, } from '../shared/constants';
|
|
11
|
+
import { collectAuthorizedKeys, collectCopyKeys } from '../ssh/sync';
|
|
11
12
|
async function findAvailablePort(start, end) {
|
|
12
13
|
for (let port = start; port <= end; port++) {
|
|
13
14
|
const available = await new Promise((resolve) => {
|
|
@@ -226,12 +227,64 @@ export class WorkspaceManager {
|
|
|
226
227
|
tempPrefix: 'ws-gitconfig',
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
|
-
async
|
|
230
|
+
async setupSSHKeys(containerName, workspaceName) {
|
|
231
|
+
if (!this.config.ssh) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await docker.execInContainer(containerName, ['mkdir', '-p', '/home/workspace/.ssh'], {
|
|
235
|
+
user: 'workspace',
|
|
236
|
+
});
|
|
237
|
+
await docker.execInContainer(containerName, ['chmod', '700', '/home/workspace/.ssh'], {
|
|
238
|
+
user: 'workspace',
|
|
239
|
+
});
|
|
240
|
+
const authorizedKeys = await collectAuthorizedKeys(this.config.ssh, workspaceName);
|
|
241
|
+
if (authorizedKeys.length > 0) {
|
|
242
|
+
const content = authorizedKeys.join('\n') + '\n';
|
|
243
|
+
const tempFile = path.join(os.tmpdir(), `ws-authkeys-${Date.now()}`);
|
|
244
|
+
try {
|
|
245
|
+
await fs.writeFile(tempFile, content);
|
|
246
|
+
await docker.copyToContainer(containerName, tempFile, '/home/workspace/.ssh/authorized_keys');
|
|
247
|
+
await docker.execInContainer(containerName, ['chown', 'workspace:workspace', '/home/workspace/.ssh/authorized_keys'], { user: 'root' });
|
|
248
|
+
await docker.execInContainer(containerName, ['chmod', '600', '/home/workspace/.ssh/authorized_keys'], { user: 'workspace' });
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await fs.unlink(tempFile).catch(() => { });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const copyKeys = await collectCopyKeys(this.config.ssh, workspaceName);
|
|
255
|
+
for (const key of copyKeys) {
|
|
256
|
+
const privateKeyPath = `/home/workspace/.ssh/${key.name}`;
|
|
257
|
+
const publicKeyPath = `/home/workspace/.ssh/${key.name}.pub`;
|
|
258
|
+
const privateTempFile = path.join(os.tmpdir(), `ws-privkey-${Date.now()}`);
|
|
259
|
+
const publicTempFile = path.join(os.tmpdir(), `ws-pubkey-${Date.now()}`);
|
|
260
|
+
try {
|
|
261
|
+
await fs.writeFile(privateTempFile, key.privateKey + '\n');
|
|
262
|
+
await fs.writeFile(publicTempFile, key.publicKey + '\n');
|
|
263
|
+
await docker.copyToContainer(containerName, privateTempFile, privateKeyPath);
|
|
264
|
+
await docker.copyToContainer(containerName, publicTempFile, publicKeyPath);
|
|
265
|
+
await docker.execInContainer(containerName, ['chown', 'workspace:workspace', privateKeyPath, publicKeyPath], { user: 'root' });
|
|
266
|
+
await docker.execInContainer(containerName, ['chmod', '600', privateKeyPath], {
|
|
267
|
+
user: 'workspace',
|
|
268
|
+
});
|
|
269
|
+
await docker.execInContainer(containerName, ['chmod', '644', publicKeyPath], {
|
|
270
|
+
user: 'workspace',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
await fs.unlink(privateTempFile).catch(() => { });
|
|
275
|
+
await fs.unlink(publicTempFile).catch(() => { });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async setupWorkspaceCredentials(containerName, workspaceName) {
|
|
230
280
|
await this.copyGitConfig(containerName);
|
|
231
281
|
await this.copyCredentialFiles(containerName);
|
|
232
282
|
await this.setupClaudeCodeConfig(containerName);
|
|
233
283
|
await this.copyCodexCredentials(containerName);
|
|
234
284
|
await this.setupOpencodeConfig(containerName);
|
|
285
|
+
if (workspaceName) {
|
|
286
|
+
await this.setupSSHKeys(containerName, workspaceName);
|
|
287
|
+
}
|
|
235
288
|
}
|
|
236
289
|
async runPostStartScript(containerName) {
|
|
237
290
|
const scriptPath = this.config.scripts.post_start;
|
|
@@ -347,7 +400,7 @@ export class WorkspaceManager {
|
|
|
347
400
|
await this.state.setWorkspace(workspace);
|
|
348
401
|
await docker.startContainer(containerName);
|
|
349
402
|
await docker.waitForContainerReady(containerName);
|
|
350
|
-
await this.setupWorkspaceCredentials(containerName);
|
|
403
|
+
await this.setupWorkspaceCredentials(containerName, name);
|
|
351
404
|
workspace.status = 'running';
|
|
352
405
|
await this.state.setWorkspace(workspace);
|
|
353
406
|
await this.runPostStartScript(containerName);
|
|
@@ -413,7 +466,7 @@ export class WorkspaceManager {
|
|
|
413
466
|
}
|
|
414
467
|
await docker.startContainer(containerName);
|
|
415
468
|
await docker.waitForContainerReady(containerName);
|
|
416
|
-
await this.setupWorkspaceCredentials(containerName);
|
|
469
|
+
await this.setupWorkspaceCredentials(containerName, name);
|
|
417
470
|
workspace.status = 'running';
|
|
418
471
|
await this.state.setWorkspace(workspace);
|
|
419
472
|
await this.runPostStartScript(containerName);
|
|
@@ -481,6 +534,6 @@ export class WorkspaceManager {
|
|
|
481
534
|
if (!running) {
|
|
482
535
|
throw new Error(`Workspace '${name}' is not running`);
|
|
483
536
|
}
|
|
484
|
-
await this.setupWorkspaceCredentials(containerName);
|
|
537
|
+
await this.setupWorkspaceCredentials(containerName, name);
|
|
485
538
|
}
|
|
486
539
|
}
|