wave-agent-sdk 1.0.5 → 1.0.7

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 (202) hide show
  1. package/dist/agent.d.ts +34 -1
  2. package/dist/agent.js +49 -4
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +1 -1
  5. package/dist/managers/aiManager.d.ts +36 -0
  6. package/dist/managers/aiManager.js +240 -11
  7. package/dist/managers/messageManager.d.ts +2 -4
  8. package/dist/managers/messageManager.js +14 -7
  9. package/dist/managers/slashCommandManager.js +28 -0
  10. package/dist/managers/subagentManager.d.ts +0 -2
  11. package/dist/services/aiService.d.ts +2 -3
  12. package/dist/services/aiService.js +4 -18
  13. package/dist/services/configurationService.d.ts +7 -0
  14. package/dist/services/configurationService.js +10 -0
  15. package/dist/telemetry/instrumentation.d.ts +1 -1
  16. package/dist/telemetry/instrumentation.js +57 -28
  17. package/dist/tools/bashTool.js +16 -6
  18. package/dist/tools/grepTool.js +11 -3
  19. package/dist/types/core.d.ts +1 -34
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/messaging.d.ts +2 -0
  22. package/dist/utils/cacheControlUtils.d.ts +5 -27
  23. package/dist/utils/cacheControlUtils.js +12 -73
  24. package/dist/utils/containerSetup.js +0 -3
  25. package/dist/utils/messageOperations.d.ts +2 -1
  26. package/dist/utils/messageOperations.js +2 -1
  27. package/dist/utils/notificationXml.js +5 -0
  28. package/dist/utils/ripgrep.d.ts +4 -4
  29. package/dist/utils/ripgrep.js +4 -27
  30. package/package.json +20 -8
  31. package/bin/rg +0 -79
  32. package/dist/managers/forkedAgentManager.d.ts +0 -50
  33. package/dist/managers/forkedAgentManager.js +0 -130
  34. package/scripts/install_ripgrep.js +0 -111
  35. package/scripts/postinstall.js +0 -38
  36. package/src/agent.ts +0 -1271
  37. package/src/constants/subagents.ts +0 -4
  38. package/src/constants/toolLimits.ts +0 -15
  39. package/src/constants/tools.ts +0 -24
  40. package/src/core/plugin.ts +0 -237
  41. package/src/core/session.ts +0 -9
  42. package/src/index.ts +0 -38
  43. package/src/managers/MemoryRuleManager.ts +0 -198
  44. package/src/managers/aiManager.ts +0 -2429
  45. package/src/managers/backgroundTaskManager.ts +0 -480
  46. package/src/managers/bangManager.ts +0 -119
  47. package/src/managers/cronManager.ts +0 -401
  48. package/src/managers/foregroundTaskManager.ts +0 -30
  49. package/src/managers/forkedAgentManager.ts +0 -216
  50. package/src/managers/hookManager.ts +0 -1089
  51. package/src/managers/liveConfigManager.ts +0 -445
  52. package/src/managers/lspManager.ts +0 -470
  53. package/src/managers/mcpManager.ts +0 -1010
  54. package/src/managers/messageManager.ts +0 -1184
  55. package/src/managers/messageQueue.ts +0 -157
  56. package/src/managers/permissionManager.ts +0 -1140
  57. package/src/managers/planManager.ts +0 -112
  58. package/src/managers/pluginManager.ts +0 -330
  59. package/src/managers/pluginScopeManager.ts +0 -124
  60. package/src/managers/reversionManager.ts +0 -178
  61. package/src/managers/skillManager.ts +0 -578
  62. package/src/managers/slashCommandManager.ts +0 -564
  63. package/src/managers/subagentManager.ts +0 -915
  64. package/src/managers/toolManager.ts +0 -489
  65. package/src/managers/workflowManager.ts +0 -493
  66. package/src/prompts/autoMemory.ts +0 -33
  67. package/src/prompts/autoMemoryExtraction.ts +0 -146
  68. package/src/prompts/index.ts +0 -506
  69. package/src/prompts/planModeReminders.ts +0 -126
  70. package/src/services/GitService.ts +0 -131
  71. package/src/services/MarketplaceService.ts +0 -1052
  72. package/src/services/MemoryRuleService.ts +0 -71
  73. package/src/services/aiService.ts +0 -919
  74. package/src/services/authService.ts +0 -565
  75. package/src/services/autoMemoryService.ts +0 -280
  76. package/src/services/configurationService.ts +0 -1520
  77. package/src/services/fileWatcher.ts +0 -319
  78. package/src/services/hook.ts +0 -348
  79. package/src/services/initializationService.ts +0 -357
  80. package/src/services/interactionService.ts +0 -229
  81. package/src/services/jsonlHandler.ts +0 -295
  82. package/src/services/memory.ts +0 -234
  83. package/src/services/pluginLoader.ts +0 -252
  84. package/src/services/remoteSettingsService.ts +0 -366
  85. package/src/services/reversionService.ts +0 -122
  86. package/src/services/session.ts +0 -917
  87. package/src/services/taskManager.ts +0 -321
  88. package/src/telemetry/events.ts +0 -65
  89. package/src/telemetry/instrumentation.ts +0 -499
  90. package/src/telemetry/sessionTracing.ts +0 -348
  91. package/src/tools/agentTool.ts +0 -322
  92. package/src/tools/askUserQuestion.ts +0 -160
  93. package/src/tools/bashTool.ts +0 -621
  94. package/src/tools/buildTool.ts +0 -61
  95. package/src/tools/cronCreateTool.ts +0 -161
  96. package/src/tools/cronDeleteTool.ts +0 -51
  97. package/src/tools/cronListTool.ts +0 -47
  98. package/src/tools/editTool.ts +0 -313
  99. package/src/tools/enterPlanMode.ts +0 -124
  100. package/src/tools/enterWorktreeTool.ts +0 -190
  101. package/src/tools/exitPlanMode.ts +0 -139
  102. package/src/tools/exitWorktreeTool.ts +0 -236
  103. package/src/tools/globTool.ts +0 -167
  104. package/src/tools/grepTool.ts +0 -399
  105. package/src/tools/lspTool.ts +0 -883
  106. package/src/tools/readTool.ts +0 -426
  107. package/src/tools/skillTool.ts +0 -248
  108. package/src/tools/taskManagementTools.ts +0 -574
  109. package/src/tools/taskStopTool.ts +0 -78
  110. package/src/tools/types.ts +0 -137
  111. package/src/tools/webFetchTool.ts +0 -373
  112. package/src/tools/workflowTool.ts +0 -205
  113. package/src/tools/writeTool.ts +0 -267
  114. package/src/types/agent.ts +0 -128
  115. package/src/types/auth.ts +0 -19
  116. package/src/types/commands.ts +0 -30
  117. package/src/types/config.ts +0 -43
  118. package/src/types/configuration.ts +0 -168
  119. package/src/types/core.ts +0 -105
  120. package/src/types/cron.ts +0 -13
  121. package/src/types/environment.ts +0 -106
  122. package/src/types/fileSearch.ts +0 -4
  123. package/src/types/history.ts +0 -7
  124. package/src/types/hooks.ts +0 -256
  125. package/src/types/index.ts +0 -41
  126. package/src/types/lsp.ts +0 -98
  127. package/src/types/marketplace.ts +0 -71
  128. package/src/types/mcp.ts +0 -43
  129. package/src/types/memoryRule.ts +0 -31
  130. package/src/types/messaging.ts +0 -114
  131. package/src/types/permissions.ts +0 -80
  132. package/src/types/plugins.ts +0 -39
  133. package/src/types/processes.ts +0 -99
  134. package/src/types/reversion.ts +0 -29
  135. package/src/types/session.ts +0 -12
  136. package/src/types/skills.ts +0 -108
  137. package/src/types/tasks.ts +0 -13
  138. package/src/types/telemetry.ts +0 -98
  139. package/src/types/tools.ts +0 -44
  140. package/src/types/workflow.ts +0 -6
  141. package/src/utils/abortUtils.ts +0 -118
  142. package/src/utils/atomicWrite.ts +0 -61
  143. package/src/utils/bashParser.ts +0 -899
  144. package/src/utils/cacheControlUtils.ts +0 -426
  145. package/src/utils/commandArgumentParser.ts +0 -161
  146. package/src/utils/commandPathResolver.ts +0 -88
  147. package/src/utils/configPaths.ts +0 -220
  148. package/src/utils/configValidator.ts +0 -100
  149. package/src/utils/constants.ts +0 -39
  150. package/src/utils/container.ts +0 -92
  151. package/src/utils/containerSetup.ts +0 -371
  152. package/src/utils/convertMessagesForAPI.ts +0 -326
  153. package/src/utils/cronTasks.ts +0 -128
  154. package/src/utils/cronTasksLock.ts +0 -182
  155. package/src/utils/cronToHuman.ts +0 -99
  156. package/src/utils/customCommands.ts +0 -95
  157. package/src/utils/editUtils.ts +0 -20
  158. package/src/utils/fileFormat.ts +0 -40
  159. package/src/utils/fileSearch.ts +0 -151
  160. package/src/utils/fileUtils.ts +0 -223
  161. package/src/utils/gitUtils.ts +0 -315
  162. package/src/utils/globalLogger.ts +0 -128
  163. package/src/utils/groupMessagesByApiRound.ts +0 -121
  164. package/src/utils/hookMatcher.ts +0 -176
  165. package/src/utils/markdownParser.ts +0 -317
  166. package/src/utils/mcpUtils.ts +0 -138
  167. package/src/utils/messageOperations.ts +0 -659
  168. package/src/utils/modelCapabilities.ts +0 -30
  169. package/src/utils/nameGenerator.ts +0 -99
  170. package/src/utils/notificationXml.ts +0 -57
  171. package/src/utils/openaiClient.ts +0 -251
  172. package/src/utils/parseCronExpression.ts +0 -78
  173. package/src/utils/path.ts +0 -72
  174. package/src/utils/pathEncoder.ts +0 -400
  175. package/src/utils/pathSafety.ts +0 -40
  176. package/src/utils/promptHistory.ts +0 -168
  177. package/src/utils/ripgrep.ts +0 -35
  178. package/src/utils/shellResolver.ts +0 -190
  179. package/src/utils/skillParser.ts +0 -246
  180. package/src/utils/stringUtils.ts +0 -156
  181. package/src/utils/subagentParser.ts +0 -279
  182. package/src/utils/taskReminder.ts +0 -96
  183. package/src/utils/tokenCalculation.ts +0 -43
  184. package/src/utils/tokenEstimate.ts +0 -34
  185. package/src/utils/toolResultStorage.ts +0 -117
  186. package/src/utils/worktreeSession.ts +0 -26
  187. package/src/utils/worktreeUtils.ts +0 -786
  188. package/src/workflow/budgetTracker.ts +0 -34
  189. package/src/workflow/concurrencyLimiter.ts +0 -47
  190. package/src/workflow/journal.ts +0 -95
  191. package/src/workflow/progressReporter.ts +0 -141
  192. package/src/workflow/runState.ts +0 -65
  193. package/src/workflow/scriptRuntime.ts +0 -274
  194. package/src/workflow/structuredOutput.ts +0 -123
  195. package/src/workflow/types.ts +0 -101
  196. package/src/workflow/workflowApis.ts +0 -410
  197. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  198. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  199. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  200. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  201. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  202. 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
- }