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