kaizen/core/
machine_registry.rs1use anyhow::{Context, Result};
5use rusqlite::{Connection, params};
6use std::path::{Path, PathBuf};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use crate::core::paths::{canonical, kaizen_dir};
10
11mod connection;
12mod legacy;
13mod metadata;
14mod sql;
15
16const MACHINE_DB: &str = "machine.db";
17
18pub fn db_path() -> Option<PathBuf> {
20 kaizen_dir().map(|d| d.join(MACHINE_DB))
21}
22
23fn db_path_for_write(workspace: &Path) -> Result<Option<PathBuf>> {
24 if kaizen_dir().is_none() {
25 return Ok(None);
26 }
27 crate::core::home_paths::sqlite_file_for_write(workspace, MACHINE_DB).map(Some)
28}
29
30fn now_ms() -> i64 {
31 SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .unwrap_or_default()
34 .as_millis() as i64
35}
36
37fn name_for_path(path: &Path) -> String {
38 path.file_name()
39 .and_then(|n| n.to_str())
40 .map(str::to_string)
41 .unwrap_or_default()
42}
43
44fn open_conn_write(workspace: &Path) -> Result<Option<Connection>> {
45 connection::open_write(workspace)
46}
47
48fn open_conn_read() -> Result<Option<Connection>> {
49 connection::open_read()
50}
51
52fn with_write<F>(workspace: &Path, f: F) -> Result<()>
53where
54 F: FnOnce(&Connection) -> Result<()>,
55{
56 let Some(conn) = open_conn_write(workspace)? else {
57 return Ok(());
58 };
59 legacy::migrate(&conn)?;
60 f(&conn)
61}
62
63pub fn upsert_from_resolve(path: &Path) -> Result<()> {
65 with_write(path, |conn| upsert_seen(conn, path))
66}
67
68fn upsert_seen(conn: &Connection, path: &Path) -> Result<()> {
69 let c = canonical(path);
70 let t = now_ms();
71 let name = name_for_path(&c);
72 let p = c.to_string_lossy();
73 conn.execute(sql::UPSERT_SEEN, params![p.as_ref(), &name, t, t])
74 .context("machine registry upsert from resolve")?;
75 Ok(())
76}
77
78pub fn record_init(path: &Path) -> Result<()> {
80 with_write(path, |conn| insert_init(conn, path))
81}
82
83fn insert_init(conn: &Connection, path: &Path) -> Result<()> {
84 let c = canonical(path);
85 let t = now_ms();
86 let name = name_for_path(&c);
87 let p = c.to_string_lossy();
88 let origin = metadata::git_remote_origin(&c);
89 let values = params![
90 p.as_ref(),
91 &name,
92 t,
93 t,
94 t,
95 origin.as_deref(),
96 env!("CARGO_PKG_VERSION")
97 ];
98 conn.execute(sql::RECORD_INIT, values)
99 .context("machine registry record init")?;
100 Ok(())
101}
102
103pub fn list_paths() -> Result<Vec<PathBuf>> {
105 Ok(list_paths_including_missing()?
106 .into_iter()
107 .filter(|path| path.is_dir())
108 .collect())
109}
110
111pub fn list_paths_including_missing() -> Result<Vec<PathBuf>> {
113 let mut paths = legacy::read_paths();
114 if let Some(conn) = open_conn_read()? {
115 extend_unique(&mut paths, query_paths(&conn)?);
116 }
117 Ok(paths)
118}
119
120fn extend_unique(paths: &mut Vec<PathBuf>, additions: impl IntoIterator<Item = PathBuf>) {
121 additions.into_iter().for_each(|path| {
122 if !paths.contains(&path) {
123 paths.push(path);
124 }
125 });
126}
127
128fn query_paths(conn: &Connection) -> Result<Vec<PathBuf>> {
129 let mut stmt = conn
130 .prepare(sql::LIST_PATHS)
131 .context("machine registry list paths")?;
132 let rows = stmt
133 .query_map([], |row| row.get::<_, String>(0).map(PathBuf::from))
134 .context("query machine registry")?;
135 Ok(rows.collect::<rusqlite::Result<_>>()?)
136}
137
138pub fn is_registered(path: &Path) -> bool {
140 let canonical = canonical(path);
141 if legacy::read_paths().contains(&canonical) {
142 return true;
143 }
144 let Some(conn) = open_conn_read().ok().flatten() else {
145 return false;
146 };
147 registered(&conn, &canonical)
148}
149
150fn registered(conn: &Connection, path: &Path) -> bool {
151 let c = canonical(path);
152 let p = c.to_string_lossy();
153 conn.query_row(sql::IS_REGISTERED, [p.as_ref()], |_| Ok(()))
154 .is_ok()
155}
156
157pub fn status() -> Result<Option<(PathBuf, usize)>> {
159 let Some(path) = db_path() else {
160 return Ok(None);
161 };
162 let legacy = legacy::read_paths();
163 let conn = open_conn_read()?;
164 Ok(Some((path, total_project_count(conn.as_ref(), &legacy))))
165}
166
167fn total_project_count(conn: Option<&Connection>, legacy: &[PathBuf]) -> usize {
168 match conn {
169 Some(conn) => project_count(conn) + legacy.iter().filter(|p| !registered(conn, p)).count(),
170 None => legacy.len(),
171 }
172}
173
174fn project_count(conn: &Connection) -> usize {
175 conn.query_row(sql::PROJECT_COUNT, [], |row| row.get::<_, i64>(0))
176 .unwrap_or(0) as usize
177}