Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ codex-threadripper install-service

- `status`:查看当前 provider、SQLite 里各 provider 的线程分布,以及后台服务的运行状态
- `sync`:立即执行一次收敛。执行前会在 SQLite 状态库旁边的 `backups/` 目录里写一份带时间戳的备份;如果 rollout 首行需要改写,也会写 compact journal
- `sync --dry-run`:预览将要变更的内容(SQLite 行数和 rollout 文件数),不实际写入任何数据
- `restore`:从备份恢复 SQLite 状态库
- 不带参数:列出所有可用备份并退出
- `restore <BACKUP_PATH>`:从指定备份文件恢复
- `restore --latest`:恢复最近一次备份(按文件名时间戳)
- `--force`:跳过确认提示
- `prune-backups`:删除旧备份,保留最近的 N 个(默认 5)
- `--keep N`:指定保留数量
- `--force`:跳过确认提示
- 无法解析时间戳的 `.bak` 文件会被跳过并发出警告
- `bucket switch <provider>`:把全部历史切到指定 provider 可见桶;首行有 padding 时只做原地 patch
- `bucket prepare`:给 rollout 首行补足 JSON 空白 padding,让后续 provider 桶切换保持快速
- `watch`:持续监听 `config.toml` 的变化,同时定时收敛新增的线程记录
Expand Down Expand Up @@ -220,6 +230,16 @@ codex-threadripper install-service

- `status`: show the active provider, the per-provider thread counts from SQLite, and whether the background service is running
- `sync`: reconcile once right now, writing a timestamped backup to the `backups/` directory beside the SQLite state DB; rollout first-line changes also get a compact journal
- `sync --dry-run`: preview what would change (SQLite rows and rollout files) without writing anything
- `restore`: restore the SQLite state DB from a backup file
- No argument: list all available backups and exit
- `restore <BACKUP_PATH>`: restore from the specified backup file
- `restore --latest`: restore the most recent backup (by filename timestamp)
- `--force`: skip the confirmation prompt
- `prune-backups`: remove old backups, keeping the N most recent (default 5)
- `--keep N`: number of backups to keep
- `--force`: skip the confirmation prompt
- `.bak` files with unparseable timestamps are skipped with a warning
- `bucket switch <provider>`: move all history into the requested provider visibility bucket; prepared first lines are patched in place
- `bucket prepare`: reserve JSON whitespace padding in rollout first lines so future provider bucket switches stay fast
- `watch`: keep watching `config.toml` for provider changes and periodically reconcile any newly added thread rows
Expand Down
63 changes: 63 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub(crate) enum Command {
Sync {
#[arg(long, help = "placeholder")]
sqlite_only: bool,
#[arg(long, help = "placeholder")]
dry_run: bool,
},
/// placeholder
Bucket {
Expand Down Expand Up @@ -92,6 +94,27 @@ pub(crate) enum Command {
/// placeholder
#[command(name = "uninstall-service", alias = "uninstall-launchd")]
UninstallService,
/// placeholder
Restore {
#[arg(
value_name = "BACKUP_PATH",
help = "placeholder",
conflicts_with = "latest"
)]
backup_path: Option<PathBuf>,
#[arg(long, help = "placeholder")]
latest: bool,
#[arg(long, help = "placeholder")]
force: bool,
},
/// placeholder
#[command(name = "prune-backups")]
PruneBackups {
#[arg(long, default_value_t = 5, value_name = "N", help = "placeholder")]
keep: usize,
#[arg(long, help = "placeholder")]
force: bool,
},
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -184,6 +207,10 @@ pub(crate) fn localized_command(locale: Locale) -> clap::Command {
arg.help(sqlite_only_help(locale))
.help_heading(options_heading(locale))
})
.mut_arg("dry_run", |arg| {
arg.help(dry_run_help(locale))
.help_heading(options_heading(locale))
})
});
command = command.mut_subcommand("bucket", |sub| {
sub.about(bucket_about(locale))
Expand Down Expand Up @@ -274,6 +301,42 @@ pub(crate) fn localized_command(locale: Locale) -> clap::Command {
.disable_help_subcommand(true)
.help_template(help_template(locale))
});
command = command.mut_subcommand("restore", |sub| {
sub.about(restore_about(locale))
.version(APP_VERSION)
.disable_help_flag(true)
.disable_version_flag(true)
.disable_help_subcommand(true)
.help_template(help_template(locale))
.mut_arg("backup_path", |arg| {
arg.help(restore_path_help(locale))
.value_name(restore_path_value_name(locale))
})
.mut_arg("latest", |arg| {
arg.help(restore_latest_help(locale))
.help_heading(options_heading(locale))
})
.mut_arg("force", |arg| {
arg.help(restore_force_help(locale))
.help_heading(options_heading(locale))
})
});
command = command.mut_subcommand("prune-backups", |sub| {
sub.about(prune_backups_about(locale))
.version(APP_VERSION)
.disable_help_flag(true)
.disable_version_flag(true)
.disable_help_subcommand(true)
.help_template(help_template(locale))
.mut_arg("keep", |arg| {
arg.help(prune_keep_help(locale))
.help_heading(options_heading(locale))
})
.mut_arg("force", |arg| {
arg.help(prune_force_help(locale))
.help_heading(options_heading(locale))
})
});

command
}
Expand Down
178 changes: 165 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Context;
use anyhow::Result;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
Expand Down Expand Up @@ -32,16 +33,30 @@ use output::launchd_plist_message;
use output::next_steps_heading;
use output::no_launchd_plist_message;
use output::print_bucket_prepare_summary;
use output::print_dry_run_summary;
use output::print_install_service_summary;
use output::print_status;
use output::print_sync_summary;
use output::prune_cancelled;
use output::prune_complete;
use output::prune_confirm_prompt;
use output::prune_skipped_warning;
use output::restore_cancelled;
use output::restore_complete;
use output::restore_confirm_prompt;
use output::restore_list_title;
use output::restore_no_backups;
use output::run_status_next_step;
use output::sync_complete_title;
use output::uninstall_launchd_done;
use rollout::RolloutProgressConfig;
use rollout::RolloutScope;
use rollout::prepare_bucket_padding;
use state_db::list_backups;
use state_db::prune_backups;
use state_db::restore_sqlite_from_backup;
use sync::collect_status;
use sync::dry_run_sync;
use sync::reconcile_once_with_backup_and_padding;
use sync::reconcile_once_with_backup_progress;
use watch::run_watch;
Expand All @@ -59,25 +74,38 @@ fn main() -> Result<()> {
collect_status(&codex_home, cli.provider.as_deref(), cli.profile.as_deref())?;
print_status(locale, &summary);
}
Command::Sync { sqlite_only } => {
Command::Sync {
sqlite_only,
dry_run,
} => {
let rollout_scope = if sqlite_only {
RolloutScope::None
} else {
RolloutScope::AllRows
};
let progress = if sqlite_only {
None
if dry_run {
let summary = dry_run_sync(
&codex_home,
cli.provider.as_deref(),
cli.profile.as_deref(),
rollout_scope,
)?;
print_dry_run_summary(locale, &summary);
} else {
Some(RolloutProgressConfig { locale })
};
let summary = reconcile_once_with_backup_progress(
&codex_home,
cli.provider.as_deref(),
cli.profile.as_deref(),
rollout_scope,
progress,
)?;
print_sync_summary(locale, sync_complete_title(locale), &summary);
let progress = if sqlite_only {
None
} else {
Some(RolloutProgressConfig { locale })
};
let summary = reconcile_once_with_backup_progress(
&codex_home,
cli.provider.as_deref(),
cli.profile.as_deref(),
rollout_scope,
progress,
)?;
print_sync_summary(locale, sync_complete_title(locale), &summary);
}
}
Command::Bucket { command } => match command {
BucketCommand::Prepare { padding_bytes } => {
Expand Down Expand Up @@ -145,6 +173,23 @@ fn main() -> Result<()> {
Command::UninstallService => {
uninstall_service(locale, &codex_home)?;
}
Command::Restore {
backup_path,
latest,
force,
} => {
run_restore(
locale,
&codex_home,
cli.profile.as_deref(),
backup_path,
latest,
force,
)?;
}
Command::PruneBackups { keep, force } => {
run_prune_backups(locale, &codex_home, cli.profile.as_deref(), keep, force)?;
}
}

Ok(())
Expand Down Expand Up @@ -212,3 +257,110 @@ fn uninstall_service(locale: Locale, codex_home: &Path) -> Result<()> {
}
Ok(())
}

fn run_restore(
locale: Locale,
codex_home: &Path,
profile_override: Option<&str>,
backup_path: Option<PathBuf>,
latest: bool,
force: bool,
) -> Result<()> {
use crate::codex_config::resolve_sqlite_path;

let sqlite_path = resolve_sqlite_path(codex_home, profile_override)?;
let backups = list_backups(&sqlite_path)?;

let chosen_path = if let Some(path) = backup_path {
path
} else if latest {
let entry = backups
.iter()
.find(|e| e.timestamp_ms.is_some())
.ok_or_else(|| anyhow::anyhow!(restore_no_backups(locale)))?;
entry.path.clone()
} else {
if backups.is_empty() {
println!("{}", restore_no_backups(locale));
return Ok(());
}
println!("{}", restore_list_title(locale));
for entry in &backups {
let name = entry
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
if entry.timestamp_ms.is_some() {
println!(" {name}");
} else {
println!(" {name} (unparseable timestamp)");
}
}
return Ok(());
};

if !force {
print!("{}", restore_confirm_prompt(locale, &chosen_path));
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("{}", restore_cancelled(locale));
return Ok(());
}
}

restore_sqlite_from_backup(&sqlite_path, &chosen_path)?;
println!("{}", restore_complete(locale, &chosen_path));
Ok(())
}

fn run_prune_backups(
locale: Locale,
codex_home: &Path,
profile_override: Option<&str>,
keep: usize,
force: bool,
) -> Result<()> {
use crate::codex_config::resolve_sqlite_path;

let sqlite_path = resolve_sqlite_path(codex_home, profile_override)?;
let backups = list_backups(&sqlite_path)?;

for entry in &backups {
if entry.timestamp_ms.is_none() {
let name = entry
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
eprintln!("{}", prune_skipped_warning(locale, name));
}
}

let parseable: Vec<_> = backups
.iter()
.filter(|e| e.timestamp_ms.is_some())
.collect();
if parseable.len() <= keep {
println!("{}", prune_complete(locale, 0, parseable.len()));
return Ok(());
}

let to_delete_count = parseable.len() - keep;
if !force {
print!("{}", prune_confirm_prompt(locale, to_delete_count));
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("{}", prune_cancelled(locale));
return Ok(());
}
}

let (deleted, kept) = prune_backups(&backups, keep)?;
println!("{}", prune_complete(locale, deleted, kept));
Ok(())
}
Loading