| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- #!/usr/bin/env python3
- """
- Project location helpers for webnovel-writer scripts.
- Problem this solves:
- - Many scripts assumed CWD is the project root and used relative paths like `.webnovel/state.json`.
- - In this repo, commands/scripts are often invoked from the repo root, while the actual project lives
- in a subdirectory (default: `webnovel-project/`).
- These helpers provide a single, consistent way to locate the active project root.
- """
- from __future__ import annotations
- import os
- from pathlib import Path
- from typing import Iterable, Optional
- DEFAULT_PROJECT_DIR_NAMES: tuple[str, ...] = ("webnovel-project",)
- def _candidate_roots(cwd: Path) -> Iterable[Path]:
- yield cwd
- for name in DEFAULT_PROJECT_DIR_NAMES:
- yield cwd / name
- for parent in cwd.parents:
- yield parent
- for name in DEFAULT_PROJECT_DIR_NAMES:
- yield parent / name
- def _is_project_root(path: Path) -> bool:
- return (path / ".webnovel" / "state.json").is_file()
- def resolve_project_root(explicit_project_root: Optional[str] = None, *, cwd: Optional[Path] = None) -> Path:
- """
- Resolve the webnovel project root directory (the directory containing `.webnovel/state.json`).
- Resolution order:
- 1) explicit_project_root (if provided)
- 2) env var WEBNOVEL_PROJECT_ROOT (if set)
- 3) Search from cwd and parents, including common subdir `webnovel-project/`
- Raises:
- FileNotFoundError: if no valid project root can be found.
- """
- if explicit_project_root:
- root = Path(explicit_project_root).expanduser().resolve()
- if _is_project_root(root):
- return root
- raise FileNotFoundError(f"Not a webnovel project root (missing .webnovel/state.json): {root}")
- env_root = os.environ.get("WEBNOVEL_PROJECT_ROOT")
- if env_root:
- root = Path(env_root).expanduser().resolve()
- if _is_project_root(root):
- return root
- raise FileNotFoundError(f"WEBNOVEL_PROJECT_ROOT is set but invalid (missing .webnovel/state.json): {root}")
- base = (cwd or Path.cwd()).resolve()
- for candidate in _candidate_roots(base):
- if _is_project_root(candidate):
- return candidate.resolve()
- raise FileNotFoundError(
- "Unable to locate webnovel project root. Expected `.webnovel/state.json` under the current directory, "
- "a parent directory, or `webnovel-project/`. Run /webnovel-init first or pass --project-root / set "
- "WEBNOVEL_PROJECT_ROOT."
- )
- def resolve_state_file(
- explicit_state_file: Optional[str] = None,
- *,
- explicit_project_root: Optional[str] = None,
- cwd: Optional[Path] = None,
- ) -> Path:
- """
- Resolve `.webnovel/state.json` path.
- If explicit_state_file is provided, returns it as-is (resolved to absolute if relative).
- Otherwise derives it from resolve_project_root().
- """
- base = (cwd or Path.cwd()).resolve()
- if explicit_state_file:
- p = Path(explicit_state_file).expanduser()
- return (base / p).resolve() if not p.is_absolute() else p.resolve()
- root = resolve_project_root(explicit_project_root, cwd=base)
- return root / ".webnovel" / "state.json"
|