Skip to content

Commit 98cc262

Browse files
committed
feat: Implement a new scan CLI command to discover local Claude projects and execute specified claude commands on them.
1 parent d65906a commit 98cc262

2 files changed

Lines changed: 374 additions & 2 deletions

File tree

bin/cli.js

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import {
3232
linkLocalRepo,
3333
unlinkLocalRepo,
3434
getLocalInfo,
35-
generateReadmeFromClsyncJson
35+
generateReadmeFromClsyncJson,
36+
findClaudeDirs,
37+
scanLocalClaudeDirs
3638
} from "../src/repo-sync.js";
3739

3840
// Get terminal width
@@ -1434,6 +1436,175 @@ program
14341436
}
14351437
});
14361438

1439+
// ============================================================================
1440+
// SCAN: Find all .claude directories and run claude command
1441+
// ============================================================================
1442+
program
1443+
.command("scan")
1444+
.description("Find all directories with .claude and run claude command on each")
1445+
.option("-p, --path <paths...>", "Specific paths to search (can specify multiple)")
1446+
.option("-d, --depth <n>", "Maximum search depth", "4")
1447+
.option("-e, --exclude <patterns...>", "Patterns to exclude (e.g., node_modules)")
1448+
.option("-c, --cmd <args>", "Claude command/prompt to run (default: /review)")
1449+
.option("-l, --list", "Only list directories, don't run commands (dry run)")
1450+
.option("-i, --interactive", "Run claude interactively for each directory")
1451+
.option("-v, --verbose", "Show detailed output")
1452+
.action(async (options) => {
1453+
try {
1454+
console.log(chalk.cyan('\n 🔍 Scanning for Claude Code Projects\n'));
1455+
1456+
const searchPaths = options.path || undefined;
1457+
const maxDepth = parseInt(options.depth) || 4;
1458+
const exclude = options.exclude || undefined;
1459+
const claudeArgs = options.cmd ? options.cmd.split(' ') : undefined;
1460+
const dryRun = options.list || false;
1461+
const verbose = options.verbose || false;
1462+
1463+
if (searchPaths) {
1464+
console.log(chalk.dim(` Search paths: ${searchPaths.join(', ')}\n`));
1465+
}
1466+
1467+
// First, find all directories
1468+
const spinner = ora('Searching for .claude directories...').start();
1469+
1470+
const dirs = await findClaudeDirs({
1471+
searchPaths,
1472+
maxDepth,
1473+
exclude
1474+
});
1475+
1476+
spinner.succeed(`Found ${dirs.length} directories with .claude`);
1477+
console.log();
1478+
1479+
if (dirs.length === 0) {
1480+
console.log(chalk.yellow(' No directories with .claude found.\n'));
1481+
console.log(chalk.dim(' Tips:'));
1482+
console.log(chalk.dim(' - Check if you have any Claude Code projects'));
1483+
console.log(chalk.dim(' - Try specifying a path: clsync scan -p ~/Projects'));
1484+
console.log(chalk.dim(' - Increase search depth: clsync scan -d 6\n'));
1485+
process.exit(0);
1486+
}
1487+
1488+
// Display found directories
1489+
console.log(chalk.white.bold(' 📁 Found Projects:\n'));
1490+
for (let i = 0; i < dirs.length; i++) {
1491+
const dir = dirs[i];
1492+
const homeDir = os.homedir();
1493+
const displayPath = dir.startsWith(homeDir)
1494+
? dir.replace(homeDir, '~')
1495+
: dir;
1496+
console.log(chalk.dim(` ${String(i + 1).padStart(2)}. ${displayPath}`));
1497+
}
1498+
console.log();
1499+
1500+
if (dryRun) {
1501+
console.log(chalk.dim(' (Dry run - no commands executed)\n'));
1502+
process.exit(0);
1503+
}
1504+
1505+
// Ask for confirmation if running commands
1506+
const inquirer = await import('inquirer');
1507+
const { confirm } = await inquirer.default.prompt([
1508+
{
1509+
type: 'confirm',
1510+
name: 'confirm',
1511+
message: `Run claude ${claudeArgs ? claudeArgs.join(' ') : '/review'} on all ${dirs.length} directories?`,
1512+
default: false
1513+
}
1514+
]);
1515+
1516+
if (!confirm) {
1517+
console.log(chalk.dim('\n Cancelled.\n'));
1518+
process.exit(0);
1519+
}
1520+
1521+
console.log();
1522+
1523+
// If interactive mode, run one at a time
1524+
if (options.interactive) {
1525+
for (let i = 0; i < dirs.length; i++) {
1526+
const dir = dirs[i];
1527+
const homeDir = os.homedir();
1528+
const displayPath = dir.startsWith(homeDir)
1529+
? dir.replace(homeDir, '~')
1530+
: dir;
1531+
1532+
console.log(chalk.cyan(`\n [${i + 1}/${dirs.length}] 📁 ${displayPath}\n`));
1533+
1534+
const { runThis } = await inquirer.default.prompt([
1535+
{
1536+
type: 'confirm',
1537+
name: 'runThis',
1538+
message: 'Run claude interactively?',
1539+
default: true
1540+
}
1541+
]);
1542+
1543+
if (runThis) {
1544+
const { spawn } = await import('child_process');
1545+
await new Promise((resolve) => {
1546+
const proc = spawn('claude', claudeArgs || ['/review'], {
1547+
cwd: dir,
1548+
stdio: 'inherit',
1549+
shell: true
1550+
});
1551+
proc.on('close', resolve);
1552+
});
1553+
}
1554+
}
1555+
1556+
showSuccess('Scan Complete!');
1557+
} else {
1558+
// Non-interactive batch mode
1559+
const results = await scanLocalClaudeDirs({
1560+
searchPaths,
1561+
maxDepth,
1562+
exclude,
1563+
claudeArgs,
1564+
dryRun: false,
1565+
sequential: true,
1566+
onProgress: (msg) => {
1567+
if (verbose) {
1568+
console.log(chalk.dim(` ${msg}`));
1569+
}
1570+
}
1571+
});
1572+
1573+
console.log(chalk.cyan('\n 📊 Results:\n'));
1574+
1575+
let successCount = 0;
1576+
let failCount = 0;
1577+
1578+
for (const result of results.results) {
1579+
const homeDir = os.homedir();
1580+
const displayPath = result.dir.startsWith(homeDir)
1581+
? result.dir.replace(homeDir, '~')
1582+
: result.dir;
1583+
1584+
if (result.success) {
1585+
console.log(chalk.green(` ✓ ${displayPath}`));
1586+
successCount++;
1587+
} else {
1588+
console.log(chalk.red(` ✗ ${displayPath}`));
1589+
if (verbose && result.error) {
1590+
console.log(chalk.dim(` Error: ${result.error}`));
1591+
}
1592+
failCount++;
1593+
}
1594+
}
1595+
1596+
console.log();
1597+
console.log(chalk.dim(` Summary: ${successCount} succeeded, ${failCount} failed\n`));
1598+
1599+
showSuccess('Scan Complete!');
1600+
}
1601+
1602+
} catch (error) {
1603+
showError(error.message);
1604+
process.exit(1);
1605+
}
1606+
});
1607+
14371608
program.parse();
14381609

14391610
} // end of else block for interactive mode

0 commit comments

Comments
 (0)