@posthog/agent 1.30.0 → 2.0.1

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 (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +172 -1203
  35. package/dist/index.js +3704 -6826
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +4451 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +4507 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +66 -14
  51. package/src/acp-extensions.ts +93 -61
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -611
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +97 -734
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +51 -154
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/dist/templates/plan-template.md +0 -41
  119. package/src/adapters/claude/claude.ts +0 -1543
  120. package/src/adapters/claude/mcp-server.ts +0 -810
  121. package/src/adapters/claude/utils.ts +0 -267
  122. package/src/agents/execution.ts +0 -37
  123. package/src/agents/planning.ts +0 -60
  124. package/src/agents/research.ts +0 -160
  125. package/src/file-manager.ts +0 -306
  126. package/src/git-manager.ts +0 -577
  127. package/src/prompt-builder.ts +0 -499
  128. package/src/schemas.ts +0 -241
  129. package/src/session-store.ts +0 -259
  130. package/src/task-manager.ts +0 -163
  131. package/src/template-manager.ts +0 -236
  132. package/src/templates/plan-template.md +0 -41
  133. package/src/todo-manager.ts +0 -180
  134. package/src/tools/registry.ts +0 -129
  135. package/src/tools/types.ts +0 -127
  136. package/src/utils/tapped-stream.ts +0 -60
  137. package/src/workflow/config.ts +0 -53
  138. package/src/workflow/steps/build.ts +0 -135
  139. package/src/workflow/steps/finalize.ts +0 -241
  140. package/src/workflow/steps/plan.ts +0 -167
  141. package/src/workflow/steps/research.ts +0 -223
  142. package/src/workflow/types.ts +0 -62
  143. package/src/workflow/utils.ts +0 -53
  144. package/src/worktree-manager.ts +0 -928
@@ -1,928 +0,0 @@
1
- import { exec, execFile } from "node:child_process";
2
- import * as crypto from "node:crypto";
3
- import * as fs from "node:fs/promises";
4
- import * as path from "node:path";
5
- import { promisify } from "node:util";
6
- import type { WorktreeInfo } from "./types.js";
7
- import { Logger } from "./utils/logger.js";
8
-
9
- const execAsync = promisify(exec);
10
- const execFileAsync = promisify(execFile);
11
-
12
- export interface WorktreeConfig {
13
- mainRepoPath: string;
14
- worktreeBasePath?: string;
15
- logger?: Logger;
16
- }
17
-
18
- const ADJECTIVES = [
19
- "swift",
20
- "bright",
21
- "calm",
22
- "bold",
23
- "gentle",
24
- "quick",
25
- "soft",
26
- "warm",
27
- "cool",
28
- "wise",
29
- "keen",
30
- "brave",
31
- "clear",
32
- "crisp",
33
- "deep",
34
- "fair",
35
- "fine",
36
- "free",
37
- "glad",
38
- "good",
39
- "grand",
40
- "great",
41
- "happy",
42
- "kind",
43
- "light",
44
- "lively",
45
- "neat",
46
- "nice",
47
- "plain",
48
- "proud",
49
- "pure",
50
- "rare",
51
- "rich",
52
- "safe",
53
- "sharp",
54
- "shy",
55
- "simple",
56
- "slim",
57
- "smart",
58
- "smooth",
59
- "solid",
60
- "sound",
61
- "spare",
62
- "stable",
63
- "steady",
64
- "still",
65
- "strong",
66
- "sure",
67
- "sweet",
68
- "tall",
69
- "agile",
70
- "ancient",
71
- "autumn",
72
- "azure",
73
- "cosmic",
74
- "daring",
75
- "dawn",
76
- "dusty",
77
- "eager",
78
- "early",
79
- "endless",
80
- "fading",
81
- "fallen",
82
- "famous",
83
- "feral",
84
- "fierce",
85
- "fleet",
86
- "foggy",
87
- "forest",
88
- "frozen",
89
- "gleeful",
90
- "golden",
91
- "hazy",
92
- "hidden",
93
- "hollow",
94
- "humble",
95
- "hushed",
96
- "icy",
97
- "inner",
98
- "late",
99
- "lazy",
100
- "little",
101
- "lone",
102
- "long",
103
- "lost",
104
- "lucky",
105
- "lunar",
106
- "magic",
107
- "mellow",
108
- "mighty",
109
- "misty",
110
- "modest",
111
- "mossy",
112
- "mystic",
113
- "nimble",
114
- "noble",
115
- "ocean",
116
- "outer",
117
- "pale",
118
- "paper",
119
- "patient",
120
- "peaceful",
121
- "phantom",
122
- "polite",
123
- "primal",
124
- "quiet",
125
- "rapid",
126
- "restless",
127
- "rising",
128
- "roaming",
129
- "rocky",
130
- "rustic",
131
- "sacred",
132
- "sandy",
133
- "secret",
134
- "serene",
135
- "shadow",
136
- "shining",
137
- "silent",
138
- "silky",
139
- "silver",
140
- "sleek",
141
- "snowy",
142
- "solar",
143
- "solemn",
144
- "spring",
145
- "starry",
146
- "stormy",
147
- "summer",
148
- "sunny",
149
- "tender",
150
- "thorny",
151
- "tiny",
152
- "tranquil",
153
- "twilight",
154
- "upward",
155
- "velvet",
156
- "vivid",
157
- "wandering",
158
- "wary",
159
- "wild",
160
- "windy",
161
- "winter",
162
- "wispy",
163
- "young",
164
- ];
165
-
166
- const COLORS = [
167
- "blue",
168
- "red",
169
- "green",
170
- "amber",
171
- "coral",
172
- "jade",
173
- "pearl",
174
- "ruby",
175
- "sage",
176
- "teal",
177
- "gold",
178
- "silver",
179
- "bronze",
180
- "copper",
181
- "ivory",
182
- "onyx",
183
- "opal",
184
- "rose",
185
- "slate",
186
- "violet",
187
- "aqua",
188
- "azure",
189
- "beige",
190
- "black",
191
- "brass",
192
- "brick",
193
- "brown",
194
- "cedar",
195
- "charcoal",
196
- "cherry",
197
- "chestnut",
198
- "chrome",
199
- "cider",
200
- "cinnamon",
201
- "citrus",
202
- "clay",
203
- "cloud",
204
- "cobalt",
205
- "cocoa",
206
- "cream",
207
- "crimson",
208
- "crystal",
209
- "cyan",
210
- "denim",
211
- "dusk",
212
- "ebony",
213
- "ember",
214
- "emerald",
215
- "fern",
216
- "flame",
217
- "flint",
218
- "forest",
219
- "frost",
220
- "garnet",
221
- "ginger",
222
- "glacier",
223
- "granite",
224
- "grape",
225
- "gray",
226
- "hazel",
227
- "honey",
228
- "indigo",
229
- "iron",
230
- "lapis",
231
- "lava",
232
- "lavender",
233
- "lemon",
234
- "lilac",
235
- "lime",
236
- "magenta",
237
- "mahogany",
238
- "maple",
239
- "marble",
240
- "maroon",
241
- "mauve",
242
- "midnight",
243
- "mint",
244
- "mocha",
245
- "moss",
246
- "mustard",
247
- "navy",
248
- "nickel",
249
- "obsidian",
250
- "ochre",
251
- "olive",
252
- "orange",
253
- "orchid",
254
- "peach",
255
- "pine",
256
- "pink",
257
- "plum",
258
- "porcelain",
259
- "purple",
260
- "quartz",
261
- "rust",
262
- "saffron",
263
- "salmon",
264
- "sand",
265
- "sapphire",
266
- "scarlet",
267
- "sepia",
268
- "shadow",
269
- "sienna",
270
- "smoke",
271
- "snow",
272
- "steel",
273
- "stone",
274
- "storm",
275
- "sunset",
276
- "tan",
277
- "tangerine",
278
- "taupe",
279
- "terra",
280
- "timber",
281
- "topaz",
282
- "turquoise",
283
- "umber",
284
- "vanilla",
285
- "walnut",
286
- "wheat",
287
- "white",
288
- "wine",
289
- "yellow",
290
- ];
291
-
292
- const ANIMALS = [
293
- "fox",
294
- "owl",
295
- "bear",
296
- "wolf",
297
- "hawk",
298
- "deer",
299
- "lynx",
300
- "otter",
301
- "raven",
302
- "falcon",
303
- "badger",
304
- "beaver",
305
- "bison",
306
- "bobcat",
307
- "crane",
308
- "eagle",
309
- "ferret",
310
- "finch",
311
- "gopher",
312
- "heron",
313
- "jaguar",
314
- "koala",
315
- "lemur",
316
- "marten",
317
- "mink",
318
- "moose",
319
- "newt",
320
- "ocelot",
321
- "osprey",
322
- "panda",
323
- "parrot",
324
- "pelican",
325
- "puma",
326
- "quail",
327
- "rabbit",
328
- "raccoon",
329
- "salmon",
330
- "seal",
331
- "shark",
332
- "shrew",
333
- "sloth",
334
- "snake",
335
- "spider",
336
- "squid",
337
- "stork",
338
- "swan",
339
- "tiger",
340
- "toucan",
341
- "turtle",
342
- "whale",
343
- "albatross",
344
- "ant",
345
- "antelope",
346
- "armadillo",
347
- "baboon",
348
- "bat",
349
- "bee",
350
- "beetle",
351
- "buffalo",
352
- "butterfly",
353
- "camel",
354
- "cardinal",
355
- "caribou",
356
- "catfish",
357
- "cheetah",
358
- "chipmunk",
359
- "cicada",
360
- "clam",
361
- "cobra",
362
- "condor",
363
- "corgi",
364
- "cougar",
365
- "coyote",
366
- "crab",
367
- "cricket",
368
- "crow",
369
- "dolphin",
370
- "donkey",
371
- "dove",
372
- "dragonfly",
373
- "duck",
374
- "eel",
375
- "egret",
376
- "elephant",
377
- "elk",
378
- "emu",
379
- "firefly",
380
- "flamingo",
381
- "frog",
382
- "gazelle",
383
- "gecko",
384
- "gibbon",
385
- "giraffe",
386
- "goat",
387
- "goose",
388
- "gorilla",
389
- "grasshopper",
390
- "grouse",
391
- "gull",
392
- "hamster",
393
- "hare",
394
- "hedgehog",
395
- "hippo",
396
- "hornet",
397
- "horse",
398
- "hound",
399
- "hummingbird",
400
- "hyena",
401
- "ibis",
402
- "iguana",
403
- "impala",
404
- "jackal",
405
- "jay",
406
- "jellyfish",
407
- "kangaroo",
408
- "kestrel",
409
- "kingfisher",
410
- "kite",
411
- "kiwi",
412
- "lark",
413
- "leopard",
414
- "lion",
415
- "lizard",
416
- "llama",
417
- "lobster",
418
- "loon",
419
- "macaw",
420
- "magpie",
421
- "mallard",
422
- "mammoth",
423
- "manatee",
424
- "mantis",
425
- "marlin",
426
- "marmot",
427
- "meerkat",
428
- "mockingbird",
429
- "mole",
430
- "mongoose",
431
- "monkey",
432
- "moth",
433
- "mouse",
434
- "mule",
435
- "narwhal",
436
- "nightingale",
437
- "octopus",
438
- "opossum",
439
- "orangutan",
440
- "oriole",
441
- "ostrich",
442
- "oyster",
443
- "panther",
444
- "peacock",
445
- "penguin",
446
- "pheasant",
447
- "pig",
448
- "pigeon",
449
- "pike",
450
- "piranha",
451
- "platypus",
452
- "pony",
453
- "porcupine",
454
- "porpoise",
455
- "python",
456
- "raven",
457
- "ray",
458
- "reindeer",
459
- "rhino",
460
- "robin",
461
- "rooster",
462
- "salamander",
463
- "sandpiper",
464
- "sardine",
465
- "scorpion",
466
- "seagull",
467
- "seahorse",
468
- "skunk",
469
- "snail",
470
- "sparrow",
471
- "squirrel",
472
- "starfish",
473
- "starling",
474
- "stingray",
475
- "swallow",
476
- "tapir",
477
- "termite",
478
- "tern",
479
- "toad",
480
- "trout",
481
- "tuna",
482
- "viper",
483
- "vulture",
484
- "walrus",
485
- "wasp",
486
- "weasel",
487
- "wombat",
488
- "woodpecker",
489
- "wren",
490
- "yak",
491
- "zebra",
492
- ];
493
-
494
- const WORKTREE_FOLDER_NAME = ".array";
495
-
496
- export class WorktreeManager {
497
- private mainRepoPath: string;
498
- private worktreeBasePath: string | null;
499
- private repoName: string;
500
- private logger: Logger;
501
-
502
- constructor(config: WorktreeConfig) {
503
- this.mainRepoPath = config.mainRepoPath;
504
- this.worktreeBasePath = config.worktreeBasePath || null;
505
- this.repoName = path.basename(config.mainRepoPath);
506
- this.logger =
507
- config.logger ||
508
- new Logger({ debug: false, prefix: "[WorktreeManager]" });
509
- }
510
-
511
- private usesExternalPath(): boolean {
512
- return this.worktreeBasePath !== null;
513
- }
514
-
515
- private async runGitCommand(command: string): Promise<string> {
516
- try {
517
- const { stdout } = await execAsync(`git ${command}`, {
518
- cwd: this.mainRepoPath,
519
- });
520
- return stdout.trim();
521
- } catch (error) {
522
- throw new Error(`Git command failed: ${command}\n${error}`);
523
- }
524
- }
525
-
526
- private randomElement<T>(array: T[]): T {
527
- return array[crypto.randomInt(array.length)];
528
- }
529
-
530
- generateWorktreeName(): string {
531
- const adjective = this.randomElement(ADJECTIVES);
532
- const color = this.randomElement(COLORS);
533
- const animal = this.randomElement(ANIMALS);
534
- return `${adjective}-${color}-${animal}`;
535
- }
536
-
537
- private getWorktreeFolderPath(): string {
538
- if (this.worktreeBasePath) {
539
- return path.join(this.worktreeBasePath, this.repoName);
540
- }
541
- return path.join(this.mainRepoPath, WORKTREE_FOLDER_NAME);
542
- }
543
-
544
- private getWorktreePath(name: string): string {
545
- return path.join(this.getWorktreeFolderPath(), name);
546
- }
547
-
548
- async worktreeExists(name: string): Promise<boolean> {
549
- const worktreePath = this.getWorktreePath(name);
550
- try {
551
- await fs.access(worktreePath);
552
- return true;
553
- } catch {
554
- return false;
555
- }
556
- }
557
-
558
- async ensureArrayDirIgnored(): Promise<void> {
559
- // Use .git/info/exclude instead of .gitignore to avoid modifying tracked files
560
- const excludePath = path.join(this.mainRepoPath, ".git", "info", "exclude");
561
- const ignorePattern = `/${WORKTREE_FOLDER_NAME}/`;
562
-
563
- let content = "";
564
- try {
565
- content = await fs.readFile(excludePath, "utf-8");
566
- } catch {
567
- // File doesn't exist or .git/info doesn't exist
568
- }
569
-
570
- // Check if pattern is already present
571
- if (
572
- content.includes(`/${WORKTREE_FOLDER_NAME}/`) ||
573
- content.includes(`/${WORKTREE_FOLDER_NAME}`)
574
- ) {
575
- this.logger.debug("Exclude file already contains .array folder pattern");
576
- return;
577
- }
578
-
579
- // Ensure .git/info directory exists
580
- const infoDir = path.join(this.mainRepoPath, ".git", "info");
581
- await fs.mkdir(infoDir, { recursive: true });
582
-
583
- // Append the pattern
584
- const newContent = `${content.trimEnd()}\n\n# Array worktrees\n${ignorePattern}\n`;
585
- await fs.writeFile(excludePath, newContent);
586
- this.logger.info("Added .array folder to .git/info/exclude");
587
- }
588
-
589
- private async generateUniqueWorktreeName(): Promise<string> {
590
- let name = this.generateWorktreeName();
591
- let attempts = 0;
592
- const maxAttempts = 100;
593
-
594
- while ((await this.worktreeExists(name)) && attempts < maxAttempts) {
595
- name = this.generateWorktreeName();
596
- attempts++;
597
- }
598
-
599
- if (attempts >= maxAttempts) {
600
- // Fallback: append timestamp
601
- name = `${this.generateWorktreeName()}-${Date.now()}`;
602
- }
603
-
604
- return name;
605
- }
606
-
607
- private async getDefaultBranch(): Promise<string> {
608
- try {
609
- const remoteBranch = await this.runGitCommand(
610
- "symbolic-ref refs/remotes/origin/HEAD",
611
- );
612
- return remoteBranch.replace("refs/remotes/origin/", "");
613
- } catch {
614
- // Fallback: check if main exists, otherwise use master
615
- try {
616
- await this.runGitCommand("rev-parse --verify main");
617
- return "main";
618
- } catch {
619
- try {
620
- await this.runGitCommand("rev-parse --verify master");
621
- return "master";
622
- } catch {
623
- throw new Error(
624
- "Cannot determine default branch. No main or master branch found.",
625
- );
626
- }
627
- }
628
- }
629
- }
630
-
631
- async createWorktree(options?: {
632
- baseBranch?: string;
633
- }): Promise<WorktreeInfo> {
634
- // Only modify .git/info/exclude when using in-repo storage
635
- if (!this.usesExternalPath()) {
636
- await this.ensureArrayDirIgnored();
637
- }
638
-
639
- // Ensure the worktree folder exists when using external path
640
- if (this.usesExternalPath()) {
641
- const folderPath = this.getWorktreeFolderPath();
642
- await fs.mkdir(folderPath, { recursive: true });
643
- }
644
-
645
- // Generate unique worktree name
646
- const worktreeName = await this.generateUniqueWorktreeName();
647
- const worktreePath = this.getWorktreePath(worktreeName);
648
- const branchName = `array/${worktreeName}`;
649
- const baseBranch = options?.baseBranch ?? (await this.getDefaultBranch());
650
-
651
- this.logger.info("Creating worktree", {
652
- worktreeName,
653
- worktreePath,
654
- branchName,
655
- baseBranch,
656
- external: this.usesExternalPath(),
657
- });
658
-
659
- // Create the worktree with a new branch
660
- if (this.usesExternalPath()) {
661
- // Use absolute path for external worktrees
662
- await this.runGitCommand(
663
- `worktree add -b "${branchName}" "${worktreePath}" "${baseBranch}"`,
664
- );
665
- } else {
666
- // Use relative path from repo root for in-repo worktrees
667
- const relativePath = `${WORKTREE_FOLDER_NAME}/${worktreeName}`;
668
- await this.runGitCommand(
669
- `worktree add -b "${branchName}" "./${relativePath}" "${baseBranch}"`,
670
- );
671
- }
672
-
673
- const createdAt = new Date().toISOString();
674
-
675
- this.logger.info("Worktree created successfully", {
676
- worktreeName,
677
- worktreePath,
678
- branchName,
679
- });
680
-
681
- return {
682
- worktreePath,
683
- worktreeName,
684
- branchName,
685
- baseBranch,
686
- createdAt,
687
- };
688
- }
689
-
690
- async deleteWorktree(worktreePath: string): Promise<void> {
691
- const resolvedWorktreePath = path.resolve(worktreePath);
692
- const resolvedMainRepoPath = path.resolve(this.mainRepoPath);
693
-
694
- // Safety check 1: Never delete the main repo path
695
- if (resolvedWorktreePath === resolvedMainRepoPath) {
696
- const error = new Error(
697
- "Cannot delete worktree: path matches main repo path",
698
- );
699
- this.logger.error("Safety check failed", { worktreePath, error });
700
- throw error;
701
- }
702
-
703
- // Safety check 2: Never delete a parent of the main repo path
704
- if (
705
- resolvedMainRepoPath.startsWith(resolvedWorktreePath) &&
706
- resolvedMainRepoPath !== resolvedWorktreePath
707
- ) {
708
- const error = new Error(
709
- "Cannot delete worktree: path is a parent of main repo path",
710
- );
711
- this.logger.error("Safety check failed", { worktreePath, error });
712
- throw error;
713
- }
714
-
715
- // Safety check 3: Check for .git directory (indicates main repo)
716
- try {
717
- const gitPath = path.join(resolvedWorktreePath, ".git");
718
- const stat = await fs.stat(gitPath);
719
- if (stat.isDirectory()) {
720
- const error = new Error(
721
- "Cannot delete worktree: path appears to be a main repository (contains .git directory)",
722
- );
723
- this.logger.error("Safety check failed", { worktreePath, error });
724
- throw error;
725
- }
726
- } catch (error) {
727
- // If .git doesn't exist or we can't read it, proceed (unless it was the directory check above)
728
- if (
729
- error instanceof Error &&
730
- error.message.includes("Cannot delete worktree")
731
- ) {
732
- throw error;
733
- }
734
- }
735
-
736
- this.logger.info("Deleting worktree", { worktreePath });
737
-
738
- try {
739
- // First, try to remove the worktree via git using execFileAsync for safety
740
- await execFileAsync(
741
- "git",
742
- ["worktree", "remove", worktreePath, "--force"],
743
- {
744
- cwd: this.mainRepoPath,
745
- },
746
- );
747
- this.logger.info("Worktree deleted successfully", { worktreePath });
748
- } catch (error) {
749
- this.logger.warn(
750
- "Git worktree remove failed, attempting manual cleanup",
751
- {
752
- worktreePath,
753
- error,
754
- },
755
- );
756
-
757
- // Manual cleanup if git command fails
758
- try {
759
- await fs.rm(worktreePath, { recursive: true, force: true });
760
- // Also prune the worktree list
761
- await this.runGitCommand("worktree prune");
762
- this.logger.info("Worktree cleaned up manually", { worktreePath });
763
- } catch (cleanupError) {
764
- this.logger.error("Failed to cleanup worktree", {
765
- worktreePath,
766
- cleanupError,
767
- });
768
- throw cleanupError;
769
- }
770
- }
771
- }
772
-
773
- async getWorktreeInfo(worktreePath: string): Promise<WorktreeInfo | null> {
774
- try {
775
- // Parse the worktree list to find info about this worktree
776
- const output = await this.runGitCommand("worktree list --porcelain");
777
- const worktrees = this.parseWorktreeList(output);
778
-
779
- const worktree = worktrees.find((w) => w.worktreePath === worktreePath);
780
- return worktree || null;
781
- } catch (error) {
782
- this.logger.debug("Failed to get worktree info", { worktreePath, error });
783
- return null;
784
- }
785
- }
786
-
787
- async listWorktrees(): Promise<WorktreeInfo[]> {
788
- try {
789
- const output = await this.runGitCommand("worktree list --porcelain");
790
- return this.parseWorktreeList(output);
791
- } catch (error) {
792
- this.logger.debug("Failed to list worktrees", { error });
793
- return [];
794
- }
795
- }
796
-
797
- private parseWorktreeList(output: string): WorktreeInfo[] {
798
- const worktrees: WorktreeInfo[] = [];
799
- const entries = output.split("\n\n").filter((e) => e.trim());
800
- const worktreeFolderPath = this.getWorktreeFolderPath();
801
-
802
- for (const entry of entries) {
803
- const lines = entry.split("\n");
804
- let worktreePath = "";
805
- let branchName = "";
806
-
807
- for (const line of lines) {
808
- if (line.startsWith("worktree ")) {
809
- worktreePath = line.replace("worktree ", "");
810
- } else if (line.startsWith("branch refs/heads/")) {
811
- branchName = line.replace("branch refs/heads/", "");
812
- }
813
- }
814
-
815
- // Include worktrees that:
816
- // 1. Are in our worktree folder (external or in-repo)
817
- // 2. Have a posthog/ branch prefix (our naming convention)
818
- const isInWorktreeFolder = worktreePath?.startsWith(worktreeFolderPath);
819
- const isArrayBranch =
820
- branchName?.startsWith("array/") || branchName?.startsWith("posthog/");
821
-
822
- if (worktreePath && branchName && (isInWorktreeFolder || isArrayBranch)) {
823
- const worktreeName = path.basename(worktreePath);
824
- worktrees.push({
825
- worktreePath,
826
- worktreeName,
827
- branchName,
828
- baseBranch: "",
829
- createdAt: "",
830
- });
831
- }
832
- }
833
-
834
- return worktrees;
835
- }
836
-
837
- async isWorktree(repoPath: string): Promise<boolean> {
838
- try {
839
- const { stdout } = await execAsync(
840
- "git rev-parse --is-inside-work-tree",
841
- { cwd: repoPath },
842
- );
843
- if (stdout.trim() !== "true") {
844
- return false;
845
- }
846
-
847
- // Check if there's a .git file (worktrees have a .git file, not a .git directory)
848
- const gitPath = path.join(repoPath, ".git");
849
- const stat = await fs.stat(gitPath);
850
- return stat.isFile(); // Worktrees have .git as a file, main repos have .git as a directory
851
- } catch {
852
- return false;
853
- }
854
- }
855
-
856
- async getMainRepoPathFromWorktree(
857
- worktreePath: string,
858
- ): Promise<string | null> {
859
- try {
860
- const gitFilePath = path.join(worktreePath, ".git");
861
- const content = await fs.readFile(gitFilePath, "utf-8");
862
-
863
- // The .git file in a worktree contains: gitdir: /path/to/main/.git/worktrees/name
864
- const match = content.match(/gitdir:\s*(.+)/);
865
- if (match) {
866
- const gitDir = match[1].trim();
867
- // Go up from .git/worktrees/name to get the main repo path
868
- // The gitdir points to something like: /main/repo/.git/worktrees/worktree-name
869
- const mainGitDir = path.resolve(gitDir, "..", "..", "..");
870
- return mainGitDir;
871
- }
872
- return null;
873
- } catch {
874
- return null;
875
- }
876
- }
877
-
878
- async cleanupOrphanedWorktrees(associatedWorktreePaths: string[]): Promise<{
879
- deleted: string[];
880
- errors: Array<{ path: string; error: string }>;
881
- }> {
882
- this.logger.info("Starting cleanup of orphaned worktrees");
883
-
884
- const allWorktrees = await this.listWorktrees();
885
- const deleted: string[] = [];
886
- const errors: Array<{ path: string; error: string }> = [];
887
-
888
- const associatedPathsSet = new Set(
889
- associatedWorktreePaths.map((p) => path.resolve(p)),
890
- );
891
-
892
- for (const worktree of allWorktrees) {
893
- const resolvedPath = path.resolve(worktree.worktreePath);
894
-
895
- if (!associatedPathsSet.has(resolvedPath)) {
896
- this.logger.info("Found orphaned worktree", {
897
- path: worktree.worktreePath,
898
- });
899
-
900
- try {
901
- await this.deleteWorktree(worktree.worktreePath);
902
- deleted.push(worktree.worktreePath);
903
- this.logger.info("Deleted orphaned worktree", {
904
- path: worktree.worktreePath,
905
- });
906
- } catch (error) {
907
- const errorMessage =
908
- error instanceof Error ? error.message : String(error);
909
- errors.push({
910
- path: worktree.worktreePath,
911
- error: errorMessage,
912
- });
913
- this.logger.error("Failed to delete orphaned worktree", {
914
- path: worktree.worktreePath,
915
- error: errorMessage,
916
- });
917
- }
918
- }
919
- }
920
-
921
- this.logger.info("Cleanup completed", {
922
- deleted: deleted.length,
923
- errors: errors.length,
924
- });
925
-
926
- return { deleted, errors };
927
- }
928
- }