Detect tier across WSL roots, not just native home (\\wsl$\<distro>\... probe)
This commit is contained in:
parent
ef84257ddd
commit
f33bb5481b
3 changed files with 86 additions and 34 deletions
|
|
@ -1,9 +1,11 @@
|
|||
//! Tauri command surface — every JS-callable function lives here.
|
||||
|
||||
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 as detect_tier, PlanTier};
|
||||
use crate::tier::{detect_in as detect_tier_in, PlanTier};
|
||||
use crate::usage::{build_snapshot, UsageSnapshot};
|
||||
use crate::watch::refresh_and_emit;
|
||||
|
||||
|
|
@ -84,8 +86,27 @@ pub struct TierInfo {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn detect_plan_tier() -> Result<TierInfo, String> {
|
||||
let tier = detect_tier();
|
||||
pub async fn detect_plan_tier(
|
||||
state: tauri::State<'_, SharedState>,
|
||||
) -> Result<TierInfo, String> {
|
||||
// Each resolved root is a `<base>/.claude/projects` path. The tier
|
||||
// file lives in the parent (`<base>/.claude/`) — so check those, plus
|
||||
// the native home as a fallback, so this works whether Claude Code
|
||||
// runs in WSL (UNC paths) or natively.
|
||||
let mut candidates: Vec<PathBuf> = state
|
||||
.roots
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|r| r.parent().map(PathBuf::from))
|
||||
.collect();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".claude"));
|
||||
}
|
||||
// Dedupe (canonicalize is unreliable on UNC paths, so compare raw).
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
|
||||
let tier = detect_tier_in(&candidates);
|
||||
let label = tier.label();
|
||||
let recommended_caps = tier.caps();
|
||||
Ok(TierInfo { tier, label, recommended_caps })
|
||||
|
|
|
|||
|
|
@ -65,8 +65,24 @@ pub fn load() -> Settings {
|
|||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
// First run — seed caps from detected plan tier so the ring
|
||||
// doesn't peg red against the static placeholders.
|
||||
//
|
||||
// We don't have resolved roots yet at this point in startup, so
|
||||
// also check WSL homes (without a watcher running) by re-resolving
|
||||
// from default settings.
|
||||
let resolved = crate::paths::resolve_roots(None, true);
|
||||
let mut candidates: Vec<std::path::PathBuf> = resolved
|
||||
.roots
|
||||
.iter()
|
||||
.filter_map(|r| r.parent().map(std::path::PathBuf::from))
|
||||
.collect();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".claude"));
|
||||
}
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
|
||||
let mut s = Settings::default();
|
||||
s.caps = crate::tier::detect().caps();
|
||||
s.caps = crate::tier::detect_in(&candidates).caps();
|
||||
return s;
|
||||
};
|
||||
match serde_json::from_slice::<Settings>(&bytes) {
|
||||
|
|
|
|||
|
|
@ -70,23 +70,18 @@ fn classify(raw: &str) -> PlanTier {
|
|||
}
|
||||
}
|
||||
|
||||
/// Find the most recent on-disk source of `organizationRateLimitTier`.
|
||||
///
|
||||
/// Search order:
|
||||
/// 1. `~/.claude/.claude.json` if it exists (Linux / WSL)
|
||||
/// 2. `%USERPROFILE%\.claude\.claude.json` (Windows native — same logic via dirs::home_dir)
|
||||
/// 3. The newest `~/.claude/backups/.claude.json.backup.*` (this is what
|
||||
/// we observed populating in practice — `.claude.json` itself isn't
|
||||
/// always present, but the backup directory tracks every config change)
|
||||
fn locate_claude_json() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
let primary = home.join(".claude").join(".claude.json");
|
||||
if primary.is_file() {
|
||||
return Some(primary);
|
||||
}
|
||||
/// Read the tier from a single `.claude.json`-shaped file. None if the file
|
||||
/// doesn't exist, isn't valid JSON, or is missing the field.
|
||||
fn read_tier_from(path: &std::path::Path) -> Option<PlanTier> {
|
||||
let bytes = std::fs::read(path).ok()?;
|
||||
let json: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
|
||||
let raw = json.get("organizationRateLimitTier").and_then(|v| v.as_str())?;
|
||||
Some(classify(raw))
|
||||
}
|
||||
|
||||
let backups_dir = home.join(".claude").join("backups");
|
||||
let entries = std::fs::read_dir(&backups_dir).ok()?;
|
||||
/// 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()?;
|
||||
let mut latest: Option<(std::time::SystemTime, PathBuf)> = None;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
|
@ -104,23 +99,43 @@ fn locate_claude_json() -> Option<PathBuf> {
|
|||
}
|
||||
}
|
||||
}
|
||||
latest.map(|(_, p)| p)
|
||||
let (_, path) = latest?;
|
||||
read_tier_from(&path)
|
||||
}
|
||||
|
||||
pub fn detect() -> PlanTier {
|
||||
let Some(path) = locate_claude_json() else {
|
||||
return PlanTier::NotFound;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
return PlanTier::NotFound;
|
||||
};
|
||||
let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
|
||||
return PlanTier::NotFound;
|
||||
};
|
||||
match json.get("organizationRateLimitTier").and_then(|v| v.as_str()) {
|
||||
Some(raw) => classify(raw),
|
||||
None => PlanTier::NotFound,
|
||||
/// 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
|
||||
/// the resolved-roots' parents so the WSL case works.
|
||||
pub fn detect() -> PlanTier {
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
return PlanTier::NotFound;
|
||||
};
|
||||
detect_in(&[home.join(".claude")])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue