11//! Dependency commands: dep add, dep rm.
22//!
3- //! Updates both the Markdown frontmatter (canonical) and SQLite (cache).
4-
5- use std:: env;
6- use std:: fs;
7- use std:: path:: { Path , PathBuf } ;
3+ //! DB canonical — all dependency operations go through the DB (sole source of truth).
84
95use chrono:: Utc ;
106use clap:: Subcommand ;
117use serde_json:: json;
128
139use crate :: output:: { error_exit, json_output} ;
1410
15- use flowctl_core:: frontmatter;
1611use flowctl_core:: id:: { epic_id_from_task, is_task_id} ;
17- use flowctl_core:: types:: { Task , TASKS_DIR } ;
1812
1913use super :: helpers:: get_flow_dir;
2014
@@ -36,69 +30,26 @@ pub enum DepCmd {
3630 } ,
3731}
3832
39- fn ensure_flow_exists ( ) -> PathBuf {
33+ fn ensure_flow_exists ( ) -> std :: path :: PathBuf {
4034 let flow_dir = get_flow_dir ( ) ;
4135 if !flow_dir. exists ( ) {
4236 error_exit ( ".flow/ does not exist. Run 'flowctl init' first." ) ;
4337 }
4438 flow_dir
4539}
4640
47- /// Try to open a DB connection.
48- fn try_open_db ( ) -> Option < crate :: commands:: db_shim:: Connection > {
49- let cwd = env:: current_dir ( ) . ok ( ) ?;
50- crate :: commands:: db_shim:: open ( & cwd) . ok ( )
51- }
52-
53- /// Read a task document: DB first, markdown fallback.
54- fn read_task_doc ( flow_dir : & Path , task_id : & str ) -> ( PathBuf , frontmatter:: Document < Task > ) {
55- let task_path = flow_dir. join ( TASKS_DIR ) . join ( format ! ( "{}.md" , task_id) ) ;
56- // Try DB first.
57- if let Some ( conn) = try_open_db ( ) {
58- let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
59- if let Ok ( ( task, body) ) = repo. get_with_body ( task_id) {
60- return ( task_path, frontmatter:: Document { frontmatter : task, body } ) ;
61- }
62- }
63- // Fallback to markdown.
64- if !task_path. exists ( ) {
65- error_exit ( & format ! ( "Task not found: {}" , task_id) ) ;
66- }
67- let content = fs:: read_to_string ( & task_path)
68- . unwrap_or_else ( |e| error_exit ( & format ! ( "Cannot read {}: {}" , task_path. display( ) , e) ) ) ;
69- let doc: frontmatter:: Document < Task > = frontmatter:: parse ( & content)
70- . unwrap_or_else ( |e| error_exit ( & format ! ( "Cannot parse {}: {}" , task_path. display( ) , e) ) ) ;
71- ( task_path, doc)
72- }
73-
74- /// Write a task document: DB first, then export markdown.
75- fn write_task_doc ( path : & Path , doc : & frontmatter:: Document < Task > ) {
76- // Write to DB.
77- if let Some ( conn) = try_open_db ( ) {
78- let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
79- if let Err ( e) = repo. upsert_with_body ( & doc. frontmatter , & doc. body ) {
80- eprintln ! ( "warning: DB write failed for {}: {e}" , doc. frontmatter. id) ;
81- }
82- }
83- // Export to markdown.
84- let content = frontmatter:: write ( doc)
85- . unwrap_or_else ( |e| error_exit ( & format ! ( "Cannot serialize task: {}" , e) ) ) ;
86- fs:: write ( path, content)
87- . unwrap_or_else ( |e| error_exit ( & format ! ( "Cannot write {}: {}" , path. display( ) , e) ) ) ;
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}" ) ) )
8845}
8946
90- /// Update the SQLite cache for a task's dependencies (best-effort).
91- fn sync_deps_to_db ( task_id : & str , deps : & [ String ] ) {
92- let cwd = match env:: current_dir ( ) {
93- Ok ( c) => c,
94- Err ( _) => return ,
95- } ;
96- let conn = match crate :: commands:: db_shim:: open ( & cwd) {
97- Ok ( c) => c,
98- Err ( _) => return ,
99- } ;
100- let dep_repo = crate :: commands:: db_shim:: DepRepo :: new ( & conn) ;
101- let _ = dep_repo. replace_task_deps ( task_id, deps) ;
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) ) )
10253}
10354
10455pub fn dispatch ( cmd : & DepCmd , json : bool ) {
@@ -109,7 +60,7 @@ pub fn dispatch(cmd: &DepCmd, json: bool) {
10960}
11061
11162fn cmd_dep_add ( json : bool , task_id : & str , depends_on : & str ) {
112- let flow_dir = ensure_flow_exists ( ) ;
63+ ensure_flow_exists ( ) ;
11364
11465 if !is_task_id ( task_id) {
11566 error_exit ( & format ! (
@@ -136,19 +87,26 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
13687 ) ) ;
13788 }
13889
139- let ( task_path, mut doc) = read_task_doc ( & flow_dir, task_id) ;
90+ let mut task = read_task ( task_id) ;
91+
92+ if !task. depends_on . contains ( & depends_on. to_string ( ) ) {
93+ task. depends_on . push ( depends_on. to_string ( ) ) ;
94+ task. updated_at = Utc :: now ( ) ;
14095
141- if !doc. frontmatter . depends_on . contains ( & depends_on. to_string ( ) ) {
142- doc. frontmatter . depends_on . push ( depends_on. to_string ( ) ) ;
143- doc. frontmatter . updated_at = Utc :: now ( ) ;
144- write_task_doc ( & task_path, & doc) ;
145- sync_deps_to_db ( task_id, & doc. frontmatter . depends_on ) ;
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}" ) ) ;
101+ }
102+ let task_repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
103+ let _ = task_repo. upsert ( & task) ;
146104 }
147105
148106 if json {
149107 json_output ( json ! ( {
150108 "task" : task_id,
151- "depends_on" : doc . frontmatter . depends_on,
109+ "depends_on" : task . depends_on,
152110 "message" : format!( "Dependency {} added to {}" , depends_on, task_id) ,
153111 } ) ) ;
154112 } else {
@@ -157,7 +115,7 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
157115}
158116
159117fn cmd_dep_rm ( json : bool , task_id : & str , depends_on : & str ) {
160- let flow_dir = ensure_flow_exists ( ) ;
118+ ensure_flow_exists ( ) ;
161119
162120 if !is_task_id ( task_id) {
163121 error_exit ( & format ! ( "Invalid task ID: {}" , task_id) ) ;
@@ -166,18 +124,25 @@ fn cmd_dep_rm(json: bool, task_id: &str, depends_on: &str) {
166124 error_exit ( & format ! ( "Invalid dependency ID: {}" , depends_on) ) ;
167125 }
168126
169- let ( task_path, mut doc) = read_task_doc ( & flow_dir, task_id) ;
127+ let mut task = read_task ( task_id) ;
128+
129+ if let Some ( pos) = task. depends_on . iter ( ) . position ( |d| d == depends_on) {
130+ task. depends_on . remove ( pos) ;
131+ task. updated_at = Utc :: now ( ) ;
170132
171- if let Some ( pos) = doc. frontmatter . depends_on . iter ( ) . position ( |d| d == depends_on) {
172- doc. frontmatter . depends_on . remove ( pos) ;
173- doc. frontmatter . updated_at = Utc :: now ( ) ;
174- write_task_doc ( & task_path, & doc) ;
175- sync_deps_to_db ( task_id, & doc. frontmatter . depends_on ) ;
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}" ) ) ;
138+ }
139+ let task_repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
140+ let _ = task_repo. upsert ( & task) ;
176141
177142 if json {
178143 json_output ( json ! ( {
179144 "task" : task_id,
180- "depends_on" : doc . frontmatter . depends_on,
145+ "depends_on" : task . depends_on,
181146 "removed" : true ,
182147 "message" : format!( "Dependency {} removed from {}" , depends_on, task_id) ,
183148 } ) ) ;
@@ -187,7 +152,7 @@ fn cmd_dep_rm(json: bool, task_id: &str, depends_on: &str) {
187152 } else if json {
188153 json_output ( json ! ( {
189154 "task" : task_id,
190- "depends_on" : doc . frontmatter . depends_on,
155+ "depends_on" : task . depends_on,
191156 "removed" : false ,
192157 "message" : format!( "{} not in dependencies" , depends_on) ,
193158 } ) ) ;
0 commit comments