gsd-pi 0.2.9 → 0.3.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 +23 -0
- package/dist/cli.js +47 -5
- package/dist/wizard.js +2 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +6 -1
- package/src/resources/extensions/gsd/files.ts +7 -7
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/index.ts +36 -1
- package/src/resources/extensions/gsd/migrate/command.ts +215 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +53 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +89 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/worktree-command.ts +527 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +302 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Worktree Command — /worktree
|
|
3
|
+
*
|
|
4
|
+
* Create, list, merge, and remove git worktrees under .gsd/worktrees/.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* /worktree <name> — create a new worktree
|
|
8
|
+
* /worktree list — list existing worktrees
|
|
9
|
+
* /worktree merge <branch> [target] — start LLM-guided merge (default target: main)
|
|
10
|
+
* /worktree remove <name> — remove a worktree and its branch
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
15
|
+
import { autoCommitCurrentBranch } from "./worktree.js";
|
|
16
|
+
import { showConfirm } from "../shared/confirm-ui.js";
|
|
17
|
+
import {
|
|
18
|
+
createWorktree,
|
|
19
|
+
listWorktrees,
|
|
20
|
+
removeWorktree,
|
|
21
|
+
diffWorktreeGSD,
|
|
22
|
+
getMainBranch,
|
|
23
|
+
getWorktreeGSDDiff,
|
|
24
|
+
getWorktreeLog,
|
|
25
|
+
worktreeBranchName,
|
|
26
|
+
worktreePath,
|
|
27
|
+
} from "./worktree-manager.js";
|
|
28
|
+
import { existsSync, realpathSync, readFileSync, utimesSync } from "node:fs";
|
|
29
|
+
import { join, resolve } from "node:path";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tracks the original project root so we can switch back.
|
|
33
|
+
* Set when we first chdir into a worktree, cleared on return.
|
|
34
|
+
*/
|
|
35
|
+
let originalCwd: string | null = null;
|
|
36
|
+
|
|
37
|
+
/** Get the original project root if currently in a worktree, or null. */
|
|
38
|
+
export function getWorktreeOriginalCwd(): string | null {
|
|
39
|
+
return originalCwd;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the git HEAD file path for a given directory.
|
|
44
|
+
* Handles both normal repos (.git is a directory) and worktrees (.git is a file).
|
|
45
|
+
*/
|
|
46
|
+
function resolveGitHeadPath(dir: string): string | null {
|
|
47
|
+
const gitPath = join(dir, ".git");
|
|
48
|
+
if (!existsSync(gitPath)) return null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const content = readFileSync(gitPath, "utf8").trim();
|
|
52
|
+
if (content.startsWith("gitdir: ")) {
|
|
53
|
+
// Worktree — .git is a file pointing to the real gitdir
|
|
54
|
+
const gitDir = resolve(dir, content.slice(8));
|
|
55
|
+
const headPath = join(gitDir, "HEAD");
|
|
56
|
+
return existsSync(headPath) ? headPath : null;
|
|
57
|
+
}
|
|
58
|
+
// Normal repo — .git is a directory
|
|
59
|
+
const headPath = join(dir, ".git", "HEAD");
|
|
60
|
+
return existsSync(headPath) ? headPath : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Nudge pi's FooterDataProvider to re-read the git branch.
|
|
68
|
+
*
|
|
69
|
+
* The footer caches the branch and watches a single .git dir for changes.
|
|
70
|
+
* After process.chdir() into a worktree (or back), the watcher is stale —
|
|
71
|
+
* it's still watching the old git dir. We touch HEAD in both the old and
|
|
72
|
+
* new git dirs to ensure the watcher fires regardless of which one it's
|
|
73
|
+
* monitoring. This clears cachedBranch; the next getGitBranch() call uses
|
|
74
|
+
* the new process.cwd() and picks up the correct branch.
|
|
75
|
+
*/
|
|
76
|
+
function nudgeGitBranchCache(previousCwd: string): void {
|
|
77
|
+
const now = new Date();
|
|
78
|
+
for (const dir of [previousCwd, process.cwd()]) {
|
|
79
|
+
try {
|
|
80
|
+
const headPath = resolveGitHeadPath(dir);
|
|
81
|
+
if (headPath) utimesSync(headPath, now, now);
|
|
82
|
+
} catch {
|
|
83
|
+
// Best-effort — branch display may be stale
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the name of the active worktree, or null if not in one. */
|
|
89
|
+
export function getActiveWorktreeName(): string | null {
|
|
90
|
+
if (!originalCwd) return null;
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
const wtDir = join(originalCwd, ".gsd", "worktrees");
|
|
93
|
+
if (!cwd.startsWith(wtDir)) return null;
|
|
94
|
+
const rel = cwd.slice(wtDir.length + 1);
|
|
95
|
+
const name = rel.split("/")[0] ?? rel.split("\\")[0];
|
|
96
|
+
return name || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Shared completions and handler (used by both /worktree and /wt) ────────
|
|
100
|
+
|
|
101
|
+
function worktreeCompletions(prefix: string) {
|
|
102
|
+
const parts = prefix.trim().split(/\s+/);
|
|
103
|
+
const subcommands = ["list", "merge", "remove", "switch", "return"];
|
|
104
|
+
|
|
105
|
+
if (parts.length <= 1) {
|
|
106
|
+
const partial = parts[0] ?? "";
|
|
107
|
+
const cmdCompletions = subcommands
|
|
108
|
+
.filter(cmd => cmd.startsWith(partial))
|
|
109
|
+
.map(cmd => ({ value: cmd, label: cmd }));
|
|
110
|
+
try {
|
|
111
|
+
const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
|
|
112
|
+
const existing = listWorktrees(mainBase);
|
|
113
|
+
const nameCompletions = existing
|
|
114
|
+
.filter(wt => wt.name.startsWith(partial))
|
|
115
|
+
.map(wt => ({ value: wt.name, label: wt.name }));
|
|
116
|
+
return [...cmdCompletions, ...nameCompletions];
|
|
117
|
+
} catch {
|
|
118
|
+
return cmdCompletions;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ((parts[0] === "merge" || parts[0] === "remove" || parts[0] === "switch") && parts.length <= 2) {
|
|
123
|
+
const namePrefix = parts[1] ?? "";
|
|
124
|
+
try {
|
|
125
|
+
const existing = listWorktrees(process.cwd());
|
|
126
|
+
return existing
|
|
127
|
+
.filter(wt => wt.name.startsWith(namePrefix))
|
|
128
|
+
.map(wt => ({ value: `${parts[0]} ${wt.name}`, label: wt.name }));
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function worktreeHandler(
|
|
138
|
+
args: string,
|
|
139
|
+
ctx: ExtensionCommandContext,
|
|
140
|
+
pi: ExtensionAPI,
|
|
141
|
+
alias: string,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const trimmed = (typeof args === "string" ? args : "").trim();
|
|
144
|
+
const basePath = process.cwd();
|
|
145
|
+
|
|
146
|
+
if (trimmed === "") {
|
|
147
|
+
ctx.ui.notify(
|
|
148
|
+
[
|
|
149
|
+
"Usage:",
|
|
150
|
+
` /${alias} <name> — create and switch into a new worktree`,
|
|
151
|
+
` /${alias} switch <name> — switch into an existing worktree`,
|
|
152
|
+
` /${alias} return — switch back to the main project tree`,
|
|
153
|
+
` /${alias} list — list all worktrees`,
|
|
154
|
+
` /${alias} merge <branch> [target] — merge worktree into target branch`,
|
|
155
|
+
` /${alias} remove <name> — remove a worktree and its branch`,
|
|
156
|
+
].join("\n"),
|
|
157
|
+
"info",
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (trimmed === "list") {
|
|
163
|
+
await handleList(basePath, ctx);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (trimmed === "return") {
|
|
168
|
+
await handleReturn(ctx);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (trimmed.startsWith("switch ")) {
|
|
173
|
+
const name = trimmed.replace(/^switch\s+/, "").trim();
|
|
174
|
+
if (!name) {
|
|
175
|
+
ctx.ui.notify(`Usage: /${alias} switch <name>`, "warning");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await handleSwitch(basePath, name, ctx);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (trimmed.startsWith("merge ")) {
|
|
183
|
+
const mergeArgs = trimmed.replace(/^merge\s+/, "").trim().split(/\s+/);
|
|
184
|
+
const name = mergeArgs[0] ?? "";
|
|
185
|
+
const targetBranch = mergeArgs[1];
|
|
186
|
+
if (!name) {
|
|
187
|
+
ctx.ui.notify(`Usage: /${alias} merge <branch> [target]`, "warning");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const mainBase = originalCwd ?? basePath;
|
|
191
|
+
await handleMerge(mainBase, name, ctx, pi, targetBranch);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (trimmed.startsWith("remove ")) {
|
|
196
|
+
const name = trimmed.replace(/^remove\s+/, "").trim();
|
|
197
|
+
if (!name) {
|
|
198
|
+
ctx.ui.notify(`Usage: /${alias} remove <name>`, "warning");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const mainBase = originalCwd ?? basePath;
|
|
202
|
+
await handleRemove(mainBase, name, ctx);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const RESERVED = ["list", "return", "switch", "merge", "remove"];
|
|
207
|
+
if (RESERVED.includes(trimmed)) {
|
|
208
|
+
ctx.ui.notify(`Usage: /${alias} ${trimmed}${trimmed === "list" || trimmed === "return" ? "" : " <name>"}`, "warning");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const mainBase = originalCwd ?? basePath;
|
|
213
|
+
const nameOnly = trimmed.split(/\s+/)[0]!;
|
|
214
|
+
if (trimmed !== nameOnly) {
|
|
215
|
+
ctx.ui.notify(`Unknown command. Did you mean /${alias} switch ${nameOnly}?`, "warning");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const existing = listWorktrees(mainBase);
|
|
220
|
+
if (existing.some(wt => wt.name === nameOnly)) {
|
|
221
|
+
await handleSwitch(basePath, nameOnly, ctx);
|
|
222
|
+
} else {
|
|
223
|
+
await handleCreate(basePath, nameOnly, ctx);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function registerWorktreeCommand(pi: ExtensionAPI): void {
|
|
228
|
+
pi.registerCommand("worktree", {
|
|
229
|
+
description: "Git worktrees: /worktree <name> | list | merge <branch> [target] | remove <name>",
|
|
230
|
+
getArgumentCompletions: worktreeCompletions,
|
|
231
|
+
|
|
232
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
233
|
+
await worktreeHandler(args, ctx, pi, "worktree");
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// /wt alias — same handler, same completions
|
|
238
|
+
pi.registerCommand("wt", {
|
|
239
|
+
description: "Alias for /worktree — Git worktrees: /wt <name> | list | merge | remove",
|
|
240
|
+
getArgumentCompletions: worktreeCompletions,
|
|
241
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
242
|
+
await worktreeHandler(args, ctx, pi, "wt");
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Handlers ──────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
async function handleCreate(
|
|
250
|
+
basePath: string,
|
|
251
|
+
name: string,
|
|
252
|
+
ctx: ExtensionCommandContext,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
try {
|
|
255
|
+
// Create from the main tree, not from inside another worktree
|
|
256
|
+
const mainBase = originalCwd ?? basePath;
|
|
257
|
+
const info = createWorktree(mainBase, name);
|
|
258
|
+
|
|
259
|
+
// Auto-commit dirty files before leaving current workspace
|
|
260
|
+
const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
|
|
261
|
+
|
|
262
|
+
// Track original cwd before switching
|
|
263
|
+
if (!originalCwd) originalCwd = basePath;
|
|
264
|
+
|
|
265
|
+
const prevCwd = process.cwd();
|
|
266
|
+
process.chdir(info.path);
|
|
267
|
+
nudgeGitBranchCache(prevCwd);
|
|
268
|
+
|
|
269
|
+
const commitNote = commitMsg ? `\n Auto-committed on previous branch before switching.` : "";
|
|
270
|
+
ctx.ui.notify(
|
|
271
|
+
[
|
|
272
|
+
`Worktree "${name}" created and activated.`,
|
|
273
|
+
` Path: ${info.path}`,
|
|
274
|
+
` Branch: ${info.branch}`,
|
|
275
|
+
commitNote,
|
|
276
|
+
`Session is now in the worktree. All commands run here.`,
|
|
277
|
+
`Use /worktree merge ${name} to merge back when done.`,
|
|
278
|
+
`Use /worktree return to switch back to the main tree.`,
|
|
279
|
+
].filter(Boolean).join("\n"),
|
|
280
|
+
"info",
|
|
281
|
+
);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
284
|
+
ctx.ui.notify(`Failed to create worktree: ${msg}`, "error");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function handleSwitch(
|
|
289
|
+
basePath: string,
|
|
290
|
+
name: string,
|
|
291
|
+
ctx: ExtensionCommandContext,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
try {
|
|
294
|
+
const mainBase = originalCwd ?? basePath;
|
|
295
|
+
const wtPath = worktreePath(mainBase, name);
|
|
296
|
+
|
|
297
|
+
if (!existsSync(wtPath)) {
|
|
298
|
+
ctx.ui.notify(
|
|
299
|
+
`Worktree "${name}" not found. Run /worktree list to see available worktrees.`,
|
|
300
|
+
"warning",
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Auto-commit dirty files before leaving current workspace
|
|
306
|
+
const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
|
|
307
|
+
|
|
308
|
+
// Track original cwd before switching
|
|
309
|
+
if (!originalCwd) originalCwd = basePath;
|
|
310
|
+
|
|
311
|
+
const prevCwd = process.cwd();
|
|
312
|
+
process.chdir(wtPath);
|
|
313
|
+
nudgeGitBranchCache(prevCwd);
|
|
314
|
+
|
|
315
|
+
const commitNote = commitMsg ? `\n Auto-committed on previous branch before switching.` : "";
|
|
316
|
+
ctx.ui.notify(
|
|
317
|
+
[
|
|
318
|
+
`Switched to worktree "${name}".`,
|
|
319
|
+
` Path: ${wtPath}`,
|
|
320
|
+
` Branch: ${worktreeBranchName(name)}`,
|
|
321
|
+
commitNote,
|
|
322
|
+
`Use /worktree return to switch back to the main tree.`,
|
|
323
|
+
].filter(Boolean).join("\n"),
|
|
324
|
+
"info",
|
|
325
|
+
);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
328
|
+
ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function handleReturn(ctx: ExtensionCommandContext): Promise<void> {
|
|
333
|
+
if (!originalCwd) {
|
|
334
|
+
ctx.ui.notify("Already in the main project tree.", "info");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Auto-commit dirty files before leaving worktree
|
|
339
|
+
const commitMsg = autoCommitCurrentBranch(process.cwd(), "worktree-return", "worktree");
|
|
340
|
+
|
|
341
|
+
const returnTo = originalCwd;
|
|
342
|
+
originalCwd = null;
|
|
343
|
+
|
|
344
|
+
const prevCwd = process.cwd();
|
|
345
|
+
process.chdir(returnTo);
|
|
346
|
+
nudgeGitBranchCache(prevCwd);
|
|
347
|
+
|
|
348
|
+
const commitNote = commitMsg ? `\n Auto-committed on worktree branch before returning.` : "";
|
|
349
|
+
ctx.ui.notify(
|
|
350
|
+
[
|
|
351
|
+
`Returned to main project tree.`,
|
|
352
|
+
` Path: ${returnTo}`,
|
|
353
|
+
commitNote,
|
|
354
|
+
].filter(Boolean).join("\n"),
|
|
355
|
+
"info",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ANSI helpers for list formatting
|
|
360
|
+
const BOLD = "\x1b[1m";
|
|
361
|
+
const DIM = "\x1b[2m";
|
|
362
|
+
const RESET = "\x1b[0m";
|
|
363
|
+
const CYAN = "\x1b[36m";
|
|
364
|
+
const GREEN = "\x1b[32m";
|
|
365
|
+
const YELLOW = "\x1b[33m";
|
|
366
|
+
const WHITE = "\x1b[37m";
|
|
367
|
+
|
|
368
|
+
async function handleList(
|
|
369
|
+
basePath: string,
|
|
370
|
+
ctx: ExtensionCommandContext,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
try {
|
|
373
|
+
const mainBase = originalCwd ?? basePath;
|
|
374
|
+
const worktrees = listWorktrees(mainBase);
|
|
375
|
+
|
|
376
|
+
if (worktrees.length === 0) {
|
|
377
|
+
ctx.ui.notify("No GSD worktrees found. Create one with /worktree <name>.", "info");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const cwd = process.cwd();
|
|
382
|
+
const lines = [`${BOLD}${WHITE}GSD Worktrees${RESET}`, ""];
|
|
383
|
+
for (const wt of worktrees) {
|
|
384
|
+
const isCurrent = cwd === wt.path
|
|
385
|
+
|| (existsSync(cwd) && existsSync(wt.path)
|
|
386
|
+
&& realpathSync(cwd) === realpathSync(wt.path));
|
|
387
|
+
|
|
388
|
+
const nameColor = isCurrent ? GREEN : CYAN;
|
|
389
|
+
const badge = isCurrent ? ` ${GREEN}● active${RESET}` : !wt.exists ? ` ${YELLOW}✗ missing${RESET}` : "";
|
|
390
|
+
lines.push(` ${BOLD}${nameColor}${wt.name}${RESET}${badge}`);
|
|
391
|
+
lines.push(` ${DIM} branch${RESET} ${wt.branch}`);
|
|
392
|
+
lines.push(` ${DIM} path${RESET} ${DIM}${wt.path}${RESET}`);
|
|
393
|
+
lines.push("");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (originalCwd) {
|
|
397
|
+
lines.push(`${DIM}Main tree: ${originalCwd}${RESET}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
403
|
+
ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function handleMerge(
|
|
408
|
+
basePath: string,
|
|
409
|
+
name: string,
|
|
410
|
+
ctx: ExtensionCommandContext,
|
|
411
|
+
pi: ExtensionAPI,
|
|
412
|
+
targetBranch?: string,
|
|
413
|
+
): Promise<void> {
|
|
414
|
+
try {
|
|
415
|
+
const branch = worktreeBranchName(name);
|
|
416
|
+
const mainBranch = targetBranch ?? getMainBranch(basePath);
|
|
417
|
+
|
|
418
|
+
// Validate the worktree/branch exists
|
|
419
|
+
const worktrees = listWorktrees(basePath);
|
|
420
|
+
const wt = worktrees.find(w => w.name === name);
|
|
421
|
+
if (!wt) {
|
|
422
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Gather merge context
|
|
427
|
+
const diffSummary = diffWorktreeGSD(basePath, name);
|
|
428
|
+
const fullDiff = getWorktreeGSDDiff(basePath, name);
|
|
429
|
+
const commitLog = getWorktreeLog(basePath, name);
|
|
430
|
+
|
|
431
|
+
const totalChanges = diffSummary.added.length + diffSummary.modified.length + diffSummary.removed.length;
|
|
432
|
+
if (totalChanges === 0 && !commitLog.trim()) {
|
|
433
|
+
ctx.ui.notify(`Worktree "${name}" has no changes to merge.`, "info");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Preview confirmation before merge dispatch
|
|
438
|
+
const previewLines = [
|
|
439
|
+
`Merge worktree "${name}" → ${mainBranch}`,
|
|
440
|
+
"",
|
|
441
|
+
` ${diffSummary.added.length} added · ${diffSummary.modified.length} modified · ${diffSummary.removed.length} removed`,
|
|
442
|
+
];
|
|
443
|
+
if (diffSummary.added.length > 0) {
|
|
444
|
+
previewLines.push("", " Added:");
|
|
445
|
+
for (const f of diffSummary.added.slice(0, 10)) previewLines.push(` + ${f}`);
|
|
446
|
+
if (diffSummary.added.length > 10) previewLines.push(` … and ${diffSummary.added.length - 10} more`);
|
|
447
|
+
}
|
|
448
|
+
if (diffSummary.modified.length > 0) {
|
|
449
|
+
previewLines.push("", " Modified:");
|
|
450
|
+
for (const f of diffSummary.modified.slice(0, 10)) previewLines.push(` ~ ${f}`);
|
|
451
|
+
if (diffSummary.modified.length > 10) previewLines.push(` … and ${diffSummary.modified.length - 10} more`);
|
|
452
|
+
}
|
|
453
|
+
if (diffSummary.removed.length > 0) {
|
|
454
|
+
previewLines.push("", " Removed:");
|
|
455
|
+
for (const f of diffSummary.removed.slice(0, 10)) previewLines.push(` - ${f}`);
|
|
456
|
+
if (diffSummary.removed.length > 10) previewLines.push(` … and ${diffSummary.removed.length - 10} more`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const confirmed = await showConfirm(ctx, {
|
|
460
|
+
title: "Worktree Merge",
|
|
461
|
+
message: previewLines.join("\n"),
|
|
462
|
+
confirmLabel: "Merge",
|
|
463
|
+
declineLabel: "Cancel",
|
|
464
|
+
});
|
|
465
|
+
if (!confirmed) {
|
|
466
|
+
ctx.ui.notify("Merge cancelled.", "info");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Format file lists for the prompt
|
|
471
|
+
const formatFiles = (files: string[]) =>
|
|
472
|
+
files.length > 0 ? files.map(f => `- \`${f}\``).join("\n") : "_(none)_";
|
|
473
|
+
|
|
474
|
+
// Load and populate the merge prompt
|
|
475
|
+
const prompt = loadPrompt("worktree-merge", {
|
|
476
|
+
worktreeName: name,
|
|
477
|
+
worktreeBranch: branch,
|
|
478
|
+
mainBranch,
|
|
479
|
+
commitLog: commitLog || "(no commits)",
|
|
480
|
+
addedFiles: formatFiles(diffSummary.added),
|
|
481
|
+
modifiedFiles: formatFiles(diffSummary.modified),
|
|
482
|
+
removedFiles: formatFiles(diffSummary.removed),
|
|
483
|
+
fullDiff: fullDiff || "(no diff)",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Dispatch to the LLM
|
|
487
|
+
pi.sendMessage(
|
|
488
|
+
{
|
|
489
|
+
customType: "gsd-worktree-merge",
|
|
490
|
+
content: prompt,
|
|
491
|
+
display: false,
|
|
492
|
+
},
|
|
493
|
+
{ triggerTurn: true },
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
ctx.ui.notify(
|
|
497
|
+
`Merge helper started for worktree "${name}" (${totalChanges} GSD artifact change${totalChanges === 1 ? "" : "s"}).`,
|
|
498
|
+
"info",
|
|
499
|
+
);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
502
|
+
ctx.ui.notify(`Failed to start merge: ${msg}`, "error");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function handleRemove(
|
|
507
|
+
basePath: string,
|
|
508
|
+
name: string,
|
|
509
|
+
ctx: ExtensionCommandContext,
|
|
510
|
+
): Promise<void> {
|
|
511
|
+
try {
|
|
512
|
+
const mainBase = originalCwd ?? basePath;
|
|
513
|
+
const prevCwd = process.cwd();
|
|
514
|
+
removeWorktree(mainBase, name, { deleteBranch: true });
|
|
515
|
+
|
|
516
|
+
// If we were in that worktree, removeWorktree chdir'd us out — clear tracking
|
|
517
|
+
if (originalCwd && process.cwd() !== prevCwd) {
|
|
518
|
+
nudgeGitBranchCache(prevCwd);
|
|
519
|
+
originalCwd = null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
ctx.ui.notify(`Worktree "${name}" removed (branch deleted).`, "info");
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
525
|
+
ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
|
|
526
|
+
}
|
|
527
|
+
}
|