@@ -134,10 +134,15 @@ const main = async () => {
134134 process . exit ( 0 )
135135 }
136136
137- if ( await useConfig ( ) ) process . exit ( 0 )
137+ // `gitpick -i` with no args — browse cwd
138+ if ( values . interactive ) {
139+ positionals . push ( "." )
140+ } else {
141+ if ( await useConfig ( ) ) process . exit ( 0 )
138142
139- console . log ( helpMessage )
140- process . exit ( 0 )
143+ console . log ( helpMessage )
144+ process . exit ( 0 )
145+ }
141146 }
142147
143148 if ( positionals [ 0 ] === "clone" ) {
@@ -159,6 +164,156 @@ const main = async () => {
159164 watch : values . watch ,
160165 }
161166
167+ // Local directory interactive mode — detect local paths or
168+ // non-URL-like positionals when -i is set (e.g. `gitpick -i target`)
169+ const isLocalPath =
170+ url === "." ||
171+ url . startsWith ( "./" ) ||
172+ url . startsWith ( "../" ) ||
173+ url . startsWith ( "/" ) ||
174+ url . startsWith ( "~/" ) ||
175+ ( options . interactive && ! url . includes ( "/" ) && ! url . includes ( "." ) && ! url . startsWith ( "http" ) )
176+
177+ if ( isLocalPath && options . interactive ) {
178+ // If url doesn't look like a real path, it's the target (e.g. `gitpick -i hello`)
179+ if (
180+ ! fs . existsSync ( path . resolve ( url . startsWith ( "~/" ) ? url . replace ( "~" , os . homedir ( ) ) : url ) )
181+ ) {
182+ target = url
183+ url = "."
184+ }
185+ if ( ! process . stdout . isTTY ) {
186+ throw new Error ( "Interactive mode requires a TTY" )
187+ }
188+
189+ const resolvedSource = path . resolve (
190+ url . startsWith ( "~/" ) ? url . replace ( "~" , os . homedir ( ) ) : url ,
191+ )
192+
193+ if ( ! fs . existsSync ( resolvedSource ) ) {
194+ throw new Error ( `Directory not found: ${ url } ` )
195+ }
196+ if ( ! fs . statSync ( resolvedSource ) . isDirectory ( ) ) {
197+ throw new Error ( `Not a directory: ${ url } ` )
198+ }
199+
200+ const targetDir = target ? path . resolve ( target ) : null
201+
202+ const entries : TreeEntry [ ] = [ ]
203+ async function walkLocal ( dir : string , rel : string ) {
204+ const items = await fs . promises . readdir ( dir , { withFileTypes : true } )
205+ for ( const item of items ) {
206+ if ( item . name === ".git" || item . name === "node_modules" ) continue
207+ const itemRel = rel ? `${ rel } /${ item . name } ` : item . name
208+ const itemPath = path . join ( dir , item . name )
209+ if ( item . isSymbolicLink ( ) ) {
210+ const linkTarget = await fs . promises . readlink ( itemPath )
211+ let resolvedIsDir = false
212+ try {
213+ resolvedIsDir = ( await fs . promises . stat ( itemPath ) ) . isDirectory ( )
214+ } catch { }
215+ entries . push ( {
216+ path : itemRel ,
217+ type : "symlink" ,
218+ linkTarget : resolvedIsDir ? linkTarget + "/" : linkTarget ,
219+ } )
220+ } else if ( item . isDirectory ( ) ) {
221+ entries . push ( { path : itemRel , type : "tree" } )
222+ await walkLocal ( itemPath , itemRel )
223+ } else {
224+ const stat = await fs . promises . stat ( itemPath )
225+ entries . push ( { path : itemRel , type : "blob" , size : stat . size } )
226+ }
227+ }
228+ }
229+ await walkLocal ( resolvedSource , "" )
230+
231+ if ( ! entries . length ) {
232+ console . log ( yellow ( "\nDirectory is empty." ) )
233+ process . exit ( 0 )
234+ }
235+
236+ const selected = await interactivePicker (
237+ entries ,
238+ `${ displayPath ( resolvedSource ) } ` ,
239+ resolvedSource ,
240+ )
241+
242+ if ( ! selected . length ) {
243+ console . log ( "\nNo files selected." )
244+ process . exit ( 0 )
245+ }
246+
247+ if ( options . dryRun ) {
248+ console . log (
249+ `\n${ green ( "✔" ) } Would pick ${ selected . length } path${ selected . length !== 1 ? "s" : "" } :` ,
250+ )
251+ for ( const sel of selected ) console . log ( ` ${ sel } ` )
252+ console . log ( )
253+ process . exit ( 0 )
254+ }
255+
256+ if ( ! targetDir ) {
257+ // No target - just list selected paths
258+ console . log (
259+ `\n${ green ( "✔" ) } Selected ${ selected . length } path${ selected . length !== 1 ? "s" : "" } :` ,
260+ )
261+ for ( const sel of selected ) console . log ( ` ${ sel } ` )
262+ console . log ( )
263+ process . exit ( 0 )
264+ }
265+
266+ if ( path . resolve ( resolvedSource ) === path . resolve ( targetDir ) ) {
267+ throw new Error ( "Source and target directories are the same" )
268+ }
269+
270+ console . log (
271+ `\n${ green ( "✔" ) } Picking ${ selected . length } selected path${ selected . length !== 1 ? "s" : "" } ...` ,
272+ )
273+
274+ options . overwrite = options . overwrite || options . force
275+ if ( fs . existsSync ( targetDir ) && ! options . overwrite ) {
276+ if ( ( await fs . promises . readdir ( targetDir ) ) . length ) {
277+ console . log (
278+ `${ yellow ( `\nWarning: The target directory exists at ${ green ( target ! ) } and is not empty. Use ${ cyan ( "-f" ) } or ${ cyan ( "-o" ) } to overwrite.` ) } ` ,
279+ )
280+ process . exit ( 1 )
281+ }
282+ }
283+
284+ await fs . promises . mkdir ( targetDir , { recursive : true } )
285+
286+ let copiedFiles = 0
287+ for ( const sel of selected ) {
288+ const src = path . join ( resolvedSource , sel )
289+ const dest = path . join ( targetDir , sel )
290+ const stat = await fs . promises . stat ( src ) . catch ( ( ) => null )
291+ if ( ! stat ) continue
292+
293+ if ( stat . isDirectory ( ) ) {
294+ await fs . promises . mkdir ( dest , { recursive : true } )
295+ const files = await copyDir ( src , dest )
296+ copiedFiles += files . length
297+ } else {
298+ await fs . promises . mkdir ( path . dirname ( dest ) , { recursive : true } )
299+ await fs . promises . copyFile ( src , dest )
300+ copiedFiles ++
301+ }
302+ }
303+
304+ console . log (
305+ green (
306+ `✔ Copied ${ copiedFiles } file${ copiedFiles !== 1 ? "s" : "" } to ${ displayPath ( targetDir ) } ` ,
307+ ) ,
308+ )
309+ if ( options . tree ) {
310+ process . stdout . write ( `\n${ bold ( cyan ( displayPath ( targetDir ) ) ) } \n` )
311+ await printTree ( targetDir )
312+ process . stdout . write ( "\n" )
313+ }
314+ process . exit ( 0 )
315+ }
316+
162317 const silent = options . tree || options . quiet
163318
164319 if ( ! silent ) {
0 commit comments