Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit fe218ac

Browse files
committed
Merge branch 'fn-24-json-storage': core data to JSON files, DB for memory only
2 parents de49f78 + 0eb2d8e commit fe218ac

17 files changed

Lines changed: 889 additions & 420 deletions

File tree

flowctl/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flowctl/crates/flowctl-cli/src/commands/admin/status.rs

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@ use flowctl_core::types::{
1616

1717
use super::get_flow_dir;
1818

19-
/// Open DB connection (hard error on failure, DB is sole source of truth).
20-
fn require_db() -> crate::commands::db_shim::Connection {
21-
crate::commands::db_shim::require_db()
22-
.unwrap_or_else(|e| {
23-
crate::output::error_exit(&format!("Cannot open database: {e}"));
24-
})
25-
}
2619

2720
// ── Status command ──────────────────────────────────────────────────
2821

@@ -156,13 +149,10 @@ pub fn cmd_status(json: bool, interrupted: bool) {
156149
}
157150
}
158151

159-
/// Try to get status counts from SQLite database.
152+
/// Try to get status counts from JSON files.
160153
fn status_from_db() -> Option<(serde_json::Value, serde_json::Value)> {
161-
let cwd = env::current_dir().ok()?;
162-
let conn = crate::commands::db_shim::open(&cwd).ok()?;
163-
164-
let epic_repo = crate::commands::db_shim::EpicRepo::new(&conn);
165-
let epics = epic_repo.list(None).ok()?;
154+
let flow_dir = crate::commands::helpers::get_flow_dir();
155+
let epics = flowctl_core::json_store::epic_list(&flow_dir).ok()?;
166156

167157
let mut epic_open = 0u64;
168158
let mut epic_done = 0u64;
@@ -173,8 +163,7 @@ fn status_from_db() -> Option<(serde_json::Value, serde_json::Value)> {
173163
}
174164
}
175165

176-
let task_repo = crate::commands::db_shim::TaskRepo::new(&conn);
177-
let tasks = task_repo.list_all(None, None).ok()?;
166+
let tasks = flowctl_core::json_store::task_list_all(&flow_dir).ok()?;
178167

179168
let mut todo = 0u64;
180169
let mut in_progress = 0u64;
@@ -219,29 +208,21 @@ fn is_daemon_heartbeat_alive(flow_dir: &Path) -> bool {
219208
.unwrap_or(false)
220209
}
221210

222-
/// Find open epics with undone tasks (interrupted work) from DB.
223-
fn find_interrupted_epics(_flow_dir: &Path) -> Vec<serde_json::Value> {
211+
/// Find open epics with undone tasks (interrupted work) from JSON files.
212+
fn find_interrupted_epics(flow_dir: &Path) -> Vec<serde_json::Value> {
224213
let mut interrupted = Vec::new();
225214

226-
let conn = match crate::commands::db_shim::require_db() {
227-
Ok(c) => c,
228-
Err(_) => return interrupted,
229-
};
230-
231-
let epic_repo = crate::commands::db_shim::EpicRepo::new(&conn);
232-
let epics = match epic_repo.list(None) {
215+
let epics = match flowctl_core::json_store::epic_list(flow_dir) {
233216
Ok(e) => e,
234217
Err(_) => return interrupted,
235218
};
236219

237-
let task_repo = crate::commands::db_shim::TaskRepo::new(&conn);
238-
239220
for epic in epics {
240221
if epic.status != flowctl_core::types::EpicStatus::Open {
241222
continue;
242223
}
243224

244-
let tasks = match task_repo.list_by_epic(&epic.id) {
225+
let tasks = match flowctl_core::json_store::task_list_by_epic(flow_dir, &epic.id) {
245226
Ok(t) => t,
246227
Err(_) => continue,
247228
};
@@ -337,26 +318,24 @@ pub(super) fn validate_epic(flow_dir: &Path, epic_id: &str) -> (Vec<String>, Vec
337318
let mut errors = Vec::new();
338319
let mut warnings = Vec::new();
339320

340-
// Read tasks from DB (sole source of truth)
321+
// Read tasks from JSON files
341322
let mut tasks: std::collections::HashMap<String, flowctl_core::types::Task> =
342323
std::collections::HashMap::new();
343324

344-
let conn = require_db();
345-
let task_repo = crate::commands::db_shim::TaskRepo::new(&conn);
346-
if let Ok(task_list) = task_repo.list_by_epic(epic_id) {
325+
if let Ok(task_list) = flowctl_core::json_store::task_list_by_epic(flow_dir, epic_id) {
347326
for task in task_list {
348327
tasks.insert(task.id.clone(), task);
349328
}
350329
}
351330

352331
// Validate each task
353332
for (task_id, task) in &tasks {
354-
// Validate task body has required headings (read from DB)
333+
// Validate task body has required headings (read from JSON)
355334
{
356-
match task_repo.get_with_body(task_id) {
357-
Ok((_t, body)) => {
335+
match flowctl_core::json_store::task_spec_read(flow_dir, task_id) {
336+
Ok(body) => {
358337
if body.is_empty() {
359-
warnings.push(format!("Task {}: no body in DB", task_id));
338+
warnings.push(format!("Task {}: no spec body", task_id));
360339
} else {
361340
for heading in flowctl_core::types::TASK_SPEC_HEADINGS {
362341
if !body.contains(heading) {
@@ -366,7 +345,7 @@ pub(super) fn validate_epic(flow_dir: &Path, epic_id: &str) -> (Vec<String>, Vec
366345
}
367346
}
368347
Err(_) => {
369-
errors.push(format!("Task {}: could not read body from DB", task_id));
348+
errors.push(format!("Task {}: could not read spec", task_id));
370349
}
371350
}
372351
}
@@ -430,9 +409,7 @@ pub fn cmd_validate(json_mode: bool, epic: Option<String>, all: bool) {
430409
// Validate all epics
431410
let root_errors = validate_flow_root(&flow_dir);
432411

433-
let conn = require_db();
434-
let epic_repo = crate::commands::db_shim::EpicRepo::new(&conn);
435-
let mut epic_ids: Vec<String> = epic_repo.list(None)
412+
let mut epic_ids: Vec<String> = flowctl_core::json_store::epic_list(&flow_dir)
436413
.unwrap_or_default()
437414
.into_iter()
438415
.map(|e| e.id)
@@ -553,9 +530,7 @@ pub fn cmd_doctor(json_mode: bool) {
553530
let mut validate_errors = root_errors.clone();
554531

555532
{
556-
let conn = require_db();
557-
let epic_repo = crate::commands::db_shim::EpicRepo::new(&conn);
558-
if let Ok(epics) = epic_repo.list(None) {
533+
if let Ok(epics) = flowctl_core::json_store::epic_list(&flow_dir) {
559534
for epic in &epics {
560535
let (errors, _, _) = validate_epic(&flow_dir, &epic.id);
561536
validate_errors.extend(errors);

flowctl/crates/flowctl-cli/src/commands/dep.rs

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
//! Dependency commands: dep add, dep rm.
2-
//!
3-
//! DB canonical — all dependency operations go through the DB (sole source of truth).
1+
//! Dependency management commands: add, remove.
42
53
use chrono::Utc;
64
use clap::Subcommand;
@@ -38,20 +36,6 @@ fn ensure_flow_exists() -> std::path::PathBuf {
3836
flow_dir
3937
}
4038

41-
/// Open DB connection (hard error if unavailable).
42-
fn require_db() -> crate::commands::db_shim::Connection {
43-
crate::commands::db_shim::require_db()
44-
.unwrap_or_else(|e| error_exit(&format!("DB required: {e}")))
45-
}
46-
47-
/// Read a task from DB (sole source of truth).
48-
fn read_task(task_id: &str) -> flowctl_core::types::Task {
49-
let conn = require_db();
50-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
51-
repo.get(task_id)
52-
.unwrap_or_else(|_| error_exit(&format!("Task not found: {}", task_id)))
53-
}
54-
5539
pub fn dispatch(cmd: &DepCmd, json: bool) {
5640
match cmd {
5741
DepCmd::Add { task, depends_on } => cmd_dep_add(json, task, depends_on),
@@ -60,7 +44,7 @@ pub fn dispatch(cmd: &DepCmd, json: bool) {
6044
}
6145

6246
fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
63-
ensure_flow_exists();
47+
let flow_dir = ensure_flow_exists();
6448

6549
if !is_task_id(task_id) {
6650
error_exit(&format!(
@@ -87,20 +71,15 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
8771
));
8872
}
8973

90-
let mut task = read_task(task_id);
74+
let mut task = flowctl_core::json_store::task_read(&flow_dir, task_id)
75+
.unwrap_or_else(|_| error_exit(&format!("Task not found: {}", task_id)));
9176

9277
if !task.depends_on.contains(&depends_on.to_string()) {
9378
task.depends_on.push(depends_on.to_string());
9479
task.updated_at = Utc::now();
95-
96-
// Write to DB
97-
let conn = require_db();
98-
let dep_repo = crate::commands::db_shim::DepRepo::new(&conn);
99-
if let Err(e) = dep_repo.add_task_dep(task_id, depends_on) {
100-
error_exit(&format!("Failed to add dep: {e}"));
80+
if let Err(e) = flowctl_core::json_store::task_write_definition(&flow_dir, &task) {
81+
error_exit(&format!("Failed to write task: {e}"));
10182
}
102-
let task_repo = crate::commands::db_shim::TaskRepo::new(&conn);
103-
let _ = task_repo.upsert(&task);
10483
}
10584

10685
if json {
@@ -115,7 +94,7 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
11594
}
11695

11796
fn cmd_dep_rm(json: bool, task_id: &str, depends_on: &str) {
118-
ensure_flow_exists();
97+
let flow_dir = ensure_flow_exists();
11998

12099
if !is_task_id(task_id) {
121100
error_exit(&format!("Invalid task ID: {}", task_id));
@@ -124,20 +103,15 @@ fn cmd_dep_rm(json: bool, task_id: &str, depends_on: &str) {
124103
error_exit(&format!("Invalid dependency ID: {}", depends_on));
125104
}
126105

127-
let mut task = read_task(task_id);
106+
let mut task = flowctl_core::json_store::task_read(&flow_dir, task_id)
107+
.unwrap_or_else(|_| error_exit(&format!("Task not found: {}", task_id)));
128108

129109
if let Some(pos) = task.depends_on.iter().position(|d| d == depends_on) {
130110
task.depends_on.remove(pos);
131111
task.updated_at = Utc::now();
132-
133-
// Write to DB
134-
let conn = require_db();
135-
let dep_repo = crate::commands::db_shim::DepRepo::new(&conn);
136-
if let Err(e) = dep_repo.remove_task_dep(task_id, depends_on) {
137-
error_exit(&format!("Failed to remove dep: {e}"));
112+
if let Err(e) = flowctl_core::json_store::task_write_definition(&flow_dir, &task) {
113+
error_exit(&format!("Failed to write task: {e}"));
138114
}
139-
let task_repo = crate::commands::db_shim::TaskRepo::new(&conn);
140-
let _ = task_repo.upsert(&task);
141115

142116
if json {
143117
json_output(json!({
@@ -154,9 +128,9 @@ fn cmd_dep_rm(json: bool, task_id: &str, depends_on: &str) {
154128
"task": task_id,
155129
"depends_on": task.depends_on,
156130
"removed": false,
157-
"message": format!("{} not in dependencies", depends_on),
131+
"message": format!("{} was not in {}'s dependencies", depends_on, task_id),
158132
}));
159133
} else {
160-
println!("{} is not a dependency of {}", depends_on, task_id);
134+
println!("{} was not in {}'s dependencies", depends_on, task_id);
161135
}
162136
}

0 commit comments

Comments
 (0)