@@ -34,7 +34,10 @@ import {
3434 getLocalInfo ,
3535 generateReadmeFromClsyncJson ,
3636 findClaudeDirs ,
37- scanLocalClaudeDirs
37+ scanLocalClaudeDirs ,
38+ getClaudeDirsWithCache ,
39+ getScanCacheInfo ,
40+ clearScanCache
3841} from "../src/repo-sync.js" ;
3942
4043// Get terminal width
@@ -1448,9 +1451,170 @@ program
14481451 . option ( "-c, --cmd <args>" , "Claude command/prompt to run (default: /review)" )
14491452 . option ( "-l, --list" , "Only list directories, don't run commands (dry run)" )
14501453 . option ( "-i, --interactive" , "Run claude interactively for each directory" )
1454+ . option ( "-r, --refresh" , "Force fresh scan (ignore cache)" )
1455+ . option ( "-g, --go" , "Select a project and navigate to it" )
1456+ . option ( "-o, --open" , "Open selected project in Finder/file manager" )
1457+ . option ( "--cache-info" , "Show cache information" )
1458+ . option ( "--clear-cache" , "Clear scan cache" )
14511459 . option ( "-v, --verbose" , "Show detailed output" )
14521460 . action ( async ( options ) => {
14531461 try {
1462+ // Handle cache info
1463+ if ( options . cacheInfo ) {
1464+ console . log ( chalk . cyan ( '\n 📦 Scan Cache Info\n' ) ) ;
1465+
1466+ const info = await getScanCacheInfo ( ) ;
1467+
1468+ if ( ! info . exists ) {
1469+ console . log ( chalk . yellow ( ' No cache found.\n' ) ) ;
1470+ console . log ( chalk . dim ( ' Run: clsync scan --list\n' ) ) ;
1471+ } else {
1472+ console . log ( chalk . white . bold ( ' Cache Status:' ) + ( info . isValid ? chalk . green ( ' Valid' ) : chalk . yellow ( ' Expired' ) ) ) ;
1473+ console . log ( chalk . dim ( ` Directories: ${ info . dirs } ` ) ) ;
1474+ console . log ( chalk . dim ( ` Scanned at: ${ info . scannedAt } ` ) ) ;
1475+ console . log ( chalk . dim ( ` Age: ${ info . ageMinutes } minutes` ) ) ;
1476+ console . log ( chalk . dim ( ` Location: ~/.clsync/scan-cache.json\n` ) ) ;
1477+
1478+ if ( ! info . isValid ) {
1479+ console . log ( chalk . dim ( ' 💡 Cache expired. Run: clsync scan --refresh\n' ) ) ;
1480+ }
1481+ }
1482+ process . exit ( 0 ) ;
1483+ }
1484+
1485+ // Handle clear cache
1486+ if ( options . clearCache ) {
1487+ console . log ( chalk . cyan ( '\n 🗑️ Clearing scan cache...\n' ) ) ;
1488+ await clearScanCache ( ) ;
1489+ console . log ( chalk . green ( ' ✓ Cache cleared\n' ) ) ;
1490+ process . exit ( 0 ) ;
1491+ }
1492+
1493+ // Handle --go (select project)
1494+ if ( options . go || options . open ) {
1495+ console . log ( chalk . cyan ( '\n 📁 Select a Project\n' ) ) ;
1496+
1497+ const { dirs, fromCache, cacheAge } = await getClaudeDirsWithCache ( {
1498+ useCache : true ,
1499+ forceRefresh : false
1500+ } ) ;
1501+
1502+ if ( dirs . length === 0 ) {
1503+ console . log ( chalk . yellow ( ' No projects found in cache.\n' ) ) ;
1504+ console . log ( chalk . dim ( ' Run: clsync scan --list\n' ) ) ;
1505+ process . exit ( 0 ) ;
1506+ }
1507+
1508+ if ( fromCache ) {
1509+ console . log ( chalk . dim ( ` Using cache (${ cacheAge } m old, ${ dirs . length } projects)\n` ) ) ;
1510+ }
1511+
1512+ const inquirer = await import ( 'inquirer' ) ;
1513+ const homeDir = os . homedir ( ) ;
1514+
1515+ const { selectedDir } = await inquirer . default . prompt ( [
1516+ {
1517+ type : 'list' ,
1518+ name : 'selectedDir' ,
1519+ message : 'Select a project:' ,
1520+ choices : [
1521+ ...dirs . map ( ( dir , i ) => {
1522+ const displayPath = dir . startsWith ( homeDir )
1523+ ? dir . replace ( homeDir , '~' )
1524+ : dir ;
1525+ return { name : `${ String ( i + 1 ) . padStart ( 2 ) } . ${ displayPath } ` , value : dir } ;
1526+ } ) ,
1527+ new inquirer . default . Separator ( ) ,
1528+ { name : '❌ Cancel' , value : null }
1529+ ] ,
1530+ pageSize : 15
1531+ }
1532+ ] ) ;
1533+
1534+ if ( ! selectedDir ) {
1535+ console . log ( chalk . dim ( '\n Cancelled.\n' ) ) ;
1536+ process . exit ( 0 ) ;
1537+ }
1538+
1539+ const displayPath = selectedDir . startsWith ( homeDir )
1540+ ? selectedDir . replace ( homeDir , '~' )
1541+ : selectedDir ;
1542+
1543+ // If --open, open in Finder
1544+ if ( options . open ) {
1545+ console . log ( chalk . cyan ( `\n 📂 Opening: ${ displayPath } \n` ) ) ;
1546+ const { exec } = await import ( 'child_process' ) ;
1547+ const { promisify } = await import ( 'util' ) ;
1548+ const execPromise = promisify ( exec ) ;
1549+
1550+ // macOS: open, Linux: xdg-open, Windows: explorer
1551+ const openCmd = process . platform === 'darwin' ? 'open' :
1552+ process . platform === 'win32' ? 'explorer' : 'xdg-open' ;
1553+ await execPromise ( `${ openCmd } "${ selectedDir } "` ) ;
1554+ console . log ( chalk . green ( ' ✓ Opened in file manager\n' ) ) ;
1555+ process . exit ( 0 ) ;
1556+ }
1557+
1558+ // Show action menu for --go
1559+ const { action } = await inquirer . default . prompt ( [
1560+ {
1561+ type : 'list' ,
1562+ name : 'action' ,
1563+ message : `What would you like to do with ${ displayPath } ?` ,
1564+ choices : [
1565+ { name : '📋 Copy path to clipboard' , value : 'copy' } ,
1566+ { name : '📂 Open in Finder' , value : 'open' } ,
1567+ { name : '🤖 Run claude here' , value : 'claude' } ,
1568+ { name : '💻 Print cd command' , value : 'cd' } ,
1569+ new inquirer . default . Separator ( ) ,
1570+ { name : '← Back' , value : null }
1571+ ]
1572+ }
1573+ ] ) ;
1574+
1575+ if ( ! action ) {
1576+ process . exit ( 0 ) ;
1577+ }
1578+
1579+ const { exec } = await import ( 'child_process' ) ;
1580+ const { promisify } = await import ( 'util' ) ;
1581+ const execPromise = promisify ( exec ) ;
1582+
1583+ switch ( action ) {
1584+ case 'copy' :
1585+ // macOS: pbcopy, Linux: xclip, Windows: clip
1586+ const copyCmd = process . platform === 'darwin' ? 'pbcopy' :
1587+ process . platform === 'win32' ? 'clip' : 'xclip -selection clipboard' ;
1588+ await execPromise ( `echo -n "${ selectedDir } " | ${ copyCmd } ` ) ;
1589+ console . log ( chalk . green ( `\n ✓ Copied to clipboard: ${ displayPath } \n` ) ) ;
1590+ break ;
1591+
1592+ case 'open' :
1593+ const openCmd = process . platform === 'darwin' ? 'open' :
1594+ process . platform === 'win32' ? 'explorer' : 'xdg-open' ;
1595+ await execPromise ( `${ openCmd } "${ selectedDir } "` ) ;
1596+ console . log ( chalk . green ( `\n ✓ Opened: ${ displayPath } \n` ) ) ;
1597+ break ;
1598+
1599+ case 'claude' :
1600+ console . log ( chalk . cyan ( `\n 🤖 Starting Claude in: ${ displayPath } \n` ) ) ;
1601+ const { spawn } = await import ( 'child_process' ) ;
1602+ spawn ( 'claude' , [ ] , {
1603+ cwd : selectedDir ,
1604+ stdio : 'inherit' ,
1605+ shell : true
1606+ } ) ;
1607+ break ;
1608+
1609+ case 'cd' :
1610+ console . log ( chalk . cyan ( `\n 💡 Run this command:\n` ) ) ;
1611+ console . log ( chalk . white . bold ( ` cd "${ selectedDir } "\n` ) ) ;
1612+ break ;
1613+ }
1614+
1615+ process . exit ( 0 ) ;
1616+ }
1617+
14541618 console . log ( chalk . cyan ( '\n 🔍 Scanning for Claude Code Projects\n' ) ) ;
14551619
14561620 const searchPaths = options . path || undefined ;
@@ -1459,21 +1623,27 @@ program
14591623 const claudeArgs = options . cmd ? options . cmd . split ( ' ' ) : undefined ;
14601624 const dryRun = options . list || false ;
14611625 const verbose = options . verbose || false ;
1626+ const forceRefresh = options . refresh || false ;
14621627
14631628 if ( searchPaths ) {
14641629 console . log ( chalk . dim ( ` Search paths: ${ searchPaths . join ( ', ' ) } \n` ) ) ;
14651630 }
14661631
1467- // First, find all directories
1632+ // Get directories (with cache support)
14681633 const spinner = ora ( 'Searching for .claude directories...' ) . start ( ) ;
14691634
1470- const dirs = await findClaudeDirs ( {
1635+ const { dirs, fromCache , scannedAt , cacheAge } = await getClaudeDirsWithCache ( {
14711636 searchPaths,
14721637 maxDepth,
1473- exclude
1638+ exclude,
1639+ forceRefresh,
1640+ useCache : ! forceRefresh
14741641 } ) ;
14751642
1476- spinner . succeed ( `Found ${ dirs . length } directories with .claude` ) ;
1643+ if ( fromCache ) {
1644+ spinner . succeed ( `Found ${ dirs . length } directories ${ chalk . dim ( `(from cache, ${ cacheAge } m old)` ) } ` ) ;
1645+ } else {
1646+ spinner . succeed ( `Found ${ dirs . length } directories with .claude ${ chalk . dim ( '(fresh scan, cached)' ) } ` ) ; }
14771647 console . log ( ) ;
14781648
14791649 if ( dirs . length === 0 ) {
@@ -1498,7 +1668,11 @@ program
14981668 console . log ( ) ;
14991669
15001670 if ( dryRun ) {
1501- console . log ( chalk . dim ( ' (Dry run - no commands executed)\n' ) ) ;
1671+ if ( fromCache ) {
1672+ console . log ( chalk . dim ( ` (Cached ${ cacheAge } m ago. Use --refresh for fresh scan)\n` ) ) ;
1673+ } else {
1674+ console . log ( chalk . dim ( ' (Results cached to ~/.clsync/scan-cache.json)\n' ) ) ;
1675+ }
15021676 process . exit ( 0 ) ;
15031677 }
15041678
0 commit comments