use anyhow::Result;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use crate::codex_config::STATE_DB_FILENAME;
use crate::codex_config::configured_sqlite_home;
use crate::locale::Locale;
pub(crate) const APP_SQLITE_SUBDIR: &str = "sqlite";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StoreKind {
Configured,
App,
Cli,
}
impl StoreKind {
pub(crate) fn slug(self) -> &'static str {
match self {
StoreKind::Configured => "configured",
StoreKind::App => "app",
StoreKind::Cli => "cli",
}
}
pub(crate) fn label(self, locale: Locale) -> &'static str {
match (self, locale) {
(StoreKind::Configured, Locale::En) => "configured (sqlite_home / CODEX_SQLITE_HOME)",
(StoreKind::Configured, Locale::ZhHans) => "自定义(sqlite_home / CODEX_SQLITE_HOME)",
(StoreKind::App, Locale::En) => "Codex App (desktop)",
(StoreKind::App, Locale::ZhHans) => "Codex App(桌面应用)",
(StoreKind::Cli, Locale::En) => "Codex CLI",
(StoreKind::Cli, Locale::ZhHans) => "Codex CLI(命令行)",
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct StoreTarget {
pub(crate) kind: StoreKind,
pub(crate) db_path: PathBuf,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, clap::ValueEnum)]
pub(crate) enum StoreFilter {
#[default]
All,
Cli,
App,
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,
}
}
}
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)?;
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))
}
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)));
}
candidates.push((
StoreKind::App,
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 {
if !path.exists() {
if kind == StoreKind::Configured {
stores.push(StoreTarget {
kind,
db_path: path,
});
}
continue;
}
let canonical = path.canonicalize().unwrap_or(path);
if seen.insert(canonical.clone()) {
stores.push(StoreTarget {
kind,
db_path: canonical,
});
}
}
stores
}
pub(crate) fn no_store_found_message(locale: Locale, codex_home: &Path) -> String {
match locale {
Locale::En => format!(
"no Codex state database found under {} (looked at state_5.sqlite and sqlite/state_5.sqlite) — run Codex at least once to create it",
codex_home.display()
),
Locale::ZhHans => format!(
"在 {} 下未找到 Codex 状态库(已查 state_5.sqlite 与 sqlite/state_5.sqlite)— 请先运行一次 Codex 以生成它",
codex_home.display()
),
}
}
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()
),
}
}