@gricha/perry 0.3.18 → 0.3.19

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.
@@ -174,7 +174,7 @@ export class ApiClient {
174
174
  return new ApiClientError('Unknown error', 0, 'UNKNOWN');
175
175
  }
176
176
  }
177
- export function createApiClient(worker, port) {
177
+ export function createApiClient(worker, port, timeoutMs) {
178
178
  let baseUrl;
179
179
  if (worker.includes('://')) {
180
180
  baseUrl = worker;
@@ -186,5 +186,5 @@ export function createApiClient(worker, port) {
186
186
  const effectivePort = port || DEFAULT_AGENT_PORT;
187
187
  baseUrl = `http://${worker}:${effectivePort}`;
188
188
  }
189
- return new ApiClient({ baseUrl });
189
+ return new ApiClient({ baseUrl, timeout: timeoutMs });
190
190
  }
package/dist/index.js CHANGED
@@ -118,9 +118,9 @@ async function checkLocalAgent() {
118
118
  return false;
119
119
  }
120
120
  }
121
- async function getClient() {
121
+ async function getClient(timeoutMs) {
122
122
  const worker = await getWorkerWithFallback();
123
- return createApiClient(worker);
123
+ return createApiClient(worker, undefined, timeoutMs);
124
124
  }
125
125
  async function getWorkerWithFallback() {
126
126
  let worker = await getWorker();
@@ -284,7 +284,7 @@ program
284
284
  .option('-a, --all', 'Sync all running workspaces')
285
285
  .action(async (name, options) => {
286
286
  try {
287
- const client = await getClient();
287
+ const client = await getClient(5 * 60 * 1000);
288
288
  if (options.all) {
289
289
  console.log('Syncing all running workspaces...');
290
290
  const result = await client.syncAllWorkspaces();
@@ -846,7 +846,7 @@ program
846
846
  try {
847
847
  console.log('');
848
848
  console.log('Syncing all running workspaces...');
849
- const client = createApiClient(`localhost:${agentPort}`);
849
+ const client = createApiClient(`localhost:${agentPort}`, undefined, 5 * 60 * 1000);
850
850
  const result = await client.syncAllWorkspaces();
851
851
  if (result.results.length === 0) {
852
852
  console.log('No running workspaces to sync.');
package/dist/perry-worker CHANGED
Binary file
@@ -31,7 +31,11 @@ async function isWorkerRunning(ip) {
31
31
  }
32
32
  }
33
33
  async function startWorkerInContainer(containerName) {
34
- await execInContainer(containerName, ['sh', '-c', 'nohup perry worker serve > /tmp/perry-worker.log 2>&1 &'], { user: 'workspace' });
34
+ await execInContainer(containerName, [
35
+ 'sh',
36
+ '-c',
37
+ "nohup sh -c 'if [ -x /usr/local/bin/perry ]; then exec /usr/local/bin/perry worker serve; else exec perry worker serve; fi' > /tmp/perry-worker.log 2>&1 &",
38
+ ], { user: 'workspace' });
35
39
  }
36
40
  async function ensureWorkerRunning(containerName) {
37
41
  const ip = await getContainerIp(containerName);
@@ -217,35 +217,38 @@ export class WorkspaceManager {
217
217
  }
218
218
  }
219
219
  }
220
- async setupWorkspaceCredentials(containerName, workspaceName) {
220
+ async setupWorkspaceCredentials(containerName, workspaceName, options) {
221
221
  await this.copyGitConfig(containerName);
222
222
  await this.copyCredentialFiles(containerName);
223
223
  await this.syncEnvironmentFile(containerName);
224
224
  await syncAllAgents(containerName, this.config);
225
225
  await this.copyPerryWorker(containerName);
226
- await this.startWorkerServer(containerName);
226
+ await this.ensurePerryOnPath(containerName);
227
+ await this.startWorkerServer(containerName, options);
227
228
  if (workspaceName) {
228
229
  await this.setupSSHKeys(containerName, workspaceName);
229
230
  }
230
231
  }
231
232
  async copyPerryWorker(containerName) {
232
233
  const installedPath = path.join(os.homedir(), '.perry', 'bin', 'perry');
234
+ const cwdDistPath = path.join(process.cwd(), 'dist', 'perry-worker');
233
235
  const distDir = path.dirname(new URL(import.meta.url).pathname);
234
236
  const distPath = path.join(distDir, '..', 'perry-worker');
235
- let workerBinaryPath = distPath;
236
- try {
237
- await fs.access(installedPath);
238
- workerBinaryPath = installedPath;
239
- }
240
- catch {
237
+ let workerBinaryPath = null;
238
+ for (const candidate of [installedPath, cwdDistPath, distPath]) {
241
239
  try {
242
- await fs.access(distPath);
240
+ await fs.access(candidate);
241
+ workerBinaryPath = candidate;
242
+ break;
243
243
  }
244
244
  catch {
245
- console.warn(`[sync] perry binary not found at ${installedPath} or ${distPath}, session discovery may not work`);
246
- return;
245
+ // Try next
247
246
  }
248
247
  }
248
+ if (!workerBinaryPath) {
249
+ console.warn(`[sync] perry binary not found at ${installedPath}, ${cwdDistPath}, or ${distPath}, session discovery may not work`);
250
+ return;
251
+ }
249
252
  const destPath = '/usr/local/bin/perry';
250
253
  await docker.copyToContainer(containerName, workerBinaryPath, destPath);
251
254
  await docker.execInContainer(containerName, ['chown', 'root:root', destPath], {
@@ -255,6 +258,13 @@ export class WorkspaceManager {
255
258
  user: 'root',
256
259
  });
257
260
  }
261
+ async ensurePerryOnPath(containerName) {
262
+ await docker.execInContainer(containerName, [
263
+ 'sh',
264
+ '-c',
265
+ 'if [ -x /usr/local/bin/perry ]; then mkdir -p /home/workspace/.local/bin && ln -sf /usr/local/bin/perry /home/workspace/.local/bin/perry; fi',
266
+ ], { user: 'workspace' });
267
+ }
258
268
  async updateWorkerBinary(name) {
259
269
  const workspace = await this.state.getWorkspace(name);
260
270
  if (!workspace) {
@@ -267,27 +277,42 @@ export class WorkspaceManager {
267
277
  }
268
278
  await docker.execInContainer(containerName, ['sh', '-c', 'pkill -f "perry worker serve" || true'], { user: 'workspace' });
269
279
  await this.copyPerryWorker(containerName);
270
- await this.startWorkerServer(containerName);
280
+ await this.startWorkerServer(containerName, { strictWorker: true });
271
281
  }
272
- async startWorkerServer(containerName) {
282
+ async startWorkerServer(containerName, options) {
273
283
  const WORKER_PORT = 7392;
274
284
  const ip = await docker.getContainerIp(containerName);
275
285
  if (!ip) {
276
286
  console.warn(`[sync] Could not get container IP for ${containerName}, skipping worker server`);
277
287
  return;
278
288
  }
289
+ const desiredVersion = pkg.version;
290
+ const hasSyncedPerry = (await docker.execInContainer(containerName, ['sh', '-c', 'test -x /usr/local/bin/perry'], {
291
+ user: 'workspace',
292
+ })).exitCode === 0;
279
293
  try {
280
294
  const healthResponse = await fetch(`http://${ip}:${WORKER_PORT}/health`, {
281
295
  signal: AbortSignal.timeout(1000),
282
296
  });
283
297
  if (healthResponse.ok) {
284
- return;
298
+ if (!hasSyncedPerry) {
299
+ return;
300
+ }
301
+ const health = (await healthResponse.json().catch(() => null));
302
+ if (health?.version === desiredVersion) {
303
+ return;
304
+ }
305
+ await docker.execInContainer(containerName, ['sh', '-c', 'pkill -f "perry worker serve" || true'], { user: 'workspace' });
285
306
  }
286
307
  }
287
308
  catch {
288
309
  // Worker not running, start it
289
310
  }
290
- await docker.execInContainer(containerName, ['sh', '-c', 'nohup perry worker serve > /tmp/perry-worker.log 2>&1 &'], { user: 'workspace' });
311
+ await docker.execInContainer(containerName, [
312
+ 'sh',
313
+ '-c',
314
+ "nohup sh -c 'if [ -x /usr/local/bin/perry ]; then exec /usr/local/bin/perry worker serve; else exec perry worker serve; fi' > /tmp/perry-worker.log 2>&1 &",
315
+ ], { user: 'workspace' });
291
316
  const deadline = Date.now() + 10000;
292
317
  while (Date.now() < deadline) {
293
318
  await new Promise((r) => setTimeout(r, 200));
@@ -295,7 +320,14 @@ export class WorkspaceManager {
295
320
  const response = await fetch(`http://${ip}:${WORKER_PORT}/health`, {
296
321
  signal: AbortSignal.timeout(500),
297
322
  });
298
- if (response.ok) {
323
+ if (!response.ok) {
324
+ continue;
325
+ }
326
+ if (!hasSyncedPerry) {
327
+ return;
328
+ }
329
+ const health = (await response.json().catch(() => null));
330
+ if (health?.version === desiredVersion) {
299
331
  return;
300
332
  }
301
333
  }
@@ -303,6 +335,9 @@ export class WorkspaceManager {
303
335
  // Not ready yet
304
336
  }
305
337
  }
338
+ if (options.strictWorker && hasSyncedPerry) {
339
+ throw new Error(`[sync] Worker server failed to start in ${containerName}. Check /tmp/perry-worker.log`);
340
+ }
306
341
  console.warn(`[sync] Worker server failed to start in ${containerName}`);
307
342
  }
308
343
  async runUserScripts(containerName) {
@@ -473,7 +508,7 @@ export class WorkspaceManager {
473
508
  await this.state.setWorkspace(workspace);
474
509
  await docker.startContainer(containerName);
475
510
  await docker.waitForContainerReady(containerName);
476
- await this.setupWorkspaceCredentials(containerName, name);
511
+ await this.setupWorkspaceCredentials(containerName, name, { strictWorker: false });
477
512
  workspace.status = 'running';
478
513
  await this.state.setWorkspace(workspace);
479
514
  await this.runUserScripts(containerName);
@@ -551,7 +586,7 @@ export class WorkspaceManager {
551
586
  }
552
587
  await docker.startContainer(containerName);
553
588
  await docker.waitForContainerReady(containerName);
554
- await this.setupWorkspaceCredentials(containerName, name);
589
+ await this.setupWorkspaceCredentials(containerName, name, { strictWorker: false });
555
590
  workspace.status = 'running';
556
591
  workspace.lastUsed = new Date().toISOString();
557
592
  await this.state.setWorkspace(workspace);
@@ -630,7 +665,7 @@ export class WorkspaceManager {
630
665
  if (!running) {
631
666
  throw new Error(`Workspace '${name}' is not running`);
632
667
  }
633
- await this.setupWorkspaceCredentials(containerName, name);
668
+ await this.setupWorkspaceCredentials(containerName, name, { strictWorker: true });
634
669
  }
635
670
  async setPortForwards(name, forwards) {
636
671
  const workspace = await this.state.getWorkspace(name);
@@ -722,7 +757,7 @@ export class WorkspaceManager {
722
757
  await this.state.setWorkspace(workspace);
723
758
  await docker.startContainer(cloneContainerName);
724
759
  await docker.waitForContainerReady(cloneContainerName);
725
- await this.setupWorkspaceCredentials(cloneContainerName, cloneName);
760
+ await this.setupWorkspaceCredentials(cloneContainerName, cloneName, { strictWorker: false });
726
761
  workspace.status = 'running';
727
762
  await this.state.setWorkspace(workspace);
728
763
  await this.runUserScripts(cloneContainerName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gricha/perry",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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": {