gsd-pi 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +112 -5
- package/dist/loader.js +0 -0
- package/dist/resource-loader.d.ts +3 -3
- package/dist/resource-loader.js +10 -4
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.js +15 -5
- package/package.json +6 -2
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +48 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/scripts/postinstall.js +8 -0
- package/src/resources/extensions/bg-shell/index.ts +57 -8
- package/src/resources/extensions/browser-tools/index.ts +80 -7
- package/src/resources/extensions/github/gh-api.ts +46 -30
- package/src/resources/extensions/gsd/auto.ts +188 -10
- package/src/resources/extensions/gsd/commands.ts +13 -6
- package/src/resources/extensions/gsd/doctor.ts +7 -0
- package/src/resources/extensions/gsd/guided-flow.ts +9 -6
- package/src/resources/extensions/gsd/index.ts +32 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +73 -27
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +51 -17
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/worktree-command.ts +219 -49
- package/src/resources/extensions/gsd/worktree-manager.ts +106 -16
- package/src/resources/extensions/mcporter/index.ts +410 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/index.ts +2 -2
- package/src/resources/extensions/voice/index.ts +176 -0
- package/src/resources/extensions/voice/speech-recognizer +0 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +76 -0
- package/dist/modes/interactive/theme/dark.json +0 -85
- package/dist/modes/interactive/theme/light.json +0 -84
- package/dist/modes/interactive/theme/theme-schema.json +0 -335
- package/dist/modes/interactive/theme/theme.d.ts +0 -78
- package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme.js +0 -949
- package/dist/modes/interactive/theme/theme.js.map +0 -1
- package/src/resources/extensions/slash-commands/gsd-run.ts +0 -34
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Usage:
|
|
7
7
|
* /worktree <name> — create a new worktree
|
|
8
8
|
* /worktree list — list existing worktrees
|
|
9
|
-
* /worktree merge
|
|
9
|
+
* /worktree merge [name] [target] — start LLM-guided merge (auto-detects when inside a worktree)
|
|
10
10
|
* /worktree remove <name> — remove a worktree and its branch
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -18,15 +18,18 @@ import {
|
|
|
18
18
|
createWorktree,
|
|
19
19
|
listWorktrees,
|
|
20
20
|
removeWorktree,
|
|
21
|
-
|
|
21
|
+
diffWorktreeAll,
|
|
22
|
+
diffWorktreeNumstat,
|
|
22
23
|
getMainBranch,
|
|
23
24
|
getWorktreeGSDDiff,
|
|
25
|
+
getWorktreeCodeDiff,
|
|
24
26
|
getWorktreeLog,
|
|
25
27
|
worktreeBranchName,
|
|
26
28
|
worktreePath,
|
|
27
29
|
} from "./worktree-manager.js";
|
|
30
|
+
import type { FileLineStat } from "./worktree-manager.js";
|
|
28
31
|
import { existsSync, realpathSync, readFileSync, utimesSync } from "node:fs";
|
|
29
|
-
import { join, resolve } from "node:path";
|
|
32
|
+
import { join, resolve, sep } from "node:path";
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* Tracks the original project root so we can switch back.
|
|
@@ -100,7 +103,7 @@ export function getActiveWorktreeName(): string | null {
|
|
|
100
103
|
|
|
101
104
|
function worktreeCompletions(prefix: string) {
|
|
102
105
|
const parts = prefix.trim().split(/\s+/);
|
|
103
|
-
const subcommands = ["list", "merge", "remove", "switch", "return"];
|
|
106
|
+
const subcommands = ["list", "merge", "remove", "switch", "create", "return"];
|
|
104
107
|
|
|
105
108
|
if (parts.length <= 1) {
|
|
106
109
|
const partial = parts[0] ?? "";
|
|
@@ -119,13 +122,21 @@ function worktreeCompletions(prefix: string) {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
if ((parts[0] === "merge" || parts[0] === "remove" || parts[0] === "switch") && parts.length <= 2) {
|
|
125
|
+
if ((parts[0] === "merge" || parts[0] === "remove" || parts[0] === "switch" || parts[0] === "create") && parts.length <= 2) {
|
|
123
126
|
const namePrefix = parts[1] ?? "";
|
|
124
127
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
128
|
+
const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
|
|
129
|
+
const existing = listWorktrees(mainBase);
|
|
130
|
+
const nameCompletions = existing
|
|
127
131
|
.filter(wt => wt.name.startsWith(namePrefix))
|
|
128
132
|
.map(wt => ({ value: `${parts[0]} ${wt.name}`, label: wt.name }));
|
|
133
|
+
|
|
134
|
+
// Add "all" option for remove
|
|
135
|
+
if (parts[0] === "remove" && "all".startsWith(namePrefix)) {
|
|
136
|
+
nameCompletions.push({ value: "remove all", label: "all" });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return nameCompletions;
|
|
129
140
|
} catch {
|
|
130
141
|
return [];
|
|
131
142
|
}
|
|
@@ -151,8 +162,8 @@ async function worktreeHandler(
|
|
|
151
162
|
` /${alias} switch <name> — switch into an existing worktree`,
|
|
152
163
|
` /${alias} return — switch back to the main project tree`,
|
|
153
164
|
` /${alias} list — list all worktrees`,
|
|
154
|
-
` /${alias} merge
|
|
155
|
-
` /${alias} remove <name> — remove a worktree and its branch`,
|
|
165
|
+
` /${alias} merge [name] [target] — merge worktree into target branch (auto-detects when inside a worktree)`,
|
|
166
|
+
` /${alias} remove <name|all> — remove a worktree (or all) and its branch`,
|
|
156
167
|
].join("\n"),
|
|
157
168
|
"info",
|
|
158
169
|
);
|
|
@@ -169,41 +180,76 @@ async function worktreeHandler(
|
|
|
169
180
|
return;
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
if (trimmed.startsWith("switch ")) {
|
|
173
|
-
const name = trimmed.replace(/^switch\s+/, "").trim();
|
|
183
|
+
if (trimmed.startsWith("switch ") || trimmed.startsWith("create ")) {
|
|
184
|
+
const name = trimmed.replace(/^(?:switch|create)\s+/, "").trim();
|
|
174
185
|
if (!name) {
|
|
175
|
-
ctx.ui.notify(`Usage: /${alias}
|
|
186
|
+
ctx.ui.notify(`Usage: /${alias} ${trimmed.split(" ")[0]} <name>`, "warning");
|
|
176
187
|
return;
|
|
177
188
|
}
|
|
178
|
-
|
|
189
|
+
// create and switch both do the same thing: switch if exists, create if not
|
|
190
|
+
const mainBase = originalCwd ?? basePath;
|
|
191
|
+
const existing = listWorktrees(mainBase);
|
|
192
|
+
if (existing.some(wt => wt.name === name)) {
|
|
193
|
+
await handleSwitch(basePath, name, ctx);
|
|
194
|
+
} else {
|
|
195
|
+
await handleCreate(basePath, name, ctx);
|
|
196
|
+
}
|
|
179
197
|
return;
|
|
180
198
|
}
|
|
181
199
|
|
|
182
|
-
if (trimmed.startsWith("merge ")) {
|
|
183
|
-
const mergeArgs = trimmed.replace(/^merge\s
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
if (trimmed === "merge" || trimmed.startsWith("merge ")) {
|
|
201
|
+
const mergeArgs = trimmed.replace(/^merge\s*/, "").trim().split(/\s+/).filter(Boolean);
|
|
202
|
+
const mainBase = originalCwd ?? basePath;
|
|
203
|
+
const activeWt = getActiveWorktreeName();
|
|
204
|
+
|
|
205
|
+
if (mergeArgs.length === 0) {
|
|
206
|
+
// Bare "/worktree merge" — only valid when inside a worktree
|
|
207
|
+
if (!activeWt) {
|
|
208
|
+
ctx.ui.notify(`Usage: /${alias} merge <name> [target]`, "warning");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await handleMerge(mainBase, activeWt, ctx, pi, undefined);
|
|
188
212
|
return;
|
|
189
213
|
}
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
|
|
215
|
+
const name = mergeArgs[0]!;
|
|
216
|
+
const targetBranch = mergeArgs[1];
|
|
217
|
+
|
|
218
|
+
// Check if 'name' is an actual worktree
|
|
219
|
+
const worktrees = listWorktrees(mainBase);
|
|
220
|
+
const isWorktree = worktrees.some(w => w.name === name);
|
|
221
|
+
|
|
222
|
+
if (isWorktree) {
|
|
223
|
+
await handleMerge(mainBase, name, ctx, pi, targetBranch);
|
|
224
|
+
} else if (activeWt) {
|
|
225
|
+
// Not a worktree name — user is in a worktree and gave the target branch
|
|
226
|
+
// e.g. "/worktree merge main" while inside worktree "new"
|
|
227
|
+
await handleMerge(mainBase, activeWt, ctx, pi, name);
|
|
228
|
+
} else {
|
|
229
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /${alias} list to see available worktrees.`, "warning");
|
|
230
|
+
}
|
|
192
231
|
return;
|
|
193
232
|
}
|
|
194
233
|
|
|
195
|
-
if (trimmed.startsWith("remove ")) {
|
|
196
|
-
const name = trimmed.replace(/^remove\s
|
|
234
|
+
if (trimmed === "remove" || trimmed.startsWith("remove ")) {
|
|
235
|
+
const name = trimmed.replace(/^remove\s*/, "").trim();
|
|
236
|
+
const mainBase = originalCwd ?? basePath;
|
|
237
|
+
|
|
238
|
+
if (name === "all") {
|
|
239
|
+
await handleRemoveAll(mainBase, ctx);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
197
243
|
if (!name) {
|
|
198
|
-
ctx.ui.notify(`Usage: /${alias} remove <name>`, "warning");
|
|
244
|
+
ctx.ui.notify(`Usage: /${alias} remove <name|all>`, "warning");
|
|
199
245
|
return;
|
|
200
246
|
}
|
|
201
|
-
|
|
247
|
+
|
|
202
248
|
await handleRemove(mainBase, name, ctx);
|
|
203
249
|
return;
|
|
204
250
|
}
|
|
205
251
|
|
|
206
|
-
const RESERVED = ["list", "return", "switch", "merge", "remove"];
|
|
252
|
+
const RESERVED = ["list", "return", "switch", "create", "merge", "remove"];
|
|
207
253
|
if (RESERVED.includes(trimmed)) {
|
|
208
254
|
ctx.ui.notify(`Usage: /${alias} ${trimmed}${trimmed === "list" || trimmed === "return" ? "" : " <name>"}`, "warning");
|
|
209
255
|
return;
|
|
@@ -225,8 +271,20 @@ async function worktreeHandler(
|
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
export function registerWorktreeCommand(pi: ExtensionAPI): void {
|
|
274
|
+
// Restore worktree state after /reload.
|
|
275
|
+
// The module-level originalCwd resets to null when extensions are re-loaded,
|
|
276
|
+
// but process.cwd() is still inside the worktree. Detect this and recover.
|
|
277
|
+
if (!originalCwd) {
|
|
278
|
+
const cwd = process.cwd();
|
|
279
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
280
|
+
const markerIdx = cwd.indexOf(marker);
|
|
281
|
+
if (markerIdx !== -1) {
|
|
282
|
+
originalCwd = cwd.slice(0, markerIdx);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
228
286
|
pi.registerCommand("worktree", {
|
|
229
|
-
description: "Git worktrees: /worktree <name> | list | merge
|
|
287
|
+
description: "Git worktrees (also /wt): /worktree <name> | list | merge | remove",
|
|
230
288
|
getArgumentCompletions: worktreeCompletions,
|
|
231
289
|
|
|
232
290
|
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
@@ -236,7 +294,7 @@ export function registerWorktreeCommand(pi: ExtensionAPI): void {
|
|
|
236
294
|
|
|
237
295
|
// /wt alias — same handler, same completions
|
|
238
296
|
pi.registerCommand("wt", {
|
|
239
|
-
description: "Alias for /worktree
|
|
297
|
+
description: "Alias for /worktree",
|
|
240
298
|
getArgumentCompletions: worktreeCompletions,
|
|
241
299
|
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
242
300
|
await worktreeHandler(args, ctx, pi, "wt");
|
|
@@ -362,6 +420,7 @@ const DIM = "\x1b[2m";
|
|
|
362
420
|
const RESET = "\x1b[0m";
|
|
363
421
|
const CYAN = "\x1b[36m";
|
|
364
422
|
const GREEN = "\x1b[32m";
|
|
423
|
+
const RED = "\x1b[31m";
|
|
365
424
|
const YELLOW = "\x1b[33m";
|
|
366
425
|
const WHITE = "\x1b[37m";
|
|
367
426
|
|
|
@@ -423,9 +482,11 @@ async function handleMerge(
|
|
|
423
482
|
return;
|
|
424
483
|
}
|
|
425
484
|
|
|
426
|
-
// Gather merge context
|
|
427
|
-
const diffSummary =
|
|
428
|
-
const
|
|
485
|
+
// Gather merge context — full repo diff, not just .gsd/
|
|
486
|
+
const diffSummary = diffWorktreeAll(basePath, name);
|
|
487
|
+
const numstat = diffWorktreeNumstat(basePath, name);
|
|
488
|
+
const gsdDiff = getWorktreeGSDDiff(basePath, name);
|
|
489
|
+
const codeDiff = getWorktreeCodeDiff(basePath, name);
|
|
429
490
|
const commitLog = getWorktreeLog(basePath, name);
|
|
430
491
|
|
|
431
492
|
const totalChanges = diffSummary.added.length + diffSummary.modified.length + diffSummary.removed.length;
|
|
@@ -434,27 +495,48 @@ async function handleMerge(
|
|
|
434
495
|
return;
|
|
435
496
|
}
|
|
436
497
|
|
|
498
|
+
// Build a map of file → line stats for the preview
|
|
499
|
+
const statMap = new Map<string, FileLineStat>();
|
|
500
|
+
for (const s of numstat) statMap.set(s.file, s);
|
|
501
|
+
|
|
502
|
+
// Compute totals
|
|
503
|
+
let totalAdded = 0;
|
|
504
|
+
let totalRemoved = 0;
|
|
505
|
+
for (const s of numstat) { totalAdded += s.added; totalRemoved += s.removed; }
|
|
506
|
+
|
|
507
|
+
// Split files into code vs GSD for the preview
|
|
508
|
+
const isGSD = (f: string) => f.startsWith(".gsd/");
|
|
509
|
+
const codeChanges = diffSummary.added.filter(f => !isGSD(f)).length
|
|
510
|
+
+ diffSummary.modified.filter(f => !isGSD(f)).length
|
|
511
|
+
+ diffSummary.removed.filter(f => !isGSD(f)).length;
|
|
512
|
+
const gsdChanges = diffSummary.added.filter(isGSD).length
|
|
513
|
+
+ diffSummary.modified.filter(isGSD).length
|
|
514
|
+
+ diffSummary.removed.filter(isGSD).length;
|
|
515
|
+
|
|
516
|
+
// Format a file line with +/- stats
|
|
517
|
+
const formatFileLine = (prefix: string, file: string): string => {
|
|
518
|
+
const s = statMap.get(file);
|
|
519
|
+
const stat = s ? ` ${GREEN}+${s.added}${RESET} ${RED}-${s.removed}${RESET}` : "";
|
|
520
|
+
return ` ${prefix} ${file}${stat}`;
|
|
521
|
+
};
|
|
522
|
+
|
|
437
523
|
// Preview confirmation before merge dispatch
|
|
438
524
|
const previewLines = [
|
|
439
525
|
`Merge worktree "${name}" → ${mainBranch}`,
|
|
440
526
|
"",
|
|
441
|
-
` ${
|
|
527
|
+
` ${totalChanges} file${totalChanges === 1 ? "" : "s"} changed, ${GREEN}+${totalAdded}${RESET} ${RED}-${totalRemoved}${RESET} lines (${codeChanges} code, ${gsdChanges} GSD)`,
|
|
442
528
|
];
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
}
|
|
529
|
+
|
|
530
|
+
const appendFileList = (label: string, files: string[], prefix: string, limit = 10) => {
|
|
531
|
+
if (files.length === 0) return;
|
|
532
|
+
previewLines.push("", ` ${label}:`);
|
|
533
|
+
for (const f of files.slice(0, limit)) previewLines.push(formatFileLine(prefix, f));
|
|
534
|
+
if (files.length > limit) previewLines.push(` … and ${files.length - limit} more`);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
appendFileList("Added", diffSummary.added, "+");
|
|
538
|
+
appendFileList("Modified", diffSummary.modified, "~");
|
|
539
|
+
appendFileList("Removed", diffSummary.removed, "-");
|
|
458
540
|
|
|
459
541
|
const confirmed = await showConfirm(ctx, {
|
|
460
542
|
title: "Worktree Merge",
|
|
@@ -467,20 +549,34 @@ async function handleMerge(
|
|
|
467
549
|
return;
|
|
468
550
|
}
|
|
469
551
|
|
|
552
|
+
// Switch to the main tree before dispatching the merge.
|
|
553
|
+
// The LLM needs to run git merge --squash from the main branch, and if
|
|
554
|
+
// it later removes the worktree, the agent's CWD must not be inside it.
|
|
555
|
+
if (originalCwd) {
|
|
556
|
+
const prevCwd = process.cwd();
|
|
557
|
+
process.chdir(basePath);
|
|
558
|
+
nudgeGitBranchCache(prevCwd);
|
|
559
|
+
originalCwd = null;
|
|
560
|
+
}
|
|
561
|
+
|
|
470
562
|
// Format file lists for the prompt
|
|
471
563
|
const formatFiles = (files: string[]) =>
|
|
472
564
|
files.length > 0 ? files.map(f => `- \`${f}\``).join("\n") : "_(none)_";
|
|
473
565
|
|
|
474
566
|
// Load and populate the merge prompt
|
|
567
|
+
const wtPath = worktreePath(basePath, name);
|
|
475
568
|
const prompt = loadPrompt("worktree-merge", {
|
|
476
569
|
worktreeName: name,
|
|
477
570
|
worktreeBranch: branch,
|
|
478
571
|
mainBranch,
|
|
572
|
+
mainTreePath: basePath,
|
|
573
|
+
worktreePath: wtPath,
|
|
479
574
|
commitLog: commitLog || "(no commits)",
|
|
480
575
|
addedFiles: formatFiles(diffSummary.added),
|
|
481
576
|
modifiedFiles: formatFiles(diffSummary.modified),
|
|
482
577
|
removedFiles: formatFiles(diffSummary.removed),
|
|
483
|
-
|
|
578
|
+
gsdDiff: gsdDiff || "(no GSD artifact changes)",
|
|
579
|
+
codeDiff: codeDiff || "(no code changes)",
|
|
484
580
|
});
|
|
485
581
|
|
|
486
582
|
// Dispatch to the LLM
|
|
@@ -494,7 +590,7 @@ async function handleMerge(
|
|
|
494
590
|
);
|
|
495
591
|
|
|
496
592
|
ctx.ui.notify(
|
|
497
|
-
`Merge helper started for worktree "${name}" (${
|
|
593
|
+
`Merge helper started for worktree "${name}" (${codeChanges} code + ${gsdChanges} GSD artifact change${totalChanges === 1 ? "" : "s"}).`,
|
|
498
594
|
"info",
|
|
499
595
|
);
|
|
500
596
|
} catch (error) {
|
|
@@ -510,6 +606,26 @@ async function handleRemove(
|
|
|
510
606
|
): Promise<void> {
|
|
511
607
|
try {
|
|
512
608
|
const mainBase = originalCwd ?? basePath;
|
|
609
|
+
|
|
610
|
+
// Validate the worktree exists before attempting removal
|
|
611
|
+
const worktrees = listWorktrees(mainBase);
|
|
612
|
+
const wt = worktrees.find(w => w.name === name);
|
|
613
|
+
if (!wt) {
|
|
614
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const confirmed = await showConfirm(ctx, {
|
|
619
|
+
title: "Remove Worktree",
|
|
620
|
+
message: `Remove worktree "${name}" and delete branch ${wt.branch}?`,
|
|
621
|
+
confirmLabel: "Remove",
|
|
622
|
+
declineLabel: "Cancel",
|
|
623
|
+
});
|
|
624
|
+
if (!confirmed) {
|
|
625
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
513
629
|
const prevCwd = process.cwd();
|
|
514
630
|
removeWorktree(mainBase, name, { deleteBranch: true });
|
|
515
631
|
|
|
@@ -525,3 +641,57 @@ async function handleRemove(
|
|
|
525
641
|
ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
|
|
526
642
|
}
|
|
527
643
|
}
|
|
644
|
+
|
|
645
|
+
async function handleRemoveAll(
|
|
646
|
+
basePath: string,
|
|
647
|
+
ctx: ExtensionCommandContext,
|
|
648
|
+
): Promise<void> {
|
|
649
|
+
try {
|
|
650
|
+
const mainBase = originalCwd ?? basePath;
|
|
651
|
+
const worktrees = listWorktrees(mainBase);
|
|
652
|
+
|
|
653
|
+
if (worktrees.length === 0) {
|
|
654
|
+
ctx.ui.notify("No worktrees to remove.", "info");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const names = worktrees.map(w => w.name);
|
|
659
|
+
const confirmed = await showConfirm(ctx, {
|
|
660
|
+
title: "Remove All Worktrees",
|
|
661
|
+
message: `This will remove ${worktrees.length} worktree${worktrees.length === 1 ? "" : "s"} and delete their branches:\n\n${names.map(n => ` • ${n}`).join("\n")}`,
|
|
662
|
+
confirmLabel: "Remove all",
|
|
663
|
+
declineLabel: "Cancel",
|
|
664
|
+
});
|
|
665
|
+
if (!confirmed) {
|
|
666
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const prevCwd = process.cwd();
|
|
671
|
+
const removed: string[] = [];
|
|
672
|
+
const failed: string[] = [];
|
|
673
|
+
|
|
674
|
+
for (const wt of worktrees) {
|
|
675
|
+
try {
|
|
676
|
+
removeWorktree(mainBase, wt.name, { deleteBranch: true });
|
|
677
|
+
removed.push(wt.name);
|
|
678
|
+
} catch {
|
|
679
|
+
failed.push(wt.name);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// If we were in a worktree that got removed, clear tracking
|
|
684
|
+
if (originalCwd && process.cwd() !== prevCwd) {
|
|
685
|
+
nudgeGitBranchCache(prevCwd);
|
|
686
|
+
originalCwd = null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const lines: string[] = [];
|
|
690
|
+
if (removed.length > 0) lines.push(`Removed: ${removed.join(", ")}`);
|
|
691
|
+
if (failed.length > 0) lines.push(`Failed: ${failed.join(", ")}`);
|
|
692
|
+
ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
695
|
+
ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
@@ -28,6 +28,13 @@ export interface WorktreeInfo {
|
|
|
28
28
|
exists: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Per-file line change stats from git diff --numstat. */
|
|
32
|
+
export interface FileLineStat {
|
|
33
|
+
file: string;
|
|
34
|
+
added: number;
|
|
35
|
+
removed: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
export interface WorktreeDiffSummary {
|
|
32
39
|
/** Files only in the worktree .gsd/ (new artifacts) */
|
|
33
40
|
added: string[];
|
|
@@ -109,6 +116,18 @@ export function createWorktree(basePath: string, name: string): WorktreeInfo {
|
|
|
109
116
|
const mainBranch = getMainBranch(basePath);
|
|
110
117
|
|
|
111
118
|
if (branchExists) {
|
|
119
|
+
// Check if the branch is actively used by an existing worktree.
|
|
120
|
+
// `git branch -f` will fail if the branch is checked out somewhere.
|
|
121
|
+
const worktreeUsing = runGit(basePath, ["worktree", "list", "--porcelain"], { allowFailure: true });
|
|
122
|
+
const branchInUse = worktreeUsing.includes(`branch refs/heads/${branch}`);
|
|
123
|
+
|
|
124
|
+
if (branchInUse) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Branch "${branch}" is already in use by another worktree. ` +
|
|
127
|
+
`Remove the existing worktree first with /worktree remove ${name}.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
// Reset the stale branch to current main, then attach worktree to it
|
|
113
132
|
runGit(basePath, ["branch", "-f", branch, mainBranch]);
|
|
114
133
|
runGit(basePath, ["worktree", "add", wtPath, branch]);
|
|
@@ -212,19 +231,17 @@ export function removeWorktree(
|
|
|
212
231
|
}
|
|
213
232
|
}
|
|
214
233
|
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
*/
|
|
219
|
-
export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSummary {
|
|
220
|
-
const branch = worktreeBranchName(name);
|
|
221
|
-
const mainBranch = getMainBranch(basePath);
|
|
234
|
+
/** Paths to skip in all worktree diffs (internal/runtime artifacts). */
|
|
235
|
+
const SKIP_PATHS = [".gsd/worktrees/", ".gsd/runtime/", ".gsd/activity/"];
|
|
236
|
+
const SKIP_EXACT = [".gsd/STATE.md", ".gsd/auto.lock", ".gsd/metrics.json"];
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
function shouldSkipPath(filePath: string): boolean {
|
|
239
|
+
if (SKIP_PATHS.some(p => filePath.startsWith(p))) return true;
|
|
240
|
+
if (SKIP_EXACT.includes(filePath)) return true;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
227
243
|
|
|
244
|
+
function parseDiffNameStatus(diffOutput: string): WorktreeDiffSummary {
|
|
228
245
|
const added: string[] = [];
|
|
229
246
|
const modified: string[] = [];
|
|
230
247
|
const removed: string[] = [];
|
|
@@ -235,11 +252,7 @@ export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSum
|
|
|
235
252
|
const [status, ...pathParts] = line.split("\t");
|
|
236
253
|
const filePath = pathParts.join("\t");
|
|
237
254
|
|
|
238
|
-
|
|
239
|
-
if (filePath.startsWith(".gsd/worktrees/") || filePath.startsWith(".gsd/runtime/")) continue;
|
|
240
|
-
// Skip gitignored runtime files
|
|
241
|
-
if (filePath === ".gsd/STATE.md" || filePath === ".gsd/auto.lock" || filePath === ".gsd/metrics.json") continue;
|
|
242
|
-
if (filePath.startsWith(".gsd/activity/")) continue;
|
|
255
|
+
if (shouldSkipPath(filePath)) continue;
|
|
243
256
|
|
|
244
257
|
switch (status) {
|
|
245
258
|
case "A": added.push(filePath); break;
|
|
@@ -256,6 +269,68 @@ export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSum
|
|
|
256
269
|
return { added, modified, removed };
|
|
257
270
|
}
|
|
258
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Diff the .gsd/ directory between the worktree branch and main branch.
|
|
274
|
+
* Returns a summary of added, modified, and removed GSD artifacts.
|
|
275
|
+
*/
|
|
276
|
+
export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSummary {
|
|
277
|
+
const branch = worktreeBranchName(name);
|
|
278
|
+
const mainBranch = getMainBranch(basePath);
|
|
279
|
+
|
|
280
|
+
const diffOutput = runGit(basePath, [
|
|
281
|
+
"diff", "--name-status", `${mainBranch}...${branch}`, "--", ".gsd/",
|
|
282
|
+
], { allowFailure: true });
|
|
283
|
+
|
|
284
|
+
return parseDiffNameStatus(diffOutput);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Diff ALL files between the worktree branch and main branch.
|
|
289
|
+
* Returns a summary of added, modified, and removed files across the entire repo.
|
|
290
|
+
*/
|
|
291
|
+
/**
|
|
292
|
+
* Diff ALL files between the worktree branch and main branch.
|
|
293
|
+
* Uses direct diff (no merge-base) to show what will actually change
|
|
294
|
+
* on main when the merge is applied. If both branches have identical
|
|
295
|
+
* content, this correctly returns an empty diff.
|
|
296
|
+
*/
|
|
297
|
+
export function diffWorktreeAll(basePath: string, name: string): WorktreeDiffSummary {
|
|
298
|
+
const branch = worktreeBranchName(name);
|
|
299
|
+
const mainBranch = getMainBranch(basePath);
|
|
300
|
+
|
|
301
|
+
const diffOutput = runGit(basePath, [
|
|
302
|
+
"diff", "--name-status", mainBranch, branch,
|
|
303
|
+
], { allowFailure: true });
|
|
304
|
+
|
|
305
|
+
return parseDiffNameStatus(diffOutput);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get per-file line addition/deletion stats for what will change on main.
|
|
310
|
+
* Uses direct diff (not merge-base) so the preview matches the actual merge outcome.
|
|
311
|
+
*/
|
|
312
|
+
export function diffWorktreeNumstat(basePath: string, name: string): FileLineStat[] {
|
|
313
|
+
const branch = worktreeBranchName(name);
|
|
314
|
+
const mainBranch = getMainBranch(basePath);
|
|
315
|
+
|
|
316
|
+
const raw = runGit(basePath, [
|
|
317
|
+
"diff", "--numstat", mainBranch, branch,
|
|
318
|
+
], { allowFailure: true });
|
|
319
|
+
|
|
320
|
+
if (!raw.trim()) return [];
|
|
321
|
+
|
|
322
|
+
const stats: FileLineStat[] = [];
|
|
323
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
324
|
+
const [a, r, ...pathParts] = line.split("\t");
|
|
325
|
+
const file = pathParts.join("\t");
|
|
326
|
+
if (shouldSkipPath(file)) continue;
|
|
327
|
+
const added = a === "-" ? 0 : parseInt(a ?? "0", 10);
|
|
328
|
+
const removed = r === "-" ? 0 : parseInt(r ?? "0", 10);
|
|
329
|
+
stats.push({ file, added, removed });
|
|
330
|
+
}
|
|
331
|
+
return stats;
|
|
332
|
+
}
|
|
333
|
+
|
|
259
334
|
/**
|
|
260
335
|
* Get the full diff content for .gsd/ between the worktree branch and main.
|
|
261
336
|
* Returns the raw unified diff for LLM consumption.
|
|
@@ -269,6 +344,21 @@ export function getWorktreeGSDDiff(basePath: string, name: string): string {
|
|
|
269
344
|
], { allowFailure: true });
|
|
270
345
|
}
|
|
271
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Get the full diff content for non-.gsd/ files between the worktree branch and main.
|
|
349
|
+
* Returns the raw unified diff for LLM consumption.
|
|
350
|
+
*/
|
|
351
|
+
export function getWorktreeCodeDiff(basePath: string, name: string): string {
|
|
352
|
+
const branch = worktreeBranchName(name);
|
|
353
|
+
const mainBranch = getMainBranch(basePath);
|
|
354
|
+
|
|
355
|
+
// Get full diff, then exclude .gsd/ paths
|
|
356
|
+
// We use pathspec magic to exclude .gsd/
|
|
357
|
+
return runGit(basePath, [
|
|
358
|
+
"diff", `${mainBranch}...${branch}`, "--", ".", ":(exclude).gsd/",
|
|
359
|
+
], { allowFailure: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
272
362
|
/**
|
|
273
363
|
* Get commit log for the worktree branch since it diverged from main.
|
|
274
364
|
*/
|