use std::io;
use std::path::{Path, PathBuf};
pub fn smart_dir() -> io::Result<PathBuf> {
let dir = smart_dir_no_create();
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
pub fn smart_dir_no_create() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude.shared")
.join("smart")
}
pub fn sidecar(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.json"))
}
pub fn relaunch(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.relaunch"))
}
pub fn pid_file(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.pid"))
}
#[cfg_attr(unix, allow(dead_code))]
pub fn stop_flag(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.stop"))
}
pub fn switched(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.switched"))
}
pub fn detected(sid: &str) -> PathBuf {
smart_dir_no_create().join(format!("{sid}.detected"))
}
pub fn usage_cache() -> PathBuf {
smart_dir_no_create().join(".usage-cache.json")
}
pub fn fetch_failed() -> PathBuf {
smart_dir_no_create().join(".usage-fetch-failed")
}
pub fn last_switch() -> PathBuf {
smart_dir_no_create().join(".last-switch")
}
pub fn titles_tsv() -> PathBuf {
smart_dir_no_create().join("titles.tsv")
}
#[allow(dead_code)]
pub fn legacy_helper_sh() -> PathBuf {
smart_dir_no_create()
.join("bin")
.join("claude-smart-helper.sh")
}
pub fn profiles_json() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config")
.join("claude-as")
.join("profiles.json")
}
pub fn hub_local_cache() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("claude-code-usage")
.join("cache")
.join("usage-limits.json")
}
pub fn scan_index_for(project_dir: &Path) -> PathBuf {
let dir_name = project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("_unknown");
smart_dir_no_create().join(format!("scan-meta-v2.{dir_name}.tsv"))
}
pub fn session_base_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude.shared")
.join("projects")
}
pub fn encode_cwd(path: &Path) -> (String, String) {
let s = path.to_string_lossy();
let current: String = s
.chars()
.map(|c| if c == '/' || c == '.' { '-' } else { c })
.collect();
let legacy: String = s.chars().map(|c| if c == '/' { '-' } else { c }).collect();
(current, legacy)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn check(raw: &str, expected_current: &str, expected_legacy: &str) {
let (cur, leg) = encode_cwd(Path::new(raw));
assert_eq!(
cur, expected_current,
"current encoding mismatch for {raw:?}"
);
assert_eq!(leg, expected_legacy, "legacy encoding mismatch for {raw:?}");
}
#[test]
fn encode_cwd_home_github_path() {
check(
"/Users/example/Projects/github.com/some-project",
"-Users-example-Projects-github-com-some-project",
"-Users-example-Projects-github.com-some-project",
);
}
#[test]
fn encode_cwd_path_with_dots_in_segment() {
check(
"/Users/example/Projects/github.com/some.repo",
"-Users-example-Projects-github-com-some-repo",
"-Users-example-Projects-github.com-some.repo",
);
}
#[test]
fn encode_cwd_no_dots() {
check("/tmp/myproject", "-tmp-myproject", "-tmp-myproject");
}
#[test]
fn encode_cwd_root() {
check("/", "-", "-");
}
#[test]
fn encode_cwd_multiple_dots() {
check(
"/home/you/a.b.c/d.e",
"-home-you-a-b-c-d-e",
"-home-you-a.b.c-d.e",
);
}
#[test]
fn encode_cwd_current_legacy_differ_when_dots_present() {
let (cur, leg) = encode_cwd(Path::new("/foo/bar.baz"));
assert!(
cur.contains("bar-baz"),
"current should replace dots: {cur}"
);
assert!(leg.contains("bar.baz"), "legacy should keep dots: {leg}");
}
#[test]
fn encode_cwd_identical_when_no_dots() {
let (cur, leg) = encode_cwd(Path::new("/foo/bar/baz"));
assert_eq!(cur, leg);
}
#[test]
fn smart_dir_no_create_is_under_home() {
let d = smart_dir_no_create();
let s = d.to_string_lossy();
assert!(
s.contains(".claude.shared"),
"smart_dir should be under .claude.shared, got: {s}"
);
assert!(
s.ends_with("smart"),
"smart_dir should end with 'smart', got: {s}"
);
}
#[test]
fn profiles_json_is_under_config() {
let p = profiles_json();
let s = p.to_string_lossy();
assert!(
s.contains(".config"),
"profiles_json not under .config: {s}"
);
assert!(
s.contains("claude-as"),
"profiles_json not under claude-as: {s}"
);
}
#[test]
fn path_constructors_use_sid() {
let sid = "01234567-89ab-cdef-0123-456789abcdef";
assert!(sidecar(sid).to_string_lossy().contains(sid));
assert!(relaunch(sid).to_string_lossy().contains(sid));
assert!(pid_file(sid).to_string_lossy().contains(sid));
assert!(stop_flag(sid).to_string_lossy().contains(sid));
assert!(switched(sid).to_string_lossy().contains(sid));
assert!(detected(sid).to_string_lossy().contains(sid));
}
}