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.
|
//! Tauri command surface — every JS-callable function lives here.
|
||||||
|
|
||||||
|
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 as detect_tier, PlanTier};
|
use crate::tier::{detect_in as detect_tier_in, 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;
|
||||||
|
|
||||||
|
|
@ -84,8 +86,27 @@ pub struct TierInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn detect_plan_tier() -> Result<TierInfo, String> {
|
pub async fn detect_plan_tier(
|
||||||
let tier = detect_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 label = tier.label();
|
||||||
let recommended_caps = tier.caps();
|
let recommended_caps = tier.caps();
|
||||||
Ok(TierInfo { tier, label, recommended_caps })
|
Ok(TierInfo { tier, label, recommended_caps })
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,24 @@ pub fn load() -> Settings {
|
||||||
let Ok(bytes) = std::fs::read(&path) else {
|
let Ok(bytes) = std::fs::read(&path) else {
|
||||||
// First run — seed caps from detected plan tier so the ring
|
// First run — seed caps from detected plan tier so the ring
|
||||||
// doesn't peg red against the static placeholders.
|
// 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();
|
let mut s = Settings::default();
|
||||||
s.caps = crate::tier::detect().caps();
|
s.caps = crate::tier::detect_in(&candidates).caps();
|
||||||
return s;
|
return s;
|
||||||
};
|
};
|
||||||
match serde_json::from_slice::<Settings>(&bytes) {
|
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`.
|
/// 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.
|
||||||
/// Search order:
|
fn read_tier_from(path: &std::path::Path) -> Option<PlanTier> {
|
||||||
/// 1. `~/.claude/.claude.json` if it exists (Linux / WSL)
|
let bytes = std::fs::read(path).ok()?;
|
||||||
/// 2. `%USERPROFILE%\.claude\.claude.json` (Windows native — same logic via dirs::home_dir)
|
let json: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
|
||||||
/// 3. The newest `~/.claude/backups/.claude.json.backup.*` (this is what
|
let raw = json.get("organizationRateLimitTier").and_then(|v| v.as_str())?;
|
||||||
/// we observed populating in practice — `.claude.json` itself isn't
|
Some(classify(raw))
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let backups_dir = home.join(".claude").join("backups");
|
/// Find the newest `.claude.json.backup.*` in `dir` and read its tier.
|
||||||
let entries = std::fs::read_dir(&backups_dir).ok()?;
|
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;
|
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();
|
||||||
|
|
@ -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 {
|
/// Try each `.claude/` candidate directory in order. The first hit wins.
|
||||||
let Some(path) = locate_claude_json() else {
|
///
|
||||||
return PlanTier::NotFound;
|
/// For each candidate `<dir>` we look at:
|
||||||
};
|
/// 1. `<dir>/.claude.json` (e.g. ~/.claude/.claude.json)
|
||||||
let Ok(bytes) = std::fs::read(&path) else {
|
/// 2. `<dir>/backups/.claude.json.backup.*` (newest)
|
||||||
return PlanTier::NotFound;
|
/// 3. `<parent of dir>/.claude.json` (some setups put it at ~/.claude.json)
|
||||||
};
|
pub fn detect_in(claude_dirs: &[PathBuf]) -> PlanTier {
|
||||||
let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
|
for dir in claude_dirs {
|
||||||
return PlanTier::NotFound;
|
let primary = dir.join(".claude.json");
|
||||||
};
|
if let Some(t) = read_tier_from(&primary) {
|
||||||
match json.get("organizationRateLimitTier").and_then(|v| v.as_str()) {
|
return t;
|
||||||
Some(raw) => classify(raw),
|
}
|
||||||
None => PlanTier::NotFound,
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue