wsb 0.172.1105

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
use crate::prelude::*;
use tokio::sync::broadcast;

use crate::logging::{LogLine, LogRingBuffer};
use crate::persist::global::{DatabaseConfig, DatabaseService, WsbRepos};
use crate::state::read_major_version_from_file;
use crate::workspace;

#[derive(Clone)]
pub struct AppContext {
    pub db: Arc<WsbRepos>,
    pub db_service: Arc<DatabaseService>,
    /// Most senior git repository root — DB lives at `workspace_root/.wsb/wsb.db`.
    pub workspace_root: PathBuf,
    /// Nearest ancestor containing `.wsb/state.json` — project-level config lives here.
    pub project_root: PathBuf,
}

impl AppContext {
    /// Bootstrap from `project_root` (the directory containing `.wsb/state.json`).
    ///
    /// The DB is placed at the *super root* (most senior git repo), which is the
    /// same as `project_root` in the common case (no submodules).
    ///
    /// After setting up the DB, seeds the project's `major_version` from
    /// `version.txt` if the file exists at the project root and the DB has no
    /// project row yet.
    pub fn bootstrap(project_root: &Path) -> Result<Self> {
        let db_root = workspace::find_super_root_or_project(project_root);
        let db_path = db_root.join(".wsb").join("wsb.db");
        let config = DatabaseConfig::new(&db_path);
        let service = DatabaseService::new(&config)?;
        let service = Arc::new(service);
        let repos = Arc::new(WsbRepos::new(&service));

        let ctx = Self {
            db: repos,
            db_service: service,
            workspace_root: db_root,
            project_root: project_root.to_path_buf(),
        };

        // Seed the DB project row's major_version from version.txt if available.
        // This is done eagerly so that subsequent reads from the DB reflect the
        // correct major version rather than the SQL default of 0.
        if let Some(major) = read_major_version_from_file(project_root) {
            ctx.seed_major_version(major)?;
        }

        Ok(ctx)
    }

    /// Update the project major_version in the DB if no row exists yet
    /// (i.e. first-run seeding). Called from bootstrap so that the very first
    /// StateConfig::load will find the correct value already persisted.
    pub fn seed_major_version(&self, major: u32) -> Result<()> {
        let conn = self.db_service.get_connection()
            .context("Failed to get DB connection for major version seeding")?;
        let existing: Option<i64> = conn
            .query_row(
                "SELECT major_version FROM projects LIMIT 1",
                [],
                |row| row.get(0),
            )
            .ok();
        if existing.is_none() {
            // No project row yet — the lazy init will use version.txt.
            // But we can proactively insert one now with the correct major.
            conn.execute(
                "INSERT OR IGNORE INTO projects (id, code, name, description, status, major_version, version_file, auto_detect_project_files, project_files) \
                 VALUES ('P001', 'P001', 'Default Project', 'Auto-created project', 'active', ?1, 'version.txt', 1, '[]')",
                rusqlite::params![major as i32],
            )?;
        } else {
            // Project row exists — update major_version if it's still 0 (default).
            let current = existing.unwrap() as u32;
            if current == 0 && major > 0 {
                conn.execute(
                    "UPDATE projects SET major_version = ?1 WHERE id = (SELECT id FROM projects LIMIT 1)",
                    rusqlite::params![major as i32],
                )?;
            }
        }
        Ok(())
    }
}

#[derive(Clone)]
pub struct AppState {
    pub ctx: AppContext,
    pub debug: bool,
    pub port: u16,
    pub log_tx: broadcast::Sender<LogLine>,
    pub log_ring: LogRingBuffer,
}

impl AppState {
    pub fn new(
        ctx: AppContext,
        debug: bool,
        port: u16,
        log_tx: broadcast::Sender<LogLine>,
        log_ring: LogRingBuffer,
    ) -> Self {
        Self {
            ctx,
            debug,
            port,
            log_tx,
            log_ring,
        }
    }
}

#[derive(Clone, Debug, serde::Serialize)]
pub struct SyncProgress {
    pub phase: SyncPhase,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_project: Option<String>,
    pub sessions_total: usize,
    pub sessions_done: usize,
    pub messages_indexed: usize,
}

#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SyncPhase {
    Idle,
    Discovering,
    Syncing,
    Done,
}