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