opencode-pilot 0.16.1 → 0.16.3
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 +3 -2
- package/service/utils.js +5 -0
- package/service/worktree.js +52 -1
- package/test/unit/actions.test.js +34 -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
|
|
|
@@ -578,7 +578,8 @@ export async function executeAction(item, config, options = {}) {
|
|
|
578
578
|
// Auto-detect worktree support: if not explicitly configured and server is running,
|
|
579
579
|
// check if the project has sandboxes (indicating worktree workflow is set up)
|
|
580
580
|
if (!worktreeMode && serverUrl) {
|
|
581
|
-
|
|
581
|
+
// Look up project info for this specific directory (not just /project/current)
|
|
582
|
+
const projectInfo = await getProjectInfoForDirectory(serverUrl, baseCwd, { fetch: options.fetch });
|
|
582
583
|
if (projectInfo?.sandboxes?.length > 0) {
|
|
583
584
|
debug(`executeAction: auto-detected worktree support (${projectInfo.sandboxes.length} sandboxes)`);
|
|
584
585
|
worktreeMode = 'new';
|
package/service/utils.js
CHANGED
|
@@ -61,6 +61,11 @@ export function isBot(username, type) {
|
|
|
61
61
|
// Check for [bot] suffix in username
|
|
62
62
|
if (username.toLowerCase().endsWith("[bot]")) return true;
|
|
63
63
|
|
|
64
|
+
// Known bot usernames without [bot] suffix
|
|
65
|
+
// Note: 'Copilot' is intentionally NOT included - Copilot review feedback is actionable
|
|
66
|
+
const knownBots = ['linear'];
|
|
67
|
+
if (knownBots.includes(username.toLowerCase())) return true;
|
|
68
|
+
|
|
64
69
|
return false;
|
|
65
70
|
}
|
|
66
71
|
|
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');
|
|
@@ -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,
|