wave-code 0.19.7 → 0.19.9
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/dist/cli.js +1 -1
- package/dist/components/ConfirmationSelector.js +4 -3
- package/dist/components/TaskList.js +28 -12
- package/dist/contexts/useChat.js +1 -1
- package/dist/index.js +3 -2
- package/dist/print-cli.js +12 -4
- package/dist/reducers/confirmationReducer.d.ts +1 -1
- package/dist/reducers/confirmationReducer.js +9 -10
- package/dist/stdio/agentBridge.d.ts +11 -0
- package/dist/stdio/agentBridge.js +178 -1
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/utils/worktree.d.ts +8 -2
- package/dist/utils/worktree.js +35 -26
- package/package.json +2 -2
- package/src/cli.tsx +1 -1
- package/src/components/ConfirmationSelector.tsx +16 -15
- package/src/components/TaskList.tsx +30 -12
- package/src/contexts/useChat.tsx +1 -1
- package/src/index.ts +8 -2
- package/src/print-cli.ts +14 -4
- package/src/reducers/confirmationReducer.ts +9 -9
- package/src/stdio/agentBridge.ts +282 -0
- package/src/stdio/protocol.ts +14 -2
- package/src/utils/worktree.ts +57 -33
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
Agent,
|
|
20
20
|
type AgentCallbacks,
|
|
21
21
|
type AgentOptions,
|
|
22
|
+
type BackgroundTask,
|
|
23
|
+
type BackgroundTaskSummary,
|
|
24
|
+
type SerializableWorkflowRun,
|
|
22
25
|
type Message,
|
|
23
26
|
type PermissionDecision,
|
|
24
27
|
type PermissionMode,
|
|
@@ -31,6 +34,9 @@ import {
|
|
|
31
34
|
type Scope,
|
|
32
35
|
listSessions,
|
|
33
36
|
searchFiles,
|
|
37
|
+
generateRandomName,
|
|
38
|
+
getDefaultRemoteBranch,
|
|
39
|
+
getMessageContent,
|
|
34
40
|
PromptHistoryManager,
|
|
35
41
|
AuthService,
|
|
36
42
|
PluginCore,
|
|
@@ -41,6 +47,8 @@ import {
|
|
|
41
47
|
INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
|
|
42
48
|
METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
|
|
43
49
|
} from "./protocol.js";
|
|
50
|
+
import { execFileSync } from "node:child_process";
|
|
51
|
+
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
44
52
|
import { logger } from "../utils/logger.js";
|
|
45
53
|
|
|
46
54
|
export type NotificationEmitter = (
|
|
@@ -69,6 +77,8 @@ interface InitializeParams {
|
|
|
69
77
|
disallowedTools?: string[];
|
|
70
78
|
pluginDirs?: string[];
|
|
71
79
|
mcpServers?: Record<string, McpServerConfig>;
|
|
80
|
+
worktreeName?: string;
|
|
81
|
+
isNewWorktree?: boolean;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
interface UpdateConfigParams {
|
|
@@ -160,6 +170,8 @@ export class AgentBridge {
|
|
|
160
170
|
return this.clearMessages(sessionId);
|
|
161
171
|
case "rewindToMessage":
|
|
162
172
|
return this.rewindToMessage(p.messageId as string, sessionId);
|
|
173
|
+
case "listRewindCheckpoints":
|
|
174
|
+
return this.listRewindCheckpoints(sessionId);
|
|
163
175
|
case "deleteQueuedMessage":
|
|
164
176
|
return this.deleteQueuedMessage(p.index as number, sessionId);
|
|
165
177
|
case "updateQueuedMessage":
|
|
@@ -247,6 +259,19 @@ export class AgentBridge {
|
|
|
247
259
|
p.workdir as string | undefined,
|
|
248
260
|
sessionId,
|
|
249
261
|
);
|
|
262
|
+
case "getProjectSettings":
|
|
263
|
+
return this.getProjectSettings(
|
|
264
|
+
p.workdir as string | undefined,
|
|
265
|
+
sessionId,
|
|
266
|
+
);
|
|
267
|
+
case "setBuiltinPluginEnabled":
|
|
268
|
+
return this.setBuiltinPluginEnabled(
|
|
269
|
+
p.pluginId as string,
|
|
270
|
+
p.enabled as boolean,
|
|
271
|
+
p.scope as Scope | undefined,
|
|
272
|
+
p.workdir as string | undefined,
|
|
273
|
+
sessionId,
|
|
274
|
+
);
|
|
250
275
|
case "updatePlugin":
|
|
251
276
|
return this.updatePlugin(
|
|
252
277
|
p.pluginId as string,
|
|
@@ -278,6 +303,38 @@ export class AgentBridge {
|
|
|
278
303
|
p.workdir as string | undefined,
|
|
279
304
|
sessionId,
|
|
280
305
|
);
|
|
306
|
+
case "compact":
|
|
307
|
+
return this.compact(
|
|
308
|
+
p.customInstructions as string | undefined,
|
|
309
|
+
sessionId,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// ── Background tasks ──
|
|
313
|
+
case "getBackgroundTaskOutput":
|
|
314
|
+
return this.getBackgroundTaskOutput(p.taskId as string, sessionId);
|
|
315
|
+
case "stopBackgroundTask":
|
|
316
|
+
return this.stopBackgroundTask(p.taskId as string, sessionId);
|
|
317
|
+
|
|
318
|
+
case "getWorkflowRuns":
|
|
319
|
+
return this.getWorkflowRuns(sessionId);
|
|
320
|
+
case "stopWorkflowRun":
|
|
321
|
+
return this.stopWorkflowRun(p.runId as string, sessionId);
|
|
322
|
+
|
|
323
|
+
// ── Git / worktree (global — no session required) ──
|
|
324
|
+
case "listGitBranches":
|
|
325
|
+
return this.listGitBranches(p.workdir as string | undefined);
|
|
326
|
+
case "createWorktree":
|
|
327
|
+
return this.createWorktreeSession(
|
|
328
|
+
p as unknown as {
|
|
329
|
+
workdir: string;
|
|
330
|
+
baseBranch?: string;
|
|
331
|
+
name?: string;
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
case "removeWorktree":
|
|
335
|
+
return this.removeWorktreeSession(
|
|
336
|
+
p as unknown as { path: string; branch: string; repoRoot: string },
|
|
337
|
+
);
|
|
281
338
|
|
|
282
339
|
default:
|
|
283
340
|
throw new RpcError(
|
|
@@ -330,6 +387,8 @@ export class AgentBridge {
|
|
|
330
387
|
disallowedTools: params.disallowedTools,
|
|
331
388
|
plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
|
|
332
389
|
mcpServers: params.mcpServers,
|
|
390
|
+
worktreeName: params.worktreeName,
|
|
391
|
+
isNewWorktree: params.isNewWorktree,
|
|
333
392
|
canUseTool: (context: ToolPermissionContext) =>
|
|
334
393
|
this.canUseTool(context, ctx),
|
|
335
394
|
};
|
|
@@ -381,6 +440,106 @@ export class AgentBridge {
|
|
|
381
440
|
return { sessions };
|
|
382
441
|
}
|
|
383
442
|
|
|
443
|
+
// ── Git / worktree ────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
private listGitBranches(workdir?: string): {
|
|
446
|
+
branches: string[];
|
|
447
|
+
current: string | null;
|
|
448
|
+
} {
|
|
449
|
+
if (!workdir) {
|
|
450
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
451
|
+
}
|
|
452
|
+
const gitOpts = {
|
|
453
|
+
cwd: workdir,
|
|
454
|
+
encoding: "utf8" as const,
|
|
455
|
+
stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
|
|
456
|
+
};
|
|
457
|
+
let branchesRaw: string;
|
|
458
|
+
try {
|
|
459
|
+
branchesRaw = execFileSync(
|
|
460
|
+
"git",
|
|
461
|
+
["for-each-ref", "--format=%(refname:short)", "refs/heads"],
|
|
462
|
+
gitOpts,
|
|
463
|
+
).trim();
|
|
464
|
+
} catch {
|
|
465
|
+
throw new RpcError(
|
|
466
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
467
|
+
`Not a git repository (or git unavailable): ${workdir}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const branches = branchesRaw
|
|
471
|
+
? branchesRaw
|
|
472
|
+
.split("\n")
|
|
473
|
+
.map((b) => b.trim())
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
: [];
|
|
476
|
+
let current: string | null = null;
|
|
477
|
+
try {
|
|
478
|
+
const head = execFileSync(
|
|
479
|
+
"git",
|
|
480
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
481
|
+
gitOpts,
|
|
482
|
+
).trim();
|
|
483
|
+
// Detached HEAD prints "HEAD" — treat as no current branch.
|
|
484
|
+
current = head && head !== "HEAD" ? head : null;
|
|
485
|
+
} catch {
|
|
486
|
+
current = null;
|
|
487
|
+
}
|
|
488
|
+
return { branches, current };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async createWorktreeSession(params: {
|
|
492
|
+
workdir: string;
|
|
493
|
+
baseBranch?: string;
|
|
494
|
+
name?: string;
|
|
495
|
+
}): Promise<{
|
|
496
|
+
name: string;
|
|
497
|
+
path: string;
|
|
498
|
+
branch: string;
|
|
499
|
+
repoRoot: string;
|
|
500
|
+
baseBranch: string;
|
|
501
|
+
isNew: boolean;
|
|
502
|
+
}> {
|
|
503
|
+
if (!params.workdir) {
|
|
504
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
505
|
+
}
|
|
506
|
+
const name = params.name?.trim() || generateRandomName();
|
|
507
|
+
try {
|
|
508
|
+
const session = await createWorktree(name, params.workdir, {
|
|
509
|
+
baseBranch: params.baseBranch,
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
name: session.name,
|
|
513
|
+
path: session.path,
|
|
514
|
+
branch: session.branch,
|
|
515
|
+
repoRoot: session.repoRoot,
|
|
516
|
+
baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
|
|
517
|
+
isNew: session.isNew,
|
|
518
|
+
};
|
|
519
|
+
} catch (e) {
|
|
520
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private async removeWorktreeSession(params: {
|
|
525
|
+
path: string;
|
|
526
|
+
branch: string;
|
|
527
|
+
repoRoot: string;
|
|
528
|
+
}): Promise<{ ok: true }> {
|
|
529
|
+
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
530
|
+
// branches only log, never throw.
|
|
531
|
+
await removeWorktree({
|
|
532
|
+
name: "",
|
|
533
|
+
path: params.path,
|
|
534
|
+
branch: params.branch,
|
|
535
|
+
repoRoot: params.repoRoot,
|
|
536
|
+
hasUncommittedChanges: false,
|
|
537
|
+
hasNewCommits: false,
|
|
538
|
+
isNew: false,
|
|
539
|
+
});
|
|
540
|
+
return { ok: true };
|
|
541
|
+
}
|
|
542
|
+
|
|
384
543
|
private getSessionInfo(sessionId?: string): {
|
|
385
544
|
sessionId: string;
|
|
386
545
|
workingDirectory: string;
|
|
@@ -431,6 +590,9 @@ export class AgentBridge {
|
|
|
431
590
|
path: p,
|
|
432
591
|
})),
|
|
433
592
|
mcpServers: entry.storedConfig.mcpServers,
|
|
593
|
+
// Keep worktree context (permission safety) across recreation, but never
|
|
594
|
+
// re-fire WorktreeCreate — the hook ran at initial creation.
|
|
595
|
+
worktreeName: entry.storedConfig.worktreeName,
|
|
434
596
|
canUseTool: (context: ToolPermissionContext) =>
|
|
435
597
|
this.canUseTool(context, ctx),
|
|
436
598
|
};
|
|
@@ -516,6 +678,20 @@ export class AgentBridge {
|
|
|
516
678
|
return { inputContent: textBlock?.content || "" };
|
|
517
679
|
}
|
|
518
680
|
|
|
681
|
+
private async listRewindCheckpoints(sessionId?: string): Promise<{
|
|
682
|
+
checkpoints: Array<{ id: string; content: string }>;
|
|
683
|
+
}> {
|
|
684
|
+
const entry = this.requireSession(sessionId);
|
|
685
|
+
const { messages } = await entry.agent.getFullMessageThread();
|
|
686
|
+
const checkpoints = messages
|
|
687
|
+
.filter((m) => m.role === "user" && !m.isMeta && m.id)
|
|
688
|
+
.map((m) => ({
|
|
689
|
+
id: m.id as string,
|
|
690
|
+
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
691
|
+
}));
|
|
692
|
+
return { checkpoints };
|
|
693
|
+
}
|
|
694
|
+
|
|
519
695
|
private deleteQueuedMessage(index: number, sessionId?: string): null {
|
|
520
696
|
const entry = this.requireSession(sessionId);
|
|
521
697
|
entry.agent.removeQueuedMessage(index);
|
|
@@ -555,6 +731,57 @@ export class AgentBridge {
|
|
|
555
731
|
return entry.agent.getFullMessageThread();
|
|
556
732
|
}
|
|
557
733
|
|
|
734
|
+
private async compact(
|
|
735
|
+
customInstructions: string | undefined,
|
|
736
|
+
sessionId?: string,
|
|
737
|
+
): Promise<null> {
|
|
738
|
+
const entry = this.requireSession(sessionId);
|
|
739
|
+
await entry.agent.compact(customInstructions);
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ── Background tasks ──
|
|
744
|
+
|
|
745
|
+
private getBackgroundTaskOutput(
|
|
746
|
+
taskId: string,
|
|
747
|
+
sessionId?: string,
|
|
748
|
+
): { output: ReturnType<Agent["getBackgroundTaskOutput"]> } {
|
|
749
|
+
const entry = this.requireSession(sessionId);
|
|
750
|
+
return { output: entry.agent.getBackgroundTaskOutput(taskId) };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private stopBackgroundTask(
|
|
754
|
+
taskId: string,
|
|
755
|
+
sessionId?: string,
|
|
756
|
+
): { success: boolean } {
|
|
757
|
+
const entry = this.requireSession(sessionId);
|
|
758
|
+
const success = entry.agent.stopBackgroundTask(taskId);
|
|
759
|
+
return { success };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private async getWorkflowRuns(
|
|
763
|
+
sessionId?: string,
|
|
764
|
+
): Promise<{ runs: SerializableWorkflowRun[] }> {
|
|
765
|
+
const entry = this.requireSession(sessionId);
|
|
766
|
+
const runs = await entry.agent.getWorkflowRuns();
|
|
767
|
+
return {
|
|
768
|
+
runs: runs.map((r) => {
|
|
769
|
+
const { completionPromise, ...rest } = r;
|
|
770
|
+
void completionPromise;
|
|
771
|
+
return rest;
|
|
772
|
+
}),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private stopWorkflowRun(
|
|
777
|
+
runId: string,
|
|
778
|
+
sessionId?: string,
|
|
779
|
+
): { success: boolean } {
|
|
780
|
+
const entry = this.requireSession(sessionId);
|
|
781
|
+
entry.agent.stopWorkflowRun(runId);
|
|
782
|
+
return { success: true };
|
|
783
|
+
}
|
|
784
|
+
|
|
558
785
|
// ── Permissions ───────────────────────────────────────────────
|
|
559
786
|
|
|
560
787
|
private async setPermissionMode(
|
|
@@ -767,6 +994,35 @@ export class AgentBridge {
|
|
|
767
994
|
);
|
|
768
995
|
}
|
|
769
996
|
|
|
997
|
+
private async getProjectSettings(
|
|
998
|
+
workdir?: string,
|
|
999
|
+
sessionId?: string,
|
|
1000
|
+
): Promise<{ enabledPlugins: Record<string, boolean> }> {
|
|
1001
|
+
return {
|
|
1002
|
+
enabledPlugins: this.getPluginCore(
|
|
1003
|
+
workdir,
|
|
1004
|
+
sessionId,
|
|
1005
|
+
).getMergedEnabledPlugins(),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private async setBuiltinPluginEnabled(
|
|
1010
|
+
pluginId: string,
|
|
1011
|
+
enabled: boolean,
|
|
1012
|
+
scope: Scope | undefined,
|
|
1013
|
+
workdir?: string,
|
|
1014
|
+
sessionId?: string,
|
|
1015
|
+
): Promise<{ enabledPlugins: Record<string, boolean> }> {
|
|
1016
|
+
const core = this.getPluginCore(workdir, sessionId);
|
|
1017
|
+
const targetScope = scope ?? "project";
|
|
1018
|
+
if (enabled) {
|
|
1019
|
+
await core.enablePlugin(pluginId, targetScope);
|
|
1020
|
+
} else {
|
|
1021
|
+
await core.disablePlugin(pluginId, targetScope);
|
|
1022
|
+
}
|
|
1023
|
+
return { enabledPlugins: core.getMergedEnabledPlugins() };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
770
1026
|
private async updatePlugin(
|
|
771
1027
|
pluginId: string,
|
|
772
1028
|
workdir?: string,
|
|
@@ -867,6 +1123,25 @@ export class AgentBridge {
|
|
|
867
1123
|
onTasksChange: (tasks: Task[]) => {
|
|
868
1124
|
this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
|
|
869
1125
|
},
|
|
1126
|
+
onBackgroundTasksChange: (tasks: BackgroundTask[]) => {
|
|
1127
|
+
const summaries: BackgroundTaskSummary[] = tasks.map((t) => ({
|
|
1128
|
+
id: t.id,
|
|
1129
|
+
type: t.type,
|
|
1130
|
+
status: t.status,
|
|
1131
|
+
startTime: t.startTime,
|
|
1132
|
+
endTime: t.endTime,
|
|
1133
|
+
command: t.command,
|
|
1134
|
+
description: t.description,
|
|
1135
|
+
exitCode: t.exitCode,
|
|
1136
|
+
runtime: t.runtime,
|
|
1137
|
+
outputPath: t.outputPath,
|
|
1138
|
+
}));
|
|
1139
|
+
this.emit(
|
|
1140
|
+
"backgroundTasksChange",
|
|
1141
|
+
{ tasks: summaries },
|
|
1142
|
+
ctx.registeredSessionId,
|
|
1143
|
+
);
|
|
1144
|
+
},
|
|
870
1145
|
onSessionIdChange: (newSessionId: string) => {
|
|
871
1146
|
const oldSessionId = ctx.registeredSessionId;
|
|
872
1147
|
// Emit with the OLD sessionId so the client's router can deliver it
|
|
@@ -922,6 +1197,13 @@ export class AgentBridge {
|
|
|
922
1197
|
onCompactBlockAdded: (content: string) => {
|
|
923
1198
|
this.emit("compactBlockAdded", { content }, ctx.registeredSessionId);
|
|
924
1199
|
},
|
|
1200
|
+
onCompactionStateChange: (isCompacting: boolean) => {
|
|
1201
|
+
this.emit(
|
|
1202
|
+
"compactionStateChange",
|
|
1203
|
+
{ isCompacting },
|
|
1204
|
+
ctx.registeredSessionId,
|
|
1205
|
+
);
|
|
1206
|
+
},
|
|
925
1207
|
};
|
|
926
1208
|
}
|
|
927
1209
|
|
package/src/stdio/protocol.ts
CHANGED
|
@@ -57,6 +57,7 @@ export type RequestMethod =
|
|
|
57
57
|
| "abortMessage"
|
|
58
58
|
| "clearMessages"
|
|
59
59
|
| "rewindToMessage"
|
|
60
|
+
| "listRewindCheckpoints"
|
|
60
61
|
| "deleteQueuedMessage"
|
|
61
62
|
| "updateQueuedMessage"
|
|
62
63
|
| "deleteQueuedMessageById"
|
|
@@ -86,7 +87,16 @@ export type RequestMethod =
|
|
|
86
87
|
| "listMarketplaces"
|
|
87
88
|
| "addMarketplace"
|
|
88
89
|
| "removeMarketplace"
|
|
89
|
-
| "updateMarketplace"
|
|
90
|
+
| "updateMarketplace"
|
|
91
|
+
| "compact"
|
|
92
|
+
| "getBackgroundTaskOutput"
|
|
93
|
+
| "stopBackgroundTask"
|
|
94
|
+
| "getWorkflowRuns"
|
|
95
|
+
| "stopWorkflowRun"
|
|
96
|
+
// Git / worktree (global — no session required)
|
|
97
|
+
| "listGitBranches"
|
|
98
|
+
| "createWorktree"
|
|
99
|
+
| "removeWorktree";
|
|
90
100
|
|
|
91
101
|
// ── Client → Server notification methods ────────────────────────
|
|
92
102
|
|
|
@@ -116,7 +126,9 @@ export type ServerNotificationMethod =
|
|
|
116
126
|
| "notificationMessageAdded"
|
|
117
127
|
| "permissionRequest"
|
|
118
128
|
| "authUrl"
|
|
119
|
-
| "compactBlockAdded"
|
|
129
|
+
| "compactBlockAdded"
|
|
130
|
+
| "compactionStateChange"
|
|
131
|
+
| "backgroundTasksChange";
|
|
120
132
|
|
|
121
133
|
// ── Helper: is this a request (has id)? ─────────────────────────
|
|
122
134
|
|
package/src/utils/worktree.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
|
|
5
6
|
|
|
7
|
+
// Never use execFileSync here: the shared `wave --stdio` process handles all
|
|
8
|
+
// desktop sessions, so a synchronous git call (especially a multi-second
|
|
9
|
+
// recursive worktree delete or a network fetch) freezes every session.
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
6
12
|
export interface WorktreeSession {
|
|
7
13
|
name: string;
|
|
8
14
|
path: string;
|
|
@@ -17,13 +23,22 @@ export interface WorktreeSession {
|
|
|
17
23
|
* Create a new git worktree
|
|
18
24
|
* @param name Worktree name
|
|
19
25
|
* @param cwd Current working directory
|
|
26
|
+
* @param options Optional creation options
|
|
27
|
+
* @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
|
|
28
|
+
* @param options.baseBranch Explicit base branch (overrides baseRef)
|
|
20
29
|
* @returns Worktree session details
|
|
21
30
|
*/
|
|
22
|
-
export function createWorktree(
|
|
31
|
+
export async function createWorktree(
|
|
32
|
+
name: string,
|
|
33
|
+
cwd: string,
|
|
34
|
+
options?: { baseRef?: "fresh" | "head"; baseBranch?: string },
|
|
35
|
+
): Promise<WorktreeSession> {
|
|
23
36
|
const repoRoot = getGitMainRepoRoot(cwd);
|
|
24
37
|
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|
|
25
38
|
const branchName = `worktree-${name}`;
|
|
26
|
-
const
|
|
39
|
+
const useHead = options?.baseRef === "head";
|
|
40
|
+
const resolvedBaseBranch =
|
|
41
|
+
options?.baseBranch ?? (useHead ? "HEAD" : getDefaultRemoteBranch(cwd));
|
|
27
42
|
|
|
28
43
|
// Ensure parent directory exists
|
|
29
44
|
const parentDir = path.dirname(worktreePath);
|
|
@@ -47,12 +62,11 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
|
|
|
47
62
|
|
|
48
63
|
try {
|
|
49
64
|
// Create worktree and branch
|
|
50
|
-
|
|
65
|
+
await execFileAsync(
|
|
51
66
|
"git",
|
|
52
|
-
["worktree", "add", "-b", branchName, worktreePath,
|
|
67
|
+
["worktree", "add", "-b", branchName, worktreePath, resolvedBaseBranch],
|
|
53
68
|
{
|
|
54
69
|
cwd: repoRoot,
|
|
55
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
56
70
|
},
|
|
57
71
|
);
|
|
58
72
|
|
|
@@ -66,14 +80,18 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
|
|
|
66
80
|
isNew: true,
|
|
67
81
|
};
|
|
68
82
|
} catch (error: unknown) {
|
|
69
|
-
const stderr =
|
|
83
|
+
const stderr =
|
|
84
|
+
(error as { stderr?: Buffer | string }).stderr?.toString() || "";
|
|
70
85
|
if (stderr.includes("already exists")) {
|
|
71
86
|
// If branch already exists, try to add worktree without -b
|
|
72
87
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
await execFileAsync(
|
|
89
|
+
"git",
|
|
90
|
+
["worktree", "add", worktreePath, branchName],
|
|
91
|
+
{
|
|
92
|
+
cwd: repoRoot,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
77
95
|
return {
|
|
78
96
|
name,
|
|
79
97
|
path: worktreePath,
|
|
@@ -90,22 +108,28 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
|
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
if (
|
|
93
|
-
|
|
94
|
-
stderr.includes("
|
|
111
|
+
!useHead &&
|
|
112
|
+
(stderr.includes("not a valid object name") ||
|
|
113
|
+
stderr.includes("unknown revision"))
|
|
95
114
|
) {
|
|
96
115
|
// Base branch not fetched yet — try fetching then retrying
|
|
97
|
-
const branchNameOnly =
|
|
116
|
+
const branchNameOnly = resolvedBaseBranch.split("/").pop()!;
|
|
98
117
|
try {
|
|
99
|
-
|
|
118
|
+
await execFileAsync("git", ["fetch", "origin", branchNameOnly], {
|
|
100
119
|
cwd: repoRoot,
|
|
101
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
102
120
|
});
|
|
103
|
-
|
|
121
|
+
await execFileAsync(
|
|
104
122
|
"git",
|
|
105
|
-
[
|
|
123
|
+
[
|
|
124
|
+
"worktree",
|
|
125
|
+
"add",
|
|
126
|
+
"-b",
|
|
127
|
+
branchName,
|
|
128
|
+
worktreePath,
|
|
129
|
+
resolvedBaseBranch,
|
|
130
|
+
],
|
|
106
131
|
{
|
|
107
132
|
cwd: repoRoot,
|
|
108
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
109
133
|
},
|
|
110
134
|
);
|
|
111
135
|
return {
|
|
@@ -120,12 +144,11 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
|
|
|
120
144
|
} catch {
|
|
121
145
|
// Fetch or retry failed — fall back to HEAD
|
|
122
146
|
try {
|
|
123
|
-
|
|
147
|
+
await execFileAsync(
|
|
124
148
|
"git",
|
|
125
149
|
["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
|
|
126
150
|
{
|
|
127
151
|
cwd: repoRoot,
|
|
128
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
129
152
|
},
|
|
130
153
|
);
|
|
131
154
|
return {
|
|
@@ -154,37 +177,39 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
|
|
|
154
177
|
* Remove a git worktree and its associated branch
|
|
155
178
|
* @param session Worktree session details
|
|
156
179
|
*/
|
|
157
|
-
export function removeWorktree(session: WorktreeSession): void {
|
|
180
|
+
export async function removeWorktree(session: WorktreeSession): Promise<void> {
|
|
158
181
|
const repoRoot = session.repoRoot;
|
|
159
182
|
|
|
160
183
|
try {
|
|
161
184
|
// Get current branch in worktree before removing it
|
|
162
185
|
let currentBranch: string | undefined;
|
|
163
186
|
try {
|
|
164
|
-
|
|
187
|
+
const { stdout } = await execFileAsync(
|
|
165
188
|
"git",
|
|
166
189
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
167
190
|
{
|
|
168
191
|
cwd: session.path,
|
|
169
192
|
encoding: "utf8",
|
|
170
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
171
193
|
},
|
|
172
|
-
)
|
|
194
|
+
);
|
|
195
|
+
currentBranch = stdout.trim();
|
|
173
196
|
} catch {
|
|
174
197
|
// Ignore errors getting current branch
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
// Remove worktree
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
201
|
+
await execFileAsync(
|
|
202
|
+
"git",
|
|
203
|
+
["worktree", "remove", "--force", session.path],
|
|
204
|
+
{
|
|
205
|
+
cwd: repoRoot,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
182
208
|
|
|
183
209
|
// Delete original branch
|
|
184
210
|
try {
|
|
185
|
-
|
|
211
|
+
await execFileAsync("git", ["branch", "-D", session.branch], {
|
|
186
212
|
cwd: repoRoot,
|
|
187
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
188
213
|
});
|
|
189
214
|
} catch {
|
|
190
215
|
// Ignore errors deleting original branch
|
|
@@ -205,9 +230,8 @@ export function removeWorktree(session: WorktreeSession): void {
|
|
|
205
230
|
currentBranch !== "master"
|
|
206
231
|
) {
|
|
207
232
|
try {
|
|
208
|
-
|
|
233
|
+
await execFileAsync("git", ["branch", "-D", currentBranch], {
|
|
209
234
|
cwd: repoRoot,
|
|
210
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
211
235
|
});
|
|
212
236
|
} catch {
|
|
213
237
|
// Ignore errors deleting current branch
|