mod cc_switch;
mod config;
mod discovery;
mod ssh_sync;
mod sync;
mod usb;
mod web;
use anyhow::Result;
use clap::{Parser, Subcommand};
use config::Config;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "codex-sync",
version,
about = "在局域网内同步 Codex 聊天记录和配置"
)]
struct Cli {
#[arg(long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Init {
#[arg(long)]
force: bool,
},
Serve,
Peers {
#[arg(long, default_value_t = 3)]
seconds: u64,
},
Pull {
peer: String,
#[arg(long)]
overwrite: bool,
},
Status,
Usb {
#[command(subcommand)]
command: UsbCommand,
},
Smb {
#[command(subcommand)]
command: SmbCommand,
},
Relay {
#[command(subcommand)]
command: RelayCommand,
},
CcSwitch {
#[command(subcommand)]
command: CcSwitchCommand,
#[arg(long)]
codex_home: Option<PathBuf>,
#[arg(long)]
cc_switch_home: Option<PathBuf>,
},
Ssh {
#[command(subcommand)]
command: SshCommand,
},
}
#[derive(Subcommand)]
enum UsbCommand {
Export {
destination: PathBuf,
},
Import {
source: PathBuf,
#[arg(long)]
overwrite: bool,
},
List {
source: PathBuf,
},
}
#[derive(Subcommand)]
enum SmbCommand {
Push {
share: PathBuf,
},
Pull {
share: PathBuf,
#[arg(long)]
overwrite: bool,
},
List {
share: PathBuf,
},
}
#[derive(Subcommand)]
enum RelayCommand {
Push {
directory: PathBuf,
},
Pull {
directory: PathBuf,
#[arg(long)]
overwrite: bool,
},
List {
directory: PathBuf,
},
}
#[derive(Subcommand)]
enum CcSwitchCommand {
Status,
Merge {
#[arg(long)]
apply: bool,
},
}
#[derive(Subcommand)]
enum SshCommand {
Push {
host: String,
#[arg(long)]
codex_home: Option<PathBuf>,
#[arg(long)]
mirror: bool,
},
#[command(hide = true)]
Receive {
#[arg(long)]
bundle: PathBuf,
#[arg(long)]
codex_home: Option<PathBuf>,
#[arg(long)]
mirror: bool,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let path = cli.config.unwrap_or_else(Config::default_path);
if let Command::Init { force } = cli.command {
Config::init(&path, force)?;
println!("配置已创建:{}", path.display());
return Ok(());
}
if let Command::CcSwitch {
command,
codex_home,
cc_switch_home,
} = &cli.command
{
let paths = cc_switch::Paths::resolve(codex_home.clone(), cc_switch_home.clone())?;
match command {
CcSwitchCommand::Status => {
let report = cc_switch::inspect(&paths)?;
cc_switch::print_report(&report);
}
CcSwitchCommand::Merge { apply } => {
let report = cc_switch::merge(&paths, *apply)?;
cc_switch::print_report(&report);
if *apply {
if let Some(backup) = report.backup {
println!("合并完成;备份:{}", backup.display());
}
} else {
println!("以上为预览。关闭 Codex 和 CC-Switch 后,添加 --apply 执行合并。");
}
}
}
return Ok(());
}
if let Command::Ssh { command } = &cli.command {
match command {
SshCommand::Push {
host,
codex_home,
mirror,
} => {
let report = ssh_sync::push(host, codex_home.clone(), *mirror)?;
println!(
"SSH 单向同步完成:新增/更新 {},跳过 {},移除远端独有 {},冲突 {},数据库变更 {}",
report.copied,
report.skipped,
report.removed,
report.conflicts,
report.database_rows
);
println!("远端备份:{}", report.backup);
}
SshCommand::Receive {
bundle,
codex_home,
mirror,
} => {
let report = ssh_sync::receive(bundle, codex_home.clone(), *mirror)?;
println!("CODEX_SYNC_REPORT={}", serde_json::to_string(&report)?);
}
}
return Ok(());
}
let config = Config::load(&path)?;
match cli.command {
Command::Serve => web::serve(config).await?,
Command::Peers { seconds } => {
for peer in discovery::discover(&config, seconds).await? {
println!("{}\t{}\t{}", peer.name, peer.address, peer.node_id);
}
}
Command::Pull { peer, overwrite } => {
let report = sync::pull(&config, &peer, overwrite).await?;
println!(
"下载 {},跳过 {},冲突 {}",
report.downloaded, report.skipped, report.conflicts
);
}
Command::Status => {
let manifest = sync::manifest(&config)?;
println!("节点:{}", config.node_name);
println!("目录:{}", config.sync_dir.display());
println!("文件:{}", manifest.files.len());
println!("Web:http://{}", config.listen);
}
Command::Usb { command } => match command {
UsbCommand::Export { destination } => {
let path = usb::export(&config, &destination)?;
println!("快照已导出:{}", path.display());
}
UsbCommand::Import { source, overwrite } => {
let report = usb::import(&config, &source, overwrite)?;
println!(
"导入 {},跳过 {},冲突 {}",
report.downloaded, report.skipped, report.conflicts
);
}
UsbCommand::List { source } => {
for snapshot in usb::list(&source)? {
println!(
"{}\t{}\t{} 个文件\t{}",
snapshot.node_name,
snapshot.node_id,
snapshot.files,
snapshot.path.display()
);
}
}
},
Command::Smb { command } => match command {
SmbCommand::Push { share } => {
let path = usb::export(&config, &share)?;
println!("快照已推送到 SMB:{}", path.display());
}
SmbCommand::Pull { share, overwrite } => {
let report = usb::import(&config, &share, overwrite)?;
println!(
"拉取 {},跳过 {},冲突 {}",
report.downloaded, report.skipped, report.conflicts
);
}
SmbCommand::List { share } => {
for snapshot in usb::list(&share)? {
println!(
"{}\t{}\t{} 个文件\t{}",
snapshot.node_name,
snapshot.node_id,
snapshot.files,
snapshot.path.display()
);
}
}
},
Command::Relay { command } => match command {
RelayCommand::Push { directory } => {
let path = usb::export(&config, &directory)?;
println!("快照已发布:{}", path.display());
}
RelayCommand::Pull {
directory,
overwrite,
} => {
let report = usb::import(&config, &directory, overwrite)?;
println!(
"获取 {},跳过 {},冲突 {}",
report.downloaded, report.skipped, report.conflicts
);
}
RelayCommand::List { directory } => {
for snapshot in usb::list(&directory)? {
println!(
"{}\t{}\t{} 个文件\t{}",
snapshot.node_name,
snapshot.node_id,
snapshot.files,
snapshot.path.display()
);
}
}
},
Command::CcSwitch { .. } => unreachable!(),
Command::Ssh { .. } => unreachable!(),
Command::Init { .. } => unreachable!(),
}
Ok(())
}