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

Commit 6bdbb02

Browse files
z23ccclaude
andcommitted
feat(cli): migrate dep.rs and query.rs to json_store
dep.rs: rewrite add/rm commands to read/write task JSON directly. query.rs: replace helpers (get_epic, get_task, get_all_tasks, etc.) with json_store calls. Keep require_db bridge only for FileLock ops. Remaining DB calls: gap.rs (1), stats.rs (3) — deferred to task .11. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 960811b commit 6bdbb02

2 files changed

Lines changed: 50 additions & 81 deletions

File tree

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
}

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

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,13 @@ fn epic_to_json(epic: &Epic) -> serde_json::Value {
5353
fn task_to_json(task: &Task) -> serde_json::Value {
5454
let spec_path = format!(".flow/tasks/{}.md", task.id);
5555

56-
// Try to get runtime state from DB
56+
// Try to get runtime state from JSON files
5757
let mut assignee: serde_json::Value = json!(null);
5858
let mut claimed_at: serde_json::Value = json!(null);
5959
let claim_note: serde_json::Value = json!("");
6060

61-
let conn = require_db();
62-
let runtime_repo = crate::commands::db_shim::RuntimeRepo::new(&conn);
63-
if let Ok(Some(state)) = runtime_repo.get(&task.id) {
61+
let flow_dir = crate::commands::helpers::get_flow_dir();
62+
if let Ok(state) = flowctl_core::json_store::state_read(&flow_dir, &task.id) {
6463
if let Some(a) = &state.assignee {
6564
assignee = json!(a);
6665
}
@@ -114,54 +113,55 @@ fn task_list_json(task: &Task) -> serde_json::Value {
114113
})
115114
}
116115

117-
// ── DB-only data access ────────────────────────────────────────────
116+
// ── DB bridge for file locks (stays in DB) ─────────────────────────
118117

119-
/// Open DB or exit with error (DB is sole source of truth).
120118
fn require_db() -> crate::commands::db_shim::Connection {
121119
crate::commands::db_shim::require_db()
122120
.unwrap_or_else(|e| error_exit(&format!("Cannot open database: {}", e)))
123121
}
124122

125-
/// Get a single epic by ID from DB.
126-
fn get_epic(_flow_dir: &PathBuf, id: &str) -> Option<Epic> {
127-
let conn = require_db();
128-
let repo = crate::commands::db_shim::EpicRepo::new(&conn);
129-
repo.get(id).ok()
123+
// ── JSON file data access ──────────────────────────────────────────
124+
125+
/// Get a single epic by ID from JSON files.
126+
fn get_epic(flow_dir: &PathBuf, id: &str) -> Option<Epic> {
127+
flowctl_core::json_store::epic_read(flow_dir, id).ok()
130128
}
131129

132-
/// Get a single task by ID from DB.
133-
fn get_task(_flow_dir: &PathBuf, id: &str) -> Option<Task> {
134-
let conn = require_db();
135-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
136-
repo.get(id).ok()
130+
/// Get a single task by ID from JSON files.
131+
fn get_task(flow_dir: &PathBuf, id: &str) -> Option<Task> {
132+
flowctl_core::json_store::task_read(flow_dir, id).ok()
137133
}
138134

139-
/// Get all tasks for an epic from DB.
140-
fn get_epic_tasks(_flow_dir: &PathBuf, epic_id: &str) -> Vec<Task> {
141-
let conn = require_db();
142-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
143-
repo.list_by_epic(epic_id).unwrap_or_default()
135+
/// Get all tasks for an epic from JSON files.
136+
fn get_epic_tasks(flow_dir: &PathBuf, epic_id: &str) -> Vec<Task> {
137+
flowctl_core::json_store::task_list_by_epic(flow_dir, epic_id).unwrap_or_default()
144138
}
145139

146-
/// Get all epics from DB.
147-
fn get_all_epics(_flow_dir: &PathBuf) -> Vec<Epic> {
148-
let conn = require_db();
149-
let repo = crate::commands::db_shim::EpicRepo::new(&conn);
150-
repo.list(None).unwrap_or_default()
140+
/// Get all epics from JSON files.
141+
fn get_all_epics(flow_dir: &PathBuf) -> Vec<Epic> {
142+
flowctl_core::json_store::epic_list(flow_dir).unwrap_or_default()
151143
}
152144

153-
/// Get all tasks, optionally filtered, from DB.
145+
/// Get all tasks, optionally filtered, from JSON files.
154146
fn get_all_tasks(
155-
_flow_dir: &PathBuf,
147+
flow_dir: &PathBuf,
156148
epic_filter: Option<&str>,
157149
status_filter: Option<&str>,
158150
domain_filter: Option<&str>,
159151
) -> Vec<Task> {
160-
let conn = require_db();
161-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
162152
match epic_filter {
163153
Some(epic_id) => {
164-
let mut tasks = repo.list_by_epic(epic_id).unwrap_or_default();
154+
let mut tasks = flowctl_core::json_store::task_list_by_epic(flow_dir, epic_id).unwrap_or_default();
155+
if let Some(status) = status_filter {
156+
tasks.retain(|t| t.status.to_string() == status);
157+
}
158+
if let Some(domain) = domain_filter {
159+
tasks.retain(|t| t.domain.to_string() == domain);
160+
}
161+
tasks
162+
}
163+
None => {
164+
let mut tasks = flowctl_core::json_store::task_list_all(flow_dir).unwrap_or_default();
165165
if let Some(status) = status_filter {
166166
tasks.retain(|t| t.status.to_string() == status);
167167
}
@@ -170,7 +170,6 @@ fn get_all_tasks(
170170
}
171171
tasks
172172
}
173-
None => repo.list_all(status_filter, domain_filter).unwrap_or_default(),
174173
}
175174
}
176175

@@ -490,13 +489,11 @@ pub fn cmd_cat(id: String) {
490489
}
491490
}
492491
} else if is_task_id(&id) {
493-
// Task body: read from DB (sole source of truth)
494-
let conn = require_db();
495-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
496-
match repo.get_with_body(&id) {
497-
Ok((_task, body)) => {
492+
// Task body: read from JSON spec file
493+
match flowctl_core::json_store::task_spec_read(&flow_dir, &id) {
494+
Ok(body) => {
498495
if body.is_empty() {
499-
error_exit(&format!("Task body not found in DB: {}", id));
496+
error_exit(&format!("Task spec not found: {}", id));
500497
}
501498
print!("{}", body);
502499
}
@@ -527,14 +524,12 @@ pub fn cmd_files(json_mode: bool, epic: String) {
527524
let mut ownership: std::collections::BTreeMap<String, Vec<String>> =
528525
std::collections::BTreeMap::new();
529526

530-
let conn = require_db();
531527
for task in &tasks {
532528
let mut task_files: Vec<String> = task.files.clone();
533529

534-
// Fallback: parse **Files:** from task body in DB if no structured files
530+
// Fallback: parse **Files:** from task spec if no structured files
535531
if task_files.is_empty() {
536-
let repo = crate::commands::db_shim::TaskRepo::new(&conn);
537-
if let Ok((_t, body)) = repo.get_with_body(&task.id) {
532+
if let Ok(body) = flowctl_core::json_store::task_spec_read(&flow_dir, &task.id) {
538533
for line in body.lines() {
539534
if let Some(rest) = line.strip_prefix("**Files:**") {
540535
task_files = rest

0 commit comments

Comments
 (0)