Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions cmd/agent-deck/launch_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/asheshgoplani/agent-deck/internal/git"
"github.com/asheshgoplani/agent-deck/internal/session"
"github.com/asheshgoplani/agent-deck/internal/vcs"
)

// handleLaunch combines add + start + optional send into a single command.
Expand Down Expand Up @@ -141,18 +142,14 @@ func handleLaunch(profile string, args []string) {
}

// Handle worktree creation
var worktreePath, worktreeRepoRoot string
var worktreePath, worktreeRepoRoot, worktreeType string
if wtBranch != "" {
if !git.IsGitRepo(path) {
out.Error(fmt.Sprintf("%s is not a git repository", path), ErrCodeInvalidOperation)
os.Exit(1)
}

repoRoot, err := git.GetWorktreeBaseRoot(path)
backend, err := detectAndCreateBackend(path)
if err != nil {
out.Error(fmt.Sprintf("failed to get repo root: %v", err), ErrCodeInvalidOperation)
out.Error(fmt.Sprintf("%v", err), ErrCodeInvalidOperation)
os.Exit(1)
}
worktreeType = string(backend.Type())

if err := git.ValidateBranchName(wtBranch); err != nil {
out.Error(fmt.Sprintf("invalid branch name: %v", err), ErrCodeInvalidOperation)
Expand All @@ -165,16 +162,15 @@ func handleLaunch(profile string, args []string) {
location = *worktreeLocation
}

worktreePath = git.WorktreePath(git.WorktreePathOptions{
worktreePath = backend.WorktreePath(vcs.WorktreePathOptions{
Branch: wtBranch,
Location: location,
RepoDir: repoRoot,
SessionID: git.GeneratePathID(),
Template: wtSettings.Template(),
})

// Check for an existing worktree for this branch before creating a new one
if existingPath, err := git.GetWorktreeForBranch(repoRoot, wtBranch); err == nil && existingPath != "" {
if existingPath, err := backend.GetWorktreeForBranch(wtBranch); err == nil && existingPath != "" {
fmt.Fprintf(os.Stderr, "Reusing existing worktree at %s for branch %s\n", existingPath, wtBranch)
worktreePath = existingPath
} else {
Expand All @@ -188,13 +184,13 @@ func handleLaunch(profile string, args []string) {
os.Exit(1)
}

if err := git.CreateWorktree(repoRoot, worktreePath, wtBranch); err != nil {
if err := backend.CreateWorktree(worktreePath, wtBranch); err != nil {
out.Error(fmt.Sprintf("failed to create worktree: %v", err), ErrCodeInvalidOperation)
os.Exit(1)
}
}

worktreeRepoRoot = repoRoot
worktreeRepoRoot = backend.RepoDir()
path = worktreePath
}

Expand Down Expand Up @@ -272,6 +268,7 @@ func handleLaunch(profile string, args []string) {
newInstance.WorktreePath = worktreePath
newInstance.WorktreeRepoRoot = worktreeRepoRoot
newInstance.WorktreeBranch = wtBranch
newInstance.WorktreeType = worktreeType
}

if *resumeSession != "" {
Expand Down
37 changes: 18 additions & 19 deletions cmd/agent-deck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/asheshgoplani/agent-deck/internal/statedb"
"github.com/asheshgoplani/agent-deck/internal/ui"
"github.com/asheshgoplani/agent-deck/internal/update"
"github.com/asheshgoplani/agent-deck/internal/vcs"
"github.com/asheshgoplani/agent-deck/internal/web"
)

Expand Down Expand Up @@ -1014,20 +1015,14 @@ func handleAdd(profile string, args []string) {
}

// Handle worktree creation
var worktreePath, worktreeRepoRoot string
var worktreePath, worktreeRepoRoot, worktreeType string
if wtBranch != "" {
// Validate path is a git repo
if !git.IsGitRepo(path) {
fmt.Fprintf(os.Stderr, "Error: %s is not a git repository\n", path)
os.Exit(1)
}

// Get repo root (resolve through worktrees to prevent nesting)
repoRoot, err := git.GetWorktreeBaseRoot(path)
backend, err := detectAndCreateBackend(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to get repo root: %v\n", err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
worktreeType = string(backend.Type())

// Pre-validate branch name for better error messages
if err := git.ValidateBranchName(wtBranch); err != nil {
Expand All @@ -1043,16 +1038,15 @@ func handleAdd(profile string, args []string) {
}

// Generate worktree path
worktreePath = git.WorktreePath(git.WorktreePathOptions{
worktreePath = backend.WorktreePath(vcs.WorktreePathOptions{
Branch: wtBranch,
Location: location,
RepoDir: repoRoot,
SessionID: git.GeneratePathID(),
Template: wtSettings.Template(),
})

// Check for an existing worktree for this branch before creating a new one
if existingPath, err := git.GetWorktreeForBranch(repoRoot, wtBranch); err == nil && existingPath != "" {
if existingPath, err := backend.GetWorktreeForBranch(wtBranch); err == nil && existingPath != "" {
fmt.Fprintf(os.Stderr, "Reusing existing worktree at %s for branch %s\n", existingPath, wtBranch)
worktreePath = existingPath
} else {
Expand All @@ -1064,7 +1058,7 @@ func handleAdd(profile string, args []string) {

// Create worktree atomically (git handles existence checks).
// This avoids a TOCTOU race from separate check-then-create steps.
if err := git.CreateWorktree(repoRoot, worktreePath, wtBranch); err != nil {
if err := backend.CreateWorktree(worktreePath, wtBranch); err != nil {
if isWorktreeAlreadyExistsError(err) {
fmt.Fprintf(os.Stderr, "Error: worktree already exists at %s\n", worktreePath)
fmt.Fprintf(os.Stderr, "Tip: Use 'agent-deck add %s' to add the existing worktree\n", worktreePath)
Expand All @@ -1076,7 +1070,7 @@ func handleAdd(profile string, args []string) {

fmt.Printf("Created worktree at: %s\n", worktreePath)
}
worktreeRepoRoot = repoRoot
worktreeRepoRoot = backend.RepoDir()
// Update path to point to worktree so session uses worktree as working directory
path = worktreePath
}
Expand Down Expand Up @@ -1133,6 +1127,7 @@ func handleAdd(profile string, args []string) {
newInstance.WorktreePath = worktreePath
newInstance.WorktreeRepoRoot = worktreeRepoRoot
newInstance.WorktreeBranch = wtBranch
newInstance.WorktreeType = worktreeType
}

// Apply sandbox config if requested.
Expand Down Expand Up @@ -1580,12 +1575,16 @@ func handleRemove(profile string, args []string) {

// Clean up worktree directory if this is a worktree session
if inst.IsWorktree() {
if err := git.RemoveWorktree(inst.WorktreeRepoRoot, inst.WorktreePath, false); err != nil {
if !*jsonOutput {
fmt.Printf("Warning: failed to remove worktree: %v\n", err)
if wtBackend, err := inst.Backend(); err == nil {
if err := wtBackend.RemoveWorktree(inst.WorktreePath, false); err != nil {
if !*jsonOutput {
fmt.Printf("Warning: failed to remove worktree: %v\n", err)
}
}
_ = wtBackend.PruneWorktrees()
} else if !*jsonOutput {
fmt.Printf("Warning: failed to initialize VCS for worktree cleanup: %v\n", err)
}
_ = git.PruneWorktrees(inst.WorktreeRepoRoot)
}

// Direct SQL DELETE first to prevent resurrection by concurrent TUI force saves.
Expand Down
29 changes: 16 additions & 13 deletions cmd/agent-deck/session_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/asheshgoplani/agent-deck/internal/session"
"github.com/asheshgoplani/agent-deck/internal/tmux"
"github.com/asheshgoplani/agent-deck/internal/ui"
"github.com/asheshgoplani/agent-deck/internal/vcs"
)

// handleSession dispatches session subcommands
Expand Down Expand Up @@ -472,40 +473,38 @@ func handleSessionFork(profile string, args []string) {
if *worktreeBranchLong != "" {
wtBranch = *worktreeBranchLong
}
_ = *newBranch || *newBranchLong
createNewBranch := *newBranch || *newBranchLong

// Handle worktree creation
var opts *session.ClaudeOptions
var worktreeType string
if wtBranch != "" {
if !git.IsGitRepo(inst.ProjectPath) {
out.Error("session path is not a git repository", ErrCodeInvalidOperation)
os.Exit(1)
}
repoRoot, err := git.GetWorktreeBaseRoot(inst.ProjectPath)
backend, err := detectAndCreateBackend(inst.ProjectPath)
if err != nil {
out.Error(fmt.Sprintf("failed to get repo root: %v", err), ErrCodeInvalidOperation)
out.Error(fmt.Sprintf("%v", err), ErrCodeInvalidOperation)
os.Exit(1)
}
worktreeType = string(backend.Type())

if err := git.ValidateBranchName(wtBranch); err != nil {
if err := git.ValidateBranchName(wtBranch); !createNewBranch && !backend.BranchExists(wtBranch) {
out.Error(fmt.Sprintf("invalid branch name: %v", err), ErrCodeInvalidOperation)
os.Exit(1)
}

wtSettings := session.GetWorktreeSettings()
worktreePath := git.WorktreePath(git.WorktreePathOptions{
worktreePath := backend.WorktreePath(vcs.WorktreePathOptions{
Branch: wtBranch,
Location: wtSettings.DefaultLocation,
RepoDir: repoRoot,
SessionID: git.GeneratePathID(),
Template: wtSettings.Template(),
})

// Check for an existing worktree for this branch before creating a new one
if existingPath, err := git.GetWorktreeForBranch(repoRoot, wtBranch); err == nil && existingPath != "" {
if existingPath, err := backend.GetWorktreeForBranch(wtBranch); err == nil && existingPath != "" {
fmt.Fprintf(os.Stderr, "Reusing existing worktree at %s for branch %s\n", existingPath, wtBranch)
worktreePath = existingPath
} else {

if _, statErr := os.Stat(worktreePath); statErr == nil {
out.Error(fmt.Sprintf("worktree path already exists: %s", worktreePath), ErrCodeInvalidOperation)
os.Exit(1)
Expand All @@ -516,7 +515,7 @@ func handleSessionFork(profile string, args []string) {
os.Exit(1)
}

if err := git.CreateWorktree(repoRoot, worktreePath, wtBranch); err != nil {
if err := backend.CreateWorktree(worktreePath, wtBranch); err != nil {
out.Error(fmt.Sprintf("worktree creation failed: %v", err), ErrCodeInvalidOperation)
os.Exit(1)
}
Expand All @@ -526,7 +525,7 @@ func handleSessionFork(profile string, args []string) {
opts = session.NewClaudeOptions(userConfig)
opts.WorkDir = worktreePath
opts.WorktreePath = worktreePath
opts.WorktreeRepoRoot = repoRoot
opts.WorktreeRepoRoot = backend.RepoDir()
opts.WorktreeBranch = wtBranch
}

Expand All @@ -537,6 +536,10 @@ func handleSessionFork(profile string, args []string) {
os.Exit(1)
}

if worktreeType != "" {
forkedInst.WorktreeType = worktreeType
}

// Apply sandbox config if requested.
if *sandbox {
forkedInst.Sandbox = session.NewSandboxConfig(*sandboxImage)
Expand Down
25 changes: 25 additions & 0 deletions cmd/agent-deck/vcs_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"

"github.com/asheshgoplani/agent-deck/internal/git"
"github.com/asheshgoplani/agent-deck/internal/jujutsu"
"github.com/asheshgoplani/agent-deck/internal/vcs"
)

// detectAndCreateBackend detects the VCS type for the given directory and
// creates the appropriate backend. Returns the backend and the WorktreeType
// string to store on the session Instance.
func detectAndCreateBackend(dir string) (vcs.Backend, error) {
var b vcs.Backend
b, err := jujutsu.NewJJBackend(dir)
if err == nil {
return b, nil
}
b, err = git.NewGitBackend(dir)
if err == nil {
return b, nil
}
return nil, fmt.Errorf("failed to initialize backend: %w", err)
}
Loading