tier: per-path probe diagnostics — surface read errors / parse status in TierInfo.searched
This commit is contained in:
parent
c5c38d1ce5
commit
18e55cd139
2 changed files with 80 additions and 38 deletions
|
|
@ -5,7 +5,7 @@ use std::path::PathBuf;
|
|||
use crate::paths::{list_wsl_distros, resolve_roots, ResolvedRoots};
|
||||
use crate::settings::{save as save_settings, Caps, Settings};
|
||||
use crate::state::SharedState;
|
||||
use crate::tier::{detect_in as detect_tier_in, PlanTier};
|
||||
use crate::tier::{detect_in_with_diagnostics, PlanTier};
|
||||
use crate::usage::{build_snapshot, UsageSnapshot};
|
||||
use crate::watch::refresh_and_emit;
|
||||
|
||||
|
|
@ -111,12 +111,8 @@ pub async fn detect_plan_tier(
|
|||
candidates.sort();
|
||||
candidates.dedup();
|
||||
|
||||
let tier = detect_tier_in(&candidates);
|
||||
let (tier, diagnostics) = detect_in_with_diagnostics(&candidates);
|
||||
let label = tier.label();
|
||||
let recommended_caps = tier.caps();
|
||||
Ok(TierInfo { tier, label, recommended_caps, searched: paths_to_strings(&candidates) })
|
||||
}
|
||||
|
||||
fn paths_to_strings(paths: &[PathBuf]) -> Vec<String> {
|
||||
paths.iter().map(|p| p.display().to_string()).collect()
|
||||
Ok(TierInfo { tier, label, recommended_caps, searched: diagnostics })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,9 +79,82 @@ fn read_tier_from(path: &std::path::Path) -> Option<PlanTier> {
|
|||
Some(classify(raw))
|
||||
}
|
||||
|
||||
/// Find the newest `.claude.json.backup.*` in `dir` and read its tier.
|
||||
fn read_tier_from_backups(dir: &std::path::Path) -> Option<PlanTier> {
|
||||
let entries = std::fs::read_dir(dir).ok()?;
|
||||
/// Same as [`read_tier_from`] but returns a string describing what happened.
|
||||
/// Used for the diagnostic `searched` list in TierInfo.
|
||||
fn probe_tier_file(path: &std::path::Path) -> String {
|
||||
match std::fs::read(path) {
|
||||
Err(e) => format!("read_err: {}", e.kind()),
|
||||
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||||
Err(_) => format!("not_json (size={})", bytes.len()),
|
||||
Ok(json) => match json.get("organizationRateLimitTier").and_then(|v| v.as_str()) {
|
||||
None => format!("no_tier_field (size={})", bytes.len()),
|
||||
Some(raw) => format!("OK: {}", raw),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try each `.claude/` candidate directory in order. The first hit wins.
|
||||
///
|
||||
/// For each candidate `<dir>` we look at:
|
||||
/// 1. `<dir>/.claude.json` (e.g. ~/.claude/.claude.json)
|
||||
/// 2. `<dir>/backups/.claude.json.backup.*` (newest)
|
||||
/// 3. `<parent of dir>/.claude.json` (some setups put it at ~/.claude.json)
|
||||
pub fn detect_in(claude_dirs: &[PathBuf]) -> PlanTier {
|
||||
let (tier, _) = detect_in_with_diagnostics(claude_dirs);
|
||||
tier
|
||||
}
|
||||
|
||||
/// Like [`detect_in`] but also returns one diagnostic line per probed path
|
||||
/// describing what happened (for surfacing in the Settings UI).
|
||||
pub fn detect_in_with_diagnostics(
|
||||
claude_dirs: &[PathBuf],
|
||||
) -> (PlanTier, Vec<String>) {
|
||||
let mut diag: Vec<String> = Vec::new();
|
||||
let mut found: Option<PlanTier> = None;
|
||||
|
||||
for dir in claude_dirs {
|
||||
let primary = dir.join(".claude.json");
|
||||
let result = probe_tier_file(&primary);
|
||||
diag.push(format!("{} → {}", primary.display(), result));
|
||||
if found.is_none() {
|
||||
if let Some(t) = read_tier_from(&primary) {
|
||||
found = Some(t);
|
||||
}
|
||||
}
|
||||
|
||||
let backups = dir.join("backups");
|
||||
match newest_backup(&backups) {
|
||||
Err(e) => diag.push(format!("{} → readdir_err: {}", backups.display(), e)),
|
||||
Ok(None) => diag.push(format!("{} → no_backups", backups.display())),
|
||||
Ok(Some(p)) => {
|
||||
let result = probe_tier_file(&p);
|
||||
diag.push(format!("{} → {}", p.display(), result));
|
||||
if found.is_none() {
|
||||
if let Some(t) = read_tier_from(&p) {
|
||||
found = Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = dir.parent() {
|
||||
let alt = parent.join(".claude.json");
|
||||
let result = probe_tier_file(&alt);
|
||||
diag.push(format!("{} → {}", alt.display(), result));
|
||||
if found.is_none() {
|
||||
if let Some(t) = read_tier_from(&alt) {
|
||||
found = Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(found.unwrap_or(PlanTier::NotFound), diag)
|
||||
}
|
||||
|
||||
fn newest_backup(dir: &std::path::Path) -> std::io::Result<Option<PathBuf>> {
|
||||
let entries = std::fs::read_dir(dir)?;
|
||||
let mut latest: Option<(std::time::SystemTime, PathBuf)> = None;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
|
@ -99,34 +172,7 @@ fn read_tier_from_backups(dir: &std::path::Path) -> Option<PlanTier> {
|
|||
}
|
||||
}
|
||||
}
|
||||
let (_, path) = latest?;
|
||||
read_tier_from(&path)
|
||||
}
|
||||
|
||||
/// Try each `.claude/` candidate directory in order. The first hit wins.
|
||||
///
|
||||
/// For each candidate `<dir>` we look at:
|
||||
/// 1. `<dir>/.claude.json` (e.g. ~/.claude/.claude.json)
|
||||
/// 2. `<dir>/backups/.claude.json.backup.*` (newest)
|
||||
/// 3. `<parent of dir>/.claude.json` (some setups put it at ~/.claude.json)
|
||||
pub fn detect_in(claude_dirs: &[PathBuf]) -> PlanTier {
|
||||
for dir in claude_dirs {
|
||||
let primary = dir.join(".claude.json");
|
||||
if let Some(t) = read_tier_from(&primary) {
|
||||
return t;
|
||||
}
|
||||
let backups = dir.join("backups");
|
||||
if let Some(t) = read_tier_from_backups(&backups) {
|
||||
return t;
|
||||
}
|
||||
if let Some(parent) = dir.parent() {
|
||||
let alt = parent.join(".claude.json");
|
||||
if let Some(t) = read_tier_from(&alt) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
PlanTier::NotFound
|
||||
Ok(latest.map(|(_, p)| p))
|
||||
}
|
||||
|
||||
/// Native-only fallback path. Most callers should prefer `detect_in` with
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue