opencode-hive 0.2.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 +91 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +307 -0
- package/dist/services/featureService.d.ts +16 -0
- package/dist/services/featureService.js +117 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +4 -0
- package/dist/services/planService.d.ts +11 -0
- package/dist/services/planService.js +59 -0
- package/dist/services/taskService.d.ts +16 -0
- package/dist/services/taskService.js +155 -0
- package/dist/services/worktreeService.d.ts +49 -0
- package/dist/services/worktreeService.js +331 -0
- package/dist/tools/execTools.d.ts +72 -0
- package/dist/tools/execTools.js +123 -0
- package/dist/tools/featureTools.d.ts +104 -0
- package/dist/tools/featureTools.js +113 -0
- package/dist/tools/planTools.d.ts +57 -0
- package/dist/tools/planTools.js +65 -0
- package/dist/tools/taskTools.d.ts +91 -0
- package/dist/tools/taskTools.js +87 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/dist/utils/paths.d.ts +18 -0
- package/dist/utils/paths.js +75 -0
- package/package.json +47 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
|
|
3
|
+
export class TaskService {
|
|
4
|
+
projectRoot;
|
|
5
|
+
constructor(projectRoot) {
|
|
6
|
+
this.projectRoot = projectRoot;
|
|
7
|
+
}
|
|
8
|
+
sync(featureName) {
|
|
9
|
+
const planPath = getPlanPath(this.projectRoot, featureName);
|
|
10
|
+
const planContent = readText(planPath);
|
|
11
|
+
if (!planContent) {
|
|
12
|
+
throw new Error(`No plan.md found for feature '${featureName}'`);
|
|
13
|
+
}
|
|
14
|
+
const planTasks = this.parseTasksFromPlan(planContent);
|
|
15
|
+
const existingTasks = this.list(featureName);
|
|
16
|
+
const result = {
|
|
17
|
+
created: [],
|
|
18
|
+
removed: [],
|
|
19
|
+
kept: [],
|
|
20
|
+
manual: [],
|
|
21
|
+
};
|
|
22
|
+
const existingByName = new Map(existingTasks.map(t => [t.folder, t]));
|
|
23
|
+
for (const existing of existingTasks) {
|
|
24
|
+
if (existing.origin === 'manual') {
|
|
25
|
+
result.manual.push(existing.folder);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (existing.status === 'done' || existing.status === 'in_progress') {
|
|
29
|
+
result.kept.push(existing.folder);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (existing.status === 'cancelled') {
|
|
33
|
+
this.deleteTask(featureName, existing.folder);
|
|
34
|
+
result.removed.push(existing.folder);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const stillInPlan = planTasks.some(p => p.folder === existing.folder);
|
|
38
|
+
if (!stillInPlan) {
|
|
39
|
+
this.deleteTask(featureName, existing.folder);
|
|
40
|
+
result.removed.push(existing.folder);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
result.kept.push(existing.folder);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const planTask of planTasks) {
|
|
47
|
+
if (!existingByName.has(planTask.folder)) {
|
|
48
|
+
this.createFromPlan(featureName, planTask.folder, planTask.order);
|
|
49
|
+
result.created.push(planTask.folder);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
create(featureName, name, order) {
|
|
55
|
+
const tasksPath = getTasksPath(this.projectRoot, featureName);
|
|
56
|
+
const existingFolders = this.listFolders(featureName);
|
|
57
|
+
const nextOrder = order ?? this.getNextOrder(existingFolders);
|
|
58
|
+
const folder = `${String(nextOrder).padStart(2, '0')}-${name}`;
|
|
59
|
+
const taskPath = getTaskPath(this.projectRoot, featureName, folder);
|
|
60
|
+
ensureDir(taskPath);
|
|
61
|
+
const status = {
|
|
62
|
+
status: 'pending',
|
|
63
|
+
origin: 'manual',
|
|
64
|
+
};
|
|
65
|
+
writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
|
|
66
|
+
return folder;
|
|
67
|
+
}
|
|
68
|
+
createFromPlan(featureName, folder, order) {
|
|
69
|
+
const taskPath = getTaskPath(this.projectRoot, featureName, folder);
|
|
70
|
+
ensureDir(taskPath);
|
|
71
|
+
const status = {
|
|
72
|
+
status: 'pending',
|
|
73
|
+
origin: 'plan',
|
|
74
|
+
};
|
|
75
|
+
writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
|
|
76
|
+
}
|
|
77
|
+
update(featureName, taskFolder, updates) {
|
|
78
|
+
const statusPath = getTaskStatusPath(this.projectRoot, featureName, taskFolder);
|
|
79
|
+
const current = readJson(statusPath);
|
|
80
|
+
if (!current) {
|
|
81
|
+
throw new Error(`Task '${taskFolder}' not found`);
|
|
82
|
+
}
|
|
83
|
+
const updated = {
|
|
84
|
+
...current,
|
|
85
|
+
...updates,
|
|
86
|
+
};
|
|
87
|
+
if (updates.status === 'in_progress' && !current.startedAt) {
|
|
88
|
+
updated.startedAt = new Date().toISOString();
|
|
89
|
+
}
|
|
90
|
+
if (updates.status === 'done' && !current.completedAt) {
|
|
91
|
+
updated.completedAt = new Date().toISOString();
|
|
92
|
+
}
|
|
93
|
+
writeJson(statusPath, updated);
|
|
94
|
+
return updated;
|
|
95
|
+
}
|
|
96
|
+
get(featureName, taskFolder) {
|
|
97
|
+
const statusPath = getTaskStatusPath(this.projectRoot, featureName, taskFolder);
|
|
98
|
+
const status = readJson(statusPath);
|
|
99
|
+
if (!status)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
folder: taskFolder,
|
|
103
|
+
name: taskFolder.replace(/^\d+-/, ''),
|
|
104
|
+
status: status.status,
|
|
105
|
+
origin: status.origin,
|
|
106
|
+
summary: status.summary,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
list(featureName) {
|
|
110
|
+
const folders = this.listFolders(featureName);
|
|
111
|
+
return folders
|
|
112
|
+
.map(folder => this.get(featureName, folder))
|
|
113
|
+
.filter((t) => t !== null);
|
|
114
|
+
}
|
|
115
|
+
writeReport(featureName, taskFolder, report) {
|
|
116
|
+
const reportPath = getTaskReportPath(this.projectRoot, featureName, taskFolder);
|
|
117
|
+
writeText(reportPath, report);
|
|
118
|
+
return reportPath;
|
|
119
|
+
}
|
|
120
|
+
listFolders(featureName) {
|
|
121
|
+
const tasksPath = getTasksPath(this.projectRoot, featureName);
|
|
122
|
+
if (!fileExists(tasksPath))
|
|
123
|
+
return [];
|
|
124
|
+
return fs.readdirSync(tasksPath, { withFileTypes: true })
|
|
125
|
+
.filter(d => d.isDirectory())
|
|
126
|
+
.map(d => d.name)
|
|
127
|
+
.sort();
|
|
128
|
+
}
|
|
129
|
+
deleteTask(featureName, taskFolder) {
|
|
130
|
+
const taskPath = getTaskPath(this.projectRoot, featureName, taskFolder);
|
|
131
|
+
if (fileExists(taskPath)) {
|
|
132
|
+
fs.rmSync(taskPath, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
getNextOrder(existingFolders) {
|
|
136
|
+
if (existingFolders.length === 0)
|
|
137
|
+
return 1;
|
|
138
|
+
const orders = existingFolders
|
|
139
|
+
.map(f => parseInt(f.split('-')[0], 10))
|
|
140
|
+
.filter(n => !isNaN(n));
|
|
141
|
+
return Math.max(...orders, 0) + 1;
|
|
142
|
+
}
|
|
143
|
+
parseTasksFromPlan(content) {
|
|
144
|
+
const tasks = [];
|
|
145
|
+
const taskPattern = /^###\s+(\d+)\.\s+(.+)$/gm;
|
|
146
|
+
let match;
|
|
147
|
+
while ((match = taskPattern.exec(content)) !== null) {
|
|
148
|
+
const order = parseInt(match[1], 10);
|
|
149
|
+
const name = match[2].trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
150
|
+
const folder = `${String(order).padStart(2, '0')}-${name}`;
|
|
151
|
+
tasks.push({ folder, order });
|
|
152
|
+
}
|
|
153
|
+
return tasks;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface WorktreeInfo {
|
|
2
|
+
path: string;
|
|
3
|
+
branch: string;
|
|
4
|
+
commit: string;
|
|
5
|
+
feature: string;
|
|
6
|
+
step: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DiffResult {
|
|
9
|
+
hasDiff: boolean;
|
|
10
|
+
diffContent: string;
|
|
11
|
+
filesChanged: string[];
|
|
12
|
+
insertions: number;
|
|
13
|
+
deletions: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ApplyResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
filesAffected: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface WorktreeConfig {
|
|
21
|
+
baseDir: string;
|
|
22
|
+
hiveDir: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class WorktreeService {
|
|
25
|
+
private config;
|
|
26
|
+
constructor(config: WorktreeConfig);
|
|
27
|
+
private getGit;
|
|
28
|
+
private getWorktreesDir;
|
|
29
|
+
private getWorktreePath;
|
|
30
|
+
private getStepStatusPath;
|
|
31
|
+
private getBranchName;
|
|
32
|
+
create(feature: string, step: string, baseBranch?: string): Promise<WorktreeInfo>;
|
|
33
|
+
get(feature: string, step: string): Promise<WorktreeInfo | null>;
|
|
34
|
+
getDiff(feature: string, step: string, baseCommit?: string): Promise<DiffResult>;
|
|
35
|
+
exportPatch(feature: string, step: string, baseBranch?: string): Promise<string>;
|
|
36
|
+
applyDiff(feature: string, step: string, baseBranch?: string): Promise<ApplyResult>;
|
|
37
|
+
revertDiff(feature: string, step: string, baseBranch?: string): Promise<ApplyResult>;
|
|
38
|
+
private parseFilesFromDiff;
|
|
39
|
+
revertFromSavedDiff(diffPath: string): Promise<ApplyResult>;
|
|
40
|
+
remove(feature: string, step: string, deleteBranch?: boolean): Promise<void>;
|
|
41
|
+
list(feature?: string): Promise<WorktreeInfo[]>;
|
|
42
|
+
cleanup(feature?: string): Promise<{
|
|
43
|
+
removed: string[];
|
|
44
|
+
pruned: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
checkConflicts(feature: string, step: string, baseBranch?: string): Promise<string[]>;
|
|
47
|
+
checkConflictsFromSavedDiff(diffPath: string, reverse?: boolean): Promise<string[]>;
|
|
48
|
+
}
|
|
49
|
+
export declare function createWorktreeService(projectDir: string): WorktreeService;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import simpleGit from "simple-git";
|
|
4
|
+
export class WorktreeService {
|
|
5
|
+
config;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
getGit(cwd) {
|
|
10
|
+
return simpleGit(cwd || this.config.baseDir);
|
|
11
|
+
}
|
|
12
|
+
getWorktreesDir() {
|
|
13
|
+
return path.join(this.config.hiveDir, ".worktrees");
|
|
14
|
+
}
|
|
15
|
+
getWorktreePath(feature, step) {
|
|
16
|
+
return path.join(this.getWorktreesDir(), feature, step);
|
|
17
|
+
}
|
|
18
|
+
async getStepStatusPath(feature, step) {
|
|
19
|
+
const featurePath = path.join(this.config.hiveDir, "features", feature);
|
|
20
|
+
// Check v2 structure first (tasks/)
|
|
21
|
+
const tasksPath = path.join(featurePath, "tasks", step, "status.json");
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(tasksPath);
|
|
24
|
+
return tasksPath;
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
// Fall back to v1 structure (execution/)
|
|
28
|
+
return path.join(featurePath, "execution", step, "status.json");
|
|
29
|
+
}
|
|
30
|
+
getBranchName(feature, step) {
|
|
31
|
+
return `hive/${feature}/${step}`;
|
|
32
|
+
}
|
|
33
|
+
async create(feature, step, baseBranch) {
|
|
34
|
+
const worktreePath = this.getWorktreePath(feature, step);
|
|
35
|
+
const branchName = this.getBranchName(feature, step);
|
|
36
|
+
const git = this.getGit();
|
|
37
|
+
await fs.mkdir(path.dirname(worktreePath), { recursive: true });
|
|
38
|
+
const base = baseBranch || (await git.revparse(["HEAD"])).trim();
|
|
39
|
+
const existing = await this.get(feature, step);
|
|
40
|
+
if (existing) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await git.raw(["worktree", "add", "-b", branchName, worktreePath, base]);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
try {
|
|
48
|
+
await git.raw(["worktree", "add", worktreePath, branchName]);
|
|
49
|
+
}
|
|
50
|
+
catch (retryError) {
|
|
51
|
+
throw new Error(`Failed to create worktree: ${retryError}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const worktreeGit = this.getGit(worktreePath);
|
|
55
|
+
const commit = (await worktreeGit.revparse(["HEAD"])).trim();
|
|
56
|
+
return {
|
|
57
|
+
path: worktreePath,
|
|
58
|
+
branch: branchName,
|
|
59
|
+
commit,
|
|
60
|
+
feature,
|
|
61
|
+
step,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async get(feature, step) {
|
|
65
|
+
const worktreePath = this.getWorktreePath(feature, step);
|
|
66
|
+
const branchName = this.getBranchName(feature, step);
|
|
67
|
+
try {
|
|
68
|
+
await fs.access(worktreePath);
|
|
69
|
+
const worktreeGit = this.getGit(worktreePath);
|
|
70
|
+
const commit = (await worktreeGit.revparse(["HEAD"])).trim();
|
|
71
|
+
return {
|
|
72
|
+
path: worktreePath,
|
|
73
|
+
branch: branchName,
|
|
74
|
+
commit,
|
|
75
|
+
feature,
|
|
76
|
+
step,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async getDiff(feature, step, baseCommit) {
|
|
84
|
+
const worktreePath = this.getWorktreePath(feature, step);
|
|
85
|
+
const statusPath = await this.getStepStatusPath(feature, step);
|
|
86
|
+
let base = baseCommit;
|
|
87
|
+
if (!base) {
|
|
88
|
+
try {
|
|
89
|
+
const status = JSON.parse(await fs.readFile(statusPath, "utf-8"));
|
|
90
|
+
base = status.execution?.baseCommit;
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
if (!base) {
|
|
95
|
+
base = "HEAD~1";
|
|
96
|
+
}
|
|
97
|
+
const worktreeGit = this.getGit(worktreePath);
|
|
98
|
+
try {
|
|
99
|
+
await worktreeGit.raw(["add", "-A"]);
|
|
100
|
+
const diffContent = await worktreeGit.diff([`${base}...HEAD`]);
|
|
101
|
+
const stat = await worktreeGit.diff([`${base}...HEAD`, "--stat"]);
|
|
102
|
+
const statLines = stat.split("\n").filter((l) => l.trim());
|
|
103
|
+
const filesChanged = statLines
|
|
104
|
+
.slice(0, -1)
|
|
105
|
+
.map((line) => line.split("|")[0].trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
const summaryLine = statLines[statLines.length - 1] || "";
|
|
108
|
+
const insertMatch = summaryLine.match(/(\d+) insertion/);
|
|
109
|
+
const deleteMatch = summaryLine.match(/(\d+) deletion/);
|
|
110
|
+
return {
|
|
111
|
+
hasDiff: diffContent.length > 0,
|
|
112
|
+
diffContent,
|
|
113
|
+
filesChanged,
|
|
114
|
+
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
115
|
+
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return {
|
|
120
|
+
hasDiff: false,
|
|
121
|
+
diffContent: "",
|
|
122
|
+
filesChanged: [],
|
|
123
|
+
insertions: 0,
|
|
124
|
+
deletions: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async exportPatch(feature, step, baseBranch) {
|
|
129
|
+
const worktreePath = this.getWorktreePath(feature, step);
|
|
130
|
+
const patchPath = path.join(worktreePath, "..", `${step}.patch`);
|
|
131
|
+
const base = baseBranch || "HEAD~1";
|
|
132
|
+
const worktreeGit = this.getGit(worktreePath);
|
|
133
|
+
const diff = await worktreeGit.diff([`${base}...HEAD`]);
|
|
134
|
+
await fs.writeFile(patchPath, diff);
|
|
135
|
+
return patchPath;
|
|
136
|
+
}
|
|
137
|
+
async applyDiff(feature, step, baseBranch) {
|
|
138
|
+
const { hasDiff, diffContent, filesChanged } = await this.getDiff(feature, step, baseBranch);
|
|
139
|
+
if (!hasDiff) {
|
|
140
|
+
return { success: true, filesAffected: [] };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const git = this.getGit();
|
|
144
|
+
await git.applyPatch(diffContent);
|
|
145
|
+
return { success: true, filesAffected: filesChanged };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const err = error;
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: err.message || "Failed to apply patch",
|
|
152
|
+
filesAffected: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async revertDiff(feature, step, baseBranch) {
|
|
157
|
+
const { hasDiff, diffContent, filesChanged } = await this.getDiff(feature, step, baseBranch);
|
|
158
|
+
if (!hasDiff) {
|
|
159
|
+
return { success: true, filesAffected: [] };
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const git = this.getGit();
|
|
163
|
+
await git.applyPatch(diffContent, ["-R"]);
|
|
164
|
+
return { success: true, filesAffected: filesChanged };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const err = error;
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: err.message || "Failed to revert patch",
|
|
171
|
+
filesAffected: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
parseFilesFromDiff(diffContent) {
|
|
176
|
+
const files = [];
|
|
177
|
+
const regex = /^diff --git a\/(.+?) b\//gm;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = regex.exec(diffContent)) !== null) {
|
|
180
|
+
files.push(match[1]);
|
|
181
|
+
}
|
|
182
|
+
return [...new Set(files)];
|
|
183
|
+
}
|
|
184
|
+
async revertFromSavedDiff(diffPath) {
|
|
185
|
+
const diffContent = await fs.readFile(diffPath, "utf-8");
|
|
186
|
+
if (!diffContent.trim()) {
|
|
187
|
+
return { success: true, filesAffected: [] };
|
|
188
|
+
}
|
|
189
|
+
const filesChanged = this.parseFilesFromDiff(diffContent);
|
|
190
|
+
try {
|
|
191
|
+
const git = this.getGit();
|
|
192
|
+
await git.applyPatch(diffContent, ["-R"]);
|
|
193
|
+
return { success: true, filesAffected: filesChanged };
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const err = error;
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: err.message || "Failed to revert patch",
|
|
200
|
+
filesAffected: [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async remove(feature, step, deleteBranch = false) {
|
|
205
|
+
const worktreePath = this.getWorktreePath(feature, step);
|
|
206
|
+
const branchName = this.getBranchName(feature, step);
|
|
207
|
+
const git = this.getGit();
|
|
208
|
+
try {
|
|
209
|
+
await git.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
await fs.rm(worktreePath, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await git.raw(["worktree", "prune"]);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* intentional */
|
|
219
|
+
}
|
|
220
|
+
if (deleteBranch) {
|
|
221
|
+
try {
|
|
222
|
+
await git.deleteLocalBranch(branchName, true);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* intentional */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async list(feature) {
|
|
230
|
+
const worktreesDir = this.getWorktreesDir();
|
|
231
|
+
const results = [];
|
|
232
|
+
try {
|
|
233
|
+
const features = feature ? [feature] : await fs.readdir(worktreesDir);
|
|
234
|
+
for (const feat of features) {
|
|
235
|
+
const featurePath = path.join(worktreesDir, feat);
|
|
236
|
+
const stat = await fs.stat(featurePath).catch(() => null);
|
|
237
|
+
if (!stat?.isDirectory())
|
|
238
|
+
continue;
|
|
239
|
+
const steps = await fs.readdir(featurePath).catch(() => []);
|
|
240
|
+
for (const step of steps) {
|
|
241
|
+
const info = await this.get(feat, step);
|
|
242
|
+
if (info) {
|
|
243
|
+
results.push(info);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
/* intentional */
|
|
250
|
+
}
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
async cleanup(feature) {
|
|
254
|
+
const removed = [];
|
|
255
|
+
const git = this.getGit();
|
|
256
|
+
try {
|
|
257
|
+
await git.raw(["worktree", "prune"]);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* intentional */
|
|
261
|
+
}
|
|
262
|
+
const worktrees = await this.list(feature);
|
|
263
|
+
for (const wt of worktrees) {
|
|
264
|
+
try {
|
|
265
|
+
await fs.access(wt.path);
|
|
266
|
+
const worktreeGit = this.getGit(wt.path);
|
|
267
|
+
await worktreeGit.revparse(["HEAD"]);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
await this.remove(wt.feature, wt.step, false);
|
|
271
|
+
removed.push(wt.path);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { removed, pruned: true };
|
|
275
|
+
}
|
|
276
|
+
async checkConflicts(feature, step, baseBranch) {
|
|
277
|
+
const { hasDiff, diffContent } = await this.getDiff(feature, step, baseBranch);
|
|
278
|
+
if (!hasDiff) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const git = this.getGit();
|
|
283
|
+
await git.applyPatch(diffContent, ["--check"]);
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const err = error;
|
|
288
|
+
const stderr = err.message || "";
|
|
289
|
+
const conflicts = stderr
|
|
290
|
+
.split("\n")
|
|
291
|
+
.filter((line) => line.includes("error: patch failed:"))
|
|
292
|
+
.map((line) => {
|
|
293
|
+
const match = line.match(/error: patch failed: (.+):/);
|
|
294
|
+
return match ? match[1] : null;
|
|
295
|
+
})
|
|
296
|
+
.filter((f) => f !== null);
|
|
297
|
+
return conflicts;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async checkConflictsFromSavedDiff(diffPath, reverse = false) {
|
|
301
|
+
const diffContent = await fs.readFile(diffPath, "utf-8");
|
|
302
|
+
if (!diffContent.trim()) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const git = this.getGit();
|
|
307
|
+
const options = reverse ? ["--check", "-R"] : ["--check"];
|
|
308
|
+
await git.applyPatch(diffContent, options);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
const err = error;
|
|
313
|
+
const stderr = err.message || "";
|
|
314
|
+
const conflicts = stderr
|
|
315
|
+
.split("\n")
|
|
316
|
+
.filter((line) => line.includes("error: patch failed:"))
|
|
317
|
+
.map((line) => {
|
|
318
|
+
const match = line.match(/error: patch failed: (.+):/);
|
|
319
|
+
return match ? match[1] : null;
|
|
320
|
+
})
|
|
321
|
+
.filter((f) => f !== null);
|
|
322
|
+
return conflicts;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function createWorktreeService(projectDir) {
|
|
327
|
+
return new WorktreeService({
|
|
328
|
+
baseDir: projectDir,
|
|
329
|
+
hiveDir: path.join(projectDir, ".hive"),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function createExecTools(projectRoot: string): {
|
|
3
|
+
hive_exec_start: {
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: z.ZodObject<{
|
|
6
|
+
task: z.ZodString;
|
|
7
|
+
featureName: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
execute: ({ task, featureName }: {
|
|
10
|
+
task: string;
|
|
11
|
+
featureName?: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
error: string;
|
|
14
|
+
worktreePath?: undefined;
|
|
15
|
+
branch?: undefined;
|
|
16
|
+
message?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
worktreePath: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
message: string;
|
|
21
|
+
error?: undefined;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
hive_exec_complete: {
|
|
25
|
+
description: string;
|
|
26
|
+
parameters: z.ZodObject<{
|
|
27
|
+
task: z.ZodString;
|
|
28
|
+
summary: z.ZodString;
|
|
29
|
+
report: z.ZodOptional<z.ZodString>;
|
|
30
|
+
featureName: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
execute: ({ task, summary, report, featureName }: {
|
|
33
|
+
task: string;
|
|
34
|
+
summary: string;
|
|
35
|
+
report?: string;
|
|
36
|
+
featureName?: string;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
error: string;
|
|
39
|
+
completed?: undefined;
|
|
40
|
+
task?: undefined;
|
|
41
|
+
summary?: undefined;
|
|
42
|
+
message?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
completed: boolean;
|
|
45
|
+
task: string;
|
|
46
|
+
summary: string;
|
|
47
|
+
message: string;
|
|
48
|
+
error?: undefined;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
hive_exec_abort: {
|
|
52
|
+
description: string;
|
|
53
|
+
parameters: z.ZodObject<{
|
|
54
|
+
task: z.ZodString;
|
|
55
|
+
featureName: z.ZodOptional<z.ZodString>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
execute: ({ task, featureName }: {
|
|
58
|
+
task: string;
|
|
59
|
+
featureName?: string;
|
|
60
|
+
}) => Promise<{
|
|
61
|
+
error: string;
|
|
62
|
+
aborted?: undefined;
|
|
63
|
+
task?: undefined;
|
|
64
|
+
message?: undefined;
|
|
65
|
+
} | {
|
|
66
|
+
aborted: boolean;
|
|
67
|
+
task: string;
|
|
68
|
+
message: string;
|
|
69
|
+
error?: undefined;
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
};
|