Skip to content

Commit 0cd8c52

Browse files
Copilotpelikhan
andcommitted
Add support for copying .env files and running setup steps in git worktree operations
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 70b07bf commit 0cd8c52

File tree

5 files changed

+249
-10
lines changed

5 files changed

+249
-10
lines changed

docs/src/content/docs/reference/git.mdx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,35 @@ Git worktrees allow you to check out multiple branches of the same repository si
2020
Creates a new worktree at the specified path.
2121

2222
```typescript
23-
await git.worktreeAdd(path: string, commitish?: string): Promise<string>
23+
await git.worktreeAdd(path: string, commitish?: string, options?: {
24+
copyEnv?: boolean
25+
setupSteps?: boolean
26+
}): Promise<string>
2427
```
2528

2629
**Parameters:**
2730
- `path` - Path where the new worktree will be created
2831
- `commitish` - Optional commit, branch, or tag to checkout in the new worktree
32+
- `options` - Optional configuration object with:
33+
- `copyEnv` - Copy .env files from source directory to new worktree (default: false)
34+
- `setupSteps` - Run setup steps (e.g., npm install) in the new worktree (default: false)
2935

3036
**Example:**
3137
```javascript
32-
// Create a worktree for the main branch
38+
// Create a basic worktree for the main branch
3339
await git.worktreeAdd("/path/to/feature-worktree")
3440

3541
// Create a worktree and checkout a specific branch
3642
await git.worktreeAdd("/path/to/feature-worktree", "feature-branch")
3743

3844
// Create a worktree from a specific commit
3945
await git.worktreeAdd("/path/to/hotfix-worktree", "abc123")
46+
47+
// Create a worktree with .env files copied and setup steps run
48+
await git.worktreeAdd("/path/to/dev-worktree", "develop", {
49+
copyEnv: true,
50+
setupSteps: true
51+
})
4052
```
4153

4254
#### worktreeList
@@ -134,18 +146,22 @@ Lists all worktrees in a human-readable format.
134146

135147
#### git_worktree_add
136148

137-
Creates a new worktree with optional branch specification.
149+
Creates a new worktree with optional branch specification, environment copying, and setup steps.
138150

139151
**Parameters:**
140152
- `path` (required) - Path where the new worktree will be created
141153
- `commitish` (optional) - Commit, branch, or tag to checkout
154+
- `copyEnv` (optional) - Copy .env files from source directory to new worktree (default: false)
155+
- `setupSteps` (optional) - Run setup steps (e.g., npm install) in the new worktree (default: false)
142156

143157
**Usage:**
144158
```javascript
145159
// The LLM can create worktrees based on conversation context
146160
// Examples:
147161
// - "Create a worktree at /tmp/feature for the feature-branch"
148162
// - "Set up a worktree for testing the hotfix commit abc123"
163+
// - "Create a development worktree with environment files and run npm install"
164+
// - "Set up a complete development environment in a new worktree"
149165
```
150166

151167
#### git_worktree_remove

packages/cli/genaisrc/system.git_worktree.genai.mts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,25 @@ export default function (ctx: ChatGenerationContext) {
4949
type: "string",
5050
description: "Optional commit, branch, or tag to checkout in the new worktree",
5151
},
52+
copyEnv: {
53+
type: "boolean",
54+
description: "Copy .env files from source directory to new worktree",
55+
default: false,
56+
},
57+
setupSteps: {
58+
type: "boolean",
59+
description: "Run setup steps (e.g., npm install) in the new worktree",
60+
default: false,
61+
},
5262
},
5363
required: ["path"],
5464
},
5565
async (args) => {
56-
const { context, path, commitish } = args
57-
const result = await client.worktreeAdd(path, commitish)
66+
const { context, path, commitish, copyEnv, setupSteps } = args
67+
const result = await client.worktreeAdd(path, commitish, {
68+
copyEnv,
69+
setupSteps,
70+
})
5871
context.debug(result)
5972
return result
6073
}

packages/core/src/git.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,120 @@ bare
154154
client.exec = originalExec
155155
})
156156
})
157+
158+
describe("worktreeAdd with options", () => {
159+
test("calls basic worktreeAdd without options", async () => {
160+
const client = new GitClient(".")
161+
let capturedArgs: string[] = []
162+
163+
// Mock exec to capture arguments
164+
const originalExec = client.exec
165+
client.exec = async (args: string[]) => {
166+
capturedArgs = args
167+
return "Mock response"
168+
}
169+
170+
await client.worktreeAdd("/path/to/worktree")
171+
assert.deepEqual(capturedArgs, ["worktree", "add", "/path/to/worktree"])
172+
173+
// Restore original exec
174+
client.exec = originalExec
175+
})
176+
177+
test("calls worktreeAdd with commitish", async () => {
178+
const client = new GitClient(".")
179+
let capturedArgs: string[] = []
180+
181+
// Mock exec to capture arguments
182+
const originalExec = client.exec
183+
client.exec = async (args: string[]) => {
184+
capturedArgs = args
185+
return "Mock response"
186+
}
187+
188+
await client.worktreeAdd("/path/to/worktree", "feature-branch")
189+
assert.deepEqual(capturedArgs, ["worktree", "add", "/path/to/worktree", "feature-branch"])
190+
191+
// Restore original exec
192+
client.exec = originalExec
193+
})
194+
195+
test("handles copyEnv and setupSteps options", async () => {
196+
const client = new GitClient(".")
197+
let capturedArgs: string[] = []
198+
let copyEnvCalled = false
199+
let setupStepsCalled = false
200+
201+
// Mock exec to capture arguments
202+
const originalExec = client.exec
203+
client.exec = async (args: string[]) => {
204+
capturedArgs = args
205+
return "Mock response"
206+
}
207+
208+
// Mock the private methods by replacing them on the prototype
209+
const originalCopyEnvFiles = (client as any).copyEnvFiles
210+
const originalRunSetupSteps = (client as any).runSetupSteps
211+
212+
;(client as any).copyEnvFiles = async () => {
213+
copyEnvCalled = true
214+
}
215+
;(client as any).runSetupSteps = async () => {
216+
setupStepsCalled = true
217+
}
218+
219+
await client.worktreeAdd("/path/to/worktree", "feature-branch", {
220+
copyEnv: true,
221+
setupSteps: true
222+
})
223+
224+
assert.deepEqual(capturedArgs, ["worktree", "add", "/path/to/worktree", "feature-branch"])
225+
assert.equal(copyEnvCalled, true)
226+
assert.equal(setupStepsCalled, true)
227+
228+
// Restore original methods
229+
client.exec = originalExec
230+
;(client as any).copyEnvFiles = originalCopyEnvFiles
231+
;(client as any).runSetupSteps = originalRunSetupSteps
232+
})
233+
234+
test("skips copyEnv and setupSteps when options are false", async () => {
235+
const client = new GitClient(".")
236+
let capturedArgs: string[] = []
237+
let copyEnvCalled = false
238+
let setupStepsCalled = false
239+
240+
// Mock exec to capture arguments
241+
const originalExec = client.exec
242+
client.exec = async (args: string[]) => {
243+
capturedArgs = args
244+
return "Mock response"
245+
}
246+
247+
// Mock the private methods by replacing them on the prototype
248+
const originalCopyEnvFiles = (client as any).copyEnvFiles
249+
const originalRunSetupSteps = (client as any).runSetupSteps
250+
251+
;(client as any).copyEnvFiles = async () => {
252+
copyEnvCalled = true
253+
}
254+
;(client as any).runSetupSteps = async () => {
255+
setupStepsCalled = true
256+
}
257+
258+
await client.worktreeAdd("/path/to/worktree", "feature-branch", {
259+
copyEnv: false,
260+
setupSteps: false
261+
})
262+
263+
assert.deepEqual(capturedArgs, ["worktree", "add", "/path/to/worktree", "feature-branch"])
264+
assert.equal(copyEnvCalled, false)
265+
assert.equal(setupStepsCalled, false)
266+
267+
// Restore original methods
268+
client.exec = originalExec
269+
;(client as any).copyEnvFiles = originalCopyEnvFiles
270+
;(client as any).runSetupSteps = originalRunSetupSteps
271+
})
272+
})
157273
})

packages/core/src/git.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ import {
99
} from "./constants"
1010
import { llmifyDiff } from "./llmdiff"
1111
import { resolveFileContents } from "./file"
12-
import { tryReadText, tryStat } from "./fs"
12+
import { tryReadText, tryStat, fileExists } from "./fs"
1313
import { runtimeHost } from "./host"
1414
import { shellParse, shellQuote } from "./shell"
1515
import { arrayify, ellipse, logVerbose } from "./util"
1616
import { approximateTokens } from "./tokens"
1717
import { underscore } from "inflection"
18-
import { rm } from "node:fs/promises"
18+
import { rm, copyFile, readdir } from "node:fs/promises"
1919
import { packageResolveInstall } from "./packagemanagers"
2020
import { normalizeInt } from "./cleaners"
21+
import { join, basename } from "node:path"
2122
import { dotGenaiscriptPath } from "./workdir"
2223
import { join } from "node:path"
2324
import { genaiscriptDebug } from "./debug"
@@ -595,15 +596,37 @@ ${await this.diff({ ...options, nameOnly: true })}
595596
* Add a new worktree
596597
* @param path path to the new worktree
597598
* @param commitish optional commit, branch, or tag to checkout
599+
* @param options optional configuration for worktree creation
598600
*/
599-
async worktreeAdd(path: string, commitish?: string): Promise<string> {
601+
async worktreeAdd(path: string, commitish?: string, options?: {
602+
/**
603+
* Copy .env files from source directory to new worktree
604+
*/
605+
copyEnv?: boolean
606+
/**
607+
* Run setup steps (e.g., npm install) in the new worktree
608+
*/
609+
setupSteps?: boolean
610+
}): Promise<string> {
600611
dbg(`adding worktree: ${path}`)
601612
const args = ["worktree", "add"]
602613
args.push(path)
603614
if (commitish) {
604615
args.push(commitish)
605616
}
606-
return await this.exec(args)
617+
const result = await this.exec(args)
618+
619+
// Copy .env files if requested
620+
if (options?.copyEnv) {
621+
await this.copyEnvFiles(path)
622+
}
623+
624+
// Run setup steps if requested
625+
if (options?.setupSteps) {
626+
await this.runSetupSteps(path)
627+
}
628+
629+
return result
607630
}
608631

609632
/**
@@ -669,6 +692,67 @@ ${await this.diff({ ...options, nameOnly: true })}
669692
return await this.exec(args)
670693
}
671694

695+
/**
696+
* Copy .env files from source directory to target worktree
697+
* @param targetPath path to the new worktree
698+
*/
699+
private async copyEnvFiles(targetPath: string): Promise<void> {
700+
dbg(`copying .env files to worktree: ${targetPath}`)
701+
702+
try {
703+
const sourceDir = this.cwd || process.cwd()
704+
const files = await readdir(sourceDir)
705+
const envFiles = files.filter(file => file.startsWith('.env'))
706+
707+
for (const envFile of envFiles) {
708+
const sourcePath = join(sourceDir, envFile)
709+
const destPath = join(targetPath, envFile)
710+
711+
if (await fileExists(sourcePath)) {
712+
dbg(`copying ${envFile} to worktree`)
713+
await copyFile(sourcePath, destPath)
714+
}
715+
}
716+
717+
if (envFiles.length > 0) {
718+
dbg(`copied ${envFiles.length} .env file(s) to worktree`)
719+
}
720+
} catch (error) {
721+
dbg(`error copying .env files: ${error}`)
722+
// Don't fail the worktree creation if .env copying fails
723+
logVerbose(`Warning: Failed to copy .env files to worktree: ${error}`)
724+
}
725+
}
726+
727+
/**
728+
* Run setup steps in the new worktree
729+
* @param targetPath path to the new worktree
730+
*/
731+
private async runSetupSteps(targetPath: string): Promise<void> {
732+
dbg(`running setup steps in worktree: ${targetPath}`)
733+
734+
try {
735+
const { command, args } = await packageResolveInstall(targetPath)
736+
if (command) {
737+
dbg(`running setup command: ${command} ${args?.join(' ') || ''}`)
738+
const res = await runtimeHost.exec(undefined, command, args, {
739+
cwd: targetPath,
740+
})
741+
if (res.exitCode !== 0) {
742+
logVerbose(`Warning: Setup command failed with exit code ${res.exitCode}: ${res.stderr}`)
743+
} else {
744+
dbg(`setup steps completed successfully`)
745+
}
746+
} else {
747+
dbg(`no package manager detected, skipping setup steps`)
748+
}
749+
} catch (error) {
750+
dbg(`error running setup steps: ${error}`)
751+
// Don't fail the worktree creation if setup fails
752+
logVerbose(`Warning: Failed to run setup steps in worktree: ${error}`)
753+
}
754+
}
755+
672756
client(cwd: string) {
673757
return new GitClient(cwd)
674758
}

packages/core/src/types/prompt_template.d.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3305,8 +3305,18 @@ interface Git {
33053305
* Add a new worktree
33063306
* @param path path to the new worktree
33073307
* @param commitish optional commit, branch, or tag to checkout
3308+
* @param options optional configuration for worktree creation
33083309
*/
3309-
worktreeAdd(path: string, commitish?: string): Promise<string>
3310+
worktreeAdd(path: string, commitish?: string, options?: {
3311+
/**
3312+
* Copy .env files from source directory to new worktree
3313+
*/
3314+
copyEnv?: boolean
3315+
/**
3316+
* Run setup steps (e.g., npm install) in the new worktree
3317+
*/
3318+
setupSteps?: boolean
3319+
}): Promise<string>
33103320

33113321
/**
33123322
* List all worktrees

0 commit comments

Comments
 (0)