refactor: migrate snapshot and skill_state paths to ~/.codewhale

- Add resolve_project_state_dir and ensure_project_state_dir to
  codewhale-config, providing project-local .codewhale/.deepseek
  resolution matching the home-directory pattern.
- Migrate snapshot paths to prefer ~/.codewhale/snapshots with
  ~/.deepseek/snapshots fallback (snapshot_base_with_home).
- Migrate skill_state.rs to use codewhale_config::ensure_state_dir
  instead of hardcoded home.join(".deepseek").
- Update doc comments to reference canonical .codewhale paths.

Part of #2231 (state-root migration).
This commit is contained in:
Hunter Bown
2026-05-26 13:39:01 -05:00
parent 1893f797fb
commit 0251b4e8e8
3 changed files with 54 additions and 15 deletions
+30
View File
@@ -1618,6 +1618,36 @@ pub fn ensure_state_dir(subdir: &str) -> Result<PathBuf> {
Ok(dir)
}
/// Resolve a project-local state subdirectory, preferring `.codewhale/`
/// when it exists, falling back to `.deepseek/` for legacy projects.
///
/// Returns `(true, path)` when the primary `.codewhale/` path is used,
/// `(false, path)` for the legacy fallback. The boolean helps callers
/// emit a deprecation notice on legacy paths.
pub fn resolve_project_state_dir(
workspace: &Path,
subdir: &str,
) -> (bool, PathBuf) {
let primary = workspace.join(CODEWHALE_APP_DIR).join(subdir);
if primary.exists() {
return (true, primary);
}
let legacy = workspace.join(LEGACY_APP_DIR).join(subdir);
(false, legacy)
}
/// Ensure a project-local state subdirectory exists under `.codewhale/`,
/// creating it if necessary. Returns the directory path.
pub fn ensure_project_state_dir(
workspace: &Path,
subdir: &str,
) -> Result<PathBuf> {
let dir = workspace.join(CODEWHALE_APP_DIR).join(subdir);
std::fs::create_dir_all(&dir)
.with_context(|| format!("failed to create {}/", dir.display()))?;
Ok(dir)
}
pub fn resolve_config_path(explicit: Option<PathBuf>) -> Result<PathBuf> {
let path = if let Some(path) = explicit {
path
+3 -5
View File
@@ -5,7 +5,7 @@
//! filesystem-discovered `SkillRegistry`: the registry tells us which skills
//! exist on disk, and this store tells API clients which ones are marked active.
//!
//! Storage shape (TOML at `~/.deepseek/skills_state.toml`):
//! Storage shape (TOML at `~/.codewhale/skills_state.toml`, legacy `~/.deepseek/skills_state.toml`):
//!
//! ```toml
//! disabled = ["skill-name-1", "skill-name-2"]
@@ -104,10 +104,8 @@ impl SkillStateStore {
}
fn default_state_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("could not resolve $HOME for ~/.deepseek")?;
let dir = home.join(".deepseek");
fs::create_dir_all(&dir)
.with_context(|| format!("create deepseek state dir at {}", dir.display()))?;
let dir = codewhale_config::ensure_state_dir(".")
.context("could not resolve or create CodeWhale state directory")?;
Ok(dir.join(STATE_FILE_NAME))
}
+21 -10
View File
@@ -1,18 +1,20 @@
//! Path resolution for the per-workspace snapshot side-repos.
//!
//! Snapshots live in `~/.deepseek/snapshots/<project_hash>/<worktree_hash>/`.
//! The two-level hash split lets us snapshot multiple worktrees of the same
//! project independently — `git worktree list` users won't get cross-talk
//! between feature branches.
//! Snapshots live under the resolved state directory
//! (`~/.codewhale/snapshots` or legacy `~/.deepseek/snapshots`) with
//! a two-level hash split so we can snapshot multiple worktrees of the
//! same project independently — `git worktree list` users won't get
//! cross-talk between feature branches.
use std::io;
use std::path::{Path, PathBuf};
/// Compute the snapshot directory for a given workspace path.
///
/// Returns `~/.deepseek/snapshots/<project_hash>/<worktree_hash>/`. The
/// caller is responsible for creating it on disk; we purposefully don't
/// touch the filesystem here so this is cheap to call repeatedly.
/// Returns `$STATE_DIR/snapshots/<project_hash>/<worktree_hash>/` where
/// `$STATE_DIR` is resolved via `codewhale_config::resolve_state_dir`.
/// The caller is responsible for creating it on disk; we purposefully
/// don't touch the filesystem here so this is cheap to call repeatedly.
///
/// The `project_hash` is derived from the canonicalized workspace path
/// after stripping any `.worktrees/<name>` suffix — multiple worktrees
@@ -24,7 +26,7 @@ pub fn snapshot_dir_for(workspace: &Path) -> PathBuf {
}
/// Same as [`snapshot_dir_for`] but with an injectable home directory.
/// Used by tests so we never touch the user's real `~/.deepseek/`.
/// Used by tests so they never touch the user's real state directory.
pub fn snapshot_dir_with_home(workspace: &Path, home: Option<PathBuf>) -> PathBuf {
let home = home.unwrap_or_else(|| PathBuf::from("."));
let canonical = workspace
@@ -33,12 +35,21 @@ pub fn snapshot_dir_with_home(workspace: &Path, home: Option<PathBuf>) -> PathBu
let project_root = strip_worktree_suffix(&canonical);
let project_hash = stable_hex(&project_root);
let worktree_hash = stable_hex(&canonical);
home.join(".deepseek")
.join("snapshots")
snapshot_base_with_home(Some(home))
.join(project_hash)
.join(worktree_hash)
}
fn snapshot_base_with_home(home: Option<PathBuf>) -> PathBuf {
let home = home.unwrap_or_else(|| PathBuf::from("."));
// Prefer .codewhale, fall back to .deepseek
let primary = home.join(".codewhale").join("snapshots");
if primary.exists() {
return primary;
}
home.join(".deepseek").join("snapshots")
}
/// Resolve the `.git` directory inside the snapshot dir.
pub fn snapshot_git_dir(workspace: &Path) -> PathBuf {
snapshot_dir_for(workspace).join(".git")