Skip to content
Open
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
111 changes: 111 additions & 0 deletions src/main/lib/git/worktree-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export interface WorktreeConfig {
"setup-worktree-unix"?: string[] | string
"setup-worktree-windows"?: string[] | string
"setup-worktree"?: string[] | string
"archive-worktree-unix"?: string[] | string
"archive-worktree-windows"?: string[] | string
"archive-worktree"?: string[] | string
}

export type WorktreeConfigSource = "custom" | "cursor" | "1code" | null
Expand Down Expand Up @@ -161,6 +164,24 @@ export function getSetupCommands(config: WorktreeConfig): string[] | string | nu
return config["setup-worktree-unix"] ?? null
}

/**
* Get archive commands for current platform
*/
export function getArchiveCommands(config: WorktreeConfig): string[] | string | null {
// Generic archive-worktree takes priority (cross-platform)
if (config["archive-worktree"]) {
return config["archive-worktree"]
}

// Fall back to platform-specific commands
if (process.platform === "win32") {
return config["archive-worktree-windows"] ?? null
}

// Unix (darwin, linux)
return config["archive-worktree-unix"] ?? null
}

export interface WorktreeSetupResult {
success: boolean
commandsRun: number
Expand Down Expand Up @@ -249,3 +270,93 @@ export async function executeWorktreeSetup(

return result
}

export interface WorktreeArchiveResult {
success: boolean
commandsRun: number
output: string[]
errors: string[]
}

/**
* Execute worktree archive commands
* Runs when a workspace is archived (before worktree deletion if applicable)
* Useful for cleanup tasks like pruning branches, syncing, etc.
*/
export async function executeWorktreeArchive(
worktreePath: string,
mainRepoPath: string,
): Promise<WorktreeArchiveResult> {
const result: WorktreeArchiveResult = {
success: true,
commandsRun: 0,
output: [],
errors: [],
}

// Detect config from main repo
const detected = await detectWorktreeConfig(mainRepoPath)
if (!detected.config) {
result.output.push("No worktree config found, skipping archive commands")
return result
}

// Get commands for current platform
const commands = getArchiveCommands(detected.config)
if (!commands) {
result.output.push("No archive commands for current platform")
return result
}

// Normalize to array
const commandList = Array.isArray(commands) ? commands : [commands]
if (commandList.length === 0) {
result.output.push("Empty archive command list")
return result
}

console.log(`[worktree-archive] Running ${commandList.length} archive commands in ${worktreePath}`)

// Execute each command
for (const cmd of commandList) {
if (!cmd.trim()) continue

try {
result.output.push(`$ ${cmd}`)

const { stdout, stderr } = await execAsync(cmd, {
cwd: worktreePath,
env: {
...process.env,
ROOT_WORKTREE_PATH: mainRepoPath,
},
timeout: 300_000, // 5 minutes per command
})

if (stdout) {
result.output.push(stdout.trim())
}
if (stderr) {
result.output.push(`[stderr] ${stderr.trim()}`)
}

result.commandsRun++
console.log(`[worktree-archive] ✓ ${cmd}`)
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
result.errors.push(`Command failed: ${cmd}\n${errorMsg}`)
result.output.push(`[error] ${errorMsg}`)
console.error(`[worktree-archive] ✗ ${cmd}: ${errorMsg}`)
// Continue with next command, don't fail entirely
}
}

result.success = result.errors.length === 0

console.log(
`[worktree-archive] Completed: ${result.commandsRun}/${commandList.length} commands, ` +
`${result.errors.length} errors`
)

return result
}
62 changes: 42 additions & 20 deletions src/main/lib/trpc/routers/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { computeContentHash, gitCache } from "../../git/cache"
import { splitUnifiedDiffByFile } from "../../git/diff-parser"
import { execWithShellEnv } from "../../git/shell-env"
import { applyRollbackStash } from "../../git/stash"
import { executeWorktreeArchive } from "../../git/worktree-config"
import { checkInternetConnection, checkOllamaStatus } from "../../ollama"
import { terminalManager } from "../../terminal/manager"
import { publicProcedure, router } from "../index"
Expand Down Expand Up @@ -468,33 +469,54 @@ export const chatsRouter = router({
console.error(`[chats.archive] Error killing processes:`, error)
})

// Optionally delete worktree in background (don't await)
if (input.deleteWorktree && chat?.worktreePath && chat?.branch) {
// Run archive script if workspace has a worktree (in background)
if (chat?.worktreePath && chat?.branch) {
const project = db
.select()
.from(projects)
.where(eq(projects.id, chat.projectId))
.get()

if (project) {
removeWorktree(project.path, chat.worktreePath).then((worktreeResult) => {
if (worktreeResult.success) {
console.log(
`[chats.archive] Deleted worktree for workspace ${input.id}`,
)
// Clear worktreePath since it's deleted (keep branch for reference)
db.update(chats)
.set({ worktreePath: null })
.where(eq(chats.id, input.id))
.run()
} else {
console.warn(
`[chats.archive] Failed to delete worktree: ${worktreeResult.error}`,
)
}
}).catch((error) => {
console.error(`[chats.archive] Error removing worktree:`, error)
})
// Execute archive script first, then optionally delete worktree
executeWorktreeArchive(chat.worktreePath, project.path)
.then((archiveResult) => {
if (archiveResult.commandsRun > 0) {
console.log(
`[chats.archive] Ran ${archiveResult.commandsRun} archive command(s) for workspace ${input.id}`,
)
}
if (archiveResult.errors.length > 0) {
console.warn(
`[chats.archive] Archive script had ${archiveResult.errors.length} error(s)`,
)
}

// Now delete worktree if requested
if (input.deleteWorktree) {
return removeWorktree(project.path, chat.worktreePath!)
}
return null
})
.then((worktreeResult) => {
if (worktreeResult?.success) {
console.log(
`[chats.archive] Deleted worktree for workspace ${input.id}`,
)
// Clear worktreePath since it's deleted (keep branch for reference)
db.update(chats)
.set({ worktreePath: null })
.where(eq(chats.id, input.id))
.run()
} else if (worktreeResult && !worktreeResult.success) {
console.warn(
`[chats.archive] Failed to delete worktree: ${worktreeResult.error}`,
)
}
})
.catch((error) => {
console.error(`[chats.archive] Error in archive flow:`, error)
})
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/main/lib/trpc/routers/worktree-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const WorktreeConfigSchema = z.object({
"setup-worktree-unix": z.union([z.array(z.string()), z.string()]).optional(),
"setup-worktree-windows": z.union([z.array(z.string()), z.string()]).optional(),
"setup-worktree": z.union([z.array(z.string()), z.string()]).optional(),
"archive-worktree-unix": z.union([z.array(z.string()), z.string()]).optional(),
"archive-worktree-windows": z.union([z.array(z.string()), z.string()]).optional(),
"archive-worktree": z.union([z.array(z.string()), z.string()]).optional(),
})

export const worktreeConfigRouter = router({
Expand Down
Loading