opencode-pilot 0.7.2 → 0.8.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 +3 -3
- package/package.json +1 -1
- package/service/actions.js +151 -5
- package/service/poll-service.js +5 -0
- package/service/poller.js +1 -1
- package/service/repo-config.js +2 -2
- package/service/server.js +1 -1
- package/test/unit/actions.test.js +230 -0
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ npm install -g opencode-pilot
|
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
22
|
-
1. **Create config** - Copy [examples/config.yaml](examples/config.yaml) to `~/.config/opencode
|
|
22
|
+
1. **Create config** - Copy [examples/config.yaml](examples/config.yaml) to `~/.config/opencode/pilot/config.yaml` and customize
|
|
23
23
|
|
|
24
|
-
2. **Create templates** - Add prompt templates to `~/.config/opencode
|
|
24
|
+
2. **Create templates** - Add prompt templates to `~/.config/opencode/pilot/templates/`
|
|
25
25
|
|
|
26
26
|
3. **Enable the plugin** - Add to your `opencode.json`:
|
|
27
27
|
|
|
@@ -67,7 +67,7 @@ Three ways to configure sources, from simplest to most flexible:
|
|
|
67
67
|
|
|
68
68
|
### Prompt Templates
|
|
69
69
|
|
|
70
|
-
Create prompt templates as markdown files in `~/.config/opencode
|
|
70
|
+
Create prompt templates as markdown files in `~/.config/opencode/pilot/templates/`. Templates support placeholders like `{title}`, `{body}`, `{number}`, `{html_url}`, etc.
|
|
71
71
|
|
|
72
72
|
## CLI Commands
|
|
73
73
|
|
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -5,17 +5,139 @@
|
|
|
5
5
|
* Supports prompt_template for custom prompts (e.g., to invoke /devcontainer).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { spawn } from "child_process";
|
|
8
|
+
import { spawn, execSync } from "child_process";
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
10
10
|
import { debug } from "./logger.js";
|
|
11
11
|
import { getNestedValue } from "./utils.js";
|
|
12
12
|
import path from "path";
|
|
13
13
|
import os from "os";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Get running opencode server ports by parsing lsof output
|
|
17
|
+
* @returns {Promise<number[]>} Array of port numbers
|
|
18
|
+
*/
|
|
19
|
+
async function getOpencodePorts() {
|
|
20
|
+
try {
|
|
21
|
+
const output = execSync('lsof -i -P 2>/dev/null | grep -E "opencode.*LISTEN" || true', {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 30000
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const ports = [];
|
|
27
|
+
for (const line of output.split('\n')) {
|
|
28
|
+
// Parse lines like: opencode- 6897 athal 12u IPv4 ... TCP *:60993 (LISTEN)
|
|
29
|
+
const match = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
30
|
+
if (match) {
|
|
31
|
+
ports.push(parseInt(match[1], 10));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return ports;
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if targetPath is within or equal to worktree path
|
|
42
|
+
* @param {string} targetPath - The path we're looking for
|
|
43
|
+
* @param {string} worktree - The server's worktree path
|
|
44
|
+
* @param {string[]} sandboxes - Array of sandbox paths
|
|
45
|
+
* @returns {number} Match score (higher = better match, 0 = no match)
|
|
46
|
+
*/
|
|
47
|
+
function getPathMatchScore(targetPath, worktree, sandboxes = []) {
|
|
48
|
+
// Normalize paths
|
|
49
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
50
|
+
const normalizedWorktree = path.resolve(worktree);
|
|
51
|
+
|
|
52
|
+
// Exact sandbox match (highest priority)
|
|
53
|
+
for (const sandbox of sandboxes) {
|
|
54
|
+
const normalizedSandbox = path.resolve(sandbox);
|
|
55
|
+
if (normalizedTarget === normalizedSandbox || normalizedTarget.startsWith(normalizedSandbox + path.sep)) {
|
|
56
|
+
return normalizedSandbox.length + 1000; // Bonus for sandbox match
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Exact worktree match
|
|
61
|
+
if (normalizedTarget === normalizedWorktree) {
|
|
62
|
+
return normalizedWorktree.length + 500; // Bonus for exact match
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Target is subdirectory of worktree
|
|
66
|
+
if (normalizedTarget.startsWith(normalizedWorktree + path.sep)) {
|
|
67
|
+
return normalizedWorktree.length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Global project (worktree = "/") matches everything with lowest priority
|
|
71
|
+
if (normalizedWorktree === '/') {
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0; // No match
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Discover a running opencode server that matches the target directory
|
|
80
|
+
*
|
|
81
|
+
* Queries all running opencode servers and finds the best match based on:
|
|
82
|
+
* 1. Exact sandbox match (highest priority)
|
|
83
|
+
* 2. Exact worktree match
|
|
84
|
+
* 3. Target is subdirectory of worktree
|
|
85
|
+
* 4. Global project (worktree="/") as fallback
|
|
86
|
+
*
|
|
87
|
+
* @param {string} targetDir - The directory we want to work in
|
|
88
|
+
* @param {object} [options] - Options for testing/mocking
|
|
89
|
+
* @param {function} [options.getPorts] - Function to get server ports
|
|
90
|
+
* @param {function} [options.fetch] - Function to fetch URLs
|
|
91
|
+
* @returns {Promise<string|null>} Server URL (e.g., "http://localhost:4096") or null
|
|
92
|
+
*/
|
|
93
|
+
export async function discoverOpencodeServer(targetDir, options = {}) {
|
|
94
|
+
const getPorts = options.getPorts || getOpencodePorts;
|
|
95
|
+
const fetchFn = options.fetch || fetch;
|
|
96
|
+
|
|
97
|
+
const ports = await getPorts();
|
|
98
|
+
if (ports.length === 0) {
|
|
99
|
+
debug('discoverOpencodeServer: no servers found');
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
debug(`discoverOpencodeServer: checking ${ports.length} servers for ${targetDir}`);
|
|
104
|
+
|
|
105
|
+
let bestMatch = null;
|
|
106
|
+
let bestScore = 0;
|
|
107
|
+
|
|
108
|
+
for (const port of ports) {
|
|
109
|
+
const url = `http://localhost:${port}`;
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetchFn(`${url}/project/current`);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
debug(`discoverOpencodeServer: ${url} returned ${response.status}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const project = await response.json();
|
|
118
|
+
const worktree = project.worktree || '/';
|
|
119
|
+
const sandboxes = project.sandboxes || [];
|
|
120
|
+
|
|
121
|
+
const score = getPathMatchScore(targetDir, worktree, sandboxes);
|
|
122
|
+
debug(`discoverOpencodeServer: ${url} worktree=${worktree} score=${score}`);
|
|
123
|
+
|
|
124
|
+
if (score > bestScore) {
|
|
125
|
+
bestScore = score;
|
|
126
|
+
bestMatch = url;
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
debug(`discoverOpencodeServer: ${url} error: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
debug(`discoverOpencodeServer: best match=${bestMatch} score=${bestScore}`);
|
|
134
|
+
return bestMatch;
|
|
135
|
+
}
|
|
136
|
+
|
|
15
137
|
// Default templates directory
|
|
16
138
|
const DEFAULT_TEMPLATES_DIR = path.join(
|
|
17
139
|
os.homedir(),
|
|
18
|
-
".config/opencode
|
|
140
|
+
".config/opencode/pilot/templates"
|
|
19
141
|
);
|
|
20
142
|
|
|
21
143
|
/**
|
|
@@ -109,9 +231,10 @@ export function getActionConfig(source, repoConfig, defaults) {
|
|
|
109
231
|
* @param {object} item - Item to create session for
|
|
110
232
|
* @param {object} config - Merged action config
|
|
111
233
|
* @param {string} [templatesDir] - Templates directory path (for testing)
|
|
234
|
+
* @param {string} [serverUrl] - URL of running opencode server to attach to
|
|
112
235
|
* @returns {object} { args: string[], cwd: string }
|
|
113
236
|
*/
|
|
114
|
-
export function getCommandInfoNew(item, config, templatesDir) {
|
|
237
|
+
export function getCommandInfoNew(item, config, templatesDir, serverUrl) {
|
|
115
238
|
// Determine working directory: working_dir > path > repo_path > home
|
|
116
239
|
const workingDir = config.working_dir || config.path || config.repo_path || "~";
|
|
117
240
|
const cwd = expandPath(workingDir);
|
|
@@ -124,6 +247,11 @@ export function getCommandInfoNew(item, config, templatesDir) {
|
|
|
124
247
|
// Build command args
|
|
125
248
|
const args = ["opencode", "run"];
|
|
126
249
|
|
|
250
|
+
// Add --attach if server URL is provided
|
|
251
|
+
if (serverUrl) {
|
|
252
|
+
args.push("--attach", serverUrl);
|
|
253
|
+
}
|
|
254
|
+
|
|
127
255
|
// Add session title
|
|
128
256
|
args.push("--title", sessionName);
|
|
129
257
|
|
|
@@ -264,11 +392,29 @@ function runSpawn(args, options = {}) {
|
|
|
264
392
|
* @param {object} config - Repo config with action settings
|
|
265
393
|
* @param {object} [options] - Execution options
|
|
266
394
|
* @param {boolean} [options.dryRun] - If true, return command without executing
|
|
395
|
+
* @param {function} [options.discoverServer] - Custom server discovery function (for testing)
|
|
267
396
|
* @returns {Promise<object>} Result with command, stdout, stderr, exitCode
|
|
268
397
|
*/
|
|
269
398
|
export async function executeAction(item, config, options = {}) {
|
|
270
|
-
|
|
271
|
-
const
|
|
399
|
+
// Get working directory first to determine which server to attach to
|
|
400
|
+
const workingDir = config.working_dir || config.path || config.repo_path || "~";
|
|
401
|
+
const cwd = expandPath(workingDir);
|
|
402
|
+
|
|
403
|
+
// Discover running opencode server for this directory
|
|
404
|
+
const discoverFn = options.discoverServer || discoverOpencodeServer;
|
|
405
|
+
const serverUrl = await discoverFn(cwd);
|
|
406
|
+
|
|
407
|
+
debug(`executeAction: discovered server=${serverUrl} for cwd=${cwd}`);
|
|
408
|
+
|
|
409
|
+
// Build command info with server URL for --attach flag
|
|
410
|
+
const cmdInfo = getCommandInfoNew(item, config, undefined, serverUrl);
|
|
411
|
+
|
|
412
|
+
// Build command string for display
|
|
413
|
+
const quoteArgs = (args) => args.map(a =>
|
|
414
|
+
a.includes(" ") || a.includes("\n") ? `"${a.replace(/"/g, '\\"')}"` : a
|
|
415
|
+
).join(" ");
|
|
416
|
+
const cmdStr = quoteArgs(cmdInfo.args);
|
|
417
|
+
const command = cmdInfo.cwd ? `(cd ${cmdInfo.cwd} && ${cmdStr})` : cmdStr;
|
|
272
418
|
|
|
273
419
|
debug(`executeAction: command=${command}`);
|
|
274
420
|
debug(`executeAction: args=${JSON.stringify(cmdInfo.args)}, cwd=${cmdInfo.cwd}`);
|
package/service/poll-service.js
CHANGED
|
@@ -95,6 +95,11 @@ export async function pollOnce(options = {}) {
|
|
|
95
95
|
// Load configuration
|
|
96
96
|
loadRepoConfig(configPath);
|
|
97
97
|
|
|
98
|
+
// Ensure poller is initialized for state tracking
|
|
99
|
+
if (!pollerInstance) {
|
|
100
|
+
pollerInstance = createPoller({ configPath });
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
// Get all sources
|
|
99
104
|
const sources = getAllSources();
|
|
100
105
|
|
package/service/poller.js
CHANGED
|
@@ -282,7 +282,7 @@ export async function pollGenericSource(source, options = {}) {
|
|
|
282
282
|
* @returns {object} Poller instance
|
|
283
283
|
*/
|
|
284
284
|
export function createPoller(options = {}) {
|
|
285
|
-
const stateFile = options.stateFile || path.join(os.homedir(), '.config/opencode
|
|
285
|
+
const stateFile = options.stateFile || path.join(os.homedir(), '.config/opencode/pilot/poll-state.json');
|
|
286
286
|
const configPath = options.configPath;
|
|
287
287
|
|
|
288
288
|
// Load existing state
|
package/service/repo-config.js
CHANGED
|
@@ -20,13 +20,13 @@ import { expandPreset, expandGitHubShorthand, getProviderConfig } from "./preset
|
|
|
20
20
|
// Default config path
|
|
21
21
|
const DEFAULT_CONFIG_PATH = path.join(
|
|
22
22
|
os.homedir(),
|
|
23
|
-
".config/opencode
|
|
23
|
+
".config/opencode/pilot/config.yaml"
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
// Default templates directory
|
|
27
27
|
const DEFAULT_TEMPLATES_DIR = path.join(
|
|
28
28
|
os.homedir(),
|
|
29
|
-
".config/opencode
|
|
29
|
+
".config/opencode/pilot/templates"
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
// In-memory config cache (for testing and runtime)
|
package/service/server.js
CHANGED
|
@@ -13,7 +13,7 @@ import YAML from 'yaml'
|
|
|
13
13
|
|
|
14
14
|
// Default configuration
|
|
15
15
|
const DEFAULT_HTTP_PORT = 4097
|
|
16
|
-
const DEFAULT_REPOS_CONFIG = join(homedir(), '.config', 'opencode
|
|
16
|
+
const DEFAULT_REPOS_CONFIG = join(homedir(), '.config', 'opencode', 'pilot', 'config.yaml')
|
|
17
17
|
const DEFAULT_POLL_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -231,5 +231,235 @@ describe('actions.js', () => {
|
|
|
231
231
|
assert.ok(lastArg.includes('/devcontainer issue-66'), 'Prompt should include /devcontainer command');
|
|
232
232
|
assert.ok(lastArg.includes('Fix bug'), 'Prompt should include title');
|
|
233
233
|
});
|
|
234
|
+
|
|
235
|
+
test('includes --attach when serverUrl is provided', async () => {
|
|
236
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
237
|
+
|
|
238
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
239
|
+
const config = {
|
|
240
|
+
path: '~/code/backend',
|
|
241
|
+
prompt: 'default'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir, 'http://localhost:4096');
|
|
245
|
+
|
|
246
|
+
assert.ok(cmdInfo.args.includes('--attach'), 'Should include --attach flag');
|
|
247
|
+
assert.ok(cmdInfo.args.includes('http://localhost:4096'), 'Should include server URL');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('does not include --attach when serverUrl is null', async () => {
|
|
251
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
252
|
+
|
|
253
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
254
|
+
const config = {
|
|
255
|
+
path: '~/code/backend',
|
|
256
|
+
prompt: 'default'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir, null);
|
|
260
|
+
|
|
261
|
+
assert.ok(!cmdInfo.args.includes('--attach'), 'Should not include --attach flag');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('discoverOpencodeServer', () => {
|
|
266
|
+
test('returns null when no servers running', async () => {
|
|
267
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
268
|
+
|
|
269
|
+
// Mock with empty port list
|
|
270
|
+
const result = await discoverOpencodeServer('/some/path', { getPorts: async () => [] });
|
|
271
|
+
|
|
272
|
+
assert.strictEqual(result, null);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('returns matching server URL for exact worktree match', async () => {
|
|
276
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
277
|
+
|
|
278
|
+
const mockPorts = async () => [3000, 4000];
|
|
279
|
+
const mockFetch = async (url) => {
|
|
280
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
281
|
+
return { ok: true, json: async () => ({ worktree: '/Users/test/project-a', sandboxes: [] }) };
|
|
282
|
+
}
|
|
283
|
+
if (url === 'http://localhost:4000/project/current') {
|
|
284
|
+
return { ok: true, json: async () => ({ worktree: '/Users/test/project-b', sandboxes: [] }) };
|
|
285
|
+
}
|
|
286
|
+
return { ok: false };
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const result = await discoverOpencodeServer('/Users/test/project-b', {
|
|
290
|
+
getPorts: mockPorts,
|
|
291
|
+
fetch: mockFetch
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
assert.strictEqual(result, 'http://localhost:4000');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('returns matching server URL for subdirectory of worktree', async () => {
|
|
298
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
299
|
+
|
|
300
|
+
const mockPorts = async () => [3000];
|
|
301
|
+
const mockFetch = async (url) => {
|
|
302
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
303
|
+
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
304
|
+
}
|
|
305
|
+
return { ok: false };
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await discoverOpencodeServer('/Users/test/project/src/components', {
|
|
309
|
+
getPorts: mockPorts,
|
|
310
|
+
fetch: mockFetch
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
assert.strictEqual(result, 'http://localhost:3000');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('returns matching server URL when cwd is in sandboxes', async () => {
|
|
317
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
318
|
+
|
|
319
|
+
const mockPorts = async () => [3000];
|
|
320
|
+
const mockFetch = async (url) => {
|
|
321
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
322
|
+
return { ok: true, json: async () => ({
|
|
323
|
+
worktree: '/Users/test/project',
|
|
324
|
+
sandboxes: ['/Users/test/.opencode/worktree/abc/sandbox-1']
|
|
325
|
+
}) };
|
|
326
|
+
}
|
|
327
|
+
return { ok: false };
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const result = await discoverOpencodeServer('/Users/test/.opencode/worktree/abc/sandbox-1', {
|
|
331
|
+
getPorts: mockPorts,
|
|
332
|
+
fetch: mockFetch
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(result, 'http://localhost:3000');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('prefers more specific worktree match over less specific', async () => {
|
|
339
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
340
|
+
|
|
341
|
+
const mockPorts = async () => [3000, 4000];
|
|
342
|
+
const mockFetch = async (url) => {
|
|
343
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
344
|
+
// Global project
|
|
345
|
+
return { ok: true, json: async () => ({ worktree: '/', sandboxes: [] }) };
|
|
346
|
+
}
|
|
347
|
+
if (url === 'http://localhost:4000/project/current') {
|
|
348
|
+
// Specific project
|
|
349
|
+
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
350
|
+
}
|
|
351
|
+
return { ok: false };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = await discoverOpencodeServer('/Users/test/project/src', {
|
|
355
|
+
getPorts: mockPorts,
|
|
356
|
+
fetch: mockFetch
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Should prefer the more specific match (port 4000)
|
|
360
|
+
assert.strictEqual(result, 'http://localhost:4000');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('falls back to global project when no specific match', async () => {
|
|
364
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
365
|
+
|
|
366
|
+
const mockPorts = async () => [3000];
|
|
367
|
+
const mockFetch = async (url) => {
|
|
368
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
369
|
+
return { ok: true, json: async () => ({ worktree: '/', sandboxes: [] }) };
|
|
370
|
+
}
|
|
371
|
+
return { ok: false };
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = await discoverOpencodeServer('/Users/test/random/path', {
|
|
375
|
+
getPorts: mockPorts,
|
|
376
|
+
fetch: mockFetch
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
assert.strictEqual(result, 'http://localhost:3000');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('returns null when fetch fails for all servers', async () => {
|
|
383
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
384
|
+
|
|
385
|
+
const mockPorts = async () => [3000, 4000];
|
|
386
|
+
const mockFetch = async () => {
|
|
387
|
+
throw new Error('Connection refused');
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const result = await discoverOpencodeServer('/some/path', {
|
|
391
|
+
getPorts: mockPorts,
|
|
392
|
+
fetch: mockFetch
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
assert.strictEqual(result, null);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('skips servers that return non-ok response', async () => {
|
|
399
|
+
const { discoverOpencodeServer } = await import('../../service/actions.js');
|
|
400
|
+
|
|
401
|
+
const mockPorts = async () => [3000, 4000];
|
|
402
|
+
const mockFetch = async (url) => {
|
|
403
|
+
if (url === 'http://localhost:3000/project/current') {
|
|
404
|
+
return { ok: false };
|
|
405
|
+
}
|
|
406
|
+
if (url === 'http://localhost:4000/project/current') {
|
|
407
|
+
return { ok: true, json: async () => ({ worktree: '/Users/test/project', sandboxes: [] }) };
|
|
408
|
+
}
|
|
409
|
+
return { ok: false };
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const result = await discoverOpencodeServer('/Users/test/project', {
|
|
413
|
+
getPorts: mockPorts,
|
|
414
|
+
fetch: mockFetch
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
assert.strictEqual(result, 'http://localhost:4000');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('executeAction', () => {
|
|
422
|
+
test('discovers server and includes --attach in dry run', async () => {
|
|
423
|
+
const { executeAction } = await import('../../service/actions.js');
|
|
424
|
+
|
|
425
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
426
|
+
const config = {
|
|
427
|
+
path: tempDir,
|
|
428
|
+
prompt: 'default'
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Mock server discovery
|
|
432
|
+
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
433
|
+
|
|
434
|
+
const result = await executeAction(item, config, {
|
|
435
|
+
dryRun: true,
|
|
436
|
+
discoverServer: mockDiscoverServer
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
assert.ok(result.dryRun);
|
|
440
|
+
assert.ok(result.command.includes('--attach'), 'Command should include --attach flag');
|
|
441
|
+
assert.ok(result.command.includes('http://localhost:4096'), 'Command should include server URL');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('does not include --attach when no server discovered', async () => {
|
|
445
|
+
const { executeAction } = await import('../../service/actions.js');
|
|
446
|
+
|
|
447
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
448
|
+
const config = {
|
|
449
|
+
path: tempDir,
|
|
450
|
+
prompt: 'default'
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Mock no server found
|
|
454
|
+
const mockDiscoverServer = async () => null;
|
|
455
|
+
|
|
456
|
+
const result = await executeAction(item, config, {
|
|
457
|
+
dryRun: true,
|
|
458
|
+
discoverServer: mockDiscoverServer
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
assert.ok(result.dryRun);
|
|
462
|
+
assert.ok(!result.command.includes('--attach'), 'Command should not include --attach flag');
|
|
463
|
+
});
|
|
234
464
|
});
|
|
235
465
|
});
|