@ronkovic/aad 0.3.9 → 0.4.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 +292 -12
- package/package.json +6 -1
- package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
- package/src/__tests__/integration/pipeline.test.ts +1 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
- package/src/modules/cli/__tests__/resume.test.ts +1 -0
- package/src/modules/cli/__tests__/run.test.ts +1 -0
- package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
- package/src/modules/cli/commands/cleanup.ts +26 -11
- package/src/modules/cli/commands/resume.ts +3 -2
- package/src/modules/cli/commands/run.ts +57 -7
- package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
- package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
- package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
- package/src/modules/dashboard/routes/sse.ts +3 -2
- package/src/modules/dashboard/server.ts +1 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
- package/src/modules/dashboard/ui/dashboard.html +143 -18
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
- package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
- package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
- package/src/modules/git-workspace/branch-manager.ts +24 -3
- package/src/modules/git-workspace/dependency-installer.ts +113 -0
- package/src/modules/git-workspace/git-exec.ts +3 -2
- package/src/modules/git-workspace/index.ts +10 -1
- package/src/modules/git-workspace/merge-service.ts +36 -2
- package/src/modules/git-workspace/pr-manager.ts +278 -0
- package/src/modules/git-workspace/template-copy.ts +302 -0
- package/src/modules/git-workspace/worktree-manager.ts +37 -11
- package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
- package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
- package/src/modules/planning/planning.service.ts +16 -2
- package/src/modules/planning/project-detection.ts +4 -1
- package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +87 -4
- package/src/modules/task-execution/phases/implementer-green.ts +22 -5
- package/src/modules/task-execution/phases/merge.ts +44 -2
- package/src/modules/task-execution/phases/tester-red.ts +22 -5
- package/src/modules/task-execution/phases/tester-verify.ts +22 -6
- package/src/modules/task-queue/dispatcher.ts +50 -1
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/config.ts +6 -0
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/types.ts +13 -0
- package/templates/CLAUDE.md +122 -0
- package/templates/settings.json +117 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import { GitWorkspaceError } from "@aad/shared/errors";
|
|
3
|
+
|
|
4
|
+
export interface PrManagerOptions {
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DraftPrOptions {
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
baseBranch: string;
|
|
13
|
+
headBranch: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PrInfo {
|
|
17
|
+
number: number;
|
|
18
|
+
url: string;
|
|
19
|
+
state: "draft" | "open" | "closed";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manage Pull Request lifecycle via GitHub CLI (gh)
|
|
24
|
+
*/
|
|
25
|
+
export class PrManager {
|
|
26
|
+
private repoRoot: string;
|
|
27
|
+
private logger?: Logger;
|
|
28
|
+
|
|
29
|
+
constructor(options: PrManagerOptions) {
|
|
30
|
+
this.repoRoot = options.repoRoot;
|
|
31
|
+
this.logger = options.logger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if gh CLI is installed
|
|
36
|
+
*/
|
|
37
|
+
async checkGhInstalled(): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
const proc = Bun.spawn(["gh", "--version"], {
|
|
40
|
+
cwd: this.repoRoot,
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "pipe",
|
|
43
|
+
});
|
|
44
|
+
const exitCode = await proc.exited;
|
|
45
|
+
return exitCode === 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a draft PR
|
|
53
|
+
*/
|
|
54
|
+
async createDraftPr(options: DraftPrOptions): Promise<PrInfo> {
|
|
55
|
+
this.logger?.info({ headBranch: options.headBranch, baseBranch: options.baseBranch }, "Creating draft PR");
|
|
56
|
+
|
|
57
|
+
if (!(await this.checkGhInstalled())) {
|
|
58
|
+
throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
|
|
59
|
+
suggestion: "Install gh from https://cli.github.com/",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Create draft PR using gh CLI
|
|
65
|
+
const proc = Bun.spawn(
|
|
66
|
+
[
|
|
67
|
+
"gh",
|
|
68
|
+
"pr",
|
|
69
|
+
"create",
|
|
70
|
+
"--draft",
|
|
71
|
+
"--title",
|
|
72
|
+
options.title,
|
|
73
|
+
"--body",
|
|
74
|
+
options.body,
|
|
75
|
+
"--base",
|
|
76
|
+
options.baseBranch,
|
|
77
|
+
"--head",
|
|
78
|
+
options.headBranch,
|
|
79
|
+
],
|
|
80
|
+
{
|
|
81
|
+
cwd: this.repoRoot,
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "pipe",
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const exitCode = await proc.exited;
|
|
88
|
+
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
89
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
90
|
+
|
|
91
|
+
if (exitCode !== 0) {
|
|
92
|
+
throw new Error(`gh pr create failed: ${stderr}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse PR URL from stdout (gh prints the PR URL)
|
|
96
|
+
const url = stdout.trim();
|
|
97
|
+
const match = url.match(/\/pull\/(\d+)$/);
|
|
98
|
+
const number = match?.[1] ? parseInt(match[1], 10) : 0;
|
|
99
|
+
|
|
100
|
+
this.logger?.info({ prNumber: number, url }, "Draft PR created");
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
number,
|
|
104
|
+
url,
|
|
105
|
+
state: "draft",
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new GitWorkspaceError("Failed to create draft PR", {
|
|
109
|
+
headBranch: options.headBranch,
|
|
110
|
+
baseBranch: options.baseBranch,
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mark draft PR as ready for review
|
|
118
|
+
*/
|
|
119
|
+
async markPrReady(prNumber: number): Promise<void> {
|
|
120
|
+
this.logger?.info({ prNumber }, "Marking PR as ready for review");
|
|
121
|
+
|
|
122
|
+
if (!(await this.checkGhInstalled())) {
|
|
123
|
+
throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
|
|
124
|
+
suggestion: "Install gh from https://cli.github.com/",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const proc = Bun.spawn(["gh", "pr", "ready", String(prNumber)], {
|
|
130
|
+
cwd: this.repoRoot,
|
|
131
|
+
stdout: "pipe",
|
|
132
|
+
stderr: "pipe",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const exitCode = await proc.exited;
|
|
136
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
137
|
+
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
throw new Error(`gh pr ready failed: ${stderr}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.logger?.info({ prNumber }, "PR marked as ready");
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new GitWorkspaceError("Failed to mark PR as ready", {
|
|
145
|
+
prNumber,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get PR info
|
|
153
|
+
*/
|
|
154
|
+
async getPrInfo(prNumber: number): Promise<PrInfo | null> {
|
|
155
|
+
if (!(await this.checkGhInstalled())) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const proc = Bun.spawn(
|
|
161
|
+
["gh", "pr", "view", String(prNumber), "--json", "number,url,isDraft,state"],
|
|
162
|
+
{
|
|
163
|
+
cwd: this.repoRoot,
|
|
164
|
+
stdout: "pipe",
|
|
165
|
+
stderr: "pipe",
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const exitCode = await proc.exited;
|
|
170
|
+
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
171
|
+
|
|
172
|
+
if (exitCode !== 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const data = JSON.parse(stdout) as {
|
|
177
|
+
number: number;
|
|
178
|
+
url: string;
|
|
179
|
+
isDraft: boolean;
|
|
180
|
+
state: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
number: data.number,
|
|
185
|
+
url: data.url,
|
|
186
|
+
state: data.isDraft ? "draft" : data.state === "OPEN" ? "open" : "closed",
|
|
187
|
+
};
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Close PR
|
|
195
|
+
*/
|
|
196
|
+
async closePr(prNumber: number): Promise<void> {
|
|
197
|
+
this.logger?.info({ prNumber }, "Closing PR");
|
|
198
|
+
|
|
199
|
+
if (!(await this.checkGhInstalled())) {
|
|
200
|
+
throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
|
|
201
|
+
suggestion: "Install gh from https://cli.github.com/",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const proc = Bun.spawn(["gh", "pr", "close", String(prNumber)], {
|
|
207
|
+
cwd: this.repoRoot,
|
|
208
|
+
stdout: "pipe",
|
|
209
|
+
stderr: "pipe",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const exitCode = await proc.exited;
|
|
213
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
214
|
+
|
|
215
|
+
if (exitCode !== 0) {
|
|
216
|
+
throw new Error(`gh pr close failed: ${stderr}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.logger?.info({ prNumber }, "PR closed");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new GitWorkspaceError("Failed to close PR", {
|
|
222
|
+
prNumber,
|
|
223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Find PR by head branch
|
|
230
|
+
*/
|
|
231
|
+
async findPrByBranch(headBranch: string): Promise<PrInfo | null> {
|
|
232
|
+
if (!(await this.checkGhInstalled())) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const proc = Bun.spawn(
|
|
238
|
+
["gh", "pr", "list", "--head", headBranch, "--json", "number,url,isDraft,state", "--limit", "1"],
|
|
239
|
+
{
|
|
240
|
+
cwd: this.repoRoot,
|
|
241
|
+
stdout: "pipe",
|
|
242
|
+
stderr: "pipe",
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const exitCode = await proc.exited;
|
|
247
|
+
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
248
|
+
|
|
249
|
+
if (exitCode !== 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const data = JSON.parse(stdout) as Array<{
|
|
254
|
+
number: number;
|
|
255
|
+
url: string;
|
|
256
|
+
isDraft: boolean;
|
|
257
|
+
state: string;
|
|
258
|
+
}>;
|
|
259
|
+
|
|
260
|
+
if (data.length === 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const pr = data[0];
|
|
265
|
+
if (!pr) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
number: pr.number,
|
|
271
|
+
url: pr.url,
|
|
272
|
+
state: pr.isDraft ? "draft" : pr.state === "OPEN" ? "open" : "closed",
|
|
273
|
+
};
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { mkdir, readdir, cp, stat } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Logger } from "pino";
|
|
5
|
+
import { loadAndMergeSettings, saveSettings } from "./settings-merge";
|
|
6
|
+
|
|
7
|
+
export interface CopyTemplatesOptions {
|
|
8
|
+
targetDir: string; // worktreeパス
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
templateDir?: string; // default: resolveTemplateDir(projectRoot)
|
|
11
|
+
logger?: Logger;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PROJECT_CONTEXT_HEADER = "# プロジェクト固有コンテキスト\n\n";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve template directory path:
|
|
18
|
+
* 1. Project local override: {projectRoot}/.aad/templates/ (if exists)
|
|
19
|
+
* 2. Package bundled: {packageRoot}/templates/
|
|
20
|
+
*/
|
|
21
|
+
export function resolveTemplateDir(projectRoot: string): string {
|
|
22
|
+
const localTemplateDir = join(projectRoot, ".aad", "templates");
|
|
23
|
+
|
|
24
|
+
// Check if local override exists
|
|
25
|
+
if (existsSync(localTemplateDir)) {
|
|
26
|
+
return localTemplateDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback to package bundled templates
|
|
30
|
+
// src/modules/git-workspace/ -> ../../../.aad/templates
|
|
31
|
+
const packageRoot = join(import.meta.dir, "..", "..", "..");
|
|
32
|
+
return join(packageRoot, ".aad", "templates");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copy templates from .aad/templates/ to target worktree
|
|
37
|
+
* Implements overlay merge strategy:
|
|
38
|
+
* - CLAUDE.md: template base + project-specific append
|
|
39
|
+
* - .claude/: project base + template overlay (agents, hooks, rules, skills)
|
|
40
|
+
* - settings.json: deep merge
|
|
41
|
+
* - .gitignore: copy from template
|
|
42
|
+
*/
|
|
43
|
+
export async function copyTemplatesToWorktree(
|
|
44
|
+
options: CopyTemplatesOptions
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const {
|
|
47
|
+
targetDir,
|
|
48
|
+
projectRoot,
|
|
49
|
+
templateDir = resolveTemplateDir(projectRoot),
|
|
50
|
+
logger,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
logger?.info({ targetDir, templateDir }, "Copying templates to worktree");
|
|
54
|
+
|
|
55
|
+
// Ensure target directory exists
|
|
56
|
+
await mkdir(targetDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// Each step is non-critical, continue on errors
|
|
59
|
+
try {
|
|
60
|
+
// (a) CLAUDE.md: template base + project CLAUDE.md appended
|
|
61
|
+
await copyCLAUDEmd({
|
|
62
|
+
templateDir,
|
|
63
|
+
projectRoot,
|
|
64
|
+
targetDir,
|
|
65
|
+
logger,
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger?.debug({ error }, "CLAUDE.md copy failed (non-critical)");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// (b) .claude/: overlay merge (agents, hooks, rules, skills)
|
|
73
|
+
await copyClaudeDir({
|
|
74
|
+
templateDir,
|
|
75
|
+
projectRoot,
|
|
76
|
+
targetDir,
|
|
77
|
+
logger,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger?.debug({ error }, ".claude/ copy failed (non-critical)");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// (c) settings.json: deep merge
|
|
85
|
+
await mergeSettingsJson({
|
|
86
|
+
templateDir,
|
|
87
|
+
projectRoot,
|
|
88
|
+
targetDir,
|
|
89
|
+
logger,
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger?.debug({ error }, "settings.json merge failed (non-critical)");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// (d) .gitignore: copy from template
|
|
97
|
+
await copyGitignore({
|
|
98
|
+
templateDir,
|
|
99
|
+
targetDir,
|
|
100
|
+
logger,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger?.debug({ error }, ".gitignore copy failed (non-critical)");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
logger?.info({ targetDir }, "Template copy completed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Copy CLAUDE.md: template base + project content appended
|
|
111
|
+
*/
|
|
112
|
+
async function copyCLAUDEmd(opts: {
|
|
113
|
+
templateDir: string;
|
|
114
|
+
projectRoot: string;
|
|
115
|
+
targetDir: string;
|
|
116
|
+
logger?: Logger;
|
|
117
|
+
}): Promise<void> {
|
|
118
|
+
const { templateDir, projectRoot, targetDir, logger } = opts;
|
|
119
|
+
|
|
120
|
+
const templateCLAUDE = join(templateDir, "CLAUDE.md");
|
|
121
|
+
const projectCLAUDE = join(projectRoot, "CLAUDE.md");
|
|
122
|
+
const targetCLAUDE = join(targetDir, "CLAUDE.md");
|
|
123
|
+
|
|
124
|
+
let content = "";
|
|
125
|
+
|
|
126
|
+
// Start with template CLAUDE.md if exists
|
|
127
|
+
try {
|
|
128
|
+
content = await Bun.file(templateCLAUDE).text();
|
|
129
|
+
logger?.debug({ templateCLAUDE }, "Loaded template CLAUDE.md");
|
|
130
|
+
} catch {
|
|
131
|
+
logger?.debug({ templateCLAUDE }, "No template CLAUDE.md found (skipping)");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Append project CLAUDE.md if exists
|
|
135
|
+
try {
|
|
136
|
+
const projectContent = await Bun.file(projectCLAUDE).text();
|
|
137
|
+
if (projectContent.trim()) {
|
|
138
|
+
content += "\n\n" + PROJECT_CONTEXT_HEADER + projectContent;
|
|
139
|
+
logger?.debug({ projectCLAUDE }, "Appended project CLAUDE.md");
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
logger?.debug({ projectCLAUDE }, "No project CLAUDE.md found (skipping)");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Write to target
|
|
146
|
+
if (content.trim()) {
|
|
147
|
+
await Bun.write(targetCLAUDE, content);
|
|
148
|
+
logger?.debug({ targetCLAUDE }, "Written CLAUDE.md");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Copy .claude/ directory: project base + template overlay
|
|
154
|
+
* Overlay: agents, hooks, rules, skills from template
|
|
155
|
+
* Preserve: agent-memory from project (existing wins)
|
|
156
|
+
*/
|
|
157
|
+
async function copyClaudeDir(opts: {
|
|
158
|
+
templateDir: string;
|
|
159
|
+
projectRoot: string;
|
|
160
|
+
targetDir: string;
|
|
161
|
+
logger?: Logger;
|
|
162
|
+
}): Promise<void> {
|
|
163
|
+
const { templateDir, projectRoot, targetDir, logger } = opts;
|
|
164
|
+
|
|
165
|
+
const projectClaudeDir = join(projectRoot, ".claude");
|
|
166
|
+
const templateClaudeDir = join(templateDir, ".claude");
|
|
167
|
+
const targetClaudeDir = join(targetDir, ".claude");
|
|
168
|
+
|
|
169
|
+
// Ensure target .claude/ exists
|
|
170
|
+
await mkdir(targetClaudeDir, { recursive: true });
|
|
171
|
+
|
|
172
|
+
// Copy project .claude/ subdirectories as base layer (if exists), excluding agent-memory
|
|
173
|
+
// We manually copy subdirectories to exclude agent-memory
|
|
174
|
+
try {
|
|
175
|
+
let projectClaudeExists = false;
|
|
176
|
+
try {
|
|
177
|
+
const stats = await stat(projectClaudeDir);
|
|
178
|
+
projectClaudeExists = stats.isDirectory();
|
|
179
|
+
} catch {
|
|
180
|
+
projectClaudeExists = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (projectClaudeExists) {
|
|
184
|
+
const entries = await readdir(projectClaudeDir, { withFileTypes: true });
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.name === "agent-memory") {
|
|
187
|
+
continue; // Skip agent-memory, will handle separately
|
|
188
|
+
}
|
|
189
|
+
const src = join(projectClaudeDir, entry.name);
|
|
190
|
+
const dest = join(targetClaudeDir, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
await cp(src, dest, { recursive: true });
|
|
193
|
+
} else {
|
|
194
|
+
await cp(src, dest);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
logger?.debug({ projectClaudeDir }, "Copied project .claude/ as base (excluding agent-memory)");
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger?.debug({ projectClaudeDir, error }, "No project .claude/ found or copy failed");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Overlay template subdirectories (agents, hooks, rules, skills)
|
|
204
|
+
const overlayDirs = ["agents", "hooks", "rules", "skills"];
|
|
205
|
+
|
|
206
|
+
for (const subdir of overlayDirs) {
|
|
207
|
+
const templateSubdir = join(templateClaudeDir, subdir);
|
|
208
|
+
const targetSubdir = join(targetClaudeDir, subdir);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Ensure target subdir exists
|
|
212
|
+
await mkdir(targetSubdir, { recursive: true });
|
|
213
|
+
|
|
214
|
+
// Copy template files (overwrite)
|
|
215
|
+
await cp(templateSubdir, targetSubdir, { recursive: true });
|
|
216
|
+
logger?.debug({ templateSubdir, targetSubdir }, `Overlayed ${subdir}/`);
|
|
217
|
+
} catch {
|
|
218
|
+
logger?.debug({ templateSubdir }, `No template ${subdir}/ found (skipping)`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle agent-memory: existing (project) wins, don't overwrite
|
|
223
|
+
const projectMemoryDir = join(projectClaudeDir, "agent-memory");
|
|
224
|
+
const templateMemoryDir = join(templateClaudeDir, "agent-memory");
|
|
225
|
+
const targetMemoryDir = join(targetClaudeDir, "agent-memory");
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Check if project has agent-memory directory
|
|
229
|
+
let projectMemoryExists = false;
|
|
230
|
+
try {
|
|
231
|
+
const stats = await stat(projectMemoryDir);
|
|
232
|
+
projectMemoryExists = stats.isDirectory();
|
|
233
|
+
} catch {
|
|
234
|
+
projectMemoryExists = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (projectMemoryExists) {
|
|
238
|
+
// Copy project agent-memory
|
|
239
|
+
await cp(projectMemoryDir, targetMemoryDir, { recursive: true });
|
|
240
|
+
logger?.debug({ projectMemoryDir }, "Preserved project agent-memory/");
|
|
241
|
+
} else {
|
|
242
|
+
// No project memory, copy template if exists
|
|
243
|
+
try {
|
|
244
|
+
await cp(templateMemoryDir, targetMemoryDir, { recursive: true });
|
|
245
|
+
logger?.debug({ templateMemoryDir }, "Copied template agent-memory/");
|
|
246
|
+
} catch {
|
|
247
|
+
logger?.debug("No template agent-memory found");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
logger?.debug({ error }, "No agent-memory to handle");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Merge settings.json: project + template deep merge
|
|
257
|
+
*/
|
|
258
|
+
async function mergeSettingsJson(opts: {
|
|
259
|
+
templateDir: string;
|
|
260
|
+
projectRoot: string;
|
|
261
|
+
targetDir: string;
|
|
262
|
+
logger?: Logger;
|
|
263
|
+
}): Promise<void> {
|
|
264
|
+
const { templateDir, projectRoot, targetDir, logger } = opts;
|
|
265
|
+
|
|
266
|
+
const projectSettings = join(projectRoot, ".claude", "settings.json");
|
|
267
|
+
const templateSettings = join(templateDir, "settings.json");
|
|
268
|
+
const targetSettings = join(targetDir, ".claude", "settings.json");
|
|
269
|
+
|
|
270
|
+
// Load and merge settings
|
|
271
|
+
const merged = await loadAndMergeSettings(
|
|
272
|
+
[projectSettings, templateSettings],
|
|
273
|
+
logger
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Save to target
|
|
277
|
+
await mkdir(join(targetDir, ".claude"), { recursive: true });
|
|
278
|
+
await saveSettings(targetSettings, merged, logger);
|
|
279
|
+
|
|
280
|
+
logger?.debug({ targetSettings }, "Merged and saved settings.json");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Copy .gitignore from template
|
|
285
|
+
*/
|
|
286
|
+
async function copyGitignore(opts: {
|
|
287
|
+
templateDir: string;
|
|
288
|
+
targetDir: string;
|
|
289
|
+
logger?: Logger;
|
|
290
|
+
}): Promise<void> {
|
|
291
|
+
const { templateDir, targetDir, logger } = opts;
|
|
292
|
+
|
|
293
|
+
const templateGitignore = join(templateDir, ".gitignore");
|
|
294
|
+
const targetGitignore = join(targetDir, ".gitignore");
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
await cp(templateGitignore, targetGitignore, { force: true });
|
|
298
|
+
logger?.debug({ templateGitignore, targetGitignore }, "Copied .gitignore");
|
|
299
|
+
} catch {
|
|
300
|
+
logger?.debug({ templateGitignore }, "No template .gitignore found (skipping)");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -169,16 +169,20 @@ export class WorktreeManager {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// Try to force remove directory as fallback
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
// Try to force remove directory as fallback (only if force=true)
|
|
173
|
+
if (force) {
|
|
174
|
+
try {
|
|
175
|
+
await this.fsOps.rm(worktreePath, { recursive: true, force: true });
|
|
176
|
+
await this.gitOps.gitExec(["worktree", "prune"], { cwd: this.repoRoot, logger: this.logger });
|
|
177
|
+
return;
|
|
178
|
+
} catch (cleanupError) {
|
|
179
|
+
this.logger?.debug({ cleanupError, worktreePath }, "Worktree cleanup fallback failed");
|
|
180
|
+
}
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
throw new GitWorkspaceError("Failed to remove worktree", {
|
|
181
184
|
worktreePath,
|
|
185
|
+
force,
|
|
182
186
|
error: error instanceof Error ? error.message : String(error),
|
|
183
187
|
});
|
|
184
188
|
}
|
|
@@ -217,6 +221,15 @@ export class WorktreeManager {
|
|
|
217
221
|
}
|
|
218
222
|
}
|
|
219
223
|
|
|
224
|
+
// Push the last worktree if it hasn't been pushed yet
|
|
225
|
+
if (current.path && current.head) {
|
|
226
|
+
worktrees.push({
|
|
227
|
+
path: current.path,
|
|
228
|
+
branch: current.branch ?? "(detached)",
|
|
229
|
+
head: current.head,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
220
233
|
return worktrees;
|
|
221
234
|
}
|
|
222
235
|
|
|
@@ -256,16 +269,24 @@ export class WorktreeManager {
|
|
|
256
269
|
export async function cleanupOrphanedFromPreviousRuns(
|
|
257
270
|
worktreeManager: WorktreeManager,
|
|
258
271
|
logger: Logger,
|
|
259
|
-
): Promise<{ removedWorktrees: number; deletedBranches: number }> {
|
|
272
|
+
): Promise<{ removedWorktrees: number; deletedBranches: number; preservedBranches: number }> {
|
|
260
273
|
logger.info("Cleaning up orphaned worktrees/branches from previous runs");
|
|
261
274
|
|
|
262
275
|
let removedWorktrees = 0;
|
|
263
276
|
let deletedBranches = 0;
|
|
277
|
+
let preservedBranches = 0;
|
|
264
278
|
|
|
265
|
-
// 1. Remove all worktrees under .aad/worktrees/
|
|
279
|
+
// 1. Remove all worktrees under .aad/worktrees/ (except parent worktrees)
|
|
266
280
|
try {
|
|
267
281
|
const worktrees = await worktreeManager.listWorktrees();
|
|
268
|
-
const aadWorktrees = worktrees.filter((wt) =>
|
|
282
|
+
const aadWorktrees = worktrees.filter((wt) => {
|
|
283
|
+
if (!wt.path.includes("/.aad/worktrees/")) return false;
|
|
284
|
+
if (/\/parent-[^/]+$/.test(wt.path)) {
|
|
285
|
+
logger.info({ path: wt.path }, "Preserving parent worktree (contains commit history)");
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
});
|
|
269
290
|
|
|
270
291
|
for (const wt of aadWorktrees) {
|
|
271
292
|
try {
|
|
@@ -307,6 +328,11 @@ export async function cleanupOrphanedFromPreviousRuns(
|
|
|
307
328
|
.filter((b: string) => b.length > 0);
|
|
308
329
|
|
|
309
330
|
for (const branch of branches) {
|
|
331
|
+
if (branch.endsWith("/parent")) {
|
|
332
|
+
logger.info({ branch }, "Preserving parent branch (contains commit history)");
|
|
333
|
+
preservedBranches++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
310
336
|
try {
|
|
311
337
|
await gitOps.gitExec(["branch", "-D", branch], { cwd: repoRoot });
|
|
312
338
|
deletedBranches++;
|
|
@@ -323,6 +349,6 @@ export async function cleanupOrphanedFromPreviousRuns(
|
|
|
323
349
|
logger.debug({ error }, "Failed to list branches during cleanup");
|
|
324
350
|
}
|
|
325
351
|
|
|
326
|
-
logger.info({ removedWorktrees, deletedBranches }, "Orphaned cleanup completed");
|
|
327
|
-
return { removedWorktrees, deletedBranches };
|
|
352
|
+
logger.info({ removedWorktrees, deletedBranches, preservedBranches }, "Orphaned cleanup completed");
|
|
353
|
+
return { removedWorktrees, deletedBranches, preservedBranches };
|
|
328
354
|
}
|