Files
Kigi-CLI/crates/codegen/kigi-config/src/paths.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

323 lines
12 KiB
Rust

//! Filesystem locations for Kigi config files and binaries.
use std::path::PathBuf;
use std::sync::OnceLock;
static KIGI_SHARE_DIR: OnceLock<PathBuf> = OnceLock::new();
#[cfg(target_os = "macos")]
const CLAUDE_MANAGED_SETTINGS_PATH: &str =
"/Library/Application Support/ClaudeCode/managed-settings.json";
#[cfg(target_os = "linux")]
const CLAUDE_MANAGED_SETTINGS_PATH: &str = "/etc/claude-code/managed-settings.json";
/// The default user Kigi directory (`~/.kigi`, canonicalized) used when
/// `KIGI_SHARE_DIR` is unset. Exposed so callers (e.g. display helpers) can detect
/// whether [`kigi_home()`] is the default without duplicating the computation.
///
/// Uses [`dunce::canonicalize`] instead of [`std::fs::canonicalize`]: on
/// Windows, std returns a verbatim path (`\\?\C:\Users\...`) which external
/// tools choke on — e.g. `git clone` rejects `\\?\` destinations with
/// "Invalid argument", breaking marketplace cache clones under
/// `~/.kigi/marketplace-cache`. `dunce` strips the prefix whenever the path
/// is safely representable in legacy form; on non-Windows it is identical to
/// `std::fs::canonicalize`.
///
/// Keep the dunce canonicalization in sync with the hand-rolled duplicate in
/// `kigi_fast_worktree::db::resolve_kigi_home` (deliberately standalone crate).
pub fn default_kigi_home() -> PathBuf {
#[allow(deprecated)]
let home = std::env::home_dir().unwrap_or_else(|| PathBuf::from("."));
dunce::canonicalize(&home).unwrap_or(home).join(".kigi")
}
/// Per-user config directory: `$KIGI_SHARE_DIR` or `~/.kigi`. Created if needed.
pub fn kigi_home() -> PathBuf {
KIGI_SHARE_DIR
.get_or_init(|| {
let kigi_home = if let Ok(v) = std::env::var("KIGI_SHARE_DIR") {
PathBuf::from(v)
} else {
default_kigi_home()
};
let _ = std::fs::create_dir_all(&kigi_home);
kigi_home
})
.clone()
}
/// The user-global Kigi home, but only when one genuinely resolves: `Some` when
/// `$KIGI_SHARE_DIR` is set or a home directory is found, `None` otherwise. Unlike
/// [`kigi_home()`], this never falls back to a cwd-relative `.kigi`, so callers
/// that *scan* user-global Kigi resources (hooks, marketplace sources, ...) don't
/// mistake a project's `.kigi` tree for the user-global one when no home resolves.
pub fn user_kigi_home() -> Option<PathBuf> {
#[allow(deprecated)]
let resolvable = std::env::var_os("KIGI_SHARE_DIR").is_some() || std::env::home_dir().is_some();
resolvable.then(kigi_home)
}
/// Canonical kigi application path: `$KIGI_SHARE_DIR/bin/kigi` (Unix) or `kigi.exe` (Windows).
pub fn kigi_application() -> PathBuf {
let name = if cfg!(windows) { "kigi.exe" } else { "kigi" };
kigi_home().join("bin").join(name)
}
/// System-wide config directory: `/etc/kigi/` on Unix, `None` on Windows.
pub fn system_config_dir() -> Option<PathBuf> {
if cfg!(unix) {
Some(PathBuf::from("/etc/kigi"))
} else {
None
}
}
/// System path for the managed-settings.json used for settings compat, if it exists.
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn claude_managed_settings_path() -> Option<PathBuf> {
let path = PathBuf::from(CLAUDE_MANAGED_SETTINGS_PATH);
path.exists().then_some(path)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn claude_managed_settings_path() -> Option<PathBuf> {
None
}
/// The platform path where managed-settings.json would live for settings
/// compat, whether or not it exists. `None` on unsupported platforms.
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn claude_managed_settings_probe_path() -> Option<PathBuf> {
Some(PathBuf::from(CLAUDE_MANAGED_SETTINGS_PATH))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn claude_managed_settings_probe_path() -> Option<PathBuf> {
None
}
/// Max bytes for a single directory name component (macOS APFS, Linux ext4,
/// NTFS all enforce 255 bytes).
const MAX_DIRNAME_BYTES: usize = 255;
/// Encode a CWD string into a filesystem-safe directory name component.
///
/// Short CWDs (URL-encoded form <= 255 bytes) use URL-encoding for backward
/// compatibility and human readability on disk.
///
/// Long CWDs (> 255 bytes encoded) use a compact `{slug}-{blake3_hex16}`
/// form that is always <= 57 bytes. Callers must write a `.cwd` metadata
/// file via [`ensure_sessions_cwd_dir`] so the original CWD can be
/// recovered by [`decode_cwd_from_dirname`].
pub fn encode_cwd_dirname(cwd: &str) -> String {
let url_encoded = urlencoding::encode(cwd);
if url_encoded.len() <= MAX_DIRNAME_BYTES {
return url_encoded.into_owned();
}
let hash = blake3::hash(cwd.as_bytes());
let hash16 = &hash.to_hex()[..16];
let leaf = std::path::Path::new(cwd)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("workspace");
let slug = slugify(leaf, 40);
let slug = if slug.is_empty() { "workspace" } else { &slug };
format!("{slug}-{hash16}")
}
/// Recover the original CWD from a sessions CWD directory.
///
/// Tries URL-decoding the directory name first (works for short/legacy dirs).
/// Falls back to reading a `.cwd` metadata file inside the directory (written
/// by [`ensure_sessions_cwd_dir`] for hash-based dirs).
pub fn decode_cwd_from_dirname(dir: &std::path::Path) -> Option<String> {
let name = dir.file_name()?.to_str()?;
if let Ok(decoded) = urlencoding::decode(name) {
let s = decoded.into_owned();
// URL-decoded absolute CWDs always start with `/` (Unix) or a drive
// letter (Windows). The slug-hash form never does, so this
// distinguishes the two encodings unambiguously.
if s.starts_with('/') || (cfg!(windows) && s.chars().nth(1) == Some(':')) {
return Some(s);
}
}
std::fs::read_to_string(dir.join(".cwd"))
.ok()
.map(|s| s.trim().to_string())
}
/// Does **not** create the directory on disk — use [`ensure_sessions_cwd_dir`]
/// when the directory must exist.
pub fn sessions_cwd_dir(cwd: &str) -> PathBuf {
kigi_home().join("sessions").join(encode_cwd_dirname(cwd))
}
/// Create the CWD-level session directory and write a `.cwd` metadata file
/// when hash-based encoding is used (long paths).
///
/// For short paths the `.cwd` file is not written because the directory name
/// itself is reversible via URL-decoding.
pub fn ensure_sessions_cwd_dir(cwd: &str) -> std::io::Result<PathBuf> {
let encoded_name = encode_cwd_dirname(cwd);
let dir = kigi_home().join("sessions").join(&encoded_name);
std::fs::create_dir_all(&dir)?;
// Hash-based encoding is in use when the dirname differs from the
// plain URL-encoded form. Write a `.cwd` file so decode can recover
// the original path. O_CREAT|O_EXCL via create_new avoids TOCTOU
// races with parallel session starts.
if encoded_name != urlencoding::encode(cwd).as_ref() {
let cwd_file = dir.join(".cwd");
match std::fs::File::create_new(&cwd_file) {
Ok(mut f) => {
std::io::Write::write_all(&mut f, cwd.as_bytes())?;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
}
}
Ok(dir)
}
/// Output is ASCII-only, so `max_len` bounds the result in bytes as well as
/// chars — [`encode_cwd_dirname`] relies on that for its length guarantee.
fn slugify(input: &str, max_len: usize) -> String {
let mut result = String::with_capacity(input.len());
let mut prev_dash = false;
for c in input.to_lowercase().chars() {
if c.is_ascii_alphanumeric() {
result.push(c);
prev_dash = false;
} else if !prev_dash {
result.push('-');
prev_dash = true;
}
}
let trimmed = result.trim_matches('-');
trimmed.chars().take(max_len).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
/// Realistic CWDs that trigger the bug (URL-encoded > 255 bytes).
const LONG_CWDS: &[&str] = &[
"/Users/dev/Documents/開発プロジェクト/機能追加/テスト環境/ソースコード/main-branch",
"/Users/user/Library/Mobile Documents/com~apple~CloudDocs/项目文件/深层嵌套目录/更深层次的/工作区域/project",
"/Users/user/Library/CloudStorage/OneDrive-대한민국회사/프로젝트/개발환경/소스코드/백엔드/서비스/my-app",
"/Users/user/Documents/工作文件夹/二零二六年项目/子目录一/子目录二/子目录三/源代码/code",
];
#[test]
fn long_cwd_uses_hash_fallback_within_name_max() {
let long_cwd = format!("/Users/test/{}", "中".repeat(30));
let encoded = encode_cwd_dirname(&long_cwd);
assert!(encoded.len() <= MAX_DIRNAME_BYTES);
assert!(!encoded.starts_with("%2F"));
}
#[test]
fn different_long_paths_produce_different_hashes() {
let a = format!("/Users/test/{}", "中".repeat(30));
let b = format!("/Users/test/{}", "日".repeat(30));
assert_ne!(encode_cwd_dirname(&a), encode_cwd_dirname(&b));
}
#[test]
fn decode_reads_cwd_file_for_hash_dirs() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("some-slug-abcdef0123456789");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(".cwd"), "/original/long/path").unwrap();
assert_eq!(
decode_cwd_from_dirname(&dir),
Some("/original/long/path".to_string())
);
}
#[test]
fn decode_returns_none_without_cwd_file() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("some-slug-abcdef0123456789");
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(decode_cwd_from_dirname(&dir), None);
}
#[test]
fn cwd_file_write_is_idempotent_via_excl() {
let tmp = TempDir::new().unwrap();
let long_cwd = format!("/Users/test/{}", "中".repeat(30));
let dir = tmp.path().join(encode_cwd_dirname(&long_cwd));
std::fs::create_dir_all(&dir).unwrap();
let cwd_file = dir.join(".cwd");
std::fs::write(&cwd_file, &long_cwd).unwrap();
match std::fs::File::create_new(&cwd_file) {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
other => panic!("expected AlreadyExists, got: {other:?}"),
}
assert_eq!(std::fs::read_to_string(&cwd_file).unwrap(), long_cwd);
}
#[test]
fn url_encoded_long_cwd_fails_on_real_filesystem() {
let tmp = TempDir::new().unwrap();
let url_encoded = urlencoding::encode(LONG_CWDS[0]).into_owned();
let result = std::fs::create_dir_all(tmp.path().join(&url_encoded));
assert!(result.is_err());
}
#[test]
fn full_roundtrip_on_real_filesystem_for_long_cwds() {
let tmp = TempDir::new().unwrap();
for cwd in LONG_CWDS {
let encoded = encode_cwd_dirname(cwd);
let dir = tmp.path().join(&encoded);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(".cwd"), cwd).unwrap();
assert_eq!(decode_cwd_from_dirname(&dir).as_deref(), Some(*cwd));
}
}
#[test]
fn short_cwds_use_url_encoding_and_roundtrip_on_real_filesystem() {
let tmp = TempDir::new().unwrap();
for cwd in [
"/Users/foo/project",
"/tmp",
"/Users/user/Documents/project-名前",
] {
let encoded = encode_cwd_dirname(cwd);
assert_eq!(encoded, urlencoding::encode(cwd).into_owned());
let dir = tmp.path().join(&encoded);
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(decode_cwd_from_dirname(&dir).as_deref(), Some(cwd));
}
}
#[test]
fn default_kigi_home_has_no_verbatim_prefix() {
// On Windows, std::fs::canonicalize returns `\\?\C:\...` verbatim
// paths that external tools (notably `git clone`) reject. The dunce
// canonicalization must yield a plain path. No-op assertion on Unix.
let home = default_kigi_home();
assert!(!home.to_string_lossy().starts_with(r"\\?\"));
assert!(home.ends_with(".kigi"));
}
#[test]
fn slugify_basic() {
assert_eq!(slugify("Hello World!", 40), "hello-world");
}
#[test]
fn slugify_cjk_produces_empty() {
assert_eq!(slugify("深层目录", 40), "");
}
#[test]
fn slugify_truncates() {
assert_eq!(slugify(&"a".repeat(100), 10).len(), 10);
}
}