From 842ab71eb87a81ad86a369c4b52dc39b4a54a661 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 9 Apr 2026 12:44:44 -0400 Subject: [PATCH] feat(types): expose bridge helpers and expand v2 accessors Stage 6.1 prep follow-ups that consumers need when walking v2 directly: - `bridge::bridge_sandbox`, `bridge_mcp_entry`, `bridge_mcps`, `bridge_hook`, `bridge_exec`, `bridge_worktree_mode`, `bridge_merge_strategy` are now `pub`, so callers can lift the runtime shape they need out of the v2 tree without round-tripping through the full `bridge_to_old` legacy Settings builder. - New `bridge::bridge_pull_request` and `bridge::bridge_run_artifacts` helpers extract their respective runtime shapes from v2 layers. - `SettingsFile::run_prepare_commands()` / `run_prepare_timeout_ms()` flatten `run.prepare.steps` into the legacy script-string vector shape consumers pass to `LifecycleOptions::setup_commands`. - `SettingsFile::run_inputs_as_strings()` stringifies `run.inputs` TOML values for var-expansion call sites. - `fabro_checkpoint::GitAuthor` now has `From<&v2::run::GitAuthorLayer>` so consumers can construct a runtime author directly from the v2 subtree without going through the legacy flat `GitAuthorSettings`. All changes are additive. `bridge_to_old` still exists and nothing has migrated off the flat `Settings` shape yet -- those moves land in follow-up commits once each consumer crate is converted independently. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-checkpoint/src/author.rs | 11 ++++ .../fabro-types/src/settings/v2/accessors.rs | 54 +++++++++++++++++++ .../fabro-types/src/settings/v2/bridge.rs | 32 ++++++++--- 3 files changed, 90 insertions(+), 7 deletions(-) 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