opencode-hive 0.4.2 → 0.5.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 +0 -2
- package/dist/e2e/opencode-runtime-smoke.test.d.ts +1 -0
- package/dist/e2e/opencode-runtime-smoke.test.js +243 -0
- package/dist/e2e/plugin-smoke.test.d.ts +1 -0
- package/dist/e2e/plugin-smoke.test.js +127 -0
- package/dist/index.js +273 -75
- package/dist/services/contextService.d.ts +15 -0
- package/dist/services/contextService.js +59 -0
- package/dist/services/featureService.d.ts +0 -2
- package/dist/services/featureService.js +1 -11
- package/dist/services/featureService.test.d.ts +1 -0
- package/dist/services/featureService.test.js +127 -0
- package/dist/services/planService.test.d.ts +1 -0
- package/dist/services/planService.test.js +115 -0
- package/dist/services/sessionService.d.ts +31 -0
- package/dist/services/sessionService.js +125 -0
- package/dist/services/taskService.d.ts +2 -1
- package/dist/services/taskService.js +87 -12
- package/dist/services/taskService.test.d.ts +1 -0
- package/dist/services/taskService.test.js +159 -0
- package/dist/services/worktreeService.js +42 -17
- package/dist/services/worktreeService.test.d.ts +1 -0
- package/dist/services/worktreeService.test.js +117 -0
- package/dist/tools/contextTools.d.ts +93 -0
- package/dist/tools/contextTools.js +83 -0
- package/dist/tools/execTools.d.ts +3 -9
- package/dist/tools/execTools.js +14 -12
- package/dist/tools/featureTools.d.ts +4 -48
- package/dist/tools/featureTools.js +11 -51
- package/dist/tools/planTools.d.ts +5 -15
- package/dist/tools/planTools.js +16 -16
- package/dist/tools/sessionTools.d.ts +35 -0
- package/dist/tools/sessionTools.js +95 -0
- package/dist/tools/taskTools.d.ts +6 -18
- package/dist/tools/taskTools.js +18 -19
- package/dist/types.d.ts +35 -0
- package/dist/utils/detection.d.ts +12 -0
- package/dist/utils/detection.js +73 -0
- package/dist/utils/paths.d.ts +1 -1
- package/dist/utils/paths.js +2 -3
- package/dist/utils/paths.test.d.ts +1 -0
- package/dist/utils/paths.test.js +100 -0
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { TaskService } from "./taskService";
|
|
4
|
+
import { FeatureService } from "./featureService";
|
|
5
|
+
import { PlanService } from "./planService";
|
|
6
|
+
import { getTaskPath, getTaskStatusPath, getTaskReportPath } from "../utils/paths";
|
|
7
|
+
const TEST_ROOT = "/tmp/hive-test-task";
|
|
8
|
+
describe("TaskService", () => {
|
|
9
|
+
let taskService;
|
|
10
|
+
let featureService;
|
|
11
|
+
let planService;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
14
|
+
fs.mkdirSync(TEST_ROOT, { recursive: true });
|
|
15
|
+
featureService = new FeatureService(TEST_ROOT);
|
|
16
|
+
planService = new PlanService(TEST_ROOT);
|
|
17
|
+
taskService = new TaskService(TEST_ROOT);
|
|
18
|
+
featureService.create("test-feature");
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
describe("sync", () => {
|
|
24
|
+
it("throws when no plan exists", () => {
|
|
25
|
+
featureService.create("no-plan");
|
|
26
|
+
expect(() => taskService.sync("no-plan")).toThrow();
|
|
27
|
+
});
|
|
28
|
+
it("creates tasks from plan", () => {
|
|
29
|
+
planService.write("test-feature", `# Plan
|
|
30
|
+
|
|
31
|
+
## Tasks
|
|
32
|
+
|
|
33
|
+
### 1. Setup Database
|
|
34
|
+
Description
|
|
35
|
+
|
|
36
|
+
### 2. Create API
|
|
37
|
+
Description
|
|
38
|
+
`);
|
|
39
|
+
const result = taskService.sync("test-feature");
|
|
40
|
+
expect(result.created).toContain("01-setup-database");
|
|
41
|
+
expect(result.created).toContain("02-create-api");
|
|
42
|
+
expect(result.created.length).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
it("keeps done tasks even if removed from plan", () => {
|
|
45
|
+
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. First Task\nDesc`);
|
|
46
|
+
taskService.sync("test-feature");
|
|
47
|
+
taskService.update("test-feature", "01-first-task", { status: "done" });
|
|
48
|
+
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Different Task\nDesc`);
|
|
49
|
+
const result = taskService.sync("test-feature");
|
|
50
|
+
expect(result.kept).toContain("01-first-task");
|
|
51
|
+
});
|
|
52
|
+
it("removes cancelled tasks", () => {
|
|
53
|
+
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Task One\nDesc`);
|
|
54
|
+
taskService.sync("test-feature");
|
|
55
|
+
taskService.update("test-feature", "01-task-one", { status: "cancelled" });
|
|
56
|
+
const result = taskService.sync("test-feature");
|
|
57
|
+
expect(result.removed).toContain("01-task-one");
|
|
58
|
+
});
|
|
59
|
+
it("preserves manual tasks", () => {
|
|
60
|
+
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Plan Task\nDesc`);
|
|
61
|
+
taskService.sync("test-feature");
|
|
62
|
+
taskService.create("test-feature", "manual-task");
|
|
63
|
+
const result = taskService.sync("test-feature");
|
|
64
|
+
expect(result.manual).toContain("02-manual-task");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("create", () => {
|
|
68
|
+
it("creates a manual task", () => {
|
|
69
|
+
const folder = taskService.create("test-feature", "my-task");
|
|
70
|
+
expect(folder).toBe("01-my-task");
|
|
71
|
+
expect(fs.existsSync(getTaskPath(TEST_ROOT, "test-feature", folder))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("auto-increments order", () => {
|
|
74
|
+
taskService.create("test-feature", "first");
|
|
75
|
+
taskService.create("test-feature", "second");
|
|
76
|
+
const third = taskService.create("test-feature", "third");
|
|
77
|
+
expect(third).toBe("03-third");
|
|
78
|
+
});
|
|
79
|
+
it("respects explicit order", () => {
|
|
80
|
+
const folder = taskService.create("test-feature", "specific", 10);
|
|
81
|
+
expect(folder).toBe("10-specific");
|
|
82
|
+
});
|
|
83
|
+
it("creates task with pending status and manual origin", () => {
|
|
84
|
+
const folder = taskService.create("test-feature", "test");
|
|
85
|
+
const task = taskService.get("test-feature", folder);
|
|
86
|
+
expect(task?.status).toBe("pending");
|
|
87
|
+
expect(task?.origin).toBe("manual");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("update", () => {
|
|
91
|
+
it("updates task status", () => {
|
|
92
|
+
const folder = taskService.create("test-feature", "task");
|
|
93
|
+
taskService.update("test-feature", folder, { status: "in_progress" });
|
|
94
|
+
const task = taskService.get("test-feature", folder);
|
|
95
|
+
expect(task?.status).toBe("in_progress");
|
|
96
|
+
});
|
|
97
|
+
it("sets startedAt when status becomes in_progress", () => {
|
|
98
|
+
const folder = taskService.create("test-feature", "task");
|
|
99
|
+
taskService.update("test-feature", folder, { status: "in_progress" });
|
|
100
|
+
const statusPath = getTaskStatusPath(TEST_ROOT, "test-feature", folder);
|
|
101
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
102
|
+
expect(status.startedAt).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
it("sets completedAt when status becomes done", () => {
|
|
105
|
+
const folder = taskService.create("test-feature", "task");
|
|
106
|
+
taskService.update("test-feature", folder, { status: "done" });
|
|
107
|
+
const statusPath = getTaskStatusPath(TEST_ROOT, "test-feature", folder);
|
|
108
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
109
|
+
expect(status.completedAt).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
it("updates summary", () => {
|
|
112
|
+
const folder = taskService.create("test-feature", "task");
|
|
113
|
+
taskService.update("test-feature", folder, { summary: "Completed setup" });
|
|
114
|
+
const task = taskService.get("test-feature", folder);
|
|
115
|
+
expect(task?.summary).toBe("Completed setup");
|
|
116
|
+
});
|
|
117
|
+
it("throws for non-existing task", () => {
|
|
118
|
+
expect(() => taskService.update("test-feature", "nope", { status: "done" })).toThrow();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("get", () => {
|
|
122
|
+
it("returns task info", () => {
|
|
123
|
+
const folder = taskService.create("test-feature", "my-task");
|
|
124
|
+
const task = taskService.get("test-feature", folder);
|
|
125
|
+
expect(task).not.toBeNull();
|
|
126
|
+
expect(task.folder).toBe("01-my-task");
|
|
127
|
+
expect(task.name).toBe("my-task");
|
|
128
|
+
expect(task.status).toBe("pending");
|
|
129
|
+
expect(task.origin).toBe("manual");
|
|
130
|
+
});
|
|
131
|
+
it("returns null for non-existing task", () => {
|
|
132
|
+
expect(taskService.get("test-feature", "nope")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe("list", () => {
|
|
136
|
+
it("returns empty array when no tasks", () => {
|
|
137
|
+
expect(taskService.list("test-feature")).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
it("returns all tasks sorted", () => {
|
|
140
|
+
taskService.create("test-feature", "third", 3);
|
|
141
|
+
taskService.create("test-feature", "first", 1);
|
|
142
|
+
taskService.create("test-feature", "second", 2);
|
|
143
|
+
const tasks = taskService.list("test-feature");
|
|
144
|
+
expect(tasks.length).toBe(3);
|
|
145
|
+
expect(tasks[0].folder).toBe("01-first");
|
|
146
|
+
expect(tasks[1].folder).toBe("02-second");
|
|
147
|
+
expect(tasks[2].folder).toBe("03-third");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe("writeReport", () => {
|
|
151
|
+
it("writes report file", () => {
|
|
152
|
+
const folder = taskService.create("test-feature", "task");
|
|
153
|
+
const report = "## Summary\n\nCompleted the task successfully.";
|
|
154
|
+
taskService.writeReport("test-feature", folder, report);
|
|
155
|
+
const reportPath = getTaskReportPath(TEST_ROOT, "test-feature", folder);
|
|
156
|
+
expect(fs.readFileSync(reportPath, "utf-8")).toBe(report);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -87,7 +87,7 @@ export class WorktreeService {
|
|
|
87
87
|
if (!base) {
|
|
88
88
|
try {
|
|
89
89
|
const status = JSON.parse(await fs.readFile(statusPath, "utf-8"));
|
|
90
|
-
base = status.
|
|
90
|
+
base = status.baseCommit; // Read baseCommit directly from task status
|
|
91
91
|
}
|
|
92
92
|
catch { }
|
|
93
93
|
}
|
|
@@ -139,12 +139,16 @@ export class WorktreeService {
|
|
|
139
139
|
if (!hasDiff) {
|
|
140
140
|
return { success: true, filesAffected: [] };
|
|
141
141
|
}
|
|
142
|
+
const patchPath = path.join(this.config.hiveDir, ".worktrees", feature, `${step}.patch`);
|
|
142
143
|
try {
|
|
144
|
+
await fs.writeFile(patchPath, diffContent);
|
|
143
145
|
const git = this.getGit();
|
|
144
|
-
await git.applyPatch(
|
|
146
|
+
await git.applyPatch(patchPath);
|
|
147
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
145
148
|
return { success: true, filesAffected: filesChanged };
|
|
146
149
|
}
|
|
147
150
|
catch (error) {
|
|
151
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
148
152
|
const err = error;
|
|
149
153
|
return {
|
|
150
154
|
success: false,
|
|
@@ -158,12 +162,16 @@ export class WorktreeService {
|
|
|
158
162
|
if (!hasDiff) {
|
|
159
163
|
return { success: true, filesAffected: [] };
|
|
160
164
|
}
|
|
165
|
+
const patchPath = path.join(this.config.hiveDir, ".worktrees", feature, `${step}.patch`);
|
|
161
166
|
try {
|
|
167
|
+
await fs.writeFile(patchPath, diffContent);
|
|
162
168
|
const git = this.getGit();
|
|
163
|
-
await git.applyPatch(
|
|
169
|
+
await git.applyPatch(patchPath, ["-R"]);
|
|
170
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
164
171
|
return { success: true, filesAffected: filesChanged };
|
|
165
172
|
}
|
|
166
173
|
catch (error) {
|
|
174
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
167
175
|
const err = error;
|
|
168
176
|
return {
|
|
169
177
|
success: false,
|
|
@@ -259,16 +267,27 @@ export class WorktreeService {
|
|
|
259
267
|
catch {
|
|
260
268
|
/* intentional */
|
|
261
269
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
catch
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
const worktreesDir = this.getWorktreesDir();
|
|
271
|
+
const features = feature ? [feature] : await fs.readdir(worktreesDir).catch(() => []);
|
|
272
|
+
for (const feat of features) {
|
|
273
|
+
const featurePath = path.join(worktreesDir, feat);
|
|
274
|
+
const stat = await fs.stat(featurePath).catch(() => null);
|
|
275
|
+
if (!stat?.isDirectory())
|
|
276
|
+
continue;
|
|
277
|
+
const steps = await fs.readdir(featurePath).catch(() => []);
|
|
278
|
+
for (const step of steps) {
|
|
279
|
+
const worktreePath = path.join(featurePath, step);
|
|
280
|
+
const stepStat = await fs.stat(worktreePath).catch(() => null);
|
|
281
|
+
if (!stepStat?.isDirectory())
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
const worktreeGit = this.getGit(worktreePath);
|
|
285
|
+
await worktreeGit.revparse(["HEAD"]);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
await this.remove(feat, step, false);
|
|
289
|
+
removed.push(worktreePath);
|
|
290
|
+
}
|
|
272
291
|
}
|
|
273
292
|
}
|
|
274
293
|
return { removed, pruned: true };
|
|
@@ -278,12 +297,16 @@ export class WorktreeService {
|
|
|
278
297
|
if (!hasDiff) {
|
|
279
298
|
return [];
|
|
280
299
|
}
|
|
300
|
+
const patchPath = path.join(this.config.hiveDir, ".worktrees", feature, `${step}-check.patch`);
|
|
281
301
|
try {
|
|
302
|
+
await fs.writeFile(patchPath, diffContent);
|
|
282
303
|
const git = this.getGit();
|
|
283
|
-
await git.applyPatch(
|
|
304
|
+
await git.applyPatch(patchPath, ["--check"]);
|
|
305
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
284
306
|
return [];
|
|
285
307
|
}
|
|
286
308
|
catch (error) {
|
|
309
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
287
310
|
const err = error;
|
|
288
311
|
const stderr = err.message || "";
|
|
289
312
|
const conflicts = stderr
|
|
@@ -298,14 +321,16 @@ export class WorktreeService {
|
|
|
298
321
|
}
|
|
299
322
|
}
|
|
300
323
|
async checkConflictsFromSavedDiff(diffPath, reverse = false) {
|
|
301
|
-
|
|
302
|
-
|
|
324
|
+
try {
|
|
325
|
+
await fs.access(diffPath);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
303
328
|
return [];
|
|
304
329
|
}
|
|
305
330
|
try {
|
|
306
331
|
const git = this.getGit();
|
|
307
332
|
const options = reverse ? ["--check", "-R"] : ["--check"];
|
|
308
|
-
await git.applyPatch(
|
|
333
|
+
await git.applyPatch(diffPath, options);
|
|
309
334
|
return [];
|
|
310
335
|
}
|
|
311
336
|
catch (error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { WorktreeService } from "./worktreeService";
|
|
5
|
+
const TEST_ROOT = "/tmp/hive-test-worktree";
|
|
6
|
+
describe("WorktreeService", () => {
|
|
7
|
+
let service;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
10
|
+
fs.mkdirSync(TEST_ROOT, { recursive: true });
|
|
11
|
+
const { execSync } = await import("child_process");
|
|
12
|
+
execSync("git init", { cwd: TEST_ROOT });
|
|
13
|
+
execSync("git config user.email 'test@test.com'", { cwd: TEST_ROOT });
|
|
14
|
+
execSync("git config user.name 'Test'", { cwd: TEST_ROOT });
|
|
15
|
+
fs.writeFileSync(path.join(TEST_ROOT, "README.md"), "# Test");
|
|
16
|
+
execSync("git add . && git commit -m 'init'", { cwd: TEST_ROOT });
|
|
17
|
+
service = new WorktreeService({ baseDir: TEST_ROOT, hiveDir: path.join(TEST_ROOT, ".hive") });
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
describe("create", () => {
|
|
23
|
+
it("creates a worktree", async () => {
|
|
24
|
+
const result = await service.create("my-feature", "01-setup");
|
|
25
|
+
expect(result.path).toContain(".hive/.worktrees/my-feature/01-setup");
|
|
26
|
+
expect(result.branch).toBe("hive/my-feature/01-setup");
|
|
27
|
+
expect(fs.existsSync(result.path)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it("worktree contains files from base branch", async () => {
|
|
30
|
+
const result = await service.create("my-feature", "01-setup");
|
|
31
|
+
expect(fs.existsSync(path.join(result.path, "README.md"))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("get", () => {
|
|
35
|
+
it("returns null for non-existing worktree", async () => {
|
|
36
|
+
const result = await service.get("nope", "nope");
|
|
37
|
+
expect(result).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
it("returns worktree info after creation", async () => {
|
|
40
|
+
await service.create("my-feature", "01-task");
|
|
41
|
+
const result = await service.get("my-feature", "01-task");
|
|
42
|
+
expect(result).not.toBeNull();
|
|
43
|
+
expect(result.feature).toBe("my-feature");
|
|
44
|
+
expect(result.step).toBe("01-task");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("list", () => {
|
|
48
|
+
it("returns empty array when no worktrees", async () => {
|
|
49
|
+
const result = await service.list();
|
|
50
|
+
expect(result).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
it("lists all worktrees", async () => {
|
|
53
|
+
await service.create("feature-a", "01-task");
|
|
54
|
+
await service.create("feature-b", "01-task");
|
|
55
|
+
const result = await service.list();
|
|
56
|
+
expect(result.length).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
it("filters by feature", async () => {
|
|
59
|
+
await service.create("feature-a", "01-task");
|
|
60
|
+
await service.create("feature-a", "02-task");
|
|
61
|
+
await service.create("feature-b", "01-task");
|
|
62
|
+
const result = await service.list("feature-a");
|
|
63
|
+
expect(result.length).toBe(2);
|
|
64
|
+
expect(result.every(w => w.feature === "feature-a")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("remove", () => {
|
|
68
|
+
it("removes worktree", async () => {
|
|
69
|
+
const created = await service.create("my-feature", "01-task");
|
|
70
|
+
expect(fs.existsSync(created.path)).toBe(true);
|
|
71
|
+
await service.remove("my-feature", "01-task");
|
|
72
|
+
expect(fs.existsSync(created.path)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it("removes branch when deleteBranch is true", async () => {
|
|
75
|
+
await service.create("my-feature", "01-task");
|
|
76
|
+
const { execSync } = await import("child_process");
|
|
77
|
+
await service.remove("my-feature", "01-task", true);
|
|
78
|
+
const branches = execSync("git branch", { cwd: TEST_ROOT, encoding: "utf-8" });
|
|
79
|
+
expect(branches).not.toContain("hive/my-feature/01-task");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("getDiff", () => {
|
|
83
|
+
it("returns empty diff when no changes", async () => {
|
|
84
|
+
await service.create("my-feature", "01-task");
|
|
85
|
+
const diff = await service.getDiff("my-feature", "01-task");
|
|
86
|
+
expect(diff.hasDiff).toBe(false);
|
|
87
|
+
expect(diff.diffContent).toBe("");
|
|
88
|
+
expect(diff.filesChanged).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
it("returns diff when files changed", async () => {
|
|
91
|
+
const worktree = await service.create("my-feature", "01-task");
|
|
92
|
+
fs.writeFileSync(path.join(worktree.path, "new-file.txt"), "content");
|
|
93
|
+
const { execSync } = await import("child_process");
|
|
94
|
+
execSync("git add .", { cwd: worktree.path });
|
|
95
|
+
execSync("git commit -m 'add file'", { cwd: worktree.path });
|
|
96
|
+
const diff = await service.getDiff("my-feature", "01-task");
|
|
97
|
+
expect(diff.hasDiff).toBe(true);
|
|
98
|
+
expect(diff.filesChanged).toContain("new-file.txt");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("cleanup", () => {
|
|
102
|
+
it("removes invalid worktrees for a feature", async () => {
|
|
103
|
+
const wt1 = await service.create("cleanup-test", "01-task");
|
|
104
|
+
const wt2 = await service.create("cleanup-test", "02-task");
|
|
105
|
+
fs.writeFileSync(path.join(wt1.path, ".git"), "gitdir: /nonexistent\n");
|
|
106
|
+
fs.writeFileSync(path.join(wt2.path, ".git"), "gitdir: /nonexistent\n");
|
|
107
|
+
expect(fs.existsSync(wt1.path)).toBe(true);
|
|
108
|
+
expect(fs.existsSync(wt2.path)).toBe(true);
|
|
109
|
+
const result = await service.cleanup("cleanup-test");
|
|
110
|
+
expect(result.removed.length).toBe(2);
|
|
111
|
+
expect(fs.existsSync(wt1.path)).toBe(false);
|
|
112
|
+
expect(fs.existsSync(wt2.path)).toBe(false);
|
|
113
|
+
const remaining = await service.list("cleanup-test");
|
|
114
|
+
expect(remaining).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function createContextTools(projectRoot: string): {
|
|
3
|
+
hive_context_write: {
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: z.ZodObject<{
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
content: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
execute: ({ name, content }: {
|
|
10
|
+
name: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
error: string;
|
|
14
|
+
success?: undefined;
|
|
15
|
+
path?: undefined;
|
|
16
|
+
} | {
|
|
17
|
+
success: boolean;
|
|
18
|
+
path: string;
|
|
19
|
+
error?: undefined;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
hive_context_read: {
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: z.ZodObject<{
|
|
25
|
+
name: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
execute: ({ name }: {
|
|
28
|
+
name?: string;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
error: string;
|
|
31
|
+
name?: undefined;
|
|
32
|
+
content?: undefined;
|
|
33
|
+
message?: undefined;
|
|
34
|
+
compiled?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
name: string;
|
|
37
|
+
content: string;
|
|
38
|
+
error?: undefined;
|
|
39
|
+
message?: undefined;
|
|
40
|
+
compiled?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
message: string;
|
|
43
|
+
error?: undefined;
|
|
44
|
+
name?: undefined;
|
|
45
|
+
content?: undefined;
|
|
46
|
+
compiled?: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
compiled: string;
|
|
49
|
+
error?: undefined;
|
|
50
|
+
name?: undefined;
|
|
51
|
+
content?: undefined;
|
|
52
|
+
message?: undefined;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
hive_context_list: {
|
|
56
|
+
description: string;
|
|
57
|
+
parameters: z.ZodObject<{}, z.core.$strip>;
|
|
58
|
+
execute: () => Promise<{
|
|
59
|
+
error: string;
|
|
60
|
+
files?: undefined;
|
|
61
|
+
message?: undefined;
|
|
62
|
+
} | {
|
|
63
|
+
files: never[];
|
|
64
|
+
message: string;
|
|
65
|
+
error?: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
files: {
|
|
68
|
+
name: string;
|
|
69
|
+
updatedAt: string;
|
|
70
|
+
previewLength: number;
|
|
71
|
+
}[];
|
|
72
|
+
error?: undefined;
|
|
73
|
+
message?: undefined;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
hive_context_delete: {
|
|
77
|
+
description: string;
|
|
78
|
+
parameters: z.ZodObject<{
|
|
79
|
+
name: z.ZodString;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
execute: ({ name }: {
|
|
82
|
+
name: string;
|
|
83
|
+
}) => Promise<{
|
|
84
|
+
error: string;
|
|
85
|
+
success?: undefined;
|
|
86
|
+
deleted?: undefined;
|
|
87
|
+
} | {
|
|
88
|
+
success: boolean;
|
|
89
|
+
deleted: string;
|
|
90
|
+
error?: undefined;
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextService } from '../services/contextService.js';
|
|
3
|
+
import { FeatureService } from '../services/featureService.js';
|
|
4
|
+
import { detectContext } from '../utils/detection.js';
|
|
5
|
+
export function createContextTools(projectRoot) {
|
|
6
|
+
const contextService = new ContextService(projectRoot);
|
|
7
|
+
const featureService = new FeatureService(projectRoot);
|
|
8
|
+
const getActiveFeature = () => {
|
|
9
|
+
const ctx = detectContext(projectRoot);
|
|
10
|
+
return ctx?.feature || null;
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
hive_context_write: {
|
|
14
|
+
description: 'Write a context file for the active feature. Context files store persistent notes, decisions, and reference material.',
|
|
15
|
+
parameters: z.object({
|
|
16
|
+
name: z.string().describe('Context file name (e.g., "architecture", "decisions", "notes")'),
|
|
17
|
+
content: z.string().describe('Markdown content to write'),
|
|
18
|
+
}),
|
|
19
|
+
execute: async ({ name, content }) => {
|
|
20
|
+
const feature = getActiveFeature();
|
|
21
|
+
if (!feature)
|
|
22
|
+
return { error: 'No active feature' };
|
|
23
|
+
const filePath = contextService.write(feature, name, content);
|
|
24
|
+
return { success: true, path: filePath };
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
hive_context_read: {
|
|
28
|
+
description: 'Read a specific context file or all context for the active feature',
|
|
29
|
+
parameters: z.object({
|
|
30
|
+
name: z.string().optional().describe('Context file name. If omitted, returns all context compiled.'),
|
|
31
|
+
}),
|
|
32
|
+
execute: async ({ name }) => {
|
|
33
|
+
const feature = getActiveFeature();
|
|
34
|
+
if (!feature)
|
|
35
|
+
return { error: 'No active feature' };
|
|
36
|
+
if (name) {
|
|
37
|
+
const content = contextService.read(feature, name);
|
|
38
|
+
if (!content)
|
|
39
|
+
return { error: `Context file '${name}' not found` };
|
|
40
|
+
return { name, content };
|
|
41
|
+
}
|
|
42
|
+
const compiled = contextService.compile(feature);
|
|
43
|
+
if (!compiled)
|
|
44
|
+
return { message: 'No context files found' };
|
|
45
|
+
return { compiled };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
hive_context_list: {
|
|
49
|
+
description: 'List all context files for the active feature',
|
|
50
|
+
parameters: z.object({}),
|
|
51
|
+
execute: async () => {
|
|
52
|
+
const feature = getActiveFeature();
|
|
53
|
+
if (!feature)
|
|
54
|
+
return { error: 'No active feature' };
|
|
55
|
+
const files = contextService.list(feature);
|
|
56
|
+
if (files.length === 0)
|
|
57
|
+
return { files: [], message: 'No context files' };
|
|
58
|
+
return {
|
|
59
|
+
files: files.map(f => ({
|
|
60
|
+
name: f.name,
|
|
61
|
+
updatedAt: f.updatedAt,
|
|
62
|
+
previewLength: f.content.length,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
hive_context_delete: {
|
|
68
|
+
description: 'Delete a context file',
|
|
69
|
+
parameters: z.object({
|
|
70
|
+
name: z.string().describe('Context file name to delete'),
|
|
71
|
+
}),
|
|
72
|
+
execute: async ({ name }) => {
|
|
73
|
+
const feature = getActiveFeature();
|
|
74
|
+
if (!feature)
|
|
75
|
+
return { error: 'No active feature' };
|
|
76
|
+
const deleted = contextService.delete(feature, name);
|
|
77
|
+
if (!deleted)
|
|
78
|
+
return { error: `Context file '${name}' not found` };
|
|
79
|
+
return { success: true, deleted: name };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -4,11 +4,9 @@ export declare function createExecTools(projectRoot: string): {
|
|
|
4
4
|
description: string;
|
|
5
5
|
parameters: z.ZodObject<{
|
|
6
6
|
task: z.ZodString;
|
|
7
|
-
featureName: z.ZodOptional<z.ZodString>;
|
|
8
7
|
}, z.core.$strip>;
|
|
9
|
-
execute: ({ task
|
|
8
|
+
execute: ({ task }: {
|
|
10
9
|
task: string;
|
|
11
|
-
featureName?: string;
|
|
12
10
|
}) => Promise<{
|
|
13
11
|
error: string;
|
|
14
12
|
worktreePath?: undefined;
|
|
@@ -27,13 +25,11 @@ export declare function createExecTools(projectRoot: string): {
|
|
|
27
25
|
task: z.ZodString;
|
|
28
26
|
summary: z.ZodString;
|
|
29
27
|
report: z.ZodOptional<z.ZodString>;
|
|
30
|
-
featureName: z.ZodOptional<z.ZodString>;
|
|
31
28
|
}, z.core.$strip>;
|
|
32
|
-
execute: ({ task, summary, report
|
|
29
|
+
execute: ({ task, summary, report }: {
|
|
33
30
|
task: string;
|
|
34
31
|
summary: string;
|
|
35
32
|
report?: string;
|
|
36
|
-
featureName?: string;
|
|
37
33
|
}) => Promise<{
|
|
38
34
|
error: string;
|
|
39
35
|
completed?: undefined;
|
|
@@ -52,11 +48,9 @@ export declare function createExecTools(projectRoot: string): {
|
|
|
52
48
|
description: string;
|
|
53
49
|
parameters: z.ZodObject<{
|
|
54
50
|
task: z.ZodString;
|
|
55
|
-
featureName: z.ZodOptional<z.ZodString>;
|
|
56
51
|
}, z.core.$strip>;
|
|
57
|
-
execute: ({ task
|
|
52
|
+
execute: ({ task }: {
|
|
58
53
|
task: string;
|
|
59
|
-
featureName?: string;
|
|
60
54
|
}) => Promise<{
|
|
61
55
|
error: string;
|
|
62
56
|
aborted?: undefined;
|