88import { Effect , Stream } from "effect"
99import { ipcMain } from "electron"
1010import { runtime , sessionManager } from "./runtime.ts"
11+ import { ProcessSpawner } from "../../../src/services/ProcessSpawner.ts"
1112import {
12- cloneRepository ,
1313 resolveClonePaths ,
1414 countFiles ,
1515 deleteBranch ,
@@ -19,6 +19,8 @@ import {
1919} from "../../../src/domain/git/operations.ts"
2020import type { CloneOptions , PushOptions } from "../../../src/services/GitClient.ts"
2121import { isContainedIn } from "../../../src/path-validation.ts"
22+ import * as fs from "node:fs"
23+ function debugLog ( msg : string ) { fs . appendFileSync ( "/tmp/runbooks-git-debug.log" , new Date ( ) . toISOString ( ) + " " + msg + "\n" ) }
2224import { PathTraversalError } from "../../../src/errors/index.ts"
2325import { validateSessionPath } from "./path-guard.ts"
2426
@@ -35,6 +37,7 @@ export function registerGitHandlers(): void {
3537 } ,
3638 ) => {
3739 return runtime . runPromise (
40+ Effect . scoped (
3841 Effect . gen ( function * ( ) {
3942 // Resolve clone destination paths
4043 const session = yield * sessionManager . getSession ( )
@@ -59,35 +62,64 @@ export function registerGitHandlers(): void {
5962 token : params . credentials ?. token ,
6063 }
6164
62- // Get the progress stream
63- const progressStream = yield * cloneRepository (
64- params . url ,
65- paths . absolutePath ,
66- options ,
67- )
65+ // Clone the repository using direct process spawning.
66+ // We avoid the GitClient's stream-based API because
67+ // Stream.runCollect hangs in Electron's runtime.runPromise.
68+ const spawner = yield * ProcessSpawner
69+ const cloneArgs = [ "clone" , "--progress" ]
70+ if ( options . ref ) cloneArgs . push ( "--branch" , options . ref )
71+
72+ const effectiveUrl = options . token
73+ ? ( ( ) => { try { const u = new URL ( params . url ) ; u . username = "x-access-token" ; u . password = options . token ! ; return u . toString ( ) ; } catch { return params . url ; } } ) ( )
74+ : params . url
6875
69- // Stream progress events to the renderer
70- yield * Stream . runForEach ( progressStream , ( progress ) =>
76+ cloneArgs . push ( effectiveUrl , paths . absolutePath )
77+
78+ // debugLog("[git:clone] spawning git process...")
79+ const proc = yield * spawner . spawn ( "git" , cloneArgs , { } )
80+
81+ // debugLog("[git:clone] draining output stream...")
82+ yield * Stream . runForEach ( proc . output , ( line ) =>
7183 Effect . sync ( ( ) => {
7284 event . sender . send ( "git:clone-progress" , {
73- line : progress . line ,
74- timestamp : progress . timestamp ,
85+ line : line . line ,
86+ timestamp : new Date ( ) . toISOString ( ) ,
7587 } )
7688 } ) ,
7789 )
7890
79- // Count files in the cloned repo
91+ // debugLog("[git:clone] getting exit code...")
92+ const exitCode = yield * proc . exitCode
93+ // debugLog("[git:clone] exit code: " + exitCode)
94+ if ( exitCode !== 0 ) {
95+ return yield * Effect . fail (
96+ new PathTraversalError ( {
97+ path : paths . absolutePath ,
98+ message : `git clone failed with exit code ${ exitCode } ` ,
99+ } ) ,
100+ )
101+ }
102+
103+ event . sender . send ( "git:clone-progress" , {
104+ line : "Clone complete. Counting files..." ,
105+ timestamp : new Date ( ) . toISOString ( ) ,
106+ } )
107+
108+ // Count tracked files using `git ls-files` (fast, ~10ms)
80109 const fileCount = yield * countFiles ( paths . absolutePath )
81110
82111 // Register the worktree path
83112 sessionManager . registerWorkTreePath ( paths . absolutePath )
113+ // debugLog("[git:clone] registered worktree, returning result")
84114
85115 return {
86116 absolutePath : paths . absolutePath ,
87117 relativePath : paths . relativePath ,
88118 fileCount,
119+ status : "success" as const ,
89120 }
90121 } ) ,
122+ ) ,
91123 )
92124 } ,
93125 )
0 commit comments