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
39 changes: 39 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::path::PathBuf;
use crate::codex_config::is_valid_profile_name;
use crate::locale::Locale;
use crate::output::*;
use crate::stores::StoreFilter;

pub(crate) const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const DEFAULT_POLL_INTERVAL_MS: u64 = 500;
Expand All @@ -36,6 +37,16 @@ pub(crate) struct Cli {
#[arg(long, global = true, value_name = "PROFILE", help = "placeholder")]
pub(crate) profile: Option<String>,

#[arg(
long,
global = true,
value_enum,
default_value_t = StoreFilter::All,
value_name = "STORE",
help = "placeholder"
)]
pub(crate) store: StoreFilter,

#[command(subcommand)]
pub(crate) command: Command,
}
Expand Down Expand Up @@ -164,6 +175,11 @@ pub(crate) fn localized_command(locale: Locale) -> clap::Command {
.help_heading(options_heading(locale))
.value_name(profile_value_name(locale))
});
command = command.mut_arg("store", |arg| {
arg.help(store_help(locale))
.help_heading(options_heading(locale))
.value_name(store_value_name(locale))
});

command = command.mut_subcommand("status", |sub| {
sub.about(status_about(locale))
Expand Down Expand Up @@ -306,6 +322,29 @@ pub(crate) fn validate_profile_override(locale: Locale, profile: Option<&str>) -
Ok(())
}

pub(crate) fn validate_store_filter_supported(
locale: Locale,
store: StoreFilter,
command: &str,
) -> Result<()> {
if store != StoreFilter::All {
anyhow::bail!(store_filter_unsupported_error(locale, command));
}
Ok(())
}

pub(crate) fn validate_store_filter_rollout_scope(
locale: Locale,
store: StoreFilter,
sqlite_only: bool,
command: &str,
) -> Result<()> {
if store != StoreFilter::All && !sqlite_only {
anyhow::bail!(store_filter_requires_sqlite_only_error(locale, command));
}
Ok(())
}

pub(crate) fn validate_provider_override_args<I, T>(locale: Locale, args: I) -> Result<()>
where
I: IntoIterator<Item = T>,
Expand Down
20 changes: 18 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use cli::DEFAULT_BUCKET_PADDING_BYTES;
use cli::parse_cli;
use cli::validate_profile_override;
use cli::validate_provider_override;
use cli::validate_store_filter_rollout_scope;
use cli::validate_store_filter_supported;
use locale::Locale;
use locale::detect_locale;
use output::bucket_switch_complete_title;
Expand Down Expand Up @@ -70,12 +72,17 @@ fn run() -> Result<ExitCode> {

match cli.command {
Command::Status => {
let summary =
collect_status(&codex_home, cli.provider.as_deref(), cli.profile.as_deref())?;
let summary = collect_status(
&codex_home,
cli.provider.as_deref(),
cli.profile.as_deref(),
cli.store,
)?;
print_status(locale, &summary);
Ok(ExitCode::SUCCESS)
}
Command::Sync { sqlite_only } => {
validate_store_filter_rollout_scope(locale, cli.store, sqlite_only, "sync")?;
let rollout_scope = if sqlite_only {
RolloutScope::None
} else {
Expand All @@ -93,6 +100,7 @@ fn run() -> Result<ExitCode> {
rollout_scope,
DEFAULT_BUCKET_PADDING_BYTES,
DEFAULT_BACKFILL_WAIT,
cli.store,
progress,
)?;
print_multi_sync_summary(locale, sync_complete_title(locale), &summary);
Expand All @@ -103,6 +111,7 @@ fn run() -> Result<ExitCode> {
}
Command::Bucket { command } => match command {
BucketCommand::Prepare { padding_bytes } => {
validate_store_filter_supported(locale, cli.store, "bucket prepare")?;
let summary =
prepare_bucket_padding(&codex_home, cli.profile.as_deref(), padding_bytes)?;
print_bucket_prepare_summary(locale, &summary);
Expand All @@ -112,6 +121,7 @@ fn run() -> Result<ExitCode> {
target_provider,
padding_bytes,
} => {
validate_store_filter_rollout_scope(locale, cli.store, false, "bucket switch")?;
validate_provider_override(locale, target_provider.as_deref())?;
let provider = match target_provider {
Some(provider) => Some(provider),
Expand All @@ -124,6 +134,7 @@ fn run() -> Result<ExitCode> {
RolloutScope::AllRows,
padding_bytes,
DEFAULT_BACKFILL_WAIT,
cli.store,
Some(RolloutProgressConfig { locale }),
)?;
print_multi_sync_summary(locale, bucket_switch_complete_title(locale), &summary);
Expand All @@ -134,11 +145,13 @@ fn run() -> Result<ExitCode> {
poll_interval_ms,
sqlite_only,
} => {
validate_store_filter_rollout_scope(locale, cli.store, sqlite_only, "watch")?;
run_watch(
locale,
&codex_home,
cli.provider.clone(),
cli.profile.clone(),
cli.store,
if sqlite_only {
RolloutScope::None
} else {
Expand All @@ -149,6 +162,7 @@ fn run() -> Result<ExitCode> {
Ok(ExitCode::SUCCESS)
}
Command::PrintServiceConfig { poll_interval_ms } => {
validate_store_filter_supported(locale, cli.store, "print-service-config")?;
let exe_path = std::env::current_exe().context(current_exe_error(locale))?;
let config = service::render_service_config(
exe_path.as_path(),
Expand All @@ -161,6 +175,7 @@ fn run() -> Result<ExitCode> {
Ok(ExitCode::SUCCESS)
}
Command::InstallService { poll_interval_ms } => {
validate_store_filter_supported(locale, cli.store, "install-service")?;
install_service(
locale,
&codex_home,
Expand All @@ -171,6 +186,7 @@ fn run() -> Result<ExitCode> {
Ok(ExitCode::SUCCESS)
}
Command::UninstallService => {
validate_store_filter_supported(locale, cli.store, "uninstall-service")?;
uninstall_service(locale, &codex_home)?;
Ok(ExitCode::SUCCESS)
}
Expand Down
42 changes: 42 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ pub(crate) fn provider_value_name(locale: Locale) -> &'static str {
}
}

pub(crate) fn store_value_name(locale: Locale) -> &'static str {
match locale {
Locale::En => "STORE",
Locale::ZhHans => "STORE",
}
}

pub(crate) fn store_help(locale: Locale) -> &'static str {
match locale {
Locale::En => {
"Limit which storage surface to operate on: cli, app, configured, or all (default: all); filtered writes require --sqlite-only"
}
Locale::ZhHans => "限定操作的存储面:cli、app、configured 或 all(默认:all)",
}
}

pub(crate) fn profile_value_name(locale: Locale) -> &'static str {
match locale {
Locale::En => "PROFILE",
Expand Down Expand Up @@ -380,6 +396,32 @@ pub(crate) fn profile_path_error(locale: Locale) -> &'static str {
}
}

pub(crate) fn store_filter_unsupported_error(locale: Locale, command: &str) -> String {
match locale {
Locale::En => format!(
"--store is not supported for `{command}`; use --store only with status, sync --sqlite-only, or watch --sqlite-only"
),
Locale::ZhHans => {
format!(
"--store 不支持 `{command}`;它只能用于 status、sync --sqlite-only 或 watch --sqlite-only"
)
}
}
}

pub(crate) fn store_filter_requires_sqlite_only_error(locale: Locale, command: &str) -> String {
match locale {
Locale::En => format!(
"--store on `{command}` can only be used with --sqlite-only because rollout JSONL is shared across stores"
),
Locale::ZhHans => {
format!(
"`{command}` 使用 --store 时必须同时使用 --sqlite-only,因为 rollout JSONL 在多个库之间共享"
)
}
}
}

pub(crate) fn help_option_help(locale: Locale) -> &'static str {
match locale {
Locale::En => "Print help",
Expand Down
3 changes: 2 additions & 1 deletion src/rollout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::output::RolloutProgressSnapshot;
use crate::output::rollout_progress_message;
use crate::state_db::ensure_sqlite_exists;
use crate::state_db::unix_timestamp_millis;
use crate::stores::StoreFilter;
use crate::stores::discover_stores;

const ROLLOUT_PROGRESS_INTERVAL: Duration = Duration::from_millis(500);
Expand Down Expand Up @@ -415,7 +416,7 @@ fn prepare_bucket_padding_unlocked(
padding_bytes: usize,
) -> Result<BucketPrepareSummary> {
let started = Instant::now();
let store_db_paths = discover_stores(codex_home, profile_override)?
let store_db_paths = discover_stores(codex_home, profile_override, StoreFilter::All)?
.into_iter()
.map(|store| store.db_path)
.collect::<Vec<_>>();
Expand Down
76 changes: 75 additions & 1 deletion src/stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,56 @@ pub(crate) struct StoreTarget {
pub(crate) db_path: PathBuf,
}

/// `--store` selector: narrow which discovered surface(s) a command operates on.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, clap::ValueEnum)]
pub(crate) enum StoreFilter {
/// Every discovered store (default).
#[default]
All,
/// Only the Codex CLI default (`<codex_home>/state_5.sqlite`).
Cli,
/// Only the Codex App store (`<codex_home>/sqlite/state_5.sqlite`).
App,
/// Only an explicit `sqlite_home` / `CODEX_SQLITE_HOME` store.
Configured,
}

impl StoreFilter {
pub(crate) fn slug(self) -> &'static str {
match self {
StoreFilter::All => "all",
StoreFilter::Cli => "cli",
StoreFilter::App => "app",
StoreFilter::Configured => "configured",
}
}

pub(crate) fn matches(self, kind: StoreKind) -> bool {
match self {
StoreFilter::All => true,
StoreFilter::Cli => kind == StoreKind::Cli,
StoreFilter::App => kind == StoreKind::App,
StoreFilter::Configured => kind == StoreKind::Configured,
}
}
}

/// Discover every existing `state_5.sqlite` store under `codex_home`,
/// canonicalized and de-duplicated. Reads the configured `sqlite_home` /
/// `CODEX_SQLITE_HOME` (if any) and then layers the App and CLI defaults.
pub(crate) fn discover_stores(
codex_home: &Path,
profile_override: Option<&str>,
filter: StoreFilter,
) -> Result<Vec<StoreTarget>> {
let configured = configured_sqlite_home(codex_home, profile_override)?;
Ok(discover_stores_with(codex_home, configured.as_deref()))
if filter == StoreFilter::All {
return Ok(discover_stores_with(codex_home, configured.as_deref()));
}
let candidates = store_candidates(codex_home, configured.as_deref())
.into_iter()
.filter(|(kind, _)| filter.matches(*kind));
Ok(discover_stores_from_candidates(candidates))
}

/// Pure core of [`discover_stores`]: builds candidates from an already-resolved
Expand All @@ -82,6 +123,13 @@ pub(crate) fn discover_stores_with(
codex_home: &Path,
configured_sqlite_home: Option<&Path>,
) -> Vec<StoreTarget> {
discover_stores_from_candidates(store_candidates(codex_home, configured_sqlite_home))
}

fn store_candidates(
codex_home: &Path,
configured_sqlite_home: Option<&Path>,
) -> Vec<(StoreKind, PathBuf)> {
let mut candidates: Vec<(StoreKind, PathBuf)> = Vec::new();
if let Some(dir) = configured_sqlite_home {
candidates.push((StoreKind::Configured, dir.join(STATE_DB_FILENAME)));
Expand All @@ -91,7 +139,13 @@ pub(crate) fn discover_stores_with(
codex_home.join(APP_SQLITE_SUBDIR).join(STATE_DB_FILENAME),
));
candidates.push((StoreKind::Cli, codex_home.join(STATE_DB_FILENAME)));
candidates
}

fn discover_stores_from_candidates<I>(candidates: I) -> Vec<StoreTarget>
where
I: IntoIterator<Item = (StoreKind, PathBuf)>,
{
let mut seen: HashSet<PathBuf> = HashSet::new();
let mut stores: Vec<StoreTarget> = Vec::new();
for (kind, path) in candidates {
Expand Down Expand Up @@ -128,3 +182,23 @@ pub(crate) fn no_store_found_message(locale: Locale, codex_home: &Path) -> Strin
),
}
}

/// Error message shown when stores exist, but none match the selected `--store`.
pub(crate) fn no_store_selected_message(
locale: Locale,
codex_home: &Path,
filter: StoreFilter,
) -> String {
match locale {
Locale::En => format!(
"no Codex state database under {} matched --store {}; run `codex-threadripper status --store all` to see detected stores",
codex_home.display(),
filter.slug()
),
Locale::ZhHans => format!(
"{} 下没有匹配 --store {} 的 Codex 状态库;可运行 `codex-threadripper status --store all` 查看已发现的存储面",
codex_home.display(),
filter.slug()
),
}
}
Loading
Loading