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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-13 23:16:18 -04:00
parent fdcebd850b
commit 8ef1fbd6f8

View file

@ -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<PathBuf> {
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<PathBuf> {
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();