@gricha/perry 0.2.2 → 0.2.4

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 (41) hide show
  1. package/README.md +11 -0
  2. package/dist/agent/file-watcher.js +134 -0
  3. package/dist/agent/router.js +48 -2
  4. package/dist/agent/run.js +44 -4
  5. package/dist/agent/web/assets/index-BmFYrCoX.css +1 -0
  6. package/dist/agent/web/assets/index-IavvQP8G.js +104 -0
  7. package/dist/agent/web/index.html +2 -2
  8. package/dist/agents/__tests__/claude-code.test.js +125 -0
  9. package/dist/agents/__tests__/codex.test.js +64 -0
  10. package/dist/agents/__tests__/opencode.test.js +130 -0
  11. package/dist/agents/__tests__/sync.test.js +272 -0
  12. package/dist/agents/index.js +177 -0
  13. package/dist/agents/sync/claude-code.js +84 -0
  14. package/dist/agents/sync/codex.js +29 -0
  15. package/dist/agents/sync/copier.js +89 -0
  16. package/dist/agents/sync/opencode.js +51 -0
  17. package/dist/agents/sync/types.js +1 -0
  18. package/dist/agents/types.js +1 -0
  19. package/dist/chat/base-chat-websocket.js +2 -2
  20. package/dist/chat/base-claude-session.js +169 -0
  21. package/dist/chat/base-opencode-session.js +181 -0
  22. package/dist/chat/handler.js +14 -157
  23. package/dist/chat/host-handler.js +13 -142
  24. package/dist/chat/host-opencode-handler.js +28 -187
  25. package/dist/chat/opencode-handler.js +38 -197
  26. package/dist/chat/opencode-websocket.js +1 -1
  27. package/dist/chat/types.js +1 -0
  28. package/dist/chat/websocket.js +2 -2
  29. package/dist/client/api.js +25 -0
  30. package/dist/config/loader.js +20 -2
  31. package/dist/docker/eager-pull.js +19 -3
  32. package/dist/docker/index.js +28 -1
  33. package/dist/index.js +83 -12
  34. package/dist/perry-worker +0 -0
  35. package/dist/shared/constants.js +1 -0
  36. package/dist/shared/types.js +0 -1
  37. package/dist/terminal/websocket.js +1 -1
  38. package/dist/workspace/manager.js +178 -115
  39. package/package.json +4 -3
  40. package/dist/agent/web/assets/index-DIOWcVH-.css +0 -1
  41. package/dist/agent/web/assets/index-DN_QW9sL.js +0 -104
package/dist/index.js CHANGED
@@ -177,6 +177,23 @@ program
177
177
  handleError(err);
178
178
  }
179
179
  });
180
+ program
181
+ .command('clone <source> <clone-name>')
182
+ .description('Clone an existing workspace')
183
+ .action(async (source, cloneName) => {
184
+ try {
185
+ const client = await getClient();
186
+ console.log(`Cloning workspace '${source}' to '${cloneName}'...`);
187
+ console.log('This may take a while for large workspaces.');
188
+ const workspace = await client.cloneWorkspace(source, cloneName);
189
+ console.log(`Workspace '${cloneName}' created.`);
190
+ console.log(` Status: ${workspace.status}`);
191
+ console.log(` SSH Port: ${workspace.ports.ssh}`);
192
+ }
193
+ catch (err) {
194
+ handleError(err);
195
+ }
196
+ });
180
197
  program
181
198
  .command('info [name]')
182
199
  .description('Show workspace or agent info')
@@ -288,15 +305,21 @@ program
288
305
  console.error(`Workspace '${name}' is not running (status: ${workspace.status})`);
289
306
  process.exit(1);
290
307
  }
308
+ let effectivePorts = ports;
291
309
  if (ports.length === 0) {
292
- console.log(`Workspace '${name}' is running.`);
293
- console.log('');
294
- console.log('Usage: perry proxy <name> <port> [<port>...]');
295
- console.log(' Examples:');
296
- console.log(' perry proxy alpha 3000 # Forward port 3000');
297
- console.log(' perry proxy alpha 8080:3000 # Forward local 8080 to remote 3000');
298
- console.log(' perry proxy alpha 3000 5173 # Forward multiple ports');
299
- return;
310
+ const configuredForwards = workspace.ports.forwards || [];
311
+ if (configuredForwards.length === 0) {
312
+ console.log(`No ports configured for workspace '${name}'.`);
313
+ console.log('');
314
+ console.log('Configure ports with: perry ports <name> <port> [<port>...]');
315
+ console.log(' Example: perry ports ' + name + ' 3000 5173');
316
+ console.log('');
317
+ console.log('Or specify ports directly: perry proxy <name> <port> [<port>...]');
318
+ console.log(' Example: perry proxy ' + name + ' 3000 5173');
319
+ return;
320
+ }
321
+ effectivePorts = configuredForwards.map((p) => String(p));
322
+ console.log(`Using configured ports: ${configuredForwards.join(', ')}`);
300
323
  }
301
324
  if (isLocalWorker(worker)) {
302
325
  const containerName = getContainerName(name);
@@ -305,7 +328,7 @@ program
305
328
  console.error(`Could not get IP for container '${containerName}'`);
306
329
  process.exit(1);
307
330
  }
308
- const forwards = ports.map(parseDockerPortForward);
331
+ const forwards = effectivePorts.map(parseDockerPortForward);
309
332
  console.log(`Forwarding ports: ${formatDockerPortForwards(forwards)}`);
310
333
  console.log(`Container IP: ${containerIp}`);
311
334
  console.log('Press Ctrl+C to stop.');
@@ -330,7 +353,7 @@ program
330
353
  await new Promise(() => { });
331
354
  }
332
355
  else {
333
- const forwards = ports.map(parsePortForward);
356
+ const forwards = effectivePorts.map(parsePortForward);
334
357
  console.log(`Forwarding ports: ${formatPortForwards(forwards)}`);
335
358
  console.log('Press Ctrl+C to stop.');
336
359
  console.log('');
@@ -354,6 +377,47 @@ program
354
377
  handleError(err);
355
378
  }
356
379
  });
380
+ program
381
+ .command('ports <name> [ports...]')
382
+ .description('Configure ports to forward for a workspace')
383
+ .action(async (name, ports) => {
384
+ try {
385
+ const client = await getClient();
386
+ const workspace = await client.getWorkspace(name);
387
+ if (!workspace) {
388
+ console.error(`Workspace '${name}' not found`);
389
+ process.exit(1);
390
+ }
391
+ if (ports.length === 0) {
392
+ const currentPorts = workspace.ports.forwards || [];
393
+ if (currentPorts.length === 0) {
394
+ console.log(`No ports configured for workspace '${name}'.`);
395
+ console.log('');
396
+ console.log('Usage: perry ports <name> <port> [<port>...]');
397
+ console.log(' Example: perry ports ' + name + ' 3000 5173 8080');
398
+ }
399
+ else {
400
+ console.log(`Ports configured for '${name}': ${currentPorts.join(', ')}`);
401
+ }
402
+ return;
403
+ }
404
+ const portNumbers = ports.map((p) => {
405
+ const num = parseInt(p, 10);
406
+ if (isNaN(num) || num < 1 || num > 65535) {
407
+ console.error(`Invalid port number: ${p}`);
408
+ process.exit(1);
409
+ }
410
+ return num;
411
+ });
412
+ await client.setPortForwards(name, portNumbers);
413
+ console.log(`Ports configured for '${name}': ${portNumbers.join(', ')}`);
414
+ console.log('');
415
+ console.log(`Run 'perry proxy ${name}' to start forwarding.`);
416
+ }
417
+ catch (err) {
418
+ handleError(err);
419
+ }
420
+ });
357
421
  const configCmd = program.command('config').description('Manage configuration');
358
422
  configCmd
359
423
  .command('show')
@@ -400,8 +464,15 @@ configCmd
400
464
  for (const [dest, src] of Object.entries(config.credentials.files)) {
401
465
  console.log(` - ${dest} <- ${src}`);
402
466
  }
403
- if (config.scripts.post_start) {
404
- console.log(` Post-start Script: ${config.scripts.post_start}`);
467
+ const scripts = config.scripts.post_start;
468
+ if (scripts && scripts.length > 0) {
469
+ console.log(` Post-start Scripts: ${scripts.length}`);
470
+ for (const script of scripts) {
471
+ console.log(` - ${script}`);
472
+ }
473
+ }
474
+ if (config.scripts.fail_on_error) {
475
+ console.log(` Scripts Fail on Error: enabled`);
405
476
  }
406
477
  });
407
478
  const sshCmd = program.command('ssh').description('Manage SSH keys for workspaces');
package/dist/perry-worker CHANGED
Binary file
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_AGENT_PORT = 7391;
2
+ export const DEFAULT_CLAUDE_MODEL = 'sonnet';
2
3
  export const SSH_PORT_RANGE_START = 2200;
3
4
  export const SSH_PORT_RANGE_END = 2400;
4
5
  export const WORKSPACE_IMAGE_LOCAL = 'perry:latest';
@@ -1,4 +1,3 @@
1
- export const HOST_WORKSPACE_NAME = '@host';
2
1
  export const DEFAULT_CONFIG_DIR = process.env.PERRY_CONFIG_DIR || `${process.env.HOME}/.config/perry`;
3
2
  export const STATE_FILE = 'state.json';
4
3
  export const CONFIG_FILE = 'config.json';
@@ -3,7 +3,7 @@ import { BaseWebSocketServer, safeSend } from '../shared/base-websocket';
3
3
  import { createTerminalSession } from './handler';
4
4
  import { createHostTerminalSession } from './host-handler';
5
5
  import { isControlMessage } from './types';
6
- import { HOST_WORKSPACE_NAME } from '../shared/types';
6
+ import { HOST_WORKSPACE_NAME } from '../shared/client-types';
7
7
  export class TerminalWebSocketServer extends BaseWebSocketServer {
8
8
  getContainerName;
9
9
  isHostAccessAllowed;
@@ -9,6 +9,7 @@ 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
11
  import { collectAuthorizedKeys, collectCopyKeys } from '../ssh/sync';
12
+ import { syncAllAgents } from '../agents';
12
13
  async function findAvailablePort(start, end) {
13
14
  for (let port = start; port <= end; port++) {
14
15
  const available = await new Promise((resolve) => {
@@ -127,97 +128,6 @@ export class WorkspaceManager {
127
128
  });
128
129
  }
129
130
  }
130
- async setupClaudeCodeConfig(containerName) {
131
- const localClaudeCredentials = expandPath('~/.claude/.credentials.json');
132
- const configContent = JSON.stringify({ hasCompletedOnboarding: true });
133
- const tempFile = path.join(os.tmpdir(), `ws-claude-config-${Date.now()}.json`);
134
- try {
135
- await fs.writeFile(tempFile, configContent);
136
- await docker.copyToContainer(containerName, tempFile, '/home/workspace/.claude.json');
137
- await docker.execInContainer(containerName, ['chown', 'workspace:workspace', '/home/workspace/.claude.json'], { user: 'root' });
138
- await docker.execInContainer(containerName, ['chmod', '644', '/home/workspace/.claude.json'], { user: 'workspace' });
139
- }
140
- finally {
141
- await fs.unlink(tempFile).catch(() => { });
142
- }
143
- try {
144
- await fs.access(localClaudeCredentials);
145
- await docker.execInContainer(containerName, ['mkdir', '-p', '/home/workspace/.claude'], {
146
- user: 'workspace',
147
- });
148
- await copyCredentialToContainer({
149
- source: '~/.claude/.credentials.json',
150
- dest: '/home/workspace/.claude/.credentials.json',
151
- containerName,
152
- filePermissions: '600',
153
- tempPrefix: 'ws-claude-creds',
154
- });
155
- }
156
- catch {
157
- // No credentials file - that's OK, user may use oauth_token env var instead
158
- }
159
- }
160
- async copyCodexCredentials(containerName) {
161
- const codexDir = expandPath('~/.codex');
162
- try {
163
- await fs.access(codexDir);
164
- }
165
- catch {
166
- return;
167
- }
168
- await docker.execInContainer(containerName, ['mkdir', '-p', '/home/workspace/.codex'], {
169
- user: 'workspace',
170
- });
171
- await copyCredentialToContainer({
172
- source: '~/.codex/auth.json',
173
- dest: '/home/workspace/.codex/auth.json',
174
- containerName,
175
- filePermissions: '600',
176
- tempPrefix: 'ws-codex-auth',
177
- });
178
- await copyCredentialToContainer({
179
- source: '~/.codex/config.toml',
180
- dest: '/home/workspace/.codex/config.toml',
181
- containerName,
182
- filePermissions: '600',
183
- tempPrefix: 'ws-codex-config',
184
- });
185
- }
186
- async setupOpencodeConfig(containerName) {
187
- const zenToken = this.config.agents?.opencode?.zen_token;
188
- if (!zenToken) {
189
- return;
190
- }
191
- const config = {
192
- provider: {
193
- opencode: {
194
- options: {
195
- apiKey: zenToken,
196
- },
197
- },
198
- },
199
- model: 'opencode/claude-sonnet-4',
200
- };
201
- const configJson = JSON.stringify(config, null, 2);
202
- const tempFile = `/tmp/ws-opencode-config-${Date.now()}.json`;
203
- await fs.writeFile(tempFile, configJson, 'utf-8');
204
- try {
205
- await docker.execInContainer(containerName, ['mkdir', '-p', '/home/workspace/.config/opencode'], {
206
- user: 'workspace',
207
- });
208
- await docker.copyToContainer(containerName, tempFile, '/home/workspace/.config/opencode/opencode.json');
209
- await docker.execInContainer(containerName, ['chown', 'workspace:workspace', '/home/workspace/.config/opencode/opencode.json'], { user: 'root' });
210
- await docker.execInContainer(containerName, ['chmod', '600', '/home/workspace/.config/opencode/opencode.json'], { user: 'workspace' });
211
- }
212
- finally {
213
- try {
214
- await fs.unlink(tempFile);
215
- }
216
- catch {
217
- // Ignore cleanup errors
218
- }
219
- }
220
- }
221
131
  async copyGitConfig(containerName) {
222
132
  await copyCredentialToContainer({
223
133
  source: '~/.gitconfig',
@@ -279,9 +189,7 @@ export class WorkspaceManager {
279
189
  async setupWorkspaceCredentials(containerName, workspaceName) {
280
190
  await this.copyGitConfig(containerName);
281
191
  await this.copyCredentialFiles(containerName);
282
- await this.setupClaudeCodeConfig(containerName);
283
- await this.copyCodexCredentials(containerName);
284
- await this.setupOpencodeConfig(containerName);
192
+ await syncAllAgents(containerName, this.config);
285
193
  await this.copyPerryWorker(containerName);
286
194
  if (workspaceName) {
287
195
  await this.setupSSHKeys(containerName, workspaceName);
@@ -306,30 +214,70 @@ export class WorkspaceManager {
306
214
  user: 'root',
307
215
  });
308
216
  }
309
- async runPostStartScript(containerName) {
310
- const scriptPath = this.config.scripts.post_start;
311
- if (!scriptPath) {
217
+ async runUserScripts(containerName) {
218
+ const scriptPaths = this.config.scripts.post_start;
219
+ if (!scriptPaths || scriptPaths.length === 0) {
312
220
  return;
313
221
  }
314
- const expandedPath = expandPath(scriptPath);
222
+ const failOnError = this.config.scripts.fail_on_error ?? false;
223
+ for (const scriptPath of scriptPaths) {
224
+ const expandedPath = expandPath(scriptPath);
225
+ try {
226
+ const stat = await fs.stat(expandedPath);
227
+ if (stat.isDirectory()) {
228
+ await this.runScriptsFromDirectory(containerName, expandedPath, failOnError);
229
+ }
230
+ else if (stat.isFile()) {
231
+ await this.runSingleScript(containerName, expandedPath, failOnError);
232
+ }
233
+ }
234
+ catch (err) {
235
+ if (err.code === 'ENOENT') {
236
+ continue;
237
+ }
238
+ console.warn(`Error accessing script path ${expandedPath}:`, err);
239
+ if (failOnError) {
240
+ throw err;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ async runScriptsFromDirectory(containerName, dirPath, failOnError) {
246
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
247
+ const scripts = entries
248
+ .filter((e) => e.isFile() && e.name.endsWith('.sh'))
249
+ .map((e) => e.name)
250
+ .sort();
251
+ for (const scriptName of scripts) {
252
+ const scriptPath = path.join(dirPath, scriptName);
253
+ await this.runSingleScript(containerName, scriptPath, failOnError);
254
+ }
255
+ }
256
+ async runSingleScript(containerName, scriptPath, failOnError) {
257
+ const scriptName = path.basename(scriptPath);
258
+ const destPath = `/workspace/.perry-script-${scriptName}`;
315
259
  try {
316
- await fs.access(expandedPath);
260
+ await docker.copyToContainer(containerName, scriptPath, destPath);
261
+ await docker.execInContainer(containerName, ['chown', 'workspace:workspace', destPath], {
262
+ user: 'root',
263
+ });
264
+ await docker.execInContainer(containerName, ['chmod', '+x', destPath], {
265
+ user: 'workspace',
266
+ });
267
+ console.log(`[scripts] Running: ${scriptPath}`);
268
+ await docker.execInContainer(containerName, ['bash', destPath], {
269
+ user: 'workspace',
270
+ });
271
+ await docker.execInContainer(containerName, ['rm', '-f', destPath], {
272
+ user: 'workspace',
273
+ });
317
274
  }
318
- catch {
319
- console.warn(`Post-start script not found, skipping: ${expandedPath}`);
320
- return;
275
+ catch (err) {
276
+ console.warn(`[scripts] Error running ${scriptPath}:`, err);
277
+ if (failOnError) {
278
+ throw err;
279
+ }
321
280
  }
322
- const destPath = '/workspace/post-start.sh';
323
- await docker.copyToContainer(containerName, expandedPath, destPath);
324
- await docker.execInContainer(containerName, ['chown', 'workspace:workspace', destPath], {
325
- user: 'root',
326
- });
327
- await docker.execInContainer(containerName, ['chmod', '+x', destPath], {
328
- user: 'workspace',
329
- });
330
- await docker.execInContainer(containerName, ['bash', destPath], {
331
- user: 'workspace',
332
- });
333
281
  }
334
282
  async syncWorkspaceStatus(workspace) {
335
283
  if (workspace.status === 'creating') {
@@ -437,7 +385,7 @@ export class WorkspaceManager {
437
385
  await this.setupWorkspaceCredentials(containerName, name);
438
386
  workspace.status = 'running';
439
387
  await this.state.setWorkspace(workspace);
440
- await this.runPostStartScript(containerName);
388
+ await this.runUserScripts(containerName);
441
389
  return workspace;
442
390
  }
443
391
  catch (err) {
@@ -516,7 +464,7 @@ export class WorkspaceManager {
516
464
  workspace.status = 'running';
517
465
  workspace.lastUsed = new Date().toISOString();
518
466
  await this.state.setWorkspace(workspace);
519
- await this.runPostStartScript(containerName);
467
+ await this.runUserScripts(containerName);
520
468
  return workspace;
521
469
  }
522
470
  catch (err) {
@@ -593,4 +541,119 @@ export class WorkspaceManager {
593
541
  }
594
542
  await this.setupWorkspaceCredentials(containerName, name);
595
543
  }
544
+ async setPortForwards(name, forwards) {
545
+ const workspace = await this.state.getWorkspace(name);
546
+ if (!workspace) {
547
+ throw new Error(`Workspace '${name}' not found`);
548
+ }
549
+ workspace.ports.forwards = forwards;
550
+ await this.state.setWorkspace(workspace);
551
+ return workspace;
552
+ }
553
+ async getPortForwards(name) {
554
+ const workspace = await this.state.getWorkspace(name);
555
+ if (!workspace) {
556
+ throw new Error(`Workspace '${name}' not found`);
557
+ }
558
+ return workspace.ports.forwards || [];
559
+ }
560
+ async clone(sourceName, cloneName) {
561
+ const source = await this.state.getWorkspace(sourceName);
562
+ if (!source) {
563
+ throw new Error(`Workspace '${sourceName}' not found`);
564
+ }
565
+ const existing = await this.state.getWorkspace(cloneName);
566
+ if (existing) {
567
+ throw new Error(`Workspace '${cloneName}' already exists`);
568
+ }
569
+ const sourceContainerName = getContainerName(sourceName);
570
+ const cloneContainerName = getContainerName(cloneName);
571
+ const sourceVolumeName = `${VOLUME_PREFIX}${sourceName}`;
572
+ const sourceDockerVolume = `${VOLUME_PREFIX}${sourceName}-docker`;
573
+ const cloneVolumeName = `${VOLUME_PREFIX}${cloneName}`;
574
+ const cloneDockerVolume = `${VOLUME_PREFIX}${cloneName}-docker`;
575
+ const workspace = {
576
+ name: cloneName,
577
+ status: 'creating',
578
+ containerId: '',
579
+ created: new Date().toISOString(),
580
+ repo: source.repo,
581
+ ports: {
582
+ ssh: 0,
583
+ forwards: source.ports.forwards ? [...source.ports.forwards] : undefined,
584
+ },
585
+ lastUsed: new Date().toISOString(),
586
+ };
587
+ await this.state.setWorkspace(workspace);
588
+ const wasRunning = await docker.containerRunning(sourceContainerName);
589
+ try {
590
+ if (wasRunning) {
591
+ await docker.stopContainer(sourceContainerName);
592
+ }
593
+ await docker.cloneVolume(sourceVolumeName, cloneVolumeName);
594
+ await docker.cloneVolume(sourceDockerVolume, cloneDockerVolume);
595
+ if (wasRunning) {
596
+ await docker.startContainer(sourceContainerName);
597
+ }
598
+ const workspaceImage = await ensureWorkspaceImage();
599
+ const sshPort = await findAvailablePort(SSH_PORT_RANGE_START, SSH_PORT_RANGE_END);
600
+ const containerEnv = {
601
+ ...this.config.credentials.env,
602
+ };
603
+ if (this.config.agents?.github?.token) {
604
+ containerEnv.GITHUB_TOKEN = this.config.agents.github.token;
605
+ }
606
+ if (this.config.agents?.claude_code?.oauth_token) {
607
+ containerEnv.CLAUDE_CODE_OAUTH_TOKEN = this.config.agents.claude_code.oauth_token;
608
+ }
609
+ if (workspace.repo) {
610
+ containerEnv.WORKSPACE_REPO_URL = workspace.repo;
611
+ }
612
+ const containerId = await docker.createContainer({
613
+ name: cloneContainerName,
614
+ image: workspaceImage,
615
+ hostname: cloneName,
616
+ privileged: true,
617
+ restartPolicy: 'unless-stopped',
618
+ env: containerEnv,
619
+ volumes: [
620
+ { source: cloneVolumeName, target: '/home/workspace', readonly: false },
621
+ { source: cloneDockerVolume, target: '/var/lib/docker', readonly: false },
622
+ ],
623
+ ports: [{ hostPort: sshPort, containerPort: 22, protocol: 'tcp' }],
624
+ labels: {
625
+ 'workspace.name': cloneName,
626
+ 'workspace.managed': 'true',
627
+ },
628
+ });
629
+ workspace.containerId = containerId;
630
+ workspace.ports.ssh = sshPort;
631
+ await this.state.setWorkspace(workspace);
632
+ await docker.startContainer(cloneContainerName);
633
+ await docker.waitForContainerReady(cloneContainerName);
634
+ await this.setupWorkspaceCredentials(cloneContainerName, cloneName);
635
+ workspace.status = 'running';
636
+ await this.state.setWorkspace(workspace);
637
+ await this.runUserScripts(cloneContainerName);
638
+ return workspace;
639
+ }
640
+ catch (err) {
641
+ workspace.status = 'error';
642
+ await this.state.setWorkspace(workspace);
643
+ if (await docker.containerExists(cloneContainerName)) {
644
+ await docker.removeContainer(cloneContainerName, true).catch(() => { });
645
+ }
646
+ if (await docker.volumeExists(cloneVolumeName)) {
647
+ await docker.removeVolume(cloneVolumeName, true).catch(() => { });
648
+ }
649
+ if (await docker.volumeExists(cloneDockerVolume)) {
650
+ await docker.removeVolume(cloneDockerVolume, true).catch(() => { });
651
+ }
652
+ await this.state.deleteWorkspace(cloneName).catch(() => { });
653
+ if (wasRunning && !(await docker.containerRunning(sourceContainerName))) {
654
+ await docker.startContainer(sourceContainerName).catch(() => { });
655
+ }
656
+ throw err;
657
+ }
658
+ }
596
659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gricha/perry",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Self-contained CLI for spinning up Docker-in-Docker development environments with SSH and proxy helpers.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "build": "rm -rf ./dist && bun run build:ts && bun run build:worker && bun run build:web && bun link",
14
14
  "build:ts": "tsc && chmod +x dist/index.js",
15
15
  "build:worker": "bun build src/index.ts --compile --outfile dist/perry-worker --target=bun",
16
- "build:web": "cp src/shared/client-types.ts web/src/lib/types.ts && cd web && bun run build && cp -r dist ../dist/agent/web",
16
+ "build:web": "cd web && bun run build && cp -r dist ../dist/agent/web",
17
17
  "test": "vitest run",
18
18
  "test:web": "playwright test",
19
19
  "test:tui": "vitest run --config vitest.tui.config.js",
@@ -24,7 +24,8 @@
24
24
  "format:check": "oxfmt --check src/ test/",
25
25
  "check": "bun run lint && bun run format:check && bun x tsc --noEmit",
26
26
  "lint:web": "cd web && bun run lint",
27
- "validate": "bun run check && bun run build && bun run test && bun run lint:web && bun run test:web"
27
+ "validate": "bun run check && bun run build && bun run test && bun run lint:web && bun run test:web",
28
+ "build:binaries": "bun run scripts/build-binaries.ts"
28
29
  },
29
30
  "engines": {
30
31
  "bun": ">=1.3.5"