@tuan_son.dinh/gsd 2.6.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/LICENSE +21 -0
- package/README.md +453 -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 +269 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +70 -0
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +418 -0
- package/dist/pi-migration.d.ts +14 -0
- package/dist/pi-migration.js +57 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +60 -0
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.js +25 -0
- package/package.json +60 -0
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -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 +127 -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 +249 -0
- package/src/resources/extensions/bg-shell/index.ts +2808 -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 +4989 -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/google-search/index.ts +323 -0
- package/src/resources/extensions/google-search/package.json +9 -0
- package/src/resources/extensions/gsd/activity-log.ts +69 -0
- package/src/resources/extensions/gsd/auto.ts +2744 -0
- package/src/resources/extensions/gsd/commands.ts +313 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
- package/src/resources/extensions/gsd/doctor.ts +690 -0
- package/src/resources/extensions/gsd/files.ts +732 -0
- package/src/resources/extensions/gsd/git-service.ts +597 -0
- package/src/resources/extensions/gsd/gitignore.ts +168 -0
- package/src/resources/extensions/gsd/guided-flow.ts +817 -0
- package/src/resources/extensions/gsd/index.ts +558 -0
- package/src/resources/extensions/gsd/metrics.ts +374 -0
- package/src/resources/extensions/gsd/migrate/command.ts +218 -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 +55 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -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 +757 -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 +29 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +61 -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 +65 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -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/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +187 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -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 +460 -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 +131 -0
- package/src/resources/extensions/gsd/templates/preferences.md +24 -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/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
- package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
- package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -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/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
- package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree-command.ts +845 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
- package/src/resources/extensions/gsd/worktree.ts +183 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/mcporter/index.ts +429 -0
- package/src/resources/extensions/remote-questions/config.ts +81 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/src/resources/extensions/remote-questions/format.ts +163 -0
- package/src/resources/extensions/remote-questions/manager.ts +192 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
- package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/src/resources/extensions/remote-questions/status.ts +31 -0
- package/src/resources/extensions/remote-questions/store.ts +77 -0
- package/src/resources/extensions/remote-questions/types.ts +75 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +65 -0
- package/src/resources/extensions/search-the-web/native-search.ts +157 -0
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -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/terminal.ts +23 -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/clear.ts +10 -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/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1020 -0
- package/src/resources/extensions/voice/index.ts +195 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
|
@@ -0,0 +1,845 @@
|
|
|
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 [name] [target] — start LLM-guided merge (auto-detects when inside a worktree)
|
|
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 { gsdRoot, milestonesDir } from "./paths.js";
|
|
18
|
+
import {
|
|
19
|
+
createWorktree,
|
|
20
|
+
listWorktrees,
|
|
21
|
+
removeWorktree,
|
|
22
|
+
mergeWorktreeToMain,
|
|
23
|
+
diffWorktreeAll,
|
|
24
|
+
diffWorktreeNumstat,
|
|
25
|
+
getMainBranch,
|
|
26
|
+
getWorktreeGSDDiff,
|
|
27
|
+
getWorktreeCodeDiff,
|
|
28
|
+
getWorktreeLog,
|
|
29
|
+
worktreeBranchName,
|
|
30
|
+
worktreePath,
|
|
31
|
+
} from "./worktree-manager.js";
|
|
32
|
+
import { inferCommitType } from "./git-service.js";
|
|
33
|
+
import type { FileLineStat } from "./worktree-manager.js";
|
|
34
|
+
import { execSync } from "node:child_process";
|
|
35
|
+
import { existsSync, realpathSync, readFileSync, readdirSync, rmSync, unlinkSync, utimesSync } from "node:fs";
|
|
36
|
+
import { join, resolve, sep } from "node:path";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tracks the original project root so we can switch back.
|
|
40
|
+
* Set when we first chdir into a worktree, cleared on return.
|
|
41
|
+
*/
|
|
42
|
+
let originalCwd: string | null = null;
|
|
43
|
+
|
|
44
|
+
/** Get the original project root if currently in a worktree, or null. */
|
|
45
|
+
export function getWorktreeOriginalCwd(): string | null {
|
|
46
|
+
return originalCwd;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the git HEAD file path for a given directory.
|
|
51
|
+
* Handles both normal repos (.git is a directory) and worktrees (.git is a file).
|
|
52
|
+
*/
|
|
53
|
+
function resolveGitHeadPath(dir: string): string | null {
|
|
54
|
+
const gitPath = join(dir, ".git");
|
|
55
|
+
if (!existsSync(gitPath)) return null;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(gitPath, "utf8").trim();
|
|
59
|
+
if (content.startsWith("gitdir: ")) {
|
|
60
|
+
// Worktree — .git is a file pointing to the real gitdir
|
|
61
|
+
const gitDir = resolve(dir, content.slice(8));
|
|
62
|
+
const headPath = join(gitDir, "HEAD");
|
|
63
|
+
return existsSync(headPath) ? headPath : null;
|
|
64
|
+
}
|
|
65
|
+
// Normal repo — .git is a directory
|
|
66
|
+
const headPath = join(dir, ".git", "HEAD");
|
|
67
|
+
return existsSync(headPath) ? headPath : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Nudge pi's FooterDataProvider to re-read the git branch.
|
|
75
|
+
*
|
|
76
|
+
* The footer caches the branch and watches a single .git dir for changes.
|
|
77
|
+
* After process.chdir() into a worktree (or back), the watcher is stale —
|
|
78
|
+
* it's still watching the old git dir. We touch HEAD in both the old and
|
|
79
|
+
* new git dirs to ensure the watcher fires regardless of which one it's
|
|
80
|
+
* monitoring. This clears cachedBranch; the next getGitBranch() call uses
|
|
81
|
+
* the new process.cwd() and picks up the correct branch.
|
|
82
|
+
*/
|
|
83
|
+
function nudgeGitBranchCache(previousCwd: string): void {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
for (const dir of [previousCwd, process.cwd()]) {
|
|
86
|
+
try {
|
|
87
|
+
const headPath = resolveGitHeadPath(dir);
|
|
88
|
+
if (headPath) utimesSync(headPath, now, now);
|
|
89
|
+
} catch {
|
|
90
|
+
// Best-effort — branch display may be stale
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Get the name of the active worktree, or null if not in one. */
|
|
96
|
+
export function getActiveWorktreeName(): string | null {
|
|
97
|
+
if (!originalCwd) return null;
|
|
98
|
+
const cwd = process.cwd();
|
|
99
|
+
const wtDir = join(originalCwd, ".gsd", "worktrees");
|
|
100
|
+
if (!cwd.startsWith(wtDir)) return null;
|
|
101
|
+
const rel = cwd.slice(wtDir.length + 1);
|
|
102
|
+
const name = rel.split("/")[0] ?? rel.split("\\")[0];
|
|
103
|
+
return name || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Shared completions and handler (used by both /worktree and /wt) ────────
|
|
107
|
+
|
|
108
|
+
function worktreeCompletions(prefix: string) {
|
|
109
|
+
const parts = prefix.trim().split(/\s+/);
|
|
110
|
+
const subcommands = ["list", "merge", "remove", "switch", "create", "return"];
|
|
111
|
+
|
|
112
|
+
if (parts.length <= 1) {
|
|
113
|
+
const partial = parts[0] ?? "";
|
|
114
|
+
const cmdCompletions = subcommands
|
|
115
|
+
.filter(cmd => cmd.startsWith(partial))
|
|
116
|
+
.map(cmd => ({ value: cmd, label: cmd }));
|
|
117
|
+
try {
|
|
118
|
+
const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
|
|
119
|
+
const existing = listWorktrees(mainBase);
|
|
120
|
+
const nameCompletions = existing
|
|
121
|
+
.filter(wt => wt.name.startsWith(partial))
|
|
122
|
+
.map(wt => ({ value: wt.name, label: wt.name }));
|
|
123
|
+
return [...cmdCompletions, ...nameCompletions];
|
|
124
|
+
} catch {
|
|
125
|
+
return cmdCompletions;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if ((parts[0] === "merge" || parts[0] === "remove" || parts[0] === "switch" || parts[0] === "create") && parts.length <= 2) {
|
|
130
|
+
const namePrefix = parts[1] ?? "";
|
|
131
|
+
try {
|
|
132
|
+
const mainBase = getWorktreeOriginalCwd() ?? process.cwd();
|
|
133
|
+
const existing = listWorktrees(mainBase);
|
|
134
|
+
const nameCompletions = existing
|
|
135
|
+
.filter(wt => wt.name.startsWith(namePrefix))
|
|
136
|
+
.map(wt => ({ value: `${parts[0]} ${wt.name}`, label: wt.name }));
|
|
137
|
+
|
|
138
|
+
// Add "all" option for remove
|
|
139
|
+
if (parts[0] === "remove" && "all".startsWith(namePrefix)) {
|
|
140
|
+
nameCompletions.push({ value: "remove all", label: "all" });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return nameCompletions;
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function worktreeHandler(
|
|
153
|
+
args: string,
|
|
154
|
+
ctx: ExtensionCommandContext,
|
|
155
|
+
pi: ExtensionAPI,
|
|
156
|
+
alias: string,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const trimmed = (typeof args === "string" ? args : "").trim();
|
|
159
|
+
const basePath = process.cwd();
|
|
160
|
+
|
|
161
|
+
if (trimmed === "") {
|
|
162
|
+
ctx.ui.notify(
|
|
163
|
+
[
|
|
164
|
+
"Usage:",
|
|
165
|
+
` /${alias} <name> — create and switch into a new worktree`,
|
|
166
|
+
` /${alias} switch <name> — switch into an existing worktree`,
|
|
167
|
+
` /${alias} return — switch back to the main project tree`,
|
|
168
|
+
` /${alias} list — list all worktrees`,
|
|
169
|
+
` /${alias} merge [name] [target] — merge worktree into target branch (auto-detects when inside a worktree)`,
|
|
170
|
+
` /${alias} remove <name|all> — remove a worktree (or all) and its branch`,
|
|
171
|
+
].join("\n"),
|
|
172
|
+
"info",
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (trimmed === "list") {
|
|
178
|
+
await handleList(basePath, ctx);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (trimmed === "return") {
|
|
183
|
+
await handleReturn(ctx);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (trimmed.startsWith("switch ") || trimmed.startsWith("create ")) {
|
|
188
|
+
const name = trimmed.replace(/^(?:switch|create)\s+/, "").trim();
|
|
189
|
+
if (!name) {
|
|
190
|
+
ctx.ui.notify(`Usage: /${alias} ${trimmed.split(" ")[0]} <name>`, "warning");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// create and switch both do the same thing: switch if exists, create if not
|
|
194
|
+
const mainBase = originalCwd ?? basePath;
|
|
195
|
+
const existing = listWorktrees(mainBase);
|
|
196
|
+
if (existing.some(wt => wt.name === name)) {
|
|
197
|
+
await handleSwitch(basePath, name, ctx);
|
|
198
|
+
} else {
|
|
199
|
+
await handleCreate(basePath, name, ctx);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (trimmed === "merge" || trimmed.startsWith("merge ")) {
|
|
205
|
+
const mergeArgs = trimmed.replace(/^merge\s*/, "").trim().split(/\s+/).filter(Boolean);
|
|
206
|
+
const mainBase = originalCwd ?? basePath;
|
|
207
|
+
const activeWt = getActiveWorktreeName();
|
|
208
|
+
|
|
209
|
+
if (mergeArgs.length === 0) {
|
|
210
|
+
// Bare "/worktree merge" — only valid when inside a worktree
|
|
211
|
+
if (!activeWt) {
|
|
212
|
+
ctx.ui.notify(`Usage: /${alias} merge <name> [target]`, "warning");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await handleMerge(mainBase, activeWt, ctx, pi, undefined);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const name = mergeArgs[0]!;
|
|
220
|
+
const targetBranch = mergeArgs[1];
|
|
221
|
+
|
|
222
|
+
// Check if 'name' is an actual worktree
|
|
223
|
+
const worktrees = listWorktrees(mainBase);
|
|
224
|
+
const isWorktree = worktrees.some(w => w.name === name);
|
|
225
|
+
|
|
226
|
+
if (isWorktree) {
|
|
227
|
+
await handleMerge(mainBase, name, ctx, pi, targetBranch);
|
|
228
|
+
} else if (activeWt) {
|
|
229
|
+
// Not a worktree name — user is in a worktree and gave the target branch
|
|
230
|
+
// e.g. "/worktree merge main" while inside worktree "new"
|
|
231
|
+
await handleMerge(mainBase, activeWt, ctx, pi, name);
|
|
232
|
+
} else {
|
|
233
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /${alias} list to see available worktrees.`, "warning");
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (trimmed === "remove" || trimmed.startsWith("remove ")) {
|
|
239
|
+
const name = trimmed.replace(/^remove\s*/, "").trim();
|
|
240
|
+
const mainBase = originalCwd ?? basePath;
|
|
241
|
+
|
|
242
|
+
if (name === "all") {
|
|
243
|
+
await handleRemoveAll(mainBase, ctx);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!name) {
|
|
248
|
+
ctx.ui.notify(`Usage: /${alias} remove <name|all>`, "warning");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await handleRemove(mainBase, name, ctx);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const RESERVED = ["list", "return", "switch", "create", "merge", "remove"];
|
|
257
|
+
if (RESERVED.includes(trimmed)) {
|
|
258
|
+
ctx.ui.notify(`Usage: /${alias} ${trimmed}${trimmed === "list" || trimmed === "return" ? "" : " <name>"}`, "warning");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const mainBase = originalCwd ?? basePath;
|
|
263
|
+
const nameOnly = trimmed.split(/\s+/)[0]!;
|
|
264
|
+
if (trimmed !== nameOnly) {
|
|
265
|
+
ctx.ui.notify(`Unknown command. Did you mean /${alias} switch ${nameOnly}?`, "warning");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const existing = listWorktrees(mainBase);
|
|
270
|
+
if (existing.some(wt => wt.name === nameOnly)) {
|
|
271
|
+
await handleSwitch(basePath, nameOnly, ctx);
|
|
272
|
+
} else {
|
|
273
|
+
await handleCreate(basePath, nameOnly, ctx);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function registerWorktreeCommand(pi: ExtensionAPI): void {
|
|
278
|
+
// Restore worktree state after /reload.
|
|
279
|
+
// The module-level originalCwd resets to null when extensions are re-loaded,
|
|
280
|
+
// but process.cwd() is still inside the worktree. Detect this and recover.
|
|
281
|
+
if (!originalCwd) {
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
284
|
+
const markerIdx = cwd.indexOf(marker);
|
|
285
|
+
if (markerIdx !== -1) {
|
|
286
|
+
originalCwd = cwd.slice(0, markerIdx);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pi.registerCommand("worktree", {
|
|
291
|
+
description: "Git worktrees (also /wt): /worktree <name> | list | merge | remove",
|
|
292
|
+
getArgumentCompletions: worktreeCompletions,
|
|
293
|
+
|
|
294
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
295
|
+
await worktreeHandler(args, ctx, pi, "worktree");
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// /wt alias — same handler, same completions
|
|
300
|
+
pi.registerCommand("wt", {
|
|
301
|
+
description: "Alias for /worktree",
|
|
302
|
+
getArgumentCompletions: worktreeCompletions,
|
|
303
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
304
|
+
await worktreeHandler(args, ctx, pi, "wt");
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Handlers ──────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if the worktree has existing GSD milestones that would
|
|
313
|
+
* cause auto-mode to continue previous work instead of starting fresh.
|
|
314
|
+
*/
|
|
315
|
+
function hasExistingMilestones(wtPath: string): boolean {
|
|
316
|
+
const mDir = milestonesDir(wtPath);
|
|
317
|
+
if (!existsSync(mDir)) return false;
|
|
318
|
+
try {
|
|
319
|
+
const entries = readdirSync(mDir, { withFileTypes: true })
|
|
320
|
+
.filter(d => d.isDirectory() && /^M\d+/.test(d.name));
|
|
321
|
+
return entries.length > 0;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Clear GSD planning artifacts so auto-mode starts fresh with the discuss flow.
|
|
329
|
+
* Keeps the .gsd/ directory structure intact but removes milestones and root planning files.
|
|
330
|
+
*/
|
|
331
|
+
function clearGSDPlans(wtPath: string): void {
|
|
332
|
+
const mDir = milestonesDir(wtPath);
|
|
333
|
+
if (existsSync(mDir)) {
|
|
334
|
+
rmSync(mDir, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove root planning files — PROJECT.md, DECISIONS.md, QUEUE.md, REQUIREMENTS.md
|
|
338
|
+
// Keep STATE.md (gitignored, will be rebuilt) and other runtime files
|
|
339
|
+
const root = gsdRoot(wtPath);
|
|
340
|
+
const planningFiles = ["PROJECT.md", "DECISIONS.md", "QUEUE.md", "REQUIREMENTS.md"];
|
|
341
|
+
for (const file of planningFiles) {
|
|
342
|
+
const filePath = join(root, file);
|
|
343
|
+
if (existsSync(filePath)) {
|
|
344
|
+
unlinkSync(filePath);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function handleCreate(
|
|
350
|
+
basePath: string,
|
|
351
|
+
name: string,
|
|
352
|
+
ctx: ExtensionCommandContext,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
try {
|
|
355
|
+
// Auto-commit dirty files before leaving current workspace (must happen
|
|
356
|
+
// before createWorktree so the new worktree forks from committed HEAD)
|
|
357
|
+
const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
|
|
358
|
+
|
|
359
|
+
// Create from the main tree, not from inside another worktree
|
|
360
|
+
const mainBase = originalCwd ?? basePath;
|
|
361
|
+
const info = createWorktree(mainBase, name);
|
|
362
|
+
|
|
363
|
+
// Track original cwd before switching
|
|
364
|
+
if (!originalCwd) originalCwd = basePath;
|
|
365
|
+
|
|
366
|
+
const prevCwd = process.cwd();
|
|
367
|
+
process.chdir(info.path);
|
|
368
|
+
nudgeGitBranchCache(prevCwd);
|
|
369
|
+
|
|
370
|
+
// If the worktree inherited existing milestones, ask whether to keep or clear them
|
|
371
|
+
let clearedPlans = false;
|
|
372
|
+
if (hasExistingMilestones(info.path)) {
|
|
373
|
+
// confirmLabel = Continue (safe default, on the left / first)
|
|
374
|
+
// declineLabel = Start fresh (destructive, on the right)
|
|
375
|
+
const keepExisting = await showConfirm(ctx, {
|
|
376
|
+
title: "Worktree Setup",
|
|
377
|
+
message: [
|
|
378
|
+
`This worktree inherited existing GSD milestones from the main branch.`,
|
|
379
|
+
``,
|
|
380
|
+
` Continue — keep milestones and pick up where main left off`,
|
|
381
|
+
` Start fresh — clear milestones so /gsd auto starts a new project`,
|
|
382
|
+
].join("\n"),
|
|
383
|
+
confirmLabel: "Continue",
|
|
384
|
+
declineLabel: "Start fresh",
|
|
385
|
+
});
|
|
386
|
+
if (!keepExisting) {
|
|
387
|
+
clearGSDPlans(info.path);
|
|
388
|
+
clearedPlans = true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const commitNote = commitMsg
|
|
393
|
+
? ` ${CLR.muted("Auto-committed on previous branch before switching.")}`
|
|
394
|
+
: "";
|
|
395
|
+
const freshNote = clearedPlans
|
|
396
|
+
? ` ${CLR.ok("✓")} Cleared milestones — ${CLR.hint("/gsd auto")} will start fresh.`
|
|
397
|
+
: "";
|
|
398
|
+
ctx.ui.notify(
|
|
399
|
+
[
|
|
400
|
+
`${CLR.ok("✓")} Worktree ${CLR.name(name)} created and activated.`,
|
|
401
|
+
"",
|
|
402
|
+
` ${CLR.label("path")} ${CLR.path(info.path)}`,
|
|
403
|
+
` ${CLR.label("branch")} ${CLR.branch(info.branch)}`,
|
|
404
|
+
commitNote,
|
|
405
|
+
freshNote,
|
|
406
|
+
"",
|
|
407
|
+
` ${CLR.hint(`/worktree merge ${name}`)} ${CLR.muted("merge back when done")}`,
|
|
408
|
+
` ${CLR.hint("/worktree return")}${" ".repeat(Math.max(1, name.length - 2))} ${CLR.muted("switch back to main tree")}`,
|
|
409
|
+
].filter(Boolean).join("\n"),
|
|
410
|
+
"info",
|
|
411
|
+
);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
414
|
+
ctx.ui.notify(`Failed to create worktree: ${msg}`, "error");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function handleSwitch(
|
|
419
|
+
basePath: string,
|
|
420
|
+
name: string,
|
|
421
|
+
ctx: ExtensionCommandContext,
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
try {
|
|
424
|
+
const mainBase = originalCwd ?? basePath;
|
|
425
|
+
const wtPath = worktreePath(mainBase, name);
|
|
426
|
+
|
|
427
|
+
if (!existsSync(wtPath)) {
|
|
428
|
+
ctx.ui.notify(
|
|
429
|
+
`Worktree "${name}" not found. Run /worktree list to see available worktrees.`,
|
|
430
|
+
"warning",
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Auto-commit dirty files before leaving current workspace
|
|
436
|
+
const commitMsg = autoCommitCurrentBranch(basePath, "worktree-switch", name);
|
|
437
|
+
|
|
438
|
+
// Track original cwd before switching
|
|
439
|
+
if (!originalCwd) originalCwd = basePath;
|
|
440
|
+
|
|
441
|
+
const prevCwd = process.cwd();
|
|
442
|
+
process.chdir(wtPath);
|
|
443
|
+
nudgeGitBranchCache(prevCwd);
|
|
444
|
+
|
|
445
|
+
const commitNote = commitMsg
|
|
446
|
+
? ` ${CLR.muted("Auto-committed on previous branch before switching.")}`
|
|
447
|
+
: "";
|
|
448
|
+
ctx.ui.notify(
|
|
449
|
+
[
|
|
450
|
+
`${CLR.ok("✓")} Switched to worktree ${CLR.name(name)}.`,
|
|
451
|
+
"",
|
|
452
|
+
` ${CLR.label("path")} ${CLR.path(wtPath)}`,
|
|
453
|
+
` ${CLR.label("branch")} ${CLR.branch(worktreeBranchName(name))}`,
|
|
454
|
+
commitNote,
|
|
455
|
+
"",
|
|
456
|
+
` ${CLR.hint("/worktree return")} ${CLR.muted("switch back to main tree")}`,
|
|
457
|
+
].filter(Boolean).join("\n"),
|
|
458
|
+
"info",
|
|
459
|
+
);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
462
|
+
ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function handleReturn(ctx: ExtensionCommandContext): Promise<void> {
|
|
467
|
+
if (!originalCwd) {
|
|
468
|
+
ctx.ui.notify("Already in the main project tree.", "info");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Auto-commit dirty files before leaving worktree
|
|
473
|
+
const commitMsg = autoCommitCurrentBranch(process.cwd(), "worktree-return", "worktree");
|
|
474
|
+
|
|
475
|
+
const returnTo = originalCwd;
|
|
476
|
+
originalCwd = null;
|
|
477
|
+
|
|
478
|
+
const prevCwd = process.cwd();
|
|
479
|
+
process.chdir(returnTo);
|
|
480
|
+
nudgeGitBranchCache(prevCwd);
|
|
481
|
+
|
|
482
|
+
const commitNote = commitMsg
|
|
483
|
+
? ` ${CLR.muted("Auto-committed on worktree branch before returning.")}`
|
|
484
|
+
: "";
|
|
485
|
+
ctx.ui.notify(
|
|
486
|
+
[
|
|
487
|
+
`${CLR.ok("✓")} Returned to main project tree.`,
|
|
488
|
+
"",
|
|
489
|
+
` ${CLR.label("path")} ${CLR.path(returnTo)}`,
|
|
490
|
+
commitNote,
|
|
491
|
+
].filter(Boolean).join("\n"),
|
|
492
|
+
"info",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── ANSI styling ─────────────────────────────────────────────────────────
|
|
497
|
+
// Consistent palette for all worktree command output.
|
|
498
|
+
|
|
499
|
+
const BOLD = "\x1b[1m";
|
|
500
|
+
const DIM = "\x1b[2m";
|
|
501
|
+
const RESET = "\x1b[0m";
|
|
502
|
+
const CYAN = "\x1b[36m";
|
|
503
|
+
const GREEN = "\x1b[32m";
|
|
504
|
+
const RED = "\x1b[31m";
|
|
505
|
+
const YELLOW = "\x1b[33m";
|
|
506
|
+
const WHITE = "\x1b[37m";
|
|
507
|
+
const MAGENTA = "\x1b[35m";
|
|
508
|
+
|
|
509
|
+
// Semantic aliases for consistent use across all handlers
|
|
510
|
+
const CLR = {
|
|
511
|
+
/** Worktree names and primary emphasis */
|
|
512
|
+
name: (s: string) => `${BOLD}${CYAN}${s}${RESET}`,
|
|
513
|
+
/** Active worktree name */
|
|
514
|
+
nameActive: (s: string) => `${BOLD}${GREEN}${s}${RESET}`,
|
|
515
|
+
/** Branch names */
|
|
516
|
+
branch: (s: string) => `${MAGENTA}${s}${RESET}`,
|
|
517
|
+
/** File paths */
|
|
518
|
+
path: (s: string) => `${DIM}${s}${RESET}`,
|
|
519
|
+
/** Labels (key in key:value pairs) */
|
|
520
|
+
label: (s: string) => `${WHITE}${s}${RESET}`,
|
|
521
|
+
/** Hints and commands the user can run */
|
|
522
|
+
hint: (s: string) => `${DIM}${CYAN}${s}${RESET}`,
|
|
523
|
+
/** Success messages and checks */
|
|
524
|
+
ok: (s: string) => `${GREEN}${s}${RESET}`,
|
|
525
|
+
/** Warning badges */
|
|
526
|
+
warn: (s: string) => `${YELLOW}${s}${RESET}`,
|
|
527
|
+
/** Section headers */
|
|
528
|
+
header: (s: string) => `${BOLD}${WHITE}${s}${RESET}`,
|
|
529
|
+
/** Muted secondary info */
|
|
530
|
+
muted: (s: string) => `${DIM}${s}${RESET}`,
|
|
531
|
+
} as const;
|
|
532
|
+
|
|
533
|
+
async function handleList(
|
|
534
|
+
basePath: string,
|
|
535
|
+
ctx: ExtensionCommandContext,
|
|
536
|
+
): Promise<void> {
|
|
537
|
+
try {
|
|
538
|
+
const mainBase = originalCwd ?? basePath;
|
|
539
|
+
const worktrees = listWorktrees(mainBase);
|
|
540
|
+
|
|
541
|
+
if (worktrees.length === 0) {
|
|
542
|
+
ctx.ui.notify("No GSD worktrees found. Create one with /worktree <name>.", "info");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const cwd = process.cwd();
|
|
547
|
+
const lines = [CLR.header("GSD Worktrees"), ""];
|
|
548
|
+
for (const wt of worktrees) {
|
|
549
|
+
const isCurrent = cwd === wt.path
|
|
550
|
+
|| (existsSync(cwd) && existsSync(wt.path)
|
|
551
|
+
&& realpathSync(cwd) === realpathSync(wt.path));
|
|
552
|
+
|
|
553
|
+
const styledName = isCurrent ? CLR.nameActive(wt.name) : CLR.name(wt.name);
|
|
554
|
+
const badge = isCurrent
|
|
555
|
+
? ` ${CLR.ok("● active")}`
|
|
556
|
+
: !wt.exists
|
|
557
|
+
? ` ${CLR.warn("✗ missing")}`
|
|
558
|
+
: "";
|
|
559
|
+
lines.push(` ${styledName}${badge}`);
|
|
560
|
+
lines.push(` ${CLR.label("branch")} ${CLR.branch(wt.branch)}`);
|
|
561
|
+
lines.push(` ${CLR.label("path")} ${CLR.path(wt.path)}`);
|
|
562
|
+
lines.push("");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (originalCwd) {
|
|
566
|
+
lines.push(` ${CLR.label("main tree")} ${CLR.path(originalCwd)}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
570
|
+
} catch (error) {
|
|
571
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
572
|
+
ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function handleMerge(
|
|
577
|
+
basePath: string,
|
|
578
|
+
name: string,
|
|
579
|
+
ctx: ExtensionCommandContext,
|
|
580
|
+
pi: ExtensionAPI,
|
|
581
|
+
targetBranch?: string,
|
|
582
|
+
): Promise<void> {
|
|
583
|
+
try {
|
|
584
|
+
const branch = worktreeBranchName(name);
|
|
585
|
+
const mainBranch = targetBranch ?? getMainBranch(basePath);
|
|
586
|
+
|
|
587
|
+
// Validate the worktree/branch exists
|
|
588
|
+
const worktrees = listWorktrees(basePath);
|
|
589
|
+
const wt = worktrees.find(w => w.name === name);
|
|
590
|
+
if (!wt) {
|
|
591
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Gather merge context — full repo diff, not just .gsd/
|
|
596
|
+
const diffSummary = diffWorktreeAll(basePath, name);
|
|
597
|
+
const numstat = diffWorktreeNumstat(basePath, name);
|
|
598
|
+
const gsdDiff = getWorktreeGSDDiff(basePath, name);
|
|
599
|
+
const codeDiff = getWorktreeCodeDiff(basePath, name);
|
|
600
|
+
const commitLog = getWorktreeLog(basePath, name);
|
|
601
|
+
|
|
602
|
+
const totalChanges = diffSummary.added.length + diffSummary.modified.length + diffSummary.removed.length;
|
|
603
|
+
if (totalChanges === 0 && !commitLog.trim()) {
|
|
604
|
+
ctx.ui.notify(`Worktree ${CLR.name(name)} has no changes to merge.`, "info");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Build a map of file → line stats for the preview
|
|
609
|
+
const statMap = new Map<string, FileLineStat>();
|
|
610
|
+
for (const s of numstat) statMap.set(s.file, s);
|
|
611
|
+
|
|
612
|
+
// Compute totals
|
|
613
|
+
let totalAdded = 0;
|
|
614
|
+
let totalRemoved = 0;
|
|
615
|
+
for (const s of numstat) { totalAdded += s.added; totalRemoved += s.removed; }
|
|
616
|
+
|
|
617
|
+
// Split files into code vs GSD for the preview
|
|
618
|
+
const isGSD = (f: string) => f.startsWith(".gsd/");
|
|
619
|
+
const codeChanges = diffSummary.added.filter(f => !isGSD(f)).length
|
|
620
|
+
+ diffSummary.modified.filter(f => !isGSD(f)).length
|
|
621
|
+
+ diffSummary.removed.filter(f => !isGSD(f)).length;
|
|
622
|
+
const gsdChanges = diffSummary.added.filter(isGSD).length
|
|
623
|
+
+ diffSummary.modified.filter(isGSD).length
|
|
624
|
+
+ diffSummary.removed.filter(isGSD).length;
|
|
625
|
+
|
|
626
|
+
// Format a file line with +/- stats
|
|
627
|
+
const formatFileLine = (prefix: string, file: string): string => {
|
|
628
|
+
const s = statMap.get(file);
|
|
629
|
+
const stat = s ? ` ${CLR.ok(`+${s.added}`)} ${RED}-${s.removed}${RESET}` : "";
|
|
630
|
+
return ` ${prefix} ${file}${stat}`;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Preview confirmation before merge dispatch
|
|
634
|
+
const previewLines = [
|
|
635
|
+
`Merge ${CLR.name(name)} → ${CLR.branch(mainBranch)}`,
|
|
636
|
+
"",
|
|
637
|
+
` ${totalChanges} file${totalChanges === 1 ? "" : "s"} changed, ${CLR.ok(`+${totalAdded}`)} ${RED}-${totalRemoved}${RESET} lines ${CLR.muted(`(${codeChanges} code, ${gsdChanges} GSD)`)}`,
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
const appendFileList = (label: string, files: string[], prefix: string, limit = 10) => {
|
|
641
|
+
if (files.length === 0) return;
|
|
642
|
+
previewLines.push("", ` ${label}:`);
|
|
643
|
+
for (const f of files.slice(0, limit)) previewLines.push(formatFileLine(prefix, f));
|
|
644
|
+
if (files.length > limit) previewLines.push(` … and ${files.length - limit} more`);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
appendFileList("Added", diffSummary.added, "+");
|
|
648
|
+
appendFileList("Modified", diffSummary.modified, "~");
|
|
649
|
+
appendFileList("Removed", diffSummary.removed, "-");
|
|
650
|
+
|
|
651
|
+
const confirmed = await showConfirm(ctx, {
|
|
652
|
+
title: "Worktree Merge",
|
|
653
|
+
message: previewLines.join("\n"),
|
|
654
|
+
confirmLabel: "Merge",
|
|
655
|
+
declineLabel: "Cancel",
|
|
656
|
+
});
|
|
657
|
+
if (!confirmed) {
|
|
658
|
+
ctx.ui.notify("Merge cancelled.", "info");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Switch to the main tree before merging.
|
|
663
|
+
// Must be on the main branch to run git merge --squash.
|
|
664
|
+
if (originalCwd) {
|
|
665
|
+
const prevCwd = process.cwd();
|
|
666
|
+
process.chdir(basePath);
|
|
667
|
+
nudgeGitBranchCache(prevCwd);
|
|
668
|
+
originalCwd = null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// --- Deterministic merge path (preferred) ---
|
|
672
|
+
// Try a direct squash-merge first. Only fall back to LLM on conflict.
|
|
673
|
+
const commitType = inferCommitType(name);
|
|
674
|
+
const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
|
|
675
|
+
try {
|
|
676
|
+
mergeWorktreeToMain(basePath, name, commitMessage);
|
|
677
|
+
ctx.ui.notify(
|
|
678
|
+
[
|
|
679
|
+
`${CLR.ok("✓")} Merged ${CLR.name(name)} → ${CLR.branch(mainBranch)} ${CLR.muted("(deterministic squash)")}`,
|
|
680
|
+
"",
|
|
681
|
+
` ${totalChanges} file${totalChanges === 1 ? "" : "s"} changed, ${CLR.ok(`+${totalAdded}`)} ${RED}-${totalRemoved}${RESET} lines`,
|
|
682
|
+
` ${CLR.muted("commit:")} ${commitMessage}`,
|
|
683
|
+
].join("\n"),
|
|
684
|
+
"info",
|
|
685
|
+
);
|
|
686
|
+
return;
|
|
687
|
+
} catch (mergeErr) {
|
|
688
|
+
const mergeMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
689
|
+
const isConflict = /conflict/i.test(mergeMsg);
|
|
690
|
+
|
|
691
|
+
if (isConflict) {
|
|
692
|
+
// Abort the failed merge so the working tree is clean for LLM retry
|
|
693
|
+
try {
|
|
694
|
+
execSync("git merge --abort", { cwd: basePath, stdio: "pipe" });
|
|
695
|
+
} catch { /* already clean */ }
|
|
696
|
+
|
|
697
|
+
ctx.ui.notify(
|
|
698
|
+
`${CLR.muted("Deterministic merge hit conflicts — falling back to LLM-guided merge.")}`,
|
|
699
|
+
"warning",
|
|
700
|
+
);
|
|
701
|
+
// Fall through to LLM dispatch below
|
|
702
|
+
} else {
|
|
703
|
+
// Non-conflict error — surface it directly, don't fall back
|
|
704
|
+
ctx.ui.notify(`Failed to merge: ${mergeMsg}`, "error");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// --- LLM fallback path (conflict resolution) ---
|
|
710
|
+
// Format file lists for the prompt
|
|
711
|
+
const formatFiles = (files: string[]) =>
|
|
712
|
+
files.length > 0 ? files.map(f => `- \`${f}\``).join("\n") : "_(none)_";
|
|
713
|
+
|
|
714
|
+
// Load and populate the merge prompt
|
|
715
|
+
const wtPath = worktreePath(basePath, name);
|
|
716
|
+
const prompt = loadPrompt("worktree-merge", {
|
|
717
|
+
worktreeName: name,
|
|
718
|
+
worktreeBranch: branch,
|
|
719
|
+
mainBranch,
|
|
720
|
+
mainTreePath: basePath,
|
|
721
|
+
worktreePath: wtPath,
|
|
722
|
+
commitLog: commitLog || "(no commits)",
|
|
723
|
+
addedFiles: formatFiles(diffSummary.added),
|
|
724
|
+
modifiedFiles: formatFiles(diffSummary.modified),
|
|
725
|
+
removedFiles: formatFiles(diffSummary.removed),
|
|
726
|
+
gsdDiff: gsdDiff || "(no GSD artifact changes)",
|
|
727
|
+
codeDiff: codeDiff || "(no code changes)",
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Dispatch to the LLM
|
|
731
|
+
pi.sendMessage(
|
|
732
|
+
{
|
|
733
|
+
customType: "gsd-worktree-merge",
|
|
734
|
+
content: prompt,
|
|
735
|
+
display: false,
|
|
736
|
+
},
|
|
737
|
+
{ triggerTurn: true },
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
ctx.ui.notify(
|
|
741
|
+
`${CLR.ok("✓")} Merge helper started for ${CLR.name(name)} ${CLR.muted(`(${codeChanges} code + ${gsdChanges} GSD artifact change${totalChanges === 1 ? "" : "s"})`)}`,
|
|
742
|
+
"info",
|
|
743
|
+
);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
746
|
+
ctx.ui.notify(`Failed to start merge: ${msg}`, "error");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function handleRemove(
|
|
751
|
+
basePath: string,
|
|
752
|
+
name: string,
|
|
753
|
+
ctx: ExtensionCommandContext,
|
|
754
|
+
): Promise<void> {
|
|
755
|
+
try {
|
|
756
|
+
const mainBase = originalCwd ?? basePath;
|
|
757
|
+
|
|
758
|
+
// Validate the worktree exists before attempting removal
|
|
759
|
+
const worktrees = listWorktrees(mainBase);
|
|
760
|
+
const wt = worktrees.find(w => w.name === name);
|
|
761
|
+
if (!wt) {
|
|
762
|
+
ctx.ui.notify(`Worktree "${name}" not found. Run /worktree list to see available worktrees.`, "warning");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const confirmed = await showConfirm(ctx, {
|
|
767
|
+
title: "Remove Worktree",
|
|
768
|
+
message: `Remove worktree ${CLR.name(name)} and delete branch ${CLR.branch(wt.branch)}?`,
|
|
769
|
+
confirmLabel: "Remove",
|
|
770
|
+
declineLabel: "Cancel",
|
|
771
|
+
});
|
|
772
|
+
if (!confirmed) {
|
|
773
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const prevCwd = process.cwd();
|
|
778
|
+
removeWorktree(mainBase, name, { deleteBranch: true });
|
|
779
|
+
|
|
780
|
+
// If we were in that worktree, removeWorktree chdir'd us out — clear tracking
|
|
781
|
+
if (originalCwd && process.cwd() !== prevCwd) {
|
|
782
|
+
nudgeGitBranchCache(prevCwd);
|
|
783
|
+
originalCwd = null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
ctx.ui.notify(`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`, "info");
|
|
787
|
+
} catch (error) {
|
|
788
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
789
|
+
ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function handleRemoveAll(
|
|
794
|
+
basePath: string,
|
|
795
|
+
ctx: ExtensionCommandContext,
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
try {
|
|
798
|
+
const mainBase = originalCwd ?? basePath;
|
|
799
|
+
const worktrees = listWorktrees(mainBase);
|
|
800
|
+
|
|
801
|
+
if (worktrees.length === 0) {
|
|
802
|
+
ctx.ui.notify("No worktrees to remove.", "info");
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const names = worktrees.map(w => w.name);
|
|
807
|
+
const confirmed = await showConfirm(ctx, {
|
|
808
|
+
title: "Remove All Worktrees",
|
|
809
|
+
message: `Remove ${worktrees.length} worktree${worktrees.length === 1 ? "" : "s"} and delete their branches?\n\n${names.map(n => ` • ${CLR.name(n)}`).join("\n")}`,
|
|
810
|
+
confirmLabel: "Remove all",
|
|
811
|
+
declineLabel: "Cancel",
|
|
812
|
+
});
|
|
813
|
+
if (!confirmed) {
|
|
814
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const prevCwd = process.cwd();
|
|
819
|
+
const removed: string[] = [];
|
|
820
|
+
const failed: string[] = [];
|
|
821
|
+
|
|
822
|
+
for (const wt of worktrees) {
|
|
823
|
+
try {
|
|
824
|
+
removeWorktree(mainBase, wt.name, { deleteBranch: true });
|
|
825
|
+
removed.push(wt.name);
|
|
826
|
+
} catch {
|
|
827
|
+
failed.push(wt.name);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// If we were in a worktree that got removed, clear tracking
|
|
832
|
+
if (originalCwd && process.cwd() !== prevCwd) {
|
|
833
|
+
nudgeGitBranchCache(prevCwd);
|
|
834
|
+
originalCwd = null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const lines: string[] = [];
|
|
838
|
+
if (removed.length > 0) lines.push(`${CLR.ok("✓")} Removed: ${removed.map(n => CLR.name(n)).join(", ")}`);
|
|
839
|
+
if (failed.length > 0) lines.push(`${CLR.warn("✗")} Failed: ${failed.map(n => CLR.name(n)).join(", ")}`);
|
|
840
|
+
ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
841
|
+
} catch (error) {
|
|
842
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
843
|
+
ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error");
|
|
844
|
+
}
|
|
845
|
+
}
|