diff --git a/lib/crates/fabro-checkpoint/src/author.rs b/lib/crates/fabro-checkpoint/src/author.rs index 857ed5572..c493275d2 100644 --- a/lib/crates/fabro-checkpoint/src/author.rs +++ b/lib/crates/fabro-checkpoint/src/author.rs @@ -1,6 +1,8 @@ use std::fmt::Write; use fabro_types::settings::server::GitAuthorSettings; +use fabro_types::settings::v2::InterpString; +use fabro_types::settings::v2::run::GitAuthorLayer; /// Resolved git author identity for checkpoint commits. #[derive(Debug, Clone, PartialEq)] @@ -54,3 +56,12 @@ impl From<&GitAuthorSettings> for GitAuthor { Self::from_options(value.name.clone(), value.email.clone()) } } + +impl From<&GitAuthorLayer> for GitAuthor { + fn from(value: &GitAuthorLayer) -> Self { + Self::from_options( + value.name.as_ref().map(InterpString::as_source), + value.email.as_ref().map(InterpString::as_source), + ) + } +} diff --git a/lib/crates/fabro-types/src/settings/v2/accessors.rs b/lib/crates/fabro-types/src/settings/v2/accessors.rs index e9b0b1624..4d496ca79 100644 --- a/lib/crates/fabro-types/src/settings/v2/accessors.rs +++ b/lib/crates/fabro-types/src/settings/v2/accessors.rs @@ -92,6 +92,40 @@ impl SettingsFile { self.run.as_ref().and_then(|r| r.prepare.as_ref()) } + /// Flattened prepare-step commands: each `script` is kept as-is, and + /// `command` argv is joined with spaces. Env-interpolation tokens are + /// emitted verbatim via [`InterpString::as_source`]. + #[must_use] + pub fn run_prepare_commands(&self) -> Vec { + let Some(prepare) = self.run_prepare() else { + return Vec::new(); + }; + prepare + .steps + .iter() + .filter_map(|step| { + if let Some(script) = &step.script { + Some(script.as_source()) + } else { + step.command.as_ref().map(|argv| { + argv.iter() + .map(InterpString::as_source) + .collect::>() + .join(" ") + }) + } + }) + .collect() + } + + /// Prepare-step timeout in milliseconds. + #[must_use] + pub fn run_prepare_timeout_ms(&self) -> Option { + self.run_prepare() + .and_then(|p| p.timeout) + .map(|d| u64::try_from(d.as_std().as_millis()).unwrap_or(u64::MAX)) + } + #[must_use] pub fn run_checkpoint(&self) -> Option<&RunCheckpointLayer> { self.run.as_ref().and_then(|r| r.checkpoint.as_ref()) @@ -132,6 +166,26 @@ impl SettingsFile { self.run.as_ref().and_then(|r| r.inputs.as_ref()) } + /// Stringified view of `run.inputs`: non-string TOML values are rendered + /// via their canonical TOML representation (integers, booleans, and + /// arrays are flattened through `Display`). Returns `None` when no + /// inputs are set. + #[must_use] + pub fn run_inputs_as_strings(&self) -> Option> { + self.run_inputs().map(|inputs| { + inputs + .iter() + .map(|(k, v)| { + let stringified = match v { + toml::Value::String(s) => s.clone(), + other => other.to_string(), + }; + (k.clone(), stringified) + }) + .collect() + }) + } + #[must_use] pub fn run_metadata(&self) -> Option<&HashMap> { self.run.as_ref().map(|r| &r.metadata) diff --git a/lib/crates/fabro-types/src/settings/v2/bridge.rs b/lib/crates/fabro-types/src/settings/v2/bridge.rs index 569247407..d40f106b0 100644 --- a/lib/crates/fabro-types/src/settings/v2/bridge.rs +++ b/lib/crates/fabro-types/src/settings/v2/bridge.rs @@ -256,7 +256,7 @@ fn bridge_run(run: &RunLayer, out: &mut Settings) { } } -fn bridge_sandbox(sb: &super::run::RunSandboxLayer) -> SandboxSettings { +pub fn bridge_sandbox(sb: &super::run::RunSandboxLayer) -> SandboxSettings { SandboxSettings { provider: sb.provider.clone(), preserve: sb.preserve, @@ -312,7 +312,7 @@ fn bridge_sandbox(sb: &super::run::RunSandboxLayer) -> SandboxSettings { } } -fn bridge_worktree_mode(m: V2WorktreeMode) -> OldWorktreeMode { +pub fn bridge_worktree_mode(m: V2WorktreeMode) -> OldWorktreeMode { match m { V2WorktreeMode::Always => OldWorktreeMode::Always, V2WorktreeMode::Clean => OldWorktreeMode::Clean, @@ -321,7 +321,7 @@ fn bridge_worktree_mode(m: V2WorktreeMode) -> OldWorktreeMode { } } -fn bridge_merge_strategy(m: V2MergeStrategy) -> OldMergeStrategy { +pub fn bridge_merge_strategy(m: V2MergeStrategy) -> OldMergeStrategy { match m { V2MergeStrategy::Squash => OldMergeStrategy::Squash, V2MergeStrategy::Merge => OldMergeStrategy::Merge, @@ -329,13 +329,31 @@ fn bridge_merge_strategy(m: V2MergeStrategy) -> OldMergeStrategy { } } -fn bridge_mcps(mcps: &HashMap) -> HashMap { +pub fn bridge_pull_request(pr: &super::run::RunPullRequestLayer) -> PullRequestSettings { + PullRequestSettings { + enabled: pr.enabled.unwrap_or(false), + draft: pr.draft.unwrap_or(true), + auto_merge: pr.auto_merge.unwrap_or(false), + merge_strategy: pr + .merge_strategy + .map(bridge_merge_strategy) + .unwrap_or_default(), + } +} + +pub fn bridge_run_artifacts(artifacts: &super::run::RunArtifactsLayer) -> ArtifactsSettings { + ArtifactsSettings { + include: artifacts.include.clone(), + } +} + +pub fn bridge_mcps(mcps: &HashMap) -> HashMap { mcps.iter() .map(|(name, entry)| (name.clone(), bridge_mcp_entry(entry))) .collect() } -fn bridge_mcp_entry(entry: &McpEntryLayer) -> McpServerEntry { +pub fn bridge_mcp_entry(entry: &McpEntryLayer) -> McpServerEntry { let transport = match entry { McpEntryLayer::Stdio { script, @@ -418,7 +436,7 @@ fn bridge_mcp_entry(entry: &McpEntryLayer) -> McpServerEntry { } } -fn bridge_hook(hook: &V2HookEntry) -> HookDefinition { +pub fn bridge_hook(hook: &V2HookEntry) -> HookDefinition { let hook_type = resolve_hook_type(hook); // If the hook is a script/command form, emit via the shorthand so the // old HookDefinition.command field holds the full command and @@ -550,7 +568,7 @@ fn bridge_cli(cli: &CliLayer, out: &mut Settings) { } } -fn bridge_exec(exec: &CliExecLayer) -> ExecSettings { +pub fn bridge_exec(exec: &CliExecLayer) -> ExecSettings { ExecSettings { provider: exec .model