Skip to content

Commit 9b61922

Browse files
committed
feat: implement scan cache for Claude directories and add interactive project selection
1 parent 98cc262 commit 9b61922

2 files changed

Lines changed: 354 additions & 12 deletions

File tree

bin/cli.js

Lines changed: 180 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)