opencode-pilot 0.16.2 → 0.16.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.
- package/package.json +1 -1
- package/service/actions.js +17 -3
- package/service/poll-service.js +6 -2
- package/service/worktree.js +52 -1
- package/test/unit/actions.test.js +76 -26
- package/test/unit/worktree.test.js +20 -0
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync, existsSync } from "fs";
|
|
|
10
10
|
import { debug } from "./logger.js";
|
|
11
11
|
import { getNestedValue } from "./utils.js";
|
|
12
12
|
import { getServerPort } from "./repo-config.js";
|
|
13
|
-
import { resolveWorktreeDirectory, getProjectInfo } from "./worktree.js";
|
|
13
|
+
import { resolveWorktreeDirectory, getProjectInfo, getProjectInfoForDirectory } from "./worktree.js";
|
|
14
14
|
import path from "path";
|
|
15
15
|
import os from "os";
|
|
16
16
|
|
|
@@ -470,6 +470,8 @@ function runSpawn(args, options = {}) {
|
|
|
470
470
|
export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
|
|
471
471
|
const fetchFn = options.fetch || fetch;
|
|
472
472
|
|
|
473
|
+
let session = null;
|
|
474
|
+
|
|
473
475
|
try {
|
|
474
476
|
// Step 1: Create a new session with the directory parameter
|
|
475
477
|
const sessionUrl = new URL('/session', serverUrl);
|
|
@@ -486,7 +488,7 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
486
488
|
throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
|
|
487
489
|
}
|
|
488
490
|
|
|
489
|
-
|
|
491
|
+
session = await createResponse.json();
|
|
490
492
|
debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
|
|
491
493
|
|
|
492
494
|
// Step 2: Update session title if provided
|
|
@@ -543,6 +545,17 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
543
545
|
};
|
|
544
546
|
} catch (err) {
|
|
545
547
|
debug(`createSessionViaApi: error - ${err.message}`);
|
|
548
|
+
// If session was created but message failed, still return success
|
|
549
|
+
// to prevent re-processing (the session exists, user can send message manually)
|
|
550
|
+
if (session) {
|
|
551
|
+
debug(`createSessionViaApi: session ${session.id} was created, marking as success despite message error`);
|
|
552
|
+
return {
|
|
553
|
+
success: true,
|
|
554
|
+
sessionId: session.id,
|
|
555
|
+
directory,
|
|
556
|
+
warning: err.message,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
546
559
|
return {
|
|
547
560
|
success: false,
|
|
548
561
|
error: err.message,
|
|
@@ -578,7 +591,8 @@ export async function executeAction(item, config, options = {}) {
|
|
|
578
591
|
// Auto-detect worktree support: if not explicitly configured and server is running,
|
|
579
592
|
// check if the project has sandboxes (indicating worktree workflow is set up)
|
|
580
593
|
if (!worktreeMode && serverUrl) {
|
|
581
|
-
|
|
594
|
+
// Look up project info for this specific directory (not just /project/current)
|
|
595
|
+
const projectInfo = await getProjectInfoForDirectory(serverUrl, baseCwd, { fetch: options.fetch });
|
|
582
596
|
if (projectInfo?.sandboxes?.length > 0) {
|
|
583
597
|
debug(`executeAction: auto-detected worktree support (${projectInfo.sandboxes.length} sandboxes)`);
|
|
584
598
|
worktreeMode = 'new';
|
package/service/poll-service.js
CHANGED
|
@@ -227,9 +227,13 @@ export async function pollOnce(options = {}) {
|
|
|
227
227
|
itemUpdatedAt: item.updated_at || null,
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
if (result.warning) {
|
|
231
|
+
console.log(`[poll] Started session for ${item.id} (warning: ${result.warning})`);
|
|
232
|
+
} else {
|
|
233
|
+
console.log(`[poll] Started session for ${item.id}`);
|
|
234
|
+
}
|
|
231
235
|
} else {
|
|
232
|
-
console.error(`[poll] Failed to start session: ${result.stderr}`);
|
|
236
|
+
console.error(`[poll] Failed to start session: ${result.error || result.stderr || 'unknown error'}`);
|
|
233
237
|
}
|
|
234
238
|
} catch (err) {
|
|
235
239
|
console.error(`[poll] Error executing action: ${err.message}`);
|
package/service/worktree.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function listWorktrees(serverUrl, options = {}) {
|
|
|
40
40
|
*
|
|
41
41
|
* @param {string} serverUrl - OpenCode server URL (e.g., "http://localhost:4096")
|
|
42
42
|
* @param {object} [options] - Options
|
|
43
|
+
* @param {string} [options.directory] - Project directory (required for global server)
|
|
43
44
|
* @param {string} [options.name] - Optional name for the worktree
|
|
44
45
|
* @param {string} [options.startCommand] - Optional startup script to run after creation
|
|
45
46
|
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
@@ -53,7 +54,14 @@ export async function createWorktree(serverUrl, options = {}) {
|
|
|
53
54
|
if (options.name) body.name = options.name;
|
|
54
55
|
if (options.startCommand) body.startCommand = options.startCommand;
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
// Build URL with directory parameter if provided
|
|
58
|
+
// This tells the global server which project to create the worktree for
|
|
59
|
+
let url = `${serverUrl}/experimental/worktree`;
|
|
60
|
+
if (options.directory) {
|
|
61
|
+
url += `?directory=${encodeURIComponent(options.directory)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await fetchFn(url, {
|
|
57
65
|
method: 'POST',
|
|
58
66
|
headers: { 'Content-Type': 'application/json' },
|
|
59
67
|
body: JSON.stringify(body),
|
|
@@ -112,6 +120,48 @@ export async function getProjectInfo(serverUrl, options = {}) {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Get project info for a specific directory by querying all projects
|
|
125
|
+
*
|
|
126
|
+
* @param {string} serverUrl - OpenCode server URL
|
|
127
|
+
* @param {string} directory - Directory path to find project for
|
|
128
|
+
* @param {object} [options] - Options
|
|
129
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
130
|
+
* @returns {Promise<object|null>} Project info or null if not found
|
|
131
|
+
*/
|
|
132
|
+
export async function getProjectInfoForDirectory(serverUrl, directory, options = {}) {
|
|
133
|
+
const fetchFn = options.fetch || fetch;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetchFn(`${serverUrl}/project`);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
debug(`getProjectInfoForDirectory: ${serverUrl} returned ${response.status}`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const projects = await response.json();
|
|
144
|
+
|
|
145
|
+
// Find project matching this directory, preferring ones with sandboxes
|
|
146
|
+
const matches = projects.filter(p => p.worktree === directory);
|
|
147
|
+
|
|
148
|
+
if (matches.length === 0) {
|
|
149
|
+
debug(`getProjectInfoForDirectory: no project found for ${directory}`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Prefer the project with sandboxes (if multiple exist for same worktree)
|
|
154
|
+
const withSandboxes = matches.find(p => p.sandboxes?.length > 0);
|
|
155
|
+
const project = withSandboxes || matches[0];
|
|
156
|
+
|
|
157
|
+
debug(`getProjectInfoForDirectory: found project ${project.id} for ${directory} with ${project.sandboxes?.length || 0} sandboxes`);
|
|
158
|
+
return project;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
debug(`getProjectInfoForDirectory: error - ${err.message}`);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
115
165
|
/**
|
|
116
166
|
* Resolve the working directory based on worktree configuration
|
|
117
167
|
*
|
|
@@ -147,6 +197,7 @@ export async function resolveWorktreeDirectory(serverUrl, baseDir, worktreeConfi
|
|
|
147
197
|
// "new" - create a fresh worktree via OpenCode API
|
|
148
198
|
if (worktreeValue === "new") {
|
|
149
199
|
const result = await createWorktree(serverUrl, {
|
|
200
|
+
directory: baseDir,
|
|
150
201
|
name: worktreeConfig.worktreeName,
|
|
151
202
|
fetch: options.fetch,
|
|
152
203
|
});
|
|
@@ -588,8 +588,12 @@ describe('actions.js', () => {
|
|
|
588
588
|
|
|
589
589
|
// Mock worktree creation via fetch
|
|
590
590
|
const mockFetch = async (url, opts) => {
|
|
591
|
-
// Worktree creation endpoint
|
|
592
|
-
if (url
|
|
591
|
+
// Worktree creation endpoint - now includes directory query param
|
|
592
|
+
if (url.startsWith('http://localhost:4096/experimental/worktree') && opts?.method === 'POST') {
|
|
593
|
+
// Verify directory parameter is passed
|
|
594
|
+
const urlObj = new URL(url);
|
|
595
|
+
assert.strictEqual(urlObj.searchParams.get('directory'), tempDir,
|
|
596
|
+
'Should pass directory as query param');
|
|
593
597
|
const body = JSON.parse(opts.body);
|
|
594
598
|
assert.strictEqual(body.name, 'feature-branch', 'Should pass worktree name');
|
|
595
599
|
return {
|
|
@@ -708,25 +712,27 @@ describe('actions.js', () => {
|
|
|
708
712
|
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
709
713
|
|
|
710
714
|
// Track API calls
|
|
711
|
-
let
|
|
715
|
+
let projectListCalled = false;
|
|
712
716
|
let worktreeCreateCalled = false;
|
|
713
717
|
|
|
714
718
|
const mockFetch = async (url, opts) => {
|
|
715
|
-
// Project
|
|
716
|
-
if (url === 'http://localhost:4096/project
|
|
717
|
-
|
|
719
|
+
// Project list endpoint - returns projects including one with sandboxes
|
|
720
|
+
if (url === 'http://localhost:4096/project') {
|
|
721
|
+
projectListCalled = true;
|
|
718
722
|
return {
|
|
719
723
|
ok: true,
|
|
720
|
-
json: async () => (
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
724
|
+
json: async () => ([
|
|
725
|
+
{
|
|
726
|
+
id: 'proj-123',
|
|
727
|
+
worktree: tempDir,
|
|
728
|
+
sandboxes: ['/data/worktree/proj-123/sandbox-1'],
|
|
729
|
+
time: { created: 1 }
|
|
730
|
+
}
|
|
731
|
+
])
|
|
726
732
|
};
|
|
727
733
|
}
|
|
728
|
-
// Worktree creation endpoint
|
|
729
|
-
if (url
|
|
734
|
+
// Worktree creation endpoint - now includes directory query param
|
|
735
|
+
if (url.startsWith('http://localhost:4096/experimental/worktree') && opts?.method === 'POST') {
|
|
730
736
|
worktreeCreateCalled = true;
|
|
731
737
|
return {
|
|
732
738
|
ok: true,
|
|
@@ -747,7 +753,7 @@ describe('actions.js', () => {
|
|
|
747
753
|
});
|
|
748
754
|
|
|
749
755
|
assert.ok(result.dryRun);
|
|
750
|
-
assert.ok(
|
|
756
|
+
assert.ok(projectListCalled, 'Should call /project to find project by directory');
|
|
751
757
|
assert.ok(worktreeCreateCalled, 'Should auto-create worktree when sandboxes detected');
|
|
752
758
|
assert.ok(result.command.includes('/data/worktree/proj-123/new-sandbox'),
|
|
753
759
|
'Should use newly created worktree directory');
|
|
@@ -766,21 +772,23 @@ describe('actions.js', () => {
|
|
|
766
772
|
// Mock server discovery
|
|
767
773
|
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
768
774
|
|
|
769
|
-
let
|
|
775
|
+
let projectListCalled = false;
|
|
770
776
|
let worktreeCreateCalled = false;
|
|
771
777
|
|
|
772
778
|
const mockFetch = async (url, opts) => {
|
|
773
|
-
// Project
|
|
774
|
-
if (url === 'http://localhost:4096/project
|
|
775
|
-
|
|
779
|
+
// Project list endpoint - returns project with empty sandboxes
|
|
780
|
+
if (url === 'http://localhost:4096/project') {
|
|
781
|
+
projectListCalled = true;
|
|
776
782
|
return {
|
|
777
783
|
ok: true,
|
|
778
|
-
json: async () => (
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
+
json: async () => ([
|
|
785
|
+
{
|
|
786
|
+
id: 'proj-456',
|
|
787
|
+
worktree: tempDir,
|
|
788
|
+
sandboxes: [],
|
|
789
|
+
time: { created: 1 }
|
|
790
|
+
}
|
|
791
|
+
])
|
|
784
792
|
};
|
|
785
793
|
}
|
|
786
794
|
if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
|
|
@@ -796,7 +804,7 @@ describe('actions.js', () => {
|
|
|
796
804
|
});
|
|
797
805
|
|
|
798
806
|
assert.ok(result.dryRun);
|
|
799
|
-
assert.ok(
|
|
807
|
+
assert.ok(projectListCalled, 'Should call /project to find project by directory');
|
|
800
808
|
assert.ok(!worktreeCreateCalled, 'Should NOT create worktree when no sandboxes');
|
|
801
809
|
assert.ok(result.command.includes(tempDir),
|
|
802
810
|
'Should use base directory when no worktree workflow detected');
|
|
@@ -922,5 +930,47 @@ describe('actions.js', () => {
|
|
|
922
930
|
assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
|
|
923
931
|
assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
|
|
924
932
|
});
|
|
933
|
+
|
|
934
|
+
test('returns success with warning when session created but message fails', async () => {
|
|
935
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
936
|
+
|
|
937
|
+
const mockSessionId = 'ses_partial123';
|
|
938
|
+
|
|
939
|
+
const mockFetch = async (url, opts) => {
|
|
940
|
+
const urlObj = new URL(url);
|
|
941
|
+
|
|
942
|
+
// Session creation succeeds
|
|
943
|
+
if (urlObj.pathname === '/session' && opts?.method === 'POST') {
|
|
944
|
+
return {
|
|
945
|
+
ok: true,
|
|
946
|
+
json: async () => ({ id: mockSessionId }),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Message send fails
|
|
951
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
952
|
+
return {
|
|
953
|
+
ok: false,
|
|
954
|
+
status: 500,
|
|
955
|
+
text: async () => 'Message send failed',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return { ok: true, json: async () => ({}) };
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const result = await createSessionViaApi(
|
|
963
|
+
'http://localhost:4096',
|
|
964
|
+
'/path/to/project',
|
|
965
|
+
'Fix the bug',
|
|
966
|
+
{ fetch: mockFetch }
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// Should return success because session was created
|
|
970
|
+
assert.ok(result.success, 'Should return success when session was created');
|
|
971
|
+
assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
|
|
972
|
+
assert.ok(result.warning, 'Should include warning about message failure');
|
|
973
|
+
assert.ok(result.warning.includes('Failed to send message'), 'Warning should mention message failure');
|
|
974
|
+
});
|
|
925
975
|
});
|
|
926
976
|
});
|
|
@@ -82,6 +82,26 @@ describe("worktree", () => {
|
|
|
82
82
|
assert.strictEqual(body.name, "my-feature");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
it("passes directory as query parameter", async () => {
|
|
86
|
+
const mockFetch = mock.fn(async (url, options) => ({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: async () => ({
|
|
89
|
+
name: "test-worktree",
|
|
90
|
+
branch: "opencode/test-worktree",
|
|
91
|
+
directory: "/data/worktree/abc123/test-worktree",
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
await createWorktree("http://localhost:4096", {
|
|
96
|
+
directory: "/Users/test/code/my-project",
|
|
97
|
+
fetch: mockFetch,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const calledUrl = mockFetch.mock.calls[0].arguments[0];
|
|
101
|
+
assert.ok(calledUrl.includes("directory="), "URL should include directory parameter");
|
|
102
|
+
assert.ok(calledUrl.includes(encodeURIComponent("/Users/test/code/my-project")), "URL should include encoded directory path");
|
|
103
|
+
});
|
|
104
|
+
|
|
85
105
|
it("returns error on failure", async () => {
|
|
86
106
|
const mockFetch = mock.fn(async () => ({
|
|
87
107
|
ok: false,
|