wave-agent-sdk 1.0.5 → 1.0.6

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.
Files changed (193) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +13 -6
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/telemetry/instrumentation.d.ts +1 -1
  10. package/dist/telemetry/instrumentation.js +57 -28
  11. package/dist/tools/bashTool.js +16 -6
  12. package/dist/tools/grepTool.js +11 -3
  13. package/dist/types/index.d.ts +3 -0
  14. package/dist/types/messaging.d.ts +2 -0
  15. package/dist/utils/containerSetup.js +0 -3
  16. package/dist/utils/messageOperations.d.ts +2 -1
  17. package/dist/utils/messageOperations.js +2 -1
  18. package/dist/utils/notificationXml.js +5 -0
  19. package/dist/utils/ripgrep.d.ts +4 -4
  20. package/dist/utils/ripgrep.js +4 -27
  21. package/package.json +20 -8
  22. package/bin/rg +0 -79
  23. package/dist/managers/forkedAgentManager.d.ts +0 -50
  24. package/dist/managers/forkedAgentManager.js +0 -130
  25. package/scripts/install_ripgrep.js +0 -111
  26. package/scripts/postinstall.js +0 -38
  27. package/src/agent.ts +0 -1271
  28. package/src/constants/subagents.ts +0 -4
  29. package/src/constants/toolLimits.ts +0 -15
  30. package/src/constants/tools.ts +0 -24
  31. package/src/core/plugin.ts +0 -237
  32. package/src/core/session.ts +0 -9
  33. package/src/index.ts +0 -38
  34. package/src/managers/MemoryRuleManager.ts +0 -198
  35. package/src/managers/aiManager.ts +0 -2429
  36. package/src/managers/backgroundTaskManager.ts +0 -480
  37. package/src/managers/bangManager.ts +0 -119
  38. package/src/managers/cronManager.ts +0 -401
  39. package/src/managers/foregroundTaskManager.ts +0 -30
  40. package/src/managers/forkedAgentManager.ts +0 -216
  41. package/src/managers/hookManager.ts +0 -1089
  42. package/src/managers/liveConfigManager.ts +0 -445
  43. package/src/managers/lspManager.ts +0 -470
  44. package/src/managers/mcpManager.ts +0 -1010
  45. package/src/managers/messageManager.ts +0 -1184
  46. package/src/managers/messageQueue.ts +0 -157
  47. package/src/managers/permissionManager.ts +0 -1140
  48. package/src/managers/planManager.ts +0 -112
  49. package/src/managers/pluginManager.ts +0 -330
  50. package/src/managers/pluginScopeManager.ts +0 -124
  51. package/src/managers/reversionManager.ts +0 -178
  52. package/src/managers/skillManager.ts +0 -578
  53. package/src/managers/slashCommandManager.ts +0 -564
  54. package/src/managers/subagentManager.ts +0 -915
  55. package/src/managers/toolManager.ts +0 -489
  56. package/src/managers/workflowManager.ts +0 -493
  57. package/src/prompts/autoMemory.ts +0 -33
  58. package/src/prompts/autoMemoryExtraction.ts +0 -146
  59. package/src/prompts/index.ts +0 -506
  60. package/src/prompts/planModeReminders.ts +0 -126
  61. package/src/services/GitService.ts +0 -131
  62. package/src/services/MarketplaceService.ts +0 -1052
  63. package/src/services/MemoryRuleService.ts +0 -71
  64. package/src/services/aiService.ts +0 -919
  65. package/src/services/authService.ts +0 -565
  66. package/src/services/autoMemoryService.ts +0 -280
  67. package/src/services/configurationService.ts +0 -1520
  68. package/src/services/fileWatcher.ts +0 -319
  69. package/src/services/hook.ts +0 -348
  70. package/src/services/initializationService.ts +0 -357
  71. package/src/services/interactionService.ts +0 -229
  72. package/src/services/jsonlHandler.ts +0 -295
  73. package/src/services/memory.ts +0 -234
  74. package/src/services/pluginLoader.ts +0 -252
  75. package/src/services/remoteSettingsService.ts +0 -366
  76. package/src/services/reversionService.ts +0 -122
  77. package/src/services/session.ts +0 -917
  78. package/src/services/taskManager.ts +0 -321
  79. package/src/telemetry/events.ts +0 -65
  80. package/src/telemetry/instrumentation.ts +0 -499
  81. package/src/telemetry/sessionTracing.ts +0 -348
  82. package/src/tools/agentTool.ts +0 -322
  83. package/src/tools/askUserQuestion.ts +0 -160
  84. package/src/tools/bashTool.ts +0 -621
  85. package/src/tools/buildTool.ts +0 -61
  86. package/src/tools/cronCreateTool.ts +0 -161
  87. package/src/tools/cronDeleteTool.ts +0 -51
  88. package/src/tools/cronListTool.ts +0 -47
  89. package/src/tools/editTool.ts +0 -313
  90. package/src/tools/enterPlanMode.ts +0 -124
  91. package/src/tools/enterWorktreeTool.ts +0 -190
  92. package/src/tools/exitPlanMode.ts +0 -139
  93. package/src/tools/exitWorktreeTool.ts +0 -236
  94. package/src/tools/globTool.ts +0 -167
  95. package/src/tools/grepTool.ts +0 -399
  96. package/src/tools/lspTool.ts +0 -883
  97. package/src/tools/readTool.ts +0 -426
  98. package/src/tools/skillTool.ts +0 -248
  99. package/src/tools/taskManagementTools.ts +0 -574
  100. package/src/tools/taskStopTool.ts +0 -78
  101. package/src/tools/types.ts +0 -137
  102. package/src/tools/webFetchTool.ts +0 -373
  103. package/src/tools/workflowTool.ts +0 -205
  104. package/src/tools/writeTool.ts +0 -267
  105. package/src/types/agent.ts +0 -128
  106. package/src/types/auth.ts +0 -19
  107. package/src/types/commands.ts +0 -30
  108. package/src/types/config.ts +0 -43
  109. package/src/types/configuration.ts +0 -168
  110. package/src/types/core.ts +0 -105
  111. package/src/types/cron.ts +0 -13
  112. package/src/types/environment.ts +0 -106
  113. package/src/types/fileSearch.ts +0 -4
  114. package/src/types/history.ts +0 -7
  115. package/src/types/hooks.ts +0 -256
  116. package/src/types/index.ts +0 -41
  117. package/src/types/lsp.ts +0 -98
  118. package/src/types/marketplace.ts +0 -71
  119. package/src/types/mcp.ts +0 -43
  120. package/src/types/memoryRule.ts +0 -31
  121. package/src/types/messaging.ts +0 -114
  122. package/src/types/permissions.ts +0 -80
  123. package/src/types/plugins.ts +0 -39
  124. package/src/types/processes.ts +0 -99
  125. package/src/types/reversion.ts +0 -29
  126. package/src/types/session.ts +0 -12
  127. package/src/types/skills.ts +0 -108
  128. package/src/types/tasks.ts +0 -13
  129. package/src/types/telemetry.ts +0 -98
  130. package/src/types/tools.ts +0 -44
  131. package/src/types/workflow.ts +0 -6
  132. package/src/utils/abortUtils.ts +0 -118
  133. package/src/utils/atomicWrite.ts +0 -61
  134. package/src/utils/bashParser.ts +0 -899
  135. package/src/utils/cacheControlUtils.ts +0 -426
  136. package/src/utils/commandArgumentParser.ts +0 -161
  137. package/src/utils/commandPathResolver.ts +0 -88
  138. package/src/utils/configPaths.ts +0 -220
  139. package/src/utils/configValidator.ts +0 -100
  140. package/src/utils/constants.ts +0 -39
  141. package/src/utils/container.ts +0 -92
  142. package/src/utils/containerSetup.ts +0 -371
  143. package/src/utils/convertMessagesForAPI.ts +0 -326
  144. package/src/utils/cronTasks.ts +0 -128
  145. package/src/utils/cronTasksLock.ts +0 -182
  146. package/src/utils/cronToHuman.ts +0 -99
  147. package/src/utils/customCommands.ts +0 -95
  148. package/src/utils/editUtils.ts +0 -20
  149. package/src/utils/fileFormat.ts +0 -40
  150. package/src/utils/fileSearch.ts +0 -151
  151. package/src/utils/fileUtils.ts +0 -223
  152. package/src/utils/gitUtils.ts +0 -315
  153. package/src/utils/globalLogger.ts +0 -128
  154. package/src/utils/groupMessagesByApiRound.ts +0 -121
  155. package/src/utils/hookMatcher.ts +0 -176
  156. package/src/utils/markdownParser.ts +0 -317
  157. package/src/utils/mcpUtils.ts +0 -138
  158. package/src/utils/messageOperations.ts +0 -659
  159. package/src/utils/modelCapabilities.ts +0 -30
  160. package/src/utils/nameGenerator.ts +0 -99
  161. package/src/utils/notificationXml.ts +0 -57
  162. package/src/utils/openaiClient.ts +0 -251
  163. package/src/utils/parseCronExpression.ts +0 -78
  164. package/src/utils/path.ts +0 -72
  165. package/src/utils/pathEncoder.ts +0 -400
  166. package/src/utils/pathSafety.ts +0 -40
  167. package/src/utils/promptHistory.ts +0 -168
  168. package/src/utils/ripgrep.ts +0 -35
  169. package/src/utils/shellResolver.ts +0 -190
  170. package/src/utils/skillParser.ts +0 -246
  171. package/src/utils/stringUtils.ts +0 -156
  172. package/src/utils/subagentParser.ts +0 -279
  173. package/src/utils/taskReminder.ts +0 -96
  174. package/src/utils/tokenCalculation.ts +0 -43
  175. package/src/utils/tokenEstimate.ts +0 -34
  176. package/src/utils/toolResultStorage.ts +0 -117
  177. package/src/utils/worktreeSession.ts +0 -26
  178. package/src/utils/worktreeUtils.ts +0 -786
  179. package/src/workflow/budgetTracker.ts +0 -34
  180. package/src/workflow/concurrencyLimiter.ts +0 -47
  181. package/src/workflow/journal.ts +0 -95
  182. package/src/workflow/progressReporter.ts +0 -141
  183. package/src/workflow/runState.ts +0 -65
  184. package/src/workflow/scriptRuntime.ts +0 -274
  185. package/src/workflow/structuredOutput.ts +0 -123
  186. package/src/workflow/types.ts +0 -101
  187. package/src/workflow/workflowApis.ts +0 -410
  188. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  189. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  190. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  191. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  192. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  193. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,786 +0,0 @@
1
- /**
2
- * Git worktree creation and removal utilities for the SDK.
3
- * Used by EnterWorktree and ExitWorktree tools.
4
- */
5
-
6
- import { execFile, execFileSync } from "node:child_process";
7
- import { promisify } from "node:util";
8
- import * as path from "node:path";
9
- import * as fs from "node:fs";
10
- import {
11
- getGitMainRepoRoot,
12
- getDefaultRemoteBranch,
13
- ensureWaveRuntimeFilesExcluded,
14
- } from "./gitUtils.js";
15
- import { logger } from "./globalLogger.js";
16
-
17
- // Post-creation setup runs inside the shared `wave --stdio` process too
18
- // (desktop sessions), so use the async execFile: a synchronous git call
19
- // (e.g. `git ls-files` over a large working tree) would freeze every session.
20
- const execFileAsync = promisify(execFile);
21
-
22
- export interface WorktreeInfo {
23
- name: string;
24
- path: string;
25
- branch: string;
26
- repoRoot: string;
27
- isNew: boolean;
28
- /** HEAD commit of the original branch at creation time, for dirty-check on exit */
29
- originalHeadCommit?: string;
30
- }
31
-
32
- /**
33
- * Validate a worktree name to prevent path traversal and invalid characters.
34
- */
35
- export function validateWorktreeName(name: string): void {
36
- const MAX_LENGTH = 64;
37
- if (name.length > MAX_LENGTH) {
38
- throw new Error(
39
- `Invalid worktree name: must be ${MAX_LENGTH} characters or fewer (got ${name.length})`,
40
- );
41
- }
42
- for (const segment of name.split("/")) {
43
- if (segment === "." || segment === "..") {
44
- throw new Error(
45
- `Invalid worktree name "${name}": must not contain "." or ".." path segments`,
46
- );
47
- }
48
- if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
49
- throw new Error(
50
- `Invalid worktree name "${name}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`,
51
- );
52
- }
53
- }
54
- }
55
-
56
- /**
57
- * Generate a random worktree name.
58
- */
59
- export function generateWorktreeName(): string {
60
- const adjectives = [
61
- "swift",
62
- "calm",
63
- "bold",
64
- "keen",
65
- "bright",
66
- "cool",
67
- "deep",
68
- "fair",
69
- "gentle",
70
- "grand",
71
- ];
72
- const nouns = [
73
- "fox",
74
- "owl",
75
- "hawk",
76
- "wolf",
77
- "bear",
78
- "lynx",
79
- "pike",
80
- "kite",
81
- "dove",
82
- "stag",
83
- ];
84
- const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
85
- const noun = nouns[Math.floor(Math.random() * nouns.length)];
86
- const num = Math.floor(Math.random() * 900) + 100;
87
- return `${adj}-${noun}-${num}`;
88
- }
89
-
90
- /**
91
- * Get the current HEAD commit SHA.
92
- */
93
- export function getHeadCommit(cwd: string): string {
94
- return execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], {
95
- encoding: "utf8",
96
- stdio: ["ignore", "pipe", "ignore"],
97
- }).trim();
98
- }
99
-
100
- /**
101
- * Create a git worktree for use during a session.
102
- * @param name Worktree name
103
- * @param cwd Current working directory (will be resolved to main repo root)
104
- * @param options Optional creation options
105
- * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
106
- */
107
- export function createWorktree(
108
- name: string,
109
- cwd: string,
110
- options?: { baseRef?: "fresh" | "head" },
111
- ): WorktreeInfo {
112
- const repoRoot = getGitMainRepoRoot(cwd);
113
- if (!repoRoot) {
114
- throw new Error(
115
- "Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.",
116
- );
117
- }
118
-
119
- // Capture HEAD commit before creating worktree (for dirty-check on exit)
120
- const originalHeadCommit = getHeadCommit(cwd);
121
-
122
- const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
123
- const branchName = `worktree-${name}`;
124
- const useHead = options?.baseRef === "head";
125
- const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
126
-
127
- // Ensure Wave runtime files are git-excluded in this repo
128
- ensureWaveRuntimeFilesExcluded(cwd);
129
-
130
- // Ensure parent directory exists
131
- const parentDir = path.dirname(worktreePath);
132
- if (!fs.existsSync(parentDir)) {
133
- fs.mkdirSync(parentDir, { recursive: true });
134
- }
135
-
136
- // Check if worktree already exists
137
- if (fs.existsSync(worktreePath)) {
138
- return {
139
- name,
140
- path: worktreePath,
141
- branch: branchName,
142
- repoRoot,
143
- isNew: false,
144
- originalHeadCommit,
145
- };
146
- }
147
-
148
- try {
149
- // Create worktree and branch
150
- execFileSync(
151
- "git",
152
- ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
153
- {
154
- cwd: repoRoot,
155
- stdio: ["ignore", "pipe", "pipe"],
156
- env: {
157
- ...process.env,
158
- GIT_TERMINAL_PROMPT: "0",
159
- GIT_ASKPASS: "",
160
- },
161
- },
162
- );
163
-
164
- return {
165
- name,
166
- path: worktreePath,
167
- branch: branchName,
168
- repoRoot,
169
- isNew: true,
170
- originalHeadCommit,
171
- };
172
- } catch (error: unknown) {
173
- const stderr = (error as { stderr?: Buffer }).stderr?.toString() || "";
174
- if (stderr.includes("already exists")) {
175
- // Branch exists but worktree doesn't — attach to existing branch
176
- try {
177
- execFileSync("git", ["worktree", "add", worktreePath, branchName], {
178
- cwd: repoRoot,
179
- stdio: ["ignore", "pipe", "pipe"],
180
- env: {
181
- ...process.env,
182
- GIT_TERMINAL_PROMPT: "0",
183
- GIT_ASKPASS: "",
184
- },
185
- });
186
- return {
187
- name,
188
- path: worktreePath,
189
- branch: branchName,
190
- repoRoot,
191
- isNew: true,
192
- originalHeadCommit,
193
- };
194
- } catch (innerError: unknown) {
195
- throw new Error(
196
- `Failed to add worktree: ${(innerError as Error).message}`,
197
- );
198
- }
199
- }
200
- if (
201
- !useHead &&
202
- (stderr.includes("not a valid object name") ||
203
- stderr.includes("unknown revision"))
204
- ) {
205
- // Base branch not fetched yet — try fetching then retrying
206
- const branchNameOnly = baseBranch.split("/").pop()!;
207
- try {
208
- execFileSync("git", ["fetch", "origin", branchNameOnly], {
209
- cwd: repoRoot,
210
- stdio: ["ignore", "pipe", "pipe"],
211
- env: {
212
- ...process.env,
213
- GIT_TERMINAL_PROMPT: "0",
214
- GIT_ASKPASS: "",
215
- },
216
- });
217
- execFileSync(
218
- "git",
219
- ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
220
- {
221
- cwd: repoRoot,
222
- stdio: ["ignore", "pipe", "pipe"],
223
- env: {
224
- ...process.env,
225
- GIT_TERMINAL_PROMPT: "0",
226
- GIT_ASKPASS: "",
227
- },
228
- },
229
- );
230
- return {
231
- name,
232
- path: worktreePath,
233
- branch: branchName,
234
- repoRoot,
235
- isNew: true,
236
- originalHeadCommit,
237
- };
238
- } catch {
239
- // Fetch or retry failed — fall back to HEAD
240
- try {
241
- execFileSync(
242
- "git",
243
- ["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
244
- {
245
- cwd: repoRoot,
246
- stdio: ["ignore", "pipe", "pipe"],
247
- env: {
248
- ...process.env,
249
- GIT_TERMINAL_PROMPT: "0",
250
- GIT_ASKPASS: "",
251
- },
252
- },
253
- );
254
- return {
255
- name,
256
- path: worktreePath,
257
- branch: branchName,
258
- repoRoot,
259
- isNew: true,
260
- originalHeadCommit,
261
- };
262
- } catch {
263
- throw new Error(
264
- `Failed to create worktree: ${(error as Error).message}\n${stderr}`,
265
- );
266
- }
267
- }
268
- }
269
- throw new Error(
270
- `Failed to create worktree: ${(error as Error).message}\n${stderr}`,
271
- );
272
- }
273
- }
274
-
275
- // --- .worktreeinclude matching ----------------------------------------------
276
-
277
- interface WorktreeIncludePattern {
278
- /** Normalized pattern text (no leading "/", no trailing "/", no "!") */
279
- raw: string;
280
- negated: boolean;
281
- /** Pattern ends with "/" — matches directories only */
282
- dirOnly: boolean;
283
- /** Pattern had a leading "/" — anchored to the repo root */
284
- anchored: boolean;
285
- /** No "/" and not anchored — matches the basename at any level */
286
- anyLevel: boolean;
287
- regex: RegExp;
288
- }
289
-
290
- /** Translate a single gitignore glob into a RegExp (gitignore semantics). */
291
- function globToRegExp(glob: string): RegExp {
292
- let source = "^";
293
- for (let i = 0; i < glob.length; i++) {
294
- const ch = glob[i];
295
- if (ch === "*") {
296
- if (glob[i + 1] === "*") {
297
- if (glob[i + 2] === "/") {
298
- // "**/" matches zero or more leading directories
299
- source += "(?:.*/)?";
300
- i += 2;
301
- } else {
302
- source += ".*";
303
- i += 1;
304
- }
305
- } else {
306
- source += "[^/]*";
307
- }
308
- } else if (ch === "?") {
309
- source += "[^/]";
310
- } else if (ch === "[") {
311
- const end = glob.indexOf("]", i + 1);
312
- if (end === -1) {
313
- source += "\\[";
314
- } else {
315
- let charClass = glob.slice(i, end + 1);
316
- if (charClass.startsWith("[!")) {
317
- // gitignore uses "[!...]" for a negated character class
318
- charClass = "[^" + charClass.slice(2);
319
- }
320
- source += charClass;
321
- i = end;
322
- }
323
- } else {
324
- source += ch.replace(/[.+^${}()|\\]/g, "\\$&");
325
- }
326
- }
327
- source += "$";
328
- return new RegExp(source);
329
- }
330
-
331
- /**
332
- * Parse .worktreeinclude content into patterns. Blank lines and "#" comments
333
- * are skipped, matching gitignore conventions.
334
- */
335
- function parseWorktreeIncludePatterns(
336
- content: string,
337
- ): WorktreeIncludePattern[] {
338
- const patterns: WorktreeIncludePattern[] = [];
339
- for (const rawLine of content.split(/\r?\n/)) {
340
- const line = rawLine.trim();
341
- if (!line || line.startsWith("#")) continue;
342
- let raw = line;
343
- const negated = raw.startsWith("!");
344
- if (negated) raw = raw.slice(1);
345
- const dirOnly = raw.endsWith("/");
346
- if (dirOnly) raw = raw.slice(0, -1);
347
- const anchored = raw.startsWith("/");
348
- if (anchored) raw = raw.slice(1);
349
- if (!raw) continue;
350
- patterns.push({
351
- raw,
352
- negated,
353
- dirOnly,
354
- anchored,
355
- anyLevel: !anchored && !raw.includes("/"),
356
- regex: globToRegExp(raw),
357
- });
358
- }
359
- return patterns;
360
- }
361
-
362
- /** Test a single pattern against one candidate path. */
363
- function testPatternAgainst(
364
- pattern: WorktreeIncludePattern,
365
- candidate: string,
366
- ): boolean {
367
- if (pattern.anyLevel) {
368
- // Match the full path or any "/"-suffix (basename at any level)
369
- if (pattern.regex.test(candidate)) return true;
370
- for (let i = 0; i < candidate.length; i++) {
371
- if (candidate[i] === "/" && pattern.regex.test(candidate.slice(i + 1))) {
372
- return true;
373
- }
374
- }
375
- return false;
376
- }
377
- return pattern.regex.test(candidate);
378
- }
379
-
380
- /**
381
- * Test a pattern against a path and every ancestor prefix. Excluding a
382
- * directory excludes everything beneath it (gitignore semantics), so ancestor
383
- * prefixes are always treated as directories.
384
- */
385
- function patternMatchesPath(
386
- pattern: WorktreeIncludePattern,
387
- relPath: string,
388
- isDir: boolean,
389
- ): boolean {
390
- let candidate = relPath;
391
- for (;;) {
392
- const candidateIsDir = candidate === relPath ? isDir : true;
393
- if (
394
- !(pattern.dirOnly && !candidateIsDir) &&
395
- testPatternAgainst(pattern, candidate)
396
- ) {
397
- return true;
398
- }
399
- const idx = candidate.lastIndexOf("/");
400
- if (idx === -1) break;
401
- candidate = candidate.slice(0, idx);
402
- }
403
- return false;
404
- }
405
-
406
- /** Last-match-wins resolution with "!" negation. */
407
- function worktreeIncludeMatches(
408
- patterns: WorktreeIncludePattern[],
409
- relPath: string,
410
- isDir: boolean,
411
- ): boolean {
412
- let matched = false;
413
- for (const pattern of patterns) {
414
- if (patternMatchesPath(pattern, relPath, isDir)) {
415
- matched = !pattern.negated;
416
- }
417
- }
418
- return matched;
419
- }
420
-
421
- /**
422
- * A positive pattern targets something inside a collapsed directory: either it
423
- * literally starts with the directory path, or the directory path starts with
424
- * the pattern's literal (pre-glob) prefix.
425
- */
426
- function needsExpansion(
427
- patterns: WorktreeIncludePattern[],
428
- dir: string,
429
- ): boolean {
430
- return patterns.some((p) => {
431
- if (p.negated) return false;
432
- if (p.raw.startsWith(dir + "/")) return true;
433
- const globIdx = p.raw.search(/[*?[]/);
434
- if (globIdx > 0 && dir.startsWith(p.raw.slice(0, globIdx))) return true;
435
- return false;
436
- });
437
- }
438
-
439
- // --- Post-creation setup -----------------------------------------------------
440
-
441
- /**
442
- * Copy <repoRoot>/.wave/settings.local.json into the worktree so local
443
- * configuration (permissions, env, ...) carries over. Missing files are
444
- * skipped silently; other failures only log a warning.
445
- */
446
- async function copyLocalSettingsToWorktree(
447
- repoRoot: string,
448
- worktreePath: string,
449
- ): Promise<void> {
450
- const relativePath = path.join(".wave", "settings.local.json");
451
- const sourcePath = path.join(repoRoot, relativePath);
452
- const destPath = path.join(worktreePath, relativePath);
453
- let content: string;
454
- try {
455
- content = await fs.promises.readFile(sourcePath, "utf8");
456
- } catch (error: unknown) {
457
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
458
- logger.warn(`Failed to read ${sourcePath}: ${(error as Error).message}`);
459
- }
460
- return;
461
- }
462
- try {
463
- await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
464
- await fs.promises.writeFile(destPath, content);
465
- } catch (error: unknown) {
466
- logger.warn(
467
- `Failed to copy ${relativePath} into worktree: ${(error as Error).message}`,
468
- );
469
- }
470
- }
471
-
472
- /**
473
- * Copy files listed in <repoRoot>/.worktreeinclude into the worktree. Each
474
- * non-comment line is a gitignore pattern; matching gitignored files are
475
- * copied at their relative paths.
476
- */
477
- async function copyWorktreeIncludeFiles(
478
- repoRoot: string,
479
- worktreePath: string,
480
- ): Promise<string[]> {
481
- const includePath = path.join(repoRoot, ".worktreeinclude");
482
- let content: string;
483
- try {
484
- content = await fs.promises.readFile(includePath, "utf8");
485
- } catch {
486
- return [];
487
- }
488
- const patterns = parseWorktreeIncludePatterns(content);
489
- if (patterns.length === 0) return [];
490
-
491
- const worktreeRelPath = path
492
- .relative(repoRoot, worktreePath)
493
- .split(path.sep)
494
- .join("/");
495
- // The worktree lives under .wave/worktrees/, which is gitignored — never
496
- // copy the worktree (or anything inside it) into itself.
497
- const isSelf = (entry: string) =>
498
- entry === worktreeRelPath || entry.startsWith(worktreeRelPath + "/");
499
-
500
- // "--directory" collapses fully-ignored directories into single entries,
501
- // which keeps the listing fast on large repos.
502
- let entries: string[];
503
- try {
504
- const { stdout } = await execFileAsync(
505
- "git",
506
- [
507
- "ls-files",
508
- "--others",
509
- "--ignored",
510
- "--exclude-standard",
511
- "--directory",
512
- ],
513
- { cwd: repoRoot, encoding: "utf8" },
514
- );
515
- entries = stdout.trim().split("\n").filter(Boolean);
516
- } catch {
517
- return [];
518
- }
519
-
520
- const copied: string[] = [];
521
- const dirsToExpand: string[] = [];
522
-
523
- const copyToWorktree = async (relativePath: string): Promise<void> => {
524
- const srcPath = path.join(repoRoot, relativePath);
525
- const destPath = path.join(worktreePath, relativePath);
526
- try {
527
- await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
528
- await fs.promises.copyFile(srcPath, destPath);
529
- copied.push(relativePath);
530
- } catch (error: unknown) {
531
- logger.warn(
532
- `Failed to copy ${relativePath} into worktree: ${(error as Error).message}`,
533
- );
534
- }
535
- };
536
-
537
- for (const entry of entries) {
538
- if (isSelf(entry)) continue;
539
- if (entry.endsWith("/")) {
540
- const dir = entry.slice(0, -1);
541
- if (worktreeIncludeMatches(patterns, dir, true)) {
542
- // The directory itself matches — copy it wholesale
543
- try {
544
- await fs.promises.cp(
545
- path.join(repoRoot, dir),
546
- path.join(worktreePath, dir),
547
- {
548
- recursive: true,
549
- },
550
- );
551
- copied.push(entry);
552
- } catch (error: unknown) {
553
- logger.warn(
554
- `Failed to copy ${entry} into worktree: ${(error as Error).message}`,
555
- );
556
- }
557
- } else if (needsExpansion(patterns, dir)) {
558
- dirsToExpand.push(dir);
559
- }
560
- } else if (worktreeIncludeMatches(patterns, entry, false)) {
561
- await copyToWorktree(entry);
562
- }
563
- }
564
-
565
- if (dirsToExpand.length > 0) {
566
- // List the files inside collapsed dirs whose contents are targeted
567
- try {
568
- const { stdout } = await execFileAsync(
569
- "git",
570
- [
571
- "ls-files",
572
- "--others",
573
- "--ignored",
574
- "--exclude-standard",
575
- "--",
576
- ...dirsToExpand,
577
- ],
578
- { cwd: repoRoot, encoding: "utf8" },
579
- );
580
- for (const file of stdout.trim().split("\n").filter(Boolean)) {
581
- if (isSelf(file)) continue;
582
- if (worktreeIncludeMatches(patterns, file, false)) {
583
- await copyToWorktree(file);
584
- }
585
- }
586
- } catch {
587
- // Expansion is best-effort; the worktree is already usable without it
588
- }
589
- }
590
-
591
- return copied;
592
- }
593
-
594
- /**
595
- * Set up a freshly created worktree: copy local settings and gitignored
596
- * project files (via .worktreeinclude) from the main repo. Best-effort — any
597
- * failure only logs a warning and never fails worktree creation.
598
- */
599
- export async function performPostCreationSetup(
600
- worktreePath: string,
601
- repoRoot: string,
602
- ): Promise<void> {
603
- await copyLocalSettingsToWorktree(repoRoot, worktreePath);
604
- await copyWorktreeIncludeFiles(repoRoot, worktreePath);
605
- }
606
-
607
- /**
608
- * Remove a git worktree and its branch.
609
- */
610
- export function removeWorktree(info: WorktreeInfo): void {
611
- const repoRoot = info.repoRoot;
612
-
613
- try {
614
- // Get current branch in worktree before removing
615
- let currentBranch: string | undefined;
616
- try {
617
- currentBranch = execFileSync(
618
- "git",
619
- ["rev-parse", "--abbrev-ref", "HEAD"],
620
- {
621
- cwd: info.path,
622
- encoding: "utf8",
623
- stdio: ["ignore", "pipe", "ignore"],
624
- },
625
- ).trim();
626
- } catch {
627
- // Ignore errors
628
- }
629
-
630
- // Remove worktree
631
- execFileSync("git", ["worktree", "remove", "--force", info.path], {
632
- cwd: repoRoot,
633
- stdio: ["ignore", "pipe", "pipe"],
634
- });
635
-
636
- // Delete worktree branch
637
- try {
638
- execFileSync("git", ["branch", "-D", info.branch], {
639
- cwd: repoRoot,
640
- stdio: ["ignore", "pipe", "pipe"],
641
- });
642
- } catch {
643
- // Ignore errors
644
- }
645
-
646
- // Delete current branch if different and not protected
647
- if (
648
- currentBranch &&
649
- currentBranch !== info.branch &&
650
- currentBranch !== "HEAD"
651
- ) {
652
- const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
653
- const defaultBranchName = defaultRemoteBranch.split("/").pop();
654
-
655
- if (
656
- currentBranch !== defaultBranchName &&
657
- currentBranch !== "main" &&
658
- currentBranch !== "master"
659
- ) {
660
- try {
661
- execFileSync("git", ["branch", "-D", currentBranch], {
662
- cwd: repoRoot,
663
- stdio: ["ignore", "pipe", "pipe"],
664
- });
665
- } catch {
666
- // Ignore errors
667
- }
668
- }
669
- }
670
- } catch (error: unknown) {
671
- logger.error("Failed to remove worktree or branch:", {
672
- error: error instanceof Error ? error.message : String(error),
673
- worktreePath: info.path,
674
- });
675
- throw error;
676
- }
677
- }
678
-
679
- /**
680
- * Validate that a worktree path is safe to remove before running git removal.
681
- * Aligns with Claude Code v2.1.216+ background-session checks:
682
- * - rejects a path whose final component is a symlink;
683
- * - rejects a path that resolves outside the repo root.
684
- *
685
- * A path that no longer exists (worktree already removed) is allowed so that
686
- * removal stays best-effort/idempotent; its nearest existing ancestor is used
687
- * for the containment check. Throws an Error on invalid paths.
688
- */
689
- export function validateWorktreeRemovalPath(
690
- worktreePath: string,
691
- repoRoot: string,
692
- ): void {
693
- const SYMLINK_PREFIX = "Refusing to remove worktree at symlink path:";
694
-
695
- // Reject a symlink as the final path component.
696
- try {
697
- if (fs.lstatSync(worktreePath).isSymbolicLink()) {
698
- throw new Error(`${SYMLINK_PREFIX} ${worktreePath}`);
699
- }
700
- } catch (error: unknown) {
701
- if (error instanceof Error && error.message.startsWith(SYMLINK_PREFIX)) {
702
- throw error;
703
- }
704
- // lstat failed (path missing) — the containment check below still applies.
705
- }
706
-
707
- const resolvedRepoRoot = fs.realpathSync(repoRoot);
708
-
709
- // Resolve the path, following symlinks in existing components. If the path
710
- // (or a parent) no longer exists, fall back to the deepest existing ancestor
711
- // so the containment check still guards against traversal outside the repo.
712
- let resolvedPath: string;
713
- let existingAncestor = worktreePath;
714
- for (;;) {
715
- try {
716
- resolvedPath = fs.realpathSync(existingAncestor);
717
- break;
718
- } catch {
719
- const parent = path.dirname(existingAncestor);
720
- if (parent === existingAncestor) {
721
- throw new Error(`Invalid worktree path for removal: ${worktreePath}`);
722
- }
723
- existingAncestor = parent;
724
- }
725
- }
726
- if (existingAncestor !== worktreePath) {
727
- resolvedPath = path.join(
728
- resolvedPath,
729
- path.relative(existingAncestor, worktreePath),
730
- );
731
- }
732
-
733
- const relative = path.relative(resolvedRepoRoot, resolvedPath);
734
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
735
- throw new Error(
736
- `Refusing to remove worktree outside repo root: ${worktreePath}`,
737
- );
738
- }
739
- }
740
-
741
- /**
742
- * Count uncommitted files and new commits in a worktree.
743
- * Returns null if git commands fail (fail-closed).
744
- */
745
- export function countWorktreeChanges(
746
- worktreePath: string,
747
- originalHeadCommit: string | undefined,
748
- ): { changedFiles: number; commits: number } | null {
749
- try {
750
- const statusOutput = execFileSync(
751
- "git",
752
- ["-C", worktreePath, "status", "--porcelain"],
753
- {
754
- encoding: "utf8",
755
- stdio: ["ignore", "pipe", "ignore"],
756
- },
757
- );
758
- const changedFiles = statusOutput
759
- .split("\n")
760
- .filter((l) => l.trim() !== "").length;
761
-
762
- if (!originalHeadCommit) {
763
- return null;
764
- }
765
-
766
- const revListOutput = execFileSync(
767
- "git",
768
- [
769
- "-C",
770
- worktreePath,
771
- "rev-list",
772
- "--count",
773
- `${originalHeadCommit}..HEAD`,
774
- ],
775
- {
776
- encoding: "utf8",
777
- stdio: ["ignore", "pipe", "ignore"],
778
- },
779
- );
780
- const commits = parseInt(revListOutput.trim(), 10) || 0;
781
-
782
- return { changedFiles, commits };
783
- } catch {
784
- return null;
785
- }
786
- }