gsd-pi 0.1.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 +341 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +35 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +69 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +48 -0
- package/dist/wizard.d.ts +20 -0
- package/dist/wizard.js +132 -0
- package/package.json +39 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +10 -0
- package/src/resources/AGENTS.md +204 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2554 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/gsd/activity-log.ts +48 -0
- package/src/resources/extensions/gsd/auto.ts +2032 -0
- package/src/resources/extensions/gsd/commands.ts +292 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/gsd/doctor.ts +683 -0
- package/src/resources/extensions/gsd/files.ts +730 -0
- package/src/resources/extensions/gsd/gitignore.ts +104 -0
- package/src/resources/extensions/gsd/guided-flow.ts +800 -0
- package/src/resources/extensions/gsd/index.ts +418 -0
- package/src/resources/extensions/gsd/metrics.ts +372 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +600 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +151 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +64 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +220 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +439 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +133 -0
- package/src/resources/extensions/gsd/templates/preferences.md +15 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +162 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree.ts +182 -0
- package/src/resources/extensions/plan-mode/README.md +65 -0
- package/src/resources/extensions/plan-mode/index.ts +521 -0
- package/src/resources/extensions/plan-mode/utils.ts +168 -0
- package/src/resources/extensions/search-the-web/cache.ts +70 -0
- package/src/resources/extensions/search-the-web/format.ts +134 -0
- package/src/resources/extensions/search-the-web/http.ts +147 -0
- package/src/resources/extensions/search-the-web/index.ts +46 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +374 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +424 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/gsd-run.ts +34 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1021 -0
- package/src/resources/extensions/worktree/index.ts +420 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Extension — /wt
|
|
3
|
+
*
|
|
4
|
+
* Manage git worktrees for parallel extension development.
|
|
5
|
+
* Each worktree gets its own branch, isolated from main.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* /wt new <name> — create worktree + branch, switch agent cwd there
|
|
9
|
+
* /wt ls — list all worktrees with status
|
|
10
|
+
* /wt switch <name>— switch agent cwd to a worktree (or main)
|
|
11
|
+
* /wt merge <name> — squash merge into main, delete worktree + branch
|
|
12
|
+
* /wt rm <name> — delete worktree + branch (discard changes)
|
|
13
|
+
* /wt status — show current worktree and git status summary
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = "/Users/lexchristopherson/.pi";
|
|
23
|
+
const WORKTREES_DIR = path.join(REPO_ROOT, "worktrees");
|
|
24
|
+
|
|
25
|
+
function run(cmd: string, cwd = REPO_ROOT): string {
|
|
26
|
+
try {
|
|
27
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
28
|
+
} catch (e: unknown) {
|
|
29
|
+
const err = e as { stderr?: string; message?: string };
|
|
30
|
+
throw new Error(err.stderr?.trim() || err.message || String(e));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface WorktreeInfo {
|
|
35
|
+
name: string;
|
|
36
|
+
path: string;
|
|
37
|
+
branch: string;
|
|
38
|
+
isMain: boolean;
|
|
39
|
+
commitCount: number;
|
|
40
|
+
dirty: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listWorktrees(): WorktreeInfo[] {
|
|
44
|
+
const raw = run("git worktree list --porcelain");
|
|
45
|
+
const entries = raw.split("\n\n").filter(Boolean);
|
|
46
|
+
|
|
47
|
+
return entries.map((entry) => {
|
|
48
|
+
const lines = entry.split("\n");
|
|
49
|
+
const wtPath = lines.find((l) => l.startsWith("worktree "))?.replace("worktree ", "") ?? "";
|
|
50
|
+
const branch = lines.find((l) => l.startsWith("branch "))?.replace("branch refs/heads/", "") ?? "(detached)";
|
|
51
|
+
const isMain = wtPath === REPO_ROOT;
|
|
52
|
+
|
|
53
|
+
let commitCount = 0;
|
|
54
|
+
let dirty = false;
|
|
55
|
+
|
|
56
|
+
if (!isMain) {
|
|
57
|
+
try {
|
|
58
|
+
const count = run(`git rev-list --count main..${branch}`, wtPath);
|
|
59
|
+
commitCount = parseInt(count, 10) || 0;
|
|
60
|
+
const status = run("git status --porcelain", wtPath);
|
|
61
|
+
dirty = status.length > 0;
|
|
62
|
+
} catch {
|
|
63
|
+
// branch might not exist yet
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const name = isMain ? "main" : path.basename(wtPath);
|
|
68
|
+
return { name, path: wtPath, branch, isMain, commitCount, dirty };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getCurrentWorktreeName(cwd: string): string {
|
|
73
|
+
if (cwd === REPO_ROOT) return "main";
|
|
74
|
+
const worktrees = listWorktrees();
|
|
75
|
+
const match = worktrees.find((w) => w.path === cwd);
|
|
76
|
+
return match?.name ?? "main";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatWorktreeList(worktrees: WorktreeInfo[], currentPath: string): string[] {
|
|
80
|
+
return worktrees.map((w) => {
|
|
81
|
+
const isCurrent = w.path === currentPath;
|
|
82
|
+
const marker = isCurrent ? "→ " : " ";
|
|
83
|
+
const dirty = w.dirty ? " *" : "";
|
|
84
|
+
const commits = w.isMain ? "" : ` (${w.commitCount} commit${w.commitCount !== 1 ? "s" : ""} ahead)`;
|
|
85
|
+
return `${marker}${w.name}${dirty}${commits} [${w.branch}]`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function updateStatus(ctx: ExtensionContext, currentWorktree: string): void {
|
|
90
|
+
if (currentWorktree === "main") {
|
|
91
|
+
ctx.ui.setStatus("worktree", undefined);
|
|
92
|
+
} else {
|
|
93
|
+
ctx.ui.setStatus("worktree", ctx.ui.theme.fg("accent", `⑂ ${currentWorktree}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getWorktreeCdCommand(wtPath: string): string {
|
|
98
|
+
return `cd ${JSON.stringify(wtPath)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getWorktreeExtensionLaunchCommand(wtPath: string): string {
|
|
102
|
+
return `pi -e ${JSON.stringify(wtPath)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatWorktreeHandoff(wtPath: string): string[] {
|
|
106
|
+
return [
|
|
107
|
+
"Human terminal handoff:",
|
|
108
|
+
` ${getWorktreeCdCommand(wtPath)}`,
|
|
109
|
+
"",
|
|
110
|
+
"To run pi against the worktree copy instead of main:",
|
|
111
|
+
` ${getWorktreeExtensionLaunchCommand(wtPath)}`,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Extension ────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export default function worktreeExtension(pi: ExtensionAPI): void {
|
|
118
|
+
// Track current cwd — starts at repo root
|
|
119
|
+
let currentCwd = REPO_ROOT;
|
|
120
|
+
|
|
121
|
+
// Inject cwd into every bash call so the agent operates in the right worktree
|
|
122
|
+
pi.on("tool_call", async (event) => {
|
|
123
|
+
if (event.toolName !== "bash" || currentCwd === REPO_ROOT) return;
|
|
124
|
+
// Prepend cd so all bash commands run in the active worktree
|
|
125
|
+
const cmd = event.input.command as string;
|
|
126
|
+
if (!cmd.startsWith(`cd ${currentCwd}`)) {
|
|
127
|
+
return {
|
|
128
|
+
input: {
|
|
129
|
+
...event.input,
|
|
130
|
+
command: `cd ${currentCwd} && ${cmd}`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Restore cwd on session resume
|
|
137
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
138
|
+
const entries = ctx.sessionManager.getEntries();
|
|
139
|
+
const last = [...entries]
|
|
140
|
+
.reverse()
|
|
141
|
+
.find(
|
|
142
|
+
(e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "worktree-cwd",
|
|
143
|
+
) as { data?: { cwd: string } } | undefined;
|
|
144
|
+
if (last?.data?.cwd) {
|
|
145
|
+
currentCwd = last.data?.cwd;
|
|
146
|
+
}
|
|
147
|
+
updateStatus(ctx, getCurrentWorktreeName(currentCwd));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── /wt command ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
pi.registerCommand("wt", {
|
|
153
|
+
description: "Manage git worktrees: /wt new|ls|switch|merge|rm|status [name]",
|
|
154
|
+
|
|
155
|
+
getArgumentCompletions: (prefix: string) => {
|
|
156
|
+
const subcommands = ["new", "ls", "switch", "merge", "rm", "status"];
|
|
157
|
+
const parts = prefix.trim().split(/\s+/);
|
|
158
|
+
|
|
159
|
+
if (parts.length <= 1) {
|
|
160
|
+
return subcommands
|
|
161
|
+
.filter((s) => s.startsWith(parts[0] ?? ""))
|
|
162
|
+
.map((s) => ({ value: s, label: s }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sub = parts[0];
|
|
166
|
+
if (["switch", "merge", "rm"].includes(sub)) {
|
|
167
|
+
try {
|
|
168
|
+
const worktrees = listWorktrees();
|
|
169
|
+
const namePrefix = parts[1] ?? "";
|
|
170
|
+
return worktrees
|
|
171
|
+
.filter((w) => !w.isMain && w.name.startsWith(namePrefix))
|
|
172
|
+
.map((w) => ({ value: `${sub} ${w.name}`, label: w.name }));
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [];
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
182
|
+
const parts = args.trim().split(/\s+/);
|
|
183
|
+
const sub = parts[0];
|
|
184
|
+
const name = parts[1];
|
|
185
|
+
|
|
186
|
+
await ctx.waitForIdle();
|
|
187
|
+
|
|
188
|
+
// ── ls ────────────────────────────────────────────────────────────────
|
|
189
|
+
if (!sub || sub === "ls") {
|
|
190
|
+
try {
|
|
191
|
+
const worktrees = listWorktrees();
|
|
192
|
+
const lines = formatWorktreeList(worktrees, currentCwd);
|
|
193
|
+
const header = `Worktrees (${worktrees.length}):`;
|
|
194
|
+
ctx.ui.notify([header, "", ...lines].join("\n"), "info");
|
|
195
|
+
} catch (e) {
|
|
196
|
+
ctx.ui.notify(`Error: ${(e as Error).message}`, "error");
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── status ────────────────────────────────────────────────────────────
|
|
202
|
+
if (sub === "status") {
|
|
203
|
+
try {
|
|
204
|
+
const wtName = getCurrentWorktreeName(currentCwd);
|
|
205
|
+
const status = run("git status --short", currentCwd);
|
|
206
|
+
const branch = run("git branch --show-current", currentCwd);
|
|
207
|
+
const lines = [
|
|
208
|
+
`Current worktree: ${wtName}`,
|
|
209
|
+
`Branch: ${branch}`,
|
|
210
|
+
`Path: ${currentCwd}`,
|
|
211
|
+
"",
|
|
212
|
+
...(wtName === "main" ? [] : [...formatWorktreeHandoff(currentCwd), ""]),
|
|
213
|
+
status || "(clean)",
|
|
214
|
+
];
|
|
215
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
216
|
+
} catch (e) {
|
|
217
|
+
ctx.ui.notify(`Error: ${(e as Error).message}`, "error");
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── new ───────────────────────────────────────────────────────────────
|
|
223
|
+
if (sub === "new") {
|
|
224
|
+
let wtName = name;
|
|
225
|
+
if (!wtName) {
|
|
226
|
+
wtName = (await ctx.ui.input("Worktree name:", "my-experiment")) ?? undefined;
|
|
227
|
+
if (!wtName) return;
|
|
228
|
+
}
|
|
229
|
+
wtName = wtName.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
|
|
230
|
+
|
|
231
|
+
const wtPath = path.join(WORKTREES_DIR, wtName);
|
|
232
|
+
try {
|
|
233
|
+
// Check if already exists
|
|
234
|
+
const existing = listWorktrees();
|
|
235
|
+
if (existing.some((w) => w.name === wtName)) {
|
|
236
|
+
ctx.ui.notify(`Worktree '${wtName}' already exists. Use /wt switch ${wtName}`, "error");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create from current HEAD of main
|
|
241
|
+
run(`git worktree add ${wtPath} -b ${wtName}`, REPO_ROOT);
|
|
242
|
+
|
|
243
|
+
// Switch agent cwd
|
|
244
|
+
currentCwd = wtPath;
|
|
245
|
+
pi.appendEntry("worktree-cwd", { cwd: currentCwd });
|
|
246
|
+
updateStatus(ctx, wtName);
|
|
247
|
+
|
|
248
|
+
ctx.ui.notify(
|
|
249
|
+
[
|
|
250
|
+
`✓ Created worktree '${wtName}'`,
|
|
251
|
+
` Branch: ${wtName}`,
|
|
252
|
+
` Path: ${wtPath}`,
|
|
253
|
+
` Agent is now working in this worktree.`,
|
|
254
|
+
"",
|
|
255
|
+
...formatWorktreeHandoff(wtPath),
|
|
256
|
+
"",
|
|
257
|
+
` Use /wt merge ${wtName} when done.`,
|
|
258
|
+
].join("\n"),
|
|
259
|
+
"info",
|
|
260
|
+
);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
ctx.ui.notify(`Failed to create worktree: ${(e as Error).message}`, "error");
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── switch ────────────────────────────────────────────────────────────
|
|
268
|
+
if (sub === "switch") {
|
|
269
|
+
if (!name) {
|
|
270
|
+
ctx.ui.notify("Usage: /wt switch <name> (use 'main' to go back)", "error");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (name === "main") {
|
|
275
|
+
currentCwd = REPO_ROOT;
|
|
276
|
+
pi.appendEntry("worktree-cwd", { cwd: currentCwd });
|
|
277
|
+
updateStatus(ctx, "main");
|
|
278
|
+
ctx.ui.notify("Switched to main repo.", "info");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const worktrees = listWorktrees();
|
|
283
|
+
const target = worktrees.find((w) => w.name === name);
|
|
284
|
+
if (!target) {
|
|
285
|
+
ctx.ui.notify(`No worktree named '${name}'. Use /wt ls to see all.`, "error");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
currentCwd = target.path;
|
|
290
|
+
pi.appendEntry("worktree-cwd", { cwd: currentCwd });
|
|
291
|
+
updateStatus(ctx, name);
|
|
292
|
+
ctx.ui.notify(
|
|
293
|
+
[
|
|
294
|
+
`Switched to worktree '${name}' (${target.branch})`,
|
|
295
|
+
` Path: ${target.path}`,
|
|
296
|
+
"",
|
|
297
|
+
...formatWorktreeHandoff(target.path),
|
|
298
|
+
].join("\n"),
|
|
299
|
+
"info",
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── merge ─────────────────────────────────────────────────────────────
|
|
305
|
+
if (sub === "merge") {
|
|
306
|
+
const wtName = name ?? getCurrentWorktreeName(currentCwd);
|
|
307
|
+
if (wtName === "main") {
|
|
308
|
+
ctx.ui.notify("Can't merge main into itself.", "error");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const worktrees = listWorktrees();
|
|
313
|
+
const target = worktrees.find((w) => w.name === wtName);
|
|
314
|
+
if (!target) {
|
|
315
|
+
ctx.ui.notify(`No worktree named '${wtName}'.`, "error");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check for uncommitted changes
|
|
320
|
+
const dirty = run("git status --porcelain", target.path);
|
|
321
|
+
if (dirty) {
|
|
322
|
+
const ok = await ctx.ui.confirm(
|
|
323
|
+
`'${wtName}' has uncommitted changes`,
|
|
324
|
+
"These will be lost on merge. Continue?",
|
|
325
|
+
);
|
|
326
|
+
if (!ok) return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Get commit log for the editor prefill
|
|
330
|
+
let commitInfo = "";
|
|
331
|
+
try {
|
|
332
|
+
const count = run(`git rev-list --count main..${wtName}`, REPO_ROOT);
|
|
333
|
+
const log = run(`git log --oneline main..${wtName}`, REPO_ROOT);
|
|
334
|
+
commitInfo = `\n\n# Squashing ${count} commit(s):\n# ${log.split("\n").join("\n# ")}`;
|
|
335
|
+
} catch {}
|
|
336
|
+
|
|
337
|
+
// Ask for squash commit message
|
|
338
|
+
const msg = await ctx.ui.editor(
|
|
339
|
+
`Squash merge '${wtName}' → main\n(Edit the commit message)`,
|
|
340
|
+
`feat(${wtName}): ${commitInfo}`,
|
|
341
|
+
);
|
|
342
|
+
if (!msg?.trim()) {
|
|
343
|
+
ctx.ui.notify("Merge cancelled — no commit message.", "info");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// If currently in this worktree, switch to main first
|
|
349
|
+
if (currentCwd === target.path) {
|
|
350
|
+
currentCwd = REPO_ROOT;
|
|
351
|
+
pi.appendEntry("worktree-cwd", { cwd: currentCwd });
|
|
352
|
+
updateStatus(ctx, "main");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Squash merge
|
|
356
|
+
run(`git merge --squash ${wtName}`, REPO_ROOT);
|
|
357
|
+
run(`git commit -m ${JSON.stringify(msg.trim())}`, REPO_ROOT);
|
|
358
|
+
|
|
359
|
+
// Clean up worktree and branch
|
|
360
|
+
run(`git worktree remove --force ${target.path}`, REPO_ROOT);
|
|
361
|
+
run(`git branch -D ${wtName}`, REPO_ROOT);
|
|
362
|
+
|
|
363
|
+
ctx.ui.notify(
|
|
364
|
+
[`✓ Squash merged '${wtName}' → main`, ` Branch and worktree deleted.`, ` Now on: main`].join("\n"),
|
|
365
|
+
"info",
|
|
366
|
+
);
|
|
367
|
+
} catch (e) {
|
|
368
|
+
ctx.ui.notify(`Merge failed: ${(e as Error).message}`, "error");
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── rm ────────────────────────────────────────────────────────────────
|
|
374
|
+
if (sub === "rm") {
|
|
375
|
+
const wtName = name ?? getCurrentWorktreeName(currentCwd);
|
|
376
|
+
if (wtName === "main") {
|
|
377
|
+
ctx.ui.notify("Can't remove main.", "error");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const worktrees = listWorktrees();
|
|
382
|
+
const target = worktrees.find((w) => w.name === wtName);
|
|
383
|
+
if (!target) {
|
|
384
|
+
ctx.ui.notify(`No worktree named '${wtName}'.`, "error");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let warn = `Delete worktree '${wtName}' and branch '${target.branch}'?`;
|
|
389
|
+
if (target.commitCount > 0) {
|
|
390
|
+
warn += `\n\n⚠️ ${target.commitCount} commit${target.commitCount !== 1 ? "s" : ""} will be lost.`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const ok = await ctx.ui.confirm(warn, "This cannot be undone.");
|
|
394
|
+
if (!ok) return;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
if (currentCwd === target.path) {
|
|
398
|
+
currentCwd = REPO_ROOT;
|
|
399
|
+
pi.appendEntry("worktree-cwd", { cwd: currentCwd });
|
|
400
|
+
updateStatus(ctx, "main");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
run(`git worktree remove --force ${target.path}`, REPO_ROOT);
|
|
404
|
+
run(`git branch -D ${target.branch}`, REPO_ROOT);
|
|
405
|
+
|
|
406
|
+
ctx.ui.notify(`✓ Deleted worktree '${wtName}' and branch '${target.branch}'.`, "info");
|
|
407
|
+
} catch (e) {
|
|
408
|
+
ctx.ui.notify(`Failed: ${(e as Error).message}`, "error");
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── fallthrough ───────────────────────────────────────────────────────
|
|
414
|
+
ctx.ui.notify(
|
|
415
|
+
["Usage: /wt <subcommand> [name]", "", " new [name] create worktree + branch, switch there", " ls list all worktrees", " switch <name> switch agent cwd (use 'main' to go back)", " merge [name] squash merge → main, delete worktree + branch", " rm [name] discard worktree + branch", " status show current worktree git status"].join("\n"),
|
|
416
|
+
"info",
|
|
417
|
+
);
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
}
|