On this page
Move session data after a project directory rename.
#claudewheel.mv
#claudewheel.mv
Move session data after a project directory rename.
#MvResult
Counters tracking the outcome of a project-directory move operation.
#_discover_profile_dirs
def _discover_profile_dirs(ws: 'Workspace') -> list[Path]Find all profile directories plus ~/.claudewheel/shared/ if it exists.
Enumerates profiles via the workspace's ProfileStore, then includes the shared store directory as a peer target (it holds the actual session data). A corrupt tokens.json raises TokenStoreError -- the uniform hard-error contract.
#_rewrite_jsonl_file
def _rewrite_jsonl_file(path: Path, old_path: str, new_path: str, dry_run: bool) -> intReplace old_path with new_path in every line of a JSONL file.
Returns the number of lines where a replacement was made.
#_plan_migrations
def _plan_migrations(old_resolved: str, new_resolved: str, descendants: set[str]) -> list[tuple[str, str]]Build the ordered (old, new) migration plan.
Includes old_resolved itself. Every destination is new_resolved plus the source's relative suffix. Longest old paths come first so a shorter prefix is never processed before its own descendants (prefix-shadowing prevention, same pattern as import_'s rewriters).
#_decode_rel
def _decode_rel(root: Path, enc: str) -> list[str]Find every existing relative dir path under root whose encoding is enc.
The path encoding is lossy ('/', '.', and literal '-' all become '-'), so one encoded string can correspond to several real paths. All matches are returned so the caller can detect ambiguity.
#_collect_project_keys
def _collect_project_keys(profile_dirs: list[Path], shared_dir: Path) -> set[str]All real-path keys under projects{} across every profile's .claude.json.
#_discover_descendants
def _discover_descendants(profile_dirs: list[Path], old_resolved: str, source_root: Path, known_keys: set[str]) -> set[str]Every real project path equal to or under old_resolved that has data.
Union of (a) .claude.json projects{} keys under OLD and (b) encoded projects/ dir names that decode to a path under OLD. Encoded names are never prefix-matched directly -- the encoding is ambiguous -- so each candidate is resolved back to a real path via the known keys plus filesystem checks under the moved tree. A candidate that resolves to a sibling path (merely sharing the encoded prefix) is skipped; one that cannot be resolved to exactly one real path is a hard error.
#_verify_destinations
def _verify_destinations(migrations: list[tuple[str, str]], old_resolved: str, source_root: Path, new_resolved: str) -> NoneHard-error unless every descendant's destination will exist on disk.
source_root is the moved tree as it currently exists (OLD before the rename, NEW in post-hoc mode), so source_root/<suffix> existing now is equivalent to NEW/<suffix> existing at migration time. On failure, every unresolvable descendant is listed and nothing is migrated.
#_rename_project_dir
def _rename_project_dir(old_project: Path, new_project: Path, dry_run: bool) -> boolRename old_project to new_project, merging when the target exists.
Returns True when a rename or merge happened (or would happen in dry run).
#_rewrite_prefixed_path
def _rewrite_prefixed_path(path: str, migrations: list[tuple[str, str]]) -> strRewrite a real path equal to or under a migrated source path.
migrations is longest-source-first, so the most specific mapping wins.
#_update_claude_json
def _update_claude_json(path: Path, migrations: list[tuple[str, str]], dry_run: bool) -> tuple[int, int]Rename project keys and rewrite githubRepoPaths in one .claude.json.
Project keys live under data["projects"]; every key matching a migration source is renamed to its destination. githubRepoPaths values (repo -> list of local paths) equal to or under a migration source are rewritten too. Returns (project_keys_updated, github_paths_updated).
#run_mv
def run_mv(ws: 'Workspace', old_path: str, new_path: str, dry_run: bool=False, quiet: bool=False, post_hoc: bool=False) -> MvResultRename a project directory and migrate Claude Code session data.
In default mode, renames old_path to new_path on the filesystem and then migrates all session data. With post_hoc=True, skips the filesystem rename (the directory was already renamed externally) and only migrates sessions.
The migration is prefix-aware: every project keyed at old_path or nested under it (Claude Code projects inside the moved tree) is migrated to new_path plus the same relative suffix. That covers the encoded projects/ dirs, the projects{} keys and githubRepoPaths entries in every profile's .claude.json, and the JSONL cwd references of every migrated project. Each descendant's destination must exist on disk under new_path; otherwise the operation aborts before touching anything.