From 8ef1fbd6f8a149ecb7f5420e1d223d7e56c73f90 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 13 Mar 2026 23:16:18 -0400 Subject: [PATCH] Add user-level workflow lookup in ~/.fabro/workflows/ `fabro run NAME` now checks ~/.fabro/workflows/ as a fallback when the workflow isn't found in the project directory, letting users have personal workflows available across all projects. Project workflows take precedence over user workflows. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fabro-workflows/src/cli/project_config.rs | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/lib/crates/fabro-workflows/src/cli/project_config.rs b/lib/crates/fabro-workflows/src/cli/project_config.rs index a25576a7c..4eda0f6da 100644 --- a/lib/crates/fabro-workflows/src/cli/project_config.rs +++ b/lib/crates/fabro-workflows/src/cli/project_config.rs @@ -116,12 +116,8 @@ fn resolve_workflow_arg_impl( return Ok(project_candidate); } - if let Some(user_wf) = user_workflows { - let user_candidate = user_wf.join(&*name).join("workflow.toml"); - if user_candidate.is_file() { - tracing::debug!(arg = %arg.display(), resolved = %user_candidate.display(), "Resolved workflow name via user workflows"); - return Ok(user_candidate); - } + if let Some(resolved) = resolve_user_workflow(user_workflows, &name, arg) { + return Ok(resolved); } let project_wf_dir = fabro_root.join("workflows"); @@ -142,12 +138,8 @@ fn resolve_workflow_arg_impl( bail!("{msg}"); } Ok(None) => { - if let Some(user_wf) = user_workflows { - let user_candidate = user_wf.join(&*name).join("workflow.toml"); - if user_candidate.is_file() { - tracing::debug!(arg = %arg.display(), resolved = %user_candidate.display(), "Resolved workflow name via user workflows (no project config)"); - return Ok(user_candidate); - } + if let Some(resolved) = resolve_user_workflow(user_workflows, &name, arg) { + return Ok(resolved); } tracing::debug!(arg = %arg.display(), "No project config found, returning literal"); Ok(arg.to_path_buf()) @@ -159,6 +151,18 @@ fn resolve_workflow_arg_impl( } } +/// Check if a workflow exists in the user-level workflows directory. +fn resolve_user_workflow(user_workflows: Option<&Path>, name: &str, arg: &Path) -> Option { + let user_wf = user_workflows?; + let candidate = user_wf.join(name).join("workflow.toml"); + if candidate.is_file() { + tracing::debug!(arg = %arg.display(), resolved = %candidate.display(), "Resolved workflow name via user workflows"); + Some(candidate) + } else { + None + } +} + /// Return the user-level workflows directory (`~/.fabro/workflows/`). fn user_workflows_dir() -> Option { dirs::home_dir().map(|h| h.join(".fabro").join("workflows")) @@ -490,12 +494,9 @@ mod tests { assert_eq!(result, wf_dir.join("workflow.toml")); } - /// Helper: create a temp dir with fabro.toml + workflows/{name}/{workflow.toml, workflow.fabro} - /// and chdir into it so `resolve_workflow` (which uses cwd) can find the config. - fn setup_workflow_project(name: &str) -> (TempDir, PathBuf) { - let tmp = TempDir::new().unwrap(); - fs::write(tmp.path().join("fabro.toml"), "version = 1\n").unwrap(); - let wf_dir = tmp.path().join("workflows").join(name); + /// Helper: create a workflow dir with workflow.toml + workflow.fabro inside `base/workflows/{name}/` + fn create_workflow_in(base: &Path, name: &str) { + let wf_dir = base.join("workflows").join(name); fs::create_dir_all(&wf_dir).unwrap(); fs::write( wf_dir.join("workflow.toml"), @@ -507,7 +508,18 @@ mod tests { "digraph G { start [shape=Mdiamond]; exit [shape=Msquare]; start -> exit }", ) .unwrap(); - let dot_path = wf_dir.join("workflow.fabro"); + } + + /// Helper: create a temp dir with fabro.toml + workflows/{name}/{workflow.toml, workflow.fabro} + fn setup_workflow_project(name: &str) -> (TempDir, PathBuf) { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("fabro.toml"), "version = 1\n").unwrap(); + create_workflow_in(tmp.path(), name); + let dot_path = tmp + .path() + .join("workflows") + .join(name) + .join("workflow.fabro"); (tmp, dot_path) } @@ -539,22 +551,6 @@ mod tests { assert!(cfg.is_none(), "expected None for .fabro path"); } - /// Helper: create a workflow dir with workflow.toml + workflow.fabro inside `base/workflows/{name}/` - fn create_workflow_in(base: &Path, name: &str) { - let wf_dir = base.join("workflows").join(name); - fs::create_dir_all(&wf_dir).unwrap(); - fs::write( - wf_dir.join("workflow.toml"), - "version = 1\ngraph = \"workflow.fabro\"\n", - ) - .unwrap(); - fs::write( - wf_dir.join("workflow.fabro"), - "digraph G { start [shape=Mdiamond]; exit [shape=Msquare]; start -> exit }", - ) - .unwrap(); - } - #[test] fn resolve_workflow_arg_user_workflow_found() { let project_dir = TempDir::new().unwrap();