//! `.skate/` runtime directory management. //! //! The `.skate/` directory lives inside the user's project and holds all //! runtime artefacts produced by a skate session: structured logs, session //! indices, and (in future) per-run snapshots. None of these should ever be //! committed to the project's VCS, so the first time the directory is created //! we drop a `.gitignore` containing `*` -- ignoring everything, including the //! `.gitignore` itself. //! //! # Lifecycle //! //! ```text //! app::run //! -> SkateDir::open(project_dir) -- creates dir + .gitignore if needed //! -> skate_dir.init_tracing() -- opens skate.log, installs subscriber //! ``` use std::path::{Path, PathBuf}; use std::sync::Mutex; use anyhow::Context; use tracing_subscriber::EnvFilter; /// The `.skate/` runtime directory inside a project. /// /// Created on first use; subsequent calls are no-ops. All knowledge of /// well-known child paths stays inside this module so callers never /// construct them by hand. pub struct SkateDir { path: PathBuf, } impl SkateDir { /// Open (or create) the `.skate/` directory inside `project_dir`. /// /// On first call this also writes a `.gitignore` containing `*` so that /// none of the runtime files are accidentally committed. Concretely: /// /// 1. `create_dir_all` -- idempotent, works whether the dir already exists /// or is being created for the first time. /// 2. `OpenOptions::create_new` on `.gitignore` -- atomic write-once; the /// `AlreadyExists` error is silently ignored so repeated calls are safe. /// /// Returns `Err` on any I/O failure other than `AlreadyExists`. pub fn open(project_dir: &Path) -> anyhow::Result { let path = project_dir.join(".skate"); std::fs::create_dir_all(&path) .with_context(|| format!("cannot create .skate directory {}", path.display()))?; // Write .gitignore on first creation; no-op if it already exists. // Content is "*": ignore everything in this directory including the // .gitignore itself -- none of the skate runtime files should be committed. let gitignore_path = path.join(".gitignore"); match std::fs::OpenOptions::new() .write(true) .create_new(true) .open(&gitignore_path) { Ok(mut f) => { use std::io::Write; f.write_all(b"*\n") .with_context(|| format!("cannot write {}", gitignore_path.display()))?; } Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} Err(e) => { return Err(e) .with_context(|| format!("cannot create {}", gitignore_path.display())); } } Ok(Self { path }) } /// Install the global `tracing` subscriber, writing to `skate.log`. /// /// Opens (or creates) `skate.log` in append mode, then registers a /// `tracing_subscriber::fmt` subscriber that writes structured JSON-ish /// text to that file. Writing to stdout is not possible because the TUI /// owns the terminal. /// /// RUST_LOG controls verbosity; falls back to `info` if absent or /// unparseable. Must be called at most once per process -- the underlying /// `tracing` registry panics on a second `init()` call. pub fn init_tracing(&self) -> anyhow::Result<()> { let log_path = self.path.join("skate.log"); let log_file = std::fs::OpenOptions::new() .create(true) .append(true) .open(&log_path) .with_context(|| format!("cannot open log file {}", log_path.display()))?; let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt() .with_writer(Mutex::new(log_file)) .with_ansi(false) .with_env_filter(filter) .init(); Ok(()) } }