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::paths::{list_wsl_distros, resolve_roots, ResolvedRoots};
|
||||||
use crate::settings::{save as save_settings, Caps, Settings};
|
use crate::settings::{save as save_settings, Caps, Settings};
|
||||||
use crate::state::SharedState;
|
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::usage::{build_snapshot, UsageSnapshot};
|
||||||
use crate::watch::refresh_and_emit;
|
use crate::watch::refresh_and_emit;
|
||||||
|
|
||||||
|
|
@ -111,12 +111,8 @@ pub async fn detect_plan_tier(
|
||||||
candidates.sort();
|
candidates.sort();
|
||||||
candidates.dedup();
|
candidates.dedup();
|
||||||
|
|
||||||
let tier = detect_tier_in(&candidates);
|
let (tier, diagnostics) = detect_in_with_diagnostics(&candidates);
|
||||||
let label = tier.label();
|
let label = tier.label();
|
||||||
let recommended_caps = tier.caps();
|
let recommended_caps = tier.caps();
|
||||||
Ok(TierInfo { tier, label, recommended_caps, searched: paths_to_strings(&candidates) })
|
Ok(TierInfo { tier, label, recommended_caps, searched: diagnostics })
|
||||||
}
|
|
||||||
|
|
||||||
fn paths_to_strings(paths: &[PathBuf]) -> Vec<String> {
|
|
||||||
paths.iter().map(|p| p.display().to_string()).collect()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,82 @@ fn read_tier_from(path: &std::path::Path) -> Option<PlanTier> {
|
||||||
Some(classify(raw))
|
Some(classify(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the newest `.claude.json.backup.*` in `dir` and read its tier.
|
/// Same as [`read_tier_from`] but returns a string describing what happened.
|
||||||
fn read_tier_from_backups(dir: &std::path::Path) -> Option<PlanTier> {
|
/// Used for the diagnostic `searched` list in TierInfo.
|
||||||
let entries = std::fs::read_dir(dir).ok()?;
|
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;
|
let mut latest: Option<(std::time::SystemTime, PathBuf)> = None;
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
@ -99,34 +172,7 @@ fn read_tier_from_backups(dir: &std::path::Path) -> Option<PlanTier> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (_, path) = latest?;
|
Ok(latest.map(|(_, p)| p))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Native-only fallback path. Most callers should prefer `detect_in` with
|
/// Native-only fallback path. Most callers should prefer `detect_in` with
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue