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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-09 12:44:44 -04:00
parent 3f32bdb874
commit 842ab71eb8
No known key found for this signature in database
3 changed files with 90 additions and 7 deletions

View file

@ -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),
)
}
}

View file

@ -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<String> {
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::<Vec<_>>()
.join(" ")
})
}
})
.collect()
}
/// Prepare-step timeout in milliseconds.
#[must_use]
pub fn run_prepare_timeout_ms(&self) -> Option<u64> {
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<HashMap<String, String>> {
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<String, String>> {
self.run.as_ref().map(|r| &r.metadata)

View file

@ -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<String, McpEntryLayer>) -> HashMap<String, McpServerEntry> {
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<String, McpEntryLayer>) -> HashMap<String, McpServerEntry> {
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