11//! Dependency management commands: add, remove.
22
3+ use std:: path:: Path ;
4+
35use chrono:: Utc ;
46use clap:: Subcommand ;
57use serde_json:: json;
68
79use crate :: output:: { error_exit, json_output} ;
810
11+ use flowctl_core:: changes:: { Changes , Mutation } ;
912use flowctl_core:: id:: { epic_id_from_task, is_task_id} ;
1013
1114use super :: helpers:: get_flow_dir;
@@ -36,16 +39,20 @@ fn ensure_flow_exists() -> std::path::PathBuf {
3639 flow_dir
3740}
3841
39- pub fn dispatch ( cmd : & DepCmd , json : bool ) {
42+ pub fn dispatch ( cmd : & DepCmd , json : bool , dry_run : bool ) {
4043 match cmd {
41- DepCmd :: Add { task, depends_on } => cmd_dep_add ( json, task, depends_on) ,
42- DepCmd :: Rm { task, depends_on } => cmd_dep_rm ( json, task, depends_on) ,
44+ DepCmd :: Add { task, depends_on } => cmd_dep_add ( json, task, depends_on, dry_run ) ,
45+ DepCmd :: Rm { task, depends_on } => cmd_dep_rm ( json, task, depends_on, dry_run ) ,
4346 }
4447}
4548
46- fn cmd_dep_add ( json : bool , task_id : & str , depends_on : & str ) {
47- let flow_dir = ensure_flow_exists ( ) ;
48-
49+ /// Pure compute: build Changes to add a dependency.
50+ /// Returns (updated task, whether dep was actually added).
51+ fn compute_dep_add (
52+ flow_dir : & Path ,
53+ task_id : & str ,
54+ depends_on : & str ,
55+ ) -> ( flowctl_core:: types:: Task , bool , Changes ) {
4956 if !is_task_id ( task_id) {
5057 error_exit ( & format ! (
5158 "Invalid task ID: {}. Expected format: fn-N.M or fn-N-slug.M" ,
@@ -59,7 +66,6 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
5966 ) ) ;
6067 }
6168
62- // Validate same epic
6369 let task_epic = epic_id_from_task ( task_id)
6470 . unwrap_or_else ( |_| error_exit ( & format ! ( "Cannot parse epic from task ID: {}" , task_id) ) ) ;
6571 let dep_epic = epic_id_from_task ( depends_on)
@@ -71,15 +77,34 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
7177 ) ) ;
7278 }
7379
74- let mut task = flowctl_core:: json_store:: task_read ( & flow_dir, task_id)
80+ let mut task = flowctl_core:: json_store:: task_read ( flow_dir, task_id)
7581 . unwrap_or_else ( |_| error_exit ( & format ! ( "Task not found: {}" , task_id) ) ) ;
7682
77- if !task. depends_on . contains ( & depends_on. to_string ( ) ) {
83+ let added = if !task. depends_on . contains ( & depends_on. to_string ( ) ) {
7884 task. depends_on . push ( depends_on. to_string ( ) ) ;
7985 task. updated_at = Utc :: now ( ) ;
80- if let Err ( e) = flowctl_core:: json_store:: task_write_definition ( & flow_dir, & task) {
81- error_exit ( & format ! ( "Failed to write task: {e}" ) ) ;
82- }
86+ true
87+ } else {
88+ false
89+ } ;
90+
91+ let changes = if added {
92+ Changes :: new ( ) . with ( Mutation :: UpdateTask { task : task. clone ( ) } )
93+ } else {
94+ Changes :: new ( )
95+ } ;
96+
97+ ( task, added, changes)
98+ }
99+
100+ fn cmd_dep_add ( json : bool , task_id : & str , depends_on : & str , dry_run : bool ) {
101+ let flow_dir = ensure_flow_exists ( ) ;
102+
103+ let ( task, _added, changes) = compute_dep_add ( & flow_dir, task_id, depends_on) ;
104+
105+ crate :: commands:: helpers:: maybe_apply_changes ( & flow_dir, & changes, dry_run) ;
106+ if dry_run {
107+ return ;
83108 }
84109
85110 if json {
@@ -93,26 +118,51 @@ fn cmd_dep_add(json: bool, task_id: &str, depends_on: &str) {
93118 }
94119}
95120
96- fn cmd_dep_rm ( json : bool , task_id : & str , depends_on : & str ) {
97- let flow_dir = ensure_flow_exists ( ) ;
98-
121+ /// Pure compute: build Changes to remove a dependency.
122+ /// Returns (updated task, whether dep was actually removed).
123+ fn compute_dep_rm (
124+ flow_dir : & Path ,
125+ task_id : & str ,
126+ depends_on : & str ,
127+ ) -> ( flowctl_core:: types:: Task , bool , Changes ) {
99128 if !is_task_id ( task_id) {
100129 error_exit ( & format ! ( "Invalid task ID: {}" , task_id) ) ;
101130 }
102131 if !is_task_id ( depends_on) {
103132 error_exit ( & format ! ( "Invalid dependency ID: {}" , depends_on) ) ;
104133 }
105134
106- let mut task = flowctl_core:: json_store:: task_read ( & flow_dir, task_id)
135+ let mut task = flowctl_core:: json_store:: task_read ( flow_dir, task_id)
107136 . unwrap_or_else ( |_| error_exit ( & format ! ( "Task not found: {}" , task_id) ) ) ;
108137
109- if let Some ( pos) = task. depends_on . iter ( ) . position ( |d| d == depends_on) {
138+ let removed = if let Some ( pos) = task. depends_on . iter ( ) . position ( |d| d == depends_on) {
110139 task. depends_on . remove ( pos) ;
111140 task. updated_at = Utc :: now ( ) ;
112- if let Err ( e) = flowctl_core:: json_store:: task_write_definition ( & flow_dir, & task) {
113- error_exit ( & format ! ( "Failed to write task: {e}" ) ) ;
114- }
141+ true
142+ } else {
143+ false
144+ } ;
145+
146+ let changes = if removed {
147+ Changes :: new ( ) . with ( Mutation :: UpdateTask { task : task. clone ( ) } )
148+ } else {
149+ Changes :: new ( )
150+ } ;
151+
152+ ( task, removed, changes)
153+ }
154+
155+ fn cmd_dep_rm ( json : bool , task_id : & str , depends_on : & str , dry_run : bool ) {
156+ let flow_dir = ensure_flow_exists ( ) ;
157+
158+ let ( task, removed, changes) = compute_dep_rm ( & flow_dir, task_id, depends_on) ;
159+
160+ crate :: commands:: helpers:: maybe_apply_changes ( & flow_dir, & changes, dry_run) ;
161+ if dry_run {
162+ return ;
163+ }
115164
165+ if removed {
116166 if json {
117167 json_output ( json ! ( {
118168 "task" : task_id,
0 commit comments