use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use cc_sync_session::{RealFileSystem, SessionSyncer, SyncOptions};
use log::warn;
use cc_sync_session::file_path_converter::dir_path_to_claude_code_stype;
#[derive(Parser, Debug)]
#[command(name = "cc-sync-session")]
#[command(about = "Sync Claude Code session files to a repository", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
}
#[derive(Subcommand, Debug)]
enum Commands {
Init {
#[arg(short = 'r', long)]
repo_dir: Option<PathBuf>,
},
Sync {
#[arg(short, long)]
source_dir: Option<PathBuf>,
#[arg(short = 'r', long)]
repo_dir: Option<PathBuf>,
#[arg(short, long)]
dry_run: bool,
},
}
fn find_repo_dir(start: &Path) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
let git_dir = current.join(".git");
let ccss_dir = current.join(".claude").join("ccss_sessions");
log::debug!("Checking directory: {} git: {}, ccss: {}",
current.display(),
git_dir.exists(),
ccss_dir.exists());
if git_dir.exists() && ccss_dir.exists() {
return Some(current);
}
if !current.pop() {
break;
}
}
None
}
fn find_git_repo(start: &Path) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
let git_dir = current.join(".git");
if git_dir.exists() {
return Some(current);
}
if !current.pop() {
break;
}
}
None
}
fn init_command(repo_dir: Option<PathBuf>) -> Result<()> {
let repo_dir = match repo_dir {
Some(dir) => dir,
None => {
let current_dir = std::env::current_dir()
.context("Failed to get current directory")?;
find_git_repo(¤t_dir)
.context("No git repository found in current directory or parent directories")?
}
};
let ccss_dir = repo_dir.join(".claude").join("ccss_sessions");
std::fs::create_dir_all(&ccss_dir)
.context("Failed to create .claude/ccss_sessions directory")?;
let gitkeep_path = ccss_dir.join(".gitkeep");
std::fs::write(&gitkeep_path, "")
.context("Failed to create .gitkeep file")?;
println!("Initialized session sync directory at: {}", ccss_dir.display());
println!("Created: {}", gitkeep_path.display());
Ok(())
}
fn sync_command(source_dir: Option<PathBuf>, repo_dir: Option<PathBuf>, dry_run: bool, verbose: bool) -> Result<()> {
let repo_dir = match repo_dir {
Some(dir) => dir,
None => {
let current_dir = std::env::current_dir()
.context("Failed to get current directory")?;
find_repo_dir(¤t_dir)
.context("No repository with .claude/ccss_sessions found. Run 'cc-sync-session init' first")?
}
};
log::info!("Using repository directory: {}", repo_dir.display());
let repo_dir_cc_style = dir_path_to_claude_code_stype(repo_dir.clone())?;
log::debug!("Converted repository directory to Claude Code style: {}", repo_dir_cc_style);
let source_root_dir = match source_dir {
Some(dir) => dir,
None => {
if let Ok(env_source) = std::env::var("CC_SYNC_SESSION_SOURCE_DIR") {
PathBuf::from(env_source)
} else {
let home = dirs::home_dir()
.context("Failed to get home directory")?;
home.join(".claude").join("projects")
}
}
};
let source_dir = source_root_dir.join(&repo_dir_cc_style);
log::info!("Using source directory: {}", source_dir.display());
let target_dir = repo_dir.join(".claude").join("ccss_sessions");
if !source_dir.exists() {
anyhow::bail!("Source directory does not exist: {}", source_dir.display());
}
if !source_dir.is_dir() {
anyhow::bail!("Source path is not a directory: {}", source_dir.display());
}
if !target_dir.exists() {
anyhow::bail!("Target directory does not exist: {}. Run 'cc-sync-session init' first", target_dir.display());
}
println!("Syncing Claude Code sessions:");
println!(" Source: {}", source_dir.display());
println!(" Target: {}", target_dir.display());
if dry_run {
println!(" Mode: DRY RUN (no changes will be made)");
}
println!();
let filesystem = RealFileSystem::new();
let syncer = SessionSyncer::new(filesystem);
let options = SyncOptions {
dry_run,
verbose,
};
let result = syncer.sync(&source_root_dir, repo_dir_cc_style.as_str(), &target_dir, &options)
.context("Failed to sync sessions")?;
println!("\nSync completed:");
println!(" Files copied: {}", result.files_copied);
println!(" Files skipped: {}", result.files_skipped);
println!(" Directories created: {}", result.directories_created);
if !result.errors.is_empty() {
println!("\nErrors encountered:");
for error in &result.errors {
warn!("{}", error);
eprintln!(" - {}", error);
}
}
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
let log_level = if cli.verbose {
"info"
} else {
"warn"
};
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
.target(env_logger::Target::Stderr)
.init();
match cli.command {
Commands::Init { repo_dir } => init_command(repo_dir),
Commands::Sync { source_dir, repo_dir, dry_run } => {
sync_command(source_dir, repo_dir, dry_run, cli.verbose)
}
}
}