mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
route project config discovery through file-based builders
This commit is contained in:
parent
5748dd3d30
commit
2ec9e8bcdc
10 changed files with 64 additions and 65 deletions
|
|
@ -11,8 +11,7 @@ use std::io::Write;
|
|||
|
||||
use anyhow::{Context, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_config::user::{active_settings_path, load_settings_config};
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tracing::debug;
|
||||
|
||||
|
|
@ -36,10 +35,9 @@ pub(crate) async fn run(
|
|||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
cwd: ctx.cwd().to_path_buf(),
|
||||
args_layer: SettingsLayer::default(),
|
||||
args_layer: Default::default(),
|
||||
args: None,
|
||||
run_id: None,
|
||||
user_layer: load_settings_config(None)?,
|
||||
user_settings_path: Some(active_settings_path(None)),
|
||||
})?;
|
||||
let client = ctx.server().await?;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub(crate) fn run(args: &ParseArgs) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
fn run_to(args: &ParseArgs, mut out: impl Write) -> anyhow::Result<()> {
|
||||
let (dot_path, _cfg) = resolve_workflow(&args.workflow)?;
|
||||
let dot_path = resolve_workflow(&args.workflow)?;
|
||||
let source = read_workflow_file(&dot_path)?;
|
||||
let ast = parse_ast(&source)?;
|
||||
serde_json::to_writer_pretty(&mut out, &ast)?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::user::{active_settings_path, load_settings_config};
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::PreflightArgs;
|
||||
|
|
@ -26,7 +26,6 @@ pub(crate) async fn execute(
|
|||
args_layer: preflight_args_layer(&args)?,
|
||||
args: preflight_manifest_args(&args),
|
||||
run_id: None,
|
||||
user_layer: load_settings_config(None)?,
|
||||
user_settings_path: Some(active_settings_path(None)),
|
||||
})?;
|
||||
let client = ctx.server().await?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use fabro_config::user::{active_settings_path, load_settings_config};
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::RunId;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
|
|
@ -41,7 +41,6 @@ pub(crate) async fn create_run(
|
|||
args_layer: cli_args_config,
|
||||
args: run_manifest_args(args),
|
||||
run_id,
|
||||
user_layer: load_settings_config(None)?,
|
||||
user_settings_path: Some(active_settings_path(None)),
|
||||
})?;
|
||||
let client = ctx.server().await?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::user::{active_settings_path, load_settings_config};
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::ValidateArgs;
|
||||
|
|
@ -19,10 +18,9 @@ pub(crate) async fn run(
|
|||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
cwd: ctx.cwd().to_path_buf(),
|
||||
args_layer: SettingsLayer::default(),
|
||||
args_layer: Default::default(),
|
||||
args: None,
|
||||
run_id: None,
|
||||
user_layer: load_settings_config(None)?,
|
||||
user_settings_path: Some(active_settings_path(None)),
|
||||
})?;
|
||||
let client = ctx.server().await?;
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ pub(super) fn create_command(args: &WorkflowCreateArgs, base_ctx: &CommandContex
|
|||
let printer = base_ctx.printer();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let Some((config_path, config)) = discover_project_config(&cwd)? else {
|
||||
let Some(config_path) = discover_project_config(&cwd)? else {
|
||||
bail!(
|
||||
"No .fabro/project.toml found in {cwd} or any parent directory",
|
||||
cwd = cwd.display()
|
||||
);
|
||||
};
|
||||
|
||||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
let fabro_root = resolve_fabro_root(&config_path);
|
||||
let created = write_workflow_scaffold(args, &fabro_root)?;
|
||||
|
||||
if base_ctx.json_output() {
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ pub(super) fn list_command(_args: &WorkflowListArgs, base_ctx: &CommandContext)
|
|||
let styles = Styles::detect_stderr();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let Some((config_path, config)) = discover_project_config(&cwd)? else {
|
||||
let Some(config_path) = discover_project_config(&cwd)? else {
|
||||
bail!(
|
||||
"No .fabro/project.toml found in {cwd} or any parent directory",
|
||||
cwd = cwd.display()
|
||||
);
|
||||
};
|
||||
|
||||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
let fabro_root = resolve_fabro_root(&config_path);
|
||||
let project_wf_dir = fabro_root.join("workflows");
|
||||
let user_wf_dir = Some(fabro_util::Home::from_env().workflows_dir());
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ pub(crate) struct ManifestBuildInput {
|
|||
pub args_layer: SettingsLayer,
|
||||
pub args: Option<types::ManifestArgs>,
|
||||
pub run_id: Option<RunId>,
|
||||
/// User-level settings layer. Production callers load via
|
||||
/// `load_settings_config(None)`; tests pass `SettingsLayer::default()`.
|
||||
pub user_layer: SettingsLayer,
|
||||
/// Path to the user settings file (for inclusion in
|
||||
/// `RunManifest.configs`). `None` skips the user config entry.
|
||||
pub user_settings_path: Option<PathBuf>,
|
||||
|
|
@ -57,7 +54,7 @@ struct WorkflowScanInput {
|
|||
|
||||
pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManifest> {
|
||||
let root_resolution = resolve_workflow_path(&input.workflow, &input.cwd)?;
|
||||
if root_resolution.workflow_config.is_none()
|
||||
if root_resolution.workflow_toml_path.is_none()
|
||||
&& !root_resolution.resolved_workflow_path.is_file()
|
||||
{
|
||||
return Err(fabro_config::Error::WorkflowNotFound(
|
||||
|
|
@ -70,16 +67,22 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
|
|||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
let project_config = discover_project_config(workflow_parent)?;
|
||||
let workflow_layer = root_resolution.workflow_config.clone().unwrap_or_default();
|
||||
let project_layer = project_config
|
||||
let mut workflow_settings_builder =
|
||||
WorkflowSettingsBuilder::new().args_layer(input.args_layer.clone());
|
||||
if let Some(path) = root_resolution.workflow_toml_path.as_ref() {
|
||||
workflow_settings_builder = workflow_settings_builder.workflow_file(path)?;
|
||||
}
|
||||
if let Some(path) = project_config.as_ref() {
|
||||
workflow_settings_builder = workflow_settings_builder.project_file(path)?;
|
||||
}
|
||||
if let Some(path) = input
|
||||
.user_settings_path
|
||||
.as_ref()
|
||||
.map(|(_, config)| config.clone())
|
||||
.unwrap_or_default();
|
||||
let workflow_settings = WorkflowSettingsBuilder::new()
|
||||
.args_layer(input.args_layer.clone())
|
||||
.workflow_layer(workflow_layer.clone())
|
||||
.project_layer(project_layer)
|
||||
.user_layer(input.user_layer.clone())
|
||||
.filter(|path| path.is_file())
|
||||
{
|
||||
workflow_settings_builder = workflow_settings_builder.user_file(path)?;
|
||||
}
|
||||
let workflow_settings = workflow_settings_builder
|
||||
.build()
|
||||
.map_err(|errors| anyhow!("failed to resolve manifest settings: {errors}"))?;
|
||||
let target_path = root_resolution.dot_path.clone();
|
||||
|
|
@ -100,7 +103,7 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
|
|||
.ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?;
|
||||
|
||||
let mut configs = Vec::new();
|
||||
if let Some((path, _config)) = project_config {
|
||||
if let Some(path) = project_config {
|
||||
let source = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
configs.push(types::ManifestConfig {
|
||||
|
|
@ -624,7 +627,6 @@ mod tests {
|
|||
args_layer: SettingsLayer::default(),
|
||||
args: None,
|
||||
run_id: None,
|
||||
user_layer: SettingsLayer::default(),
|
||||
user_settings_path: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -704,7 +706,6 @@ file = "prompts/goal.md"
|
|||
args_layer: SettingsLayer::default(),
|
||||
args: None,
|
||||
run_id: None,
|
||||
user_layer: SettingsLayer::default(),
|
||||
user_settings_path: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -757,7 +758,6 @@ file = "prompts/goal.md"
|
|||
args_layer: SettingsLayer::default(),
|
||||
args: None,
|
||||
run_id: None,
|
||||
user_layer: SettingsLayer::default(),
|
||||
user_settings_path: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use crate::resolve::{
|
|||
resolve_workflow,
|
||||
};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, Result, run};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ResolveErrors(pub Vec<ResolveError>);
|
||||
|
|
@ -186,18 +186,30 @@ impl WorkflowSettingsBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn workflow_file(self, path: &Path) -> Result<Self> {
|
||||
Ok(self.workflow_layer(run::load_run_config(path)?))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn project_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
self.project = layer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn project_file(self, path: &Path) -> Result<Self> {
|
||||
Ok(self.project_layer(load_settings_path(path)?))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
self.user = layer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user_file(self, path: &Path) -> Result<Self> {
|
||||
Ok(self.user_layer(load_settings_path(path)?))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn server_layer(mut self, layer: SettingsLayer) -> Self {
|
||||
self.server = layer;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ const CONFIG_FILENAME: &str = ".fabro/project.toml";
|
|||
pub struct WorkflowPathResolution {
|
||||
pub resolved_workflow_path: PathBuf,
|
||||
pub dot_path: PathBuf,
|
||||
pub workflow_config: Option<SettingsLayer>,
|
||||
pub workflow_toml_path: Option<PathBuf>,
|
||||
pub workflow_slug: Option<String>,
|
||||
}
|
||||
|
|
@ -32,7 +31,7 @@ pub struct WorkflowPathResolution {
|
|||
///
|
||||
/// Goes through [`load_settings_path`] so that relative `run.goal.file`
|
||||
/// paths are anchored at the directory of `path` at load time.
|
||||
pub fn load_project_config(path: &Path) -> Result<SettingsLayer> {
|
||||
fn load_project_config(path: &Path) -> Result<SettingsLayer> {
|
||||
let config = load_settings_path(path)?;
|
||||
let root = WorkflowSettingsBuilder::project_from_layer(&config)
|
||||
.map_err(|errors| Error::resolve("Failed to resolve project settings", errors.into()))?
|
||||
|
|
@ -42,14 +41,13 @@ pub fn load_project_config(path: &Path) -> Result<SettingsLayer> {
|
|||
}
|
||||
|
||||
/// Walk ancestor directories from `start` looking for `.fabro/project.toml`.
|
||||
/// Returns the config file path and parsed config, or `None` if not found.
|
||||
pub fn discover_project_config(start: &Path) -> Result<Option<(PathBuf, SettingsLayer)>> {
|
||||
/// Returns the config file path, or `None` if not found.
|
||||
pub fn discover_project_config(start: &Path) -> Result<Option<PathBuf>> {
|
||||
for ancestor in start.ancestors() {
|
||||
let candidate = ancestor.join(CONFIG_FILENAME);
|
||||
if candidate.is_file() {
|
||||
tracing::debug!(path = %candidate.display(), "Discovered project config");
|
||||
let config = load_project_config(&candidate)?;
|
||||
return Ok(Some((candidate, config)));
|
||||
return Ok(Some(candidate));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -93,7 +91,6 @@ pub fn resolve_workflow_path(workflow_path: &Path, cwd: &Path) -> Result<Workflo
|
|||
Ok(WorkflowPathResolution {
|
||||
resolved_workflow_path: path.clone(),
|
||||
dot_path,
|
||||
workflow_config: Some(cfg),
|
||||
workflow_toml_path: Some(path),
|
||||
workflow_slug,
|
||||
})
|
||||
|
|
@ -105,7 +102,6 @@ pub fn resolve_workflow_path(workflow_path: &Path, cwd: &Path) -> Result<Workflo
|
|||
Ok(WorkflowPathResolution {
|
||||
resolved_workflow_path: path.clone(),
|
||||
dot_path: path,
|
||||
workflow_config: None,
|
||||
workflow_toml_path: None,
|
||||
workflow_slug,
|
||||
})
|
||||
|
|
@ -156,8 +152,8 @@ fn resolve_workflow_arg_impl(
|
|||
|
||||
let name = arg.to_string_lossy();
|
||||
match discover_project_config(start_dir) {
|
||||
Ok(Some((config_path, config))) => {
|
||||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
Ok(Some(config_path)) => {
|
||||
let fabro_root = resolve_fabro_root(&config_path);
|
||||
let project_candidate = fabro_root
|
||||
.join("workflows")
|
||||
.join(&*name)
|
||||
|
|
@ -339,10 +335,10 @@ fn find_closest_match(input: &str, candidates: &[String]) -> Option<String> {
|
|||
}
|
||||
|
||||
/// Resolve a workflow argument to a DOT path and optional run config.
|
||||
pub fn resolve_workflow(arg: &Path) -> Result<(PathBuf, Option<SettingsLayer>)> {
|
||||
pub fn resolve_workflow(arg: &Path) -> Result<PathBuf> {
|
||||
let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let resolution = resolve_workflow_path(arg, &start)?;
|
||||
Ok((resolution.dot_path, resolution.workflow_config))
|
||||
Ok(resolution.dot_path)
|
||||
}
|
||||
|
||||
/// Check whether retros are enabled in the project config.
|
||||
|
|
@ -350,11 +346,15 @@ pub fn resolve_workflow(arg: &Path) -> Result<(PathBuf, Option<SettingsLayer>)>
|
|||
pub fn is_retro_enabled() -> bool {
|
||||
let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
match discover_project_config(&start) {
|
||||
Ok(Some((_path, config))) => config
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|r| r.execution.as_ref())
|
||||
.and_then(|e| e.retros)
|
||||
Ok(Some(path)) => load_project_config(&path)
|
||||
.ok()
|
||||
.and_then(|config| {
|
||||
config
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|r| r.execution.as_ref())
|
||||
.and_then(|e| e.retros)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
}
|
||||
|
|
@ -383,11 +383,12 @@ fn normalize_joined_path(base_dir: &Path, reference: &Path) -> PathBuf {
|
|||
/// Resolve the fabro root directory from a config file path and its config.
|
||||
/// The returned path is the config file's parent directory joined with the
|
||||
/// `project.directory` value (default: `.`).
|
||||
pub fn resolve_fabro_root(config_path: &Path, config: &SettingsLayer) -> PathBuf {
|
||||
pub fn resolve_fabro_root(config_path: &Path) -> PathBuf {
|
||||
let project_dir = config_path
|
||||
.parent()
|
||||
.expect("config_path should have a parent directory");
|
||||
let root = WorkflowSettingsBuilder::project_from_layer(config)
|
||||
let config = load_project_config(config_path).expect("project config should load");
|
||||
let root = WorkflowSettingsBuilder::project_from_layer(&config)
|
||||
.expect("project settings should resolve")
|
||||
.directory;
|
||||
normalize_joined_path(project_dir, Path::new(&root))
|
||||
|
|
@ -488,9 +489,8 @@ retros = true
|
|||
let sub = tmp.path().join("sub").join("dir");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
|
||||
let (found_path, config) = discover_project_config(&sub).unwrap().unwrap();
|
||||
let found_path = discover_project_config(&sub).unwrap().unwrap();
|
||||
assert_eq!(found_path, config_dir.join("project.toml"));
|
||||
assert_eq!(config.version, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -529,9 +529,7 @@ file = "prompts/goal.md"
|
|||
let config_path = config_dir.join("project.toml");
|
||||
fs::write(&config_path, "_version = 1\n").unwrap();
|
||||
|
||||
let config = load_project_config(&config_path).unwrap();
|
||||
|
||||
assert_eq!(resolve_fabro_root(&config_path, &config), config_dir);
|
||||
assert_eq!(resolve_fabro_root(&config_path), config_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -550,12 +548,7 @@ directory = "../custom"
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_project_config(&config_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolve_fabro_root(&config_path, &config),
|
||||
tmp.path().join("custom")
|
||||
);
|
||||
assert_eq!(resolve_fabro_root(&config_path), tmp.path().join("custom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue