opencode-pilot 0.9.0 → 0.9.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/package.json +1 -1
- package/service/actions.js +29 -2
- package/test/unit/actions.test.js +83 -9
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -18,7 +18,9 @@ import os from "os";
|
|
|
18
18
|
*/
|
|
19
19
|
async function getOpencodePorts() {
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
// Use full path to lsof since /usr/sbin may not be in PATH in all contexts
|
|
22
|
+
// (e.g., when running as a service or from certain shell environments)
|
|
23
|
+
const output = execSync('/usr/sbin/lsof -i -P 2>/dev/null | grep -E "opencode.*LISTEN" || true', {
|
|
22
24
|
encoding: 'utf-8',
|
|
23
25
|
timeout: 30000
|
|
24
26
|
});
|
|
@@ -75,6 +77,24 @@ function getPathMatchScore(targetPath, worktree, sandboxes = []) {
|
|
|
75
77
|
return 0; // No match
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Verify a server is healthy by checking it returns valid project data
|
|
82
|
+
* @param {string} url - Server URL
|
|
83
|
+
* @param {object} project - Project data already fetched from /project/current
|
|
84
|
+
* @returns {boolean} True if server appears healthy
|
|
85
|
+
*/
|
|
86
|
+
function isServerHealthy(project) {
|
|
87
|
+
// A healthy server should return a project with an id and time.created
|
|
88
|
+
// Stale/broken servers may return HTML or incomplete JSON
|
|
89
|
+
return !!(
|
|
90
|
+
project &&
|
|
91
|
+
typeof project === 'object' &&
|
|
92
|
+
project.id &&
|
|
93
|
+
project.time &&
|
|
94
|
+
typeof project.time.created === 'number'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
/**
|
|
79
99
|
* Discover a running opencode server that matches the target directory
|
|
80
100
|
*
|
|
@@ -82,7 +102,7 @@ function getPathMatchScore(targetPath, worktree, sandboxes = []) {
|
|
|
82
102
|
* 1. Exact sandbox match (highest priority)
|
|
83
103
|
* 2. Exact worktree match
|
|
84
104
|
* 3. Target is subdirectory of worktree
|
|
85
|
-
* 4. Global project (worktree="/") as fallback
|
|
105
|
+
* 4. Global project (worktree="/") as fallback (e.g., OpenCode Desktop)
|
|
86
106
|
*
|
|
87
107
|
* @param {string} targetDir - The directory we want to work in
|
|
88
108
|
* @param {object} [options] - Options for testing/mocking
|
|
@@ -115,6 +135,13 @@ export async function discoverOpencodeServer(targetDir, options = {}) {
|
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
const project = await response.json();
|
|
138
|
+
|
|
139
|
+
// Health check: verify response has expected structure
|
|
140
|
+
if (!isServerHealthy(project)) {
|
|
141
|
+
debug(`discoverOpencodeServer: ${url} failed health check (invalid project data)`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
118
145
|
const worktree = project.worktree || '/';
|
|
119
146
|
const sandboxes = project.sandboxes || [];
|
|
120
147
|
|
|
@@ -310,10 +310,10 @@ describe('actions.js', () => {
|
|
|
310
310
|
const mockPorts = async () => [3000, 4000];
|
|
311
311
|
const mockFetch = async (url) => {
|
|
312
312
|
if (url === 'http://localhost:3000/project/current') {
|
|
313
|
-
return { ok: true, json: async () => ({ worktree: '/Users/test/project-a', sandboxes: [] }) };
|
|
313
|
+
return { ok: true, json: async () => ({ id: 'proj-a', worktree: '/Users/test/project-a', sandboxes: [], time: { created: 1 } }) };
|
|
314
314
|
}
|
|
315
315
|
if (url === 'http://localhost:4000/project/current') {
|
|
316
|
-
return { ok: true, json: async () => ({ worktree: '/Users/test/project-b', sandboxes: [] }) };
|
|
316
|
+
return { ok: true, json: async () => ({ id: 'proj-b', worktree: '/Users/test/project-b', sandboxes: [], time: { created: 1 } }) };
|
|
317
317
|
}
|
|
318
318
|
return { ok: false };
|
|
319
319
|
};
|
|
@@ -332,7 +332,7 @@ describe('actions.js', () => {
|
|
|
332
332
|
const mockPorts = async () => [3000];
|
|
333
333
|
const mockFetch = async (url) => {
|
|
334
334
|
if (url === 'http://localhost:3000/project/current') {
|
|
335
|
-
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
335
|
+
return { ok: true, json: async () => ({ id: 'proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } }) };
|
|
336
336
|
}
|
|
337
337
|
return { ok: false };
|
|
338
338
|
};
|
|
@@ -352,8 +352,10 @@ describe('actions.js', () => {
|
|
|
352
352
|
const mockFetch = async (url) => {
|
|
353
353
|
if (url === 'http://localhost:3000/project/current') {
|
|
354
354
|
return { ok: true, json: async () => ({
|
|
355
|
+
id: 'proj',
|
|
355
356
|
worktree: '/Users/test/project',
|
|
356
|
-
sandboxes: ['/Users/test/.opencode/worktree/abc/sandbox-1']
|
|
357
|
+
sandboxes: ['/Users/test/.opencode/worktree/abc/sandbox-1'],
|
|
358
|
+
time: { created: 1 }
|
|
357
359
|
}) };
|
|
358
360
|
}
|
|
359
361
|
return { ok: false };
|
|
@@ -374,11 +376,11 @@ describe('actions.js', () => {
|
|
|
374
376
|
const mockFetch = async (url) => {
|
|
375
377
|
if (url === 'http://localhost:3000/project/current') {
|
|
376
378
|
// Global project
|
|
377
|
-
return { ok: true, json: async () => ({ worktree: '/', sandboxes: [] }) };
|
|
379
|
+
return { ok: true, json: async () => ({ id: 'global', worktree: '/', sandboxes: [], time: { created: 1 } }) };
|
|
378
380
|
}
|
|
379
381
|
if (url === 'http://localhost:4000/project/current') {
|
|
380
382
|
// Specific project
|
|
381
|
-
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
383
|
+
return { ok: true, json: async () => ({ id: 'proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } }) };
|
|
382
384
|
}
|
|
383
385
|
return { ok: false };
|
|
384
386
|
};
|
|
@@ -392,17 +394,19 @@ describe('actions.js', () => {
|
|
|
392
394
|
assert.strictEqual(result, 'http://localhost:4000');
|
|
393
395
|
});
|
|
394
396
|
|
|
395
|
-
test('falls back to global project when no specific match', async () => {
|
|
397
|
+
test('falls back to healthy global project when no specific match', async () => {
|
|
396
398
|
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
397
399
|
|
|
398
400
|
const mockPorts = async () => [3000];
|
|
399
401
|
const mockFetch = async (url) => {
|
|
400
402
|
if (url === 'http://localhost:3000/project/current') {
|
|
401
|
-
|
|
403
|
+
// Healthy global project with proper structure
|
|
404
|
+
return { ok: true, json: async () => ({ id: 'global', worktree: '/', sandboxes: [], time: { created: 1 } }) };
|
|
402
405
|
}
|
|
403
406
|
return { ok: false };
|
|
404
407
|
};
|
|
405
408
|
|
|
409
|
+
// Global servers (like OpenCode Desktop) should work as fallback
|
|
406
410
|
const result = await discoverOpencodeServer('/Users/test/random/path', {
|
|
407
411
|
getPorts: mockPorts,
|
|
408
412
|
fetch: mockFetch
|
|
@@ -436,7 +440,56 @@ describe('actions.js', () => {
|
|
|
436
440
|
return { ok: false };
|
|
437
441
|
}
|
|
438
442
|
if (url === 'http://localhost:4000/project/current') {
|
|
439
|
-
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
443
|
+
return { ok: true, json: async () => ({ id: 'proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } }) };
|
|
444
|
+
}
|
|
445
|
+
return { ok: false };
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const result = await discoverOpencodeServer('/Users/test/project', {
|
|
449
|
+
getPorts: mockPorts,
|
|
450
|
+
fetch: mockFetch
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
assert.strictEqual(result, 'http://localhost:4000');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('skips servers that return invalid JSON', async () => {
|
|
457
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
458
|
+
|
|
459
|
+
const mockPorts = async () => [3000, 4000];
|
|
460
|
+
const mockFetch = async (url) => {
|
|
461
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
462
|
+
// Stale server returning HTML
|
|
463
|
+
return {
|
|
464
|
+
ok: true,
|
|
465
|
+
json: async () => { throw new SyntaxError('Unexpected token < in JSON'); }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (url === 'http://localhost:4000/project/current') {
|
|
469
|
+
return { ok: true, json: async () => ({ id: 'proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } }) };
|
|
470
|
+
}
|
|
471
|
+
return { ok: false };
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const result = await discoverOpencodeServer('/Users/test/project', {
|
|
475
|
+
getPorts: mockPorts,
|
|
476
|
+
fetch: mockFetch
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
assert.strictEqual(result, 'http://localhost:4000');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('skips servers with incomplete project data (missing time)', async () => {
|
|
483
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
484
|
+
|
|
485
|
+
const mockPorts = async () => [3000, 4000];
|
|
486
|
+
const mockFetch = async (url) => {
|
|
487
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
488
|
+
// Server returning incomplete response (missing time.created)
|
|
489
|
+
return { ok: true, json: async () => ({ id: 'broken', worktree: '/Users/test/project', sandboxes: [] }) };
|
|
490
|
+
}
|
|
491
|
+
if (url === 'http://localhost:4000/project/current') {
|
|
492
|
+
return { ok: true, json: async () => ({ id: 'proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } }) };
|
|
440
493
|
}
|
|
441
494
|
return { ok: false };
|
|
442
495
|
};
|
|
@@ -448,6 +501,27 @@ describe('actions.js', () => {
|
|
|
448
501
|
|
|
449
502
|
assert.strictEqual(result, 'http://localhost:4000');
|
|
450
503
|
});
|
|
504
|
+
|
|
505
|
+
test('returns null when server returns unhealthy project data', async () => {
|
|
506
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
507
|
+
|
|
508
|
+
const mockPorts = async () => [3000];
|
|
509
|
+
const mockFetch = async (url) => {
|
|
510
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
511
|
+
// Server returns incomplete/unhealthy data (missing id and time)
|
|
512
|
+
return { ok: true, json: async () => ({ worktree: '/', sandboxes: [] }) };
|
|
513
|
+
}
|
|
514
|
+
return { ok: false };
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Server that returns unhealthy project data should be skipped
|
|
518
|
+
const result = await discoverOpencodeServer('/Users/test/specific-project', {
|
|
519
|
+
getPorts: mockPorts,
|
|
520
|
+
fetch: mockFetch
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
assert.strictEqual(result, null, 'Should return null when server returns unhealthy project data');
|
|
524
|
+
});
|
|
451
525
|
});
|
|
452
526
|
|
|
453
527
|
describe('executeAction', () => {
|