Merge remote-tracking branch 'origin/main'

# Conflicts:
#	lib/crates/fabro-server/src/lib.rs
#	lib/crates/fabro-server/src/serve.rs
#	lib/crates/fabro-server/src/server.rs
This commit is contained in:
Bryan Helmkamp 2026-04-23 08:34:44 -04:00
commit 37cd8ff45f
No known key found for this signature in database
188 changed files with 3753 additions and 3798 deletions

88
Cargo.lock generated
View file

@ -1505,7 +1505,7 @@ dependencies = [
[[package]]
name = "fabro-agent"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@ -1541,9 +1541,10 @@ dependencies = [
[[package]]
name = "fabro-api"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"chrono",
"fabro-config",
"fabro-types",
"openapiv3",
"prettyplease",
@ -1560,7 +1561,7 @@ dependencies = [
[[package]]
name = "fabro-auth"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@ -1581,7 +1582,7 @@ dependencies = [
[[package]]
name = "fabro-checkpoint"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"chrono",
"fabro-store",
@ -1596,7 +1597,7 @@ dependencies = [
[[package]]
name = "fabro-cli"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"assert_cmd",
@ -1686,7 +1687,7 @@ dependencies = [
[[package]]
name = "fabro-client"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"bytes",
@ -1713,7 +1714,7 @@ dependencies = [
[[package]]
name = "fabro-config"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"chrono",
@ -1726,6 +1727,7 @@ dependencies = [
"serde",
"serde_json",
"strsim 0.11.1",
"temp-env",
"tempfile",
"thiserror 2.0.18",
"toml 0.8.23",
@ -1735,7 +1737,7 @@ dependencies = [
[[package]]
name = "fabro-core"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"async-trait",
"fabro-types",
@ -1750,7 +1752,7 @@ dependencies = [
[[package]]
name = "fabro-devcontainer"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-util",
@ -1766,7 +1768,7 @@ dependencies = [
[[package]]
name = "fabro-github"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"base64",
"chrono",
@ -1782,7 +1784,7 @@ dependencies = [
[[package]]
name = "fabro-graphviz"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"fabro-types",
@ -1796,7 +1798,7 @@ dependencies = [
[[package]]
name = "fabro-hooks"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"async-trait",
"fabro-agent",
@ -1819,7 +1821,7 @@ dependencies = [
[[package]]
name = "fabro-http"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"http",
"reqwest 0.13.2",
@ -1828,7 +1830,7 @@ dependencies = [
[[package]]
name = "fabro-install"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"base64",
@ -1842,7 +1844,7 @@ dependencies = [
[[package]]
name = "fabro-interview"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"async-trait",
"dialoguer",
@ -1856,7 +1858,7 @@ dependencies = [
[[package]]
name = "fabro-llm"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@ -1886,7 +1888,7 @@ dependencies = [
[[package]]
name = "fabro-macros"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"proc-macro2",
"quote",
@ -1895,7 +1897,7 @@ dependencies = [
[[package]]
name = "fabro-mcp"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"fabro-config",
@ -1911,7 +1913,7 @@ dependencies = [
[[package]]
name = "fabro-model"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"insta",
"serde",
@ -1921,7 +1923,7 @@ dependencies = [
[[package]]
name = "fabro-oauth"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"axum",
"base64",
@ -1940,7 +1942,7 @@ dependencies = [
[[package]]
name = "fabro-proc"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"cc",
"libc",
@ -1949,7 +1951,7 @@ dependencies = [
[[package]]
name = "fabro-retro"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"chrono",
@ -1967,7 +1969,7 @@ dependencies = [
[[package]]
name = "fabro-sandbox"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@ -1999,7 +2001,7 @@ dependencies = [
[[package]]
name = "fabro-server"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@ -2050,6 +2052,7 @@ dependencies = [
"percent-encoding",
"rand 0.9.4",
"regex",
"reqwest 0.12.28",
"semver",
"serde",
"serde_json",
@ -2076,7 +2079,7 @@ dependencies = [
[[package]]
name = "fabro-slack"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-interview",
@ -2095,14 +2098,14 @@ dependencies = [
[[package]]
name = "fabro-spa"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"rust-embed",
]
[[package]]
name = "fabro-store"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"async-trait",
"bytes",
@ -2128,7 +2131,7 @@ dependencies = [
[[package]]
name = "fabro-telemetry"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"base64",
@ -2153,7 +2156,7 @@ dependencies = [
[[package]]
name = "fabro-template"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"fabro-util",
@ -2165,7 +2168,7 @@ dependencies = [
[[package]]
name = "fabro-test"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"assert_cmd",
"axum",
@ -2187,7 +2190,7 @@ dependencies = [
[[package]]
name = "fabro-tracker"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"async-trait",
"fabro-github",
@ -2200,7 +2203,7 @@ dependencies = [
[[package]]
name = "fabro-types"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"chrono",
"clap",
@ -2221,7 +2224,7 @@ dependencies = [
[[package]]
name = "fabro-util"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"aho-corasick",
"anyhow",
@ -2243,7 +2246,7 @@ dependencies = [
[[package]]
name = "fabro-validate"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"fabro-graphviz",
"fabro-model",
@ -2253,7 +2256,7 @@ dependencies = [
[[package]]
name = "fabro-vault"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"chrono",
"serde",
@ -2264,7 +2267,7 @@ dependencies = [
[[package]]
name = "fabro-workflow"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"assert_cmd",
@ -6421,6 +6424,15 @@ dependencies = [
"xattr",
]
[[package]]
name = "temp-env"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
dependencies = [
"parking_lot",
]
[[package]]
name = "tempfile"
version = "3.26.0"
@ -6900,7 +6912,7 @@ dependencies = [
[[package]]
name = "twin-github"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"axum",
"base64",
@ -6919,7 +6931,7 @@ dependencies = [
[[package]]
name = "twin-openai"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
dependencies = [
"anyhow",
"async-stream",

View file

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
version = "0.211.0-nightly.1"
version = "0.212.0-nightly.0"
license = "MIT"
[workspace.dependencies]

View file

@ -1,12 +1,10 @@
import type { PaginationMeta } from "@qltysh/fabro-api-client";
/**
* Opaque settings payload returned by `/api/v1/runs/:id/settings`. Mirrors the
* v2 `SettingsFile` shape in `lib/crates/fabro-types/src/settings/tree.rs`,
* with secret-bearing subtrees dropped before serialization. Treated as a
* loose JSON object on the web side consumers only render it.
* Opaque persisted `SettingsLayer` payload returned by `/api/v1/runs/:id/settings`.
* Treated as a loose JSON object on the web side consumers only render it.
*/
export type RunSettings = Record<string, unknown>;
export type RunSettingsLayer = Record<string, unknown>;
export interface WorkflowScheduleSummary {
expression: string;
@ -35,6 +33,6 @@ export interface WorkflowDetailResponse {
slug: string;
description: string;
filename: string;
settings: RunSettings;
settings: RunSettingsLayer;
graph: string;
}

View file

@ -6,14 +6,14 @@ import { apiJson } from "../api";
import { isVisibleStage } from "../data/runs";
import { formatDurationSecs } from "../lib/format";
import type { PaginatedRunStageList } from "@qltysh/fabro-api-client";
import type { RunSettings } from "../lib/workflow-api";
import type { RunSettingsLayer } from "../lib/workflow-api";
export const handle = { wide: true };
export async function loader({ request, params }: any) {
const [{ data: apiStages }, settings] = await Promise.all([
apiJson<PaginatedRunStageList>(`/runs/${params.id}/stages`, { request }),
apiJson<RunSettings>(`/runs/${params.id}/settings`, { request }),
apiJson<RunSettingsLayer>(`/runs/${params.id}/settings`, { request }),
]);
const stages: Stage[] = apiStages.filter((s) => isVisibleStage(s.id)).map((s) => ({
id: s.id,

View file

@ -1,13 +1,7 @@
import type { ServerSettings } from "@qltysh/fabro-api-client";
import { apiJson } from "../api";
import { CollapsibleFile } from "../components/collapsible-file";
/**
* Opaque server settings payload returned by `/api/v1/settings`. Mirrors the
* v2 `SettingsFile` shape with secret-bearing subtrees dropped before
* serialization. The UI only renders it as JSON.
*/
type ServerSettings = Record<string, unknown>;
export function meta({}: any) {
return [{ title: "Settings — Fabro" }];
}

View file

@ -1,22 +1,24 @@
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { Link, Outlet, useLocation, useParams } from "react-router";
import { apiJsonOrNull } from "../api";
import type { RunSettings, WorkflowDetailResponse as ApiWorkflowDetail } from "../lib/workflow-api";
import type {
RunSettingsLayer,
WorkflowDetailResponse as ApiWorkflowDetail,
} from "../lib/workflow-api";
export interface WorkflowEntry {
name: string;
slug: string;
description: string;
filename: string;
settings: RunSettings;
settings: RunSettingsLayer;
graph: string;
}
// Static sample data used by the `workflow-definition` index route for the
// hardcoded showcase workflows. Shape mirrors the v2 `SettingsFile` JSON
// returned by `/api/v1/runs/:id/settings` (see the Rust
// `fabro_types::settings::SettingsFile` type). Fields are opaque to the
// `RunSettings` TypeScript type, which is a bare `Record<string, unknown>`.
// hardcoded showcase workflows. Shape mirrors the persisted `SettingsLayer`
// JSON returned by `/api/v1/runs/:id/settings`. Fields are opaque to the
// `RunSettingsLayer` TypeScript type, which is a bare `Record<string, unknown>`.
export const workflowData: Record<string, WorkflowEntry> = {
fix_build: {
name: "Fix Build",

View file

@ -3,9 +3,16 @@ set -euo pipefail
cd "$(dirname "$0")/../.."
symbol_allowlist=(
server_symbol_allowlist=(
"lib/crates/fabro-cli/src/local_server.rs"
"lib/crates/fabro-cli/src/commands/run/runner.rs"
"lib/crates/fabro-cli/src/commands/pr/mod.rs"
"lib/crates/fabro-cli/src/commands/pr/create.rs"
)
storage_allowlist=(
"lib/crates/fabro-cli/src/commands/install.rs"
"lib/crates/fabro-cli/src/commands/uninstall.rs"
"lib/crates/fabro-cli/src/commands/run/runner.rs"
"lib/crates/fabro-cli/src/commands/pr/mod.rs"
"lib/crates/fabro-cli/src/commands/pr/create.rs"
@ -49,11 +56,19 @@ fail=0
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if ! in_array "$path" "${symbol_allowlist[@]}"; then
if ! in_array "$path" "${server_symbol_allowlist[@]}"; then
echo "boundary check failed: gated server symbol used outside allowlist: $path" >&2
fail=1
fi
done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|Storage::new')
done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b')
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if ! in_array "$path" "${storage_allowlist[@]}"; then
echo "boundary check failed: Storage::new used outside allowlist: $path" >&2
fail=1
fi
done < <(find_matches 'Storage::new')
while IFS= read -r path; do
[[ -z "$path" ]] && continue

View file

@ -20,7 +20,8 @@ while IFS= read -r match; do
case "$path:$line" in
"lib/crates/fabro-telemetry/src/spawn.rs:std::env::set_var(key, value);" | \
"lib/crates/fabro-telemetry/src/spawn.rs:std::env::remove_var(key);")
"lib/crates/fabro-telemetry/src/spawn.rs:std::env::remove_var(key);" | \
'lib/crates/fabro-server/src/install.rs:std::env::set_var("FABRO_TEST_IN_MEMORY_STORE", "1");')
continue
;;
esac

View file

@ -34,7 +34,7 @@
| Path | Direct dependency | Why it still exists | Suggested handling |
| --- | --- | --- | --- |
| `lib/crates/fabro-cli/src/commands/store/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. |
| `lib/crates/fabro-cli/src/commands/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. |
| `lib/crates/fabro-cli/src/commands/run/wait.rs` test module | `outcome::StageStatus`, `records::Conclusion`, `run_status::RunStatusRecord` | Output tests construct workflow-owned records directly. | Replace with shared fixture builders once status/conclusion DTOs move out. |
| `lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs` test module | `event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}`, `outcome::billed_model_usage_from_llm` | Progress tests build engine events directly. | Replace with shared event fixture helpers after event DTO extraction. |
| `lib/crates/fabro-cli/src/commands/run/run_progress/event.rs` test module | `event::{Event, to_run_event}` | Event rendering tests depend on engine event constructors. | Replace with shared event fixture helpers after event DTO extraction. |

View file

@ -33,7 +33,7 @@ These paths are local runtime state, not canonical event projections.
These names are still real, but they are no longer live scratch files by default:
- Metadata branch files such as `run.json`, `start.json`, `checkpoint.json`, and `retro.json`
- `fabro store dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `retro.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files
- `fabro dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `retro.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files
- Retro-agent temp uploads named `progress.jsonl`, `checkpoint.json`, `run.json`, and `start.json` inside the retro sandbox
## Notes

View file

@ -7,7 +7,7 @@ When an agent or prompt node finishes, Fabro captures its response text and prod
## Response capture
After an agent or prompt node completes, Fabro captures the full response text and persists it to `stages/{node_id}@{visit}/response.md` in metadata snapshots and `fabro store dump` output. It also writes the final outcome (status, context updates, routing directives) to `stages/{node_id}@{visit}/status.json`.
After an agent or prompt node completes, Fabro captures the full response text and persists it to `stages/{node_id}@{visit}/response.md` in metadata snapshots and `fabro dump` output. It also writes the final outcome (status, context updates, routing directives) to `stages/{node_id}@{visit}/status.json`.
## Context updates
@ -92,7 +92,7 @@ review -> approve [label="Approve"]
## Output logging
Fabro writes several files per stage to `stages/{node_id}@{visit}/` in metadata snapshots and `fabro store dump` output:
Fabro writes several files per stage to `stages/{node_id}@{visit}/` in metadata snapshots and `fabro dump` output:
| File | Contents |
|---|---|

View file

@ -295,4 +295,4 @@ Use prompt nodes for analysis, classification, and summarization tasks where too
## Prompt logging
Fabro persists the assembled prompt to `stages/{node_id}@{visit}/prompt.md` in metadata snapshots and `fabro store dump` output for every agent and prompt stage. This includes the preamble (if any) and the expanded prompt text. Use these files for debugging when an agent behaves unexpectedly.
Fabro persists the assembled prompt to `stages/{node_id}@{visit}/prompt.md` in metadata snapshots and `fabro dump` output for every agent and prompt stage. This includes the preamble (if any) and the expanded prompt text. Use these files for debugging when an agent behaves unexpectedly.

View file

@ -1376,7 +1376,7 @@ paths:
operationId: retrieveRunSettings
tags: [Run Internals]
summary: Retrieve Run Settings
description: Returns the structured settings used to launch this run.
description: Returns the persisted `SettingsLayer` used to launch this run.
parameters:
- $ref: "#/components/parameters/RunId"
responses:
@ -1385,7 +1385,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/RunSettings"
$ref: "#/components/schemas/RunSettingsLayer"
"404":
description: Run not found
content:
@ -2012,22 +2012,11 @@ paths:
tags: [Settings]
summary: Retrieve Server Settings
description: >
Returns the server settings view selected by the optional `view` query
parameter. `view=layer` (the default) returns the current sparse
redacted `SettingsLayer` payload. `view=resolved` returns the server's
dense resolved settings payload after applying the same redaction
policy.
parameters:
- $ref: "#/components/parameters/SettingsView"
Returns the server's current in-memory settings view as the typed
`ServerSettings` payload.
responses:
"200":
description: Server settings
headers:
X-Fabro-Settings-View:
description: Present with value `resolved` when the response body is the dense resolved settings view.
schema:
type: string
enum: [resolved]
content:
application/json:
schema:
@ -2069,16 +2058,6 @@ components:
type: string
example: nightly-build
SettingsView:
name: view
in: query
required: false
description: Selects the server settings representation to return.
schema:
type: string
enum: [layer, resolved]
default: layer
StageId:
name: stageId
in: path
@ -5148,32 +5127,328 @@ components:
# ── Settings Schemas ─────────────────────────────────────────────────
ServerSettings:
description: |
Redacted server settings payload.
The `/api/v1/settings` endpoint supports two response shapes:
- `view=layer` (default): the sparse redacted `SettingsLayer` shape
- `view=resolved`: the dense resolved `Settings` shape
Both views drop the same exact operational path:
- `server.listen`
For non-redacted `InterpString` fields, the wire payload preserves the
unresolved source/template string rather than any environment-resolved
secret value.
description: Current in-memory server settings view.
type: object
additionalProperties: true
required: [server, features]
properties:
server:
$ref: "#/components/schemas/ServerNamespace"
features:
$ref: "#/components/schemas/FeaturesNamespace"
RunSettings:
ServerNamespace:
type: object
required:
- listen
- api
- web
- auth
- ip_allowlist
- storage
- artifacts
- slatedb
- scheduler
- logging
- integrations
properties:
listen:
$ref: "#/components/schemas/ServerListenSettings"
api:
$ref: "#/components/schemas/ServerApiSettings"
web:
$ref: "#/components/schemas/ServerWebSettings"
auth:
$ref: "#/components/schemas/ServerAuthSettings"
ip_allowlist:
$ref: "#/components/schemas/ServerIpAllowlistSettings"
storage:
$ref: "#/components/schemas/ServerStorageSettings"
artifacts:
$ref: "#/components/schemas/ServerArtifactsSettings"
slatedb:
$ref: "#/components/schemas/ServerSlateDbSettings"
scheduler:
$ref: "#/components/schemas/ServerSchedulerSettings"
logging:
$ref: "#/components/schemas/ServerLoggingSettings"
integrations:
$ref: "#/components/schemas/ServerIntegrationsSettings"
FeaturesNamespace:
type: object
required: [session_sandboxes]
properties:
session_sandboxes:
type: boolean
ServerListenSettings:
oneOf:
- $ref: "#/components/schemas/ServerListenTcpSettings"
- $ref: "#/components/schemas/ServerListenUnixSettings"
ServerListenTcpSettings:
type: object
required: [type, address]
properties:
type:
type: string
enum: [tcp]
address:
type: string
ServerListenUnixSettings:
type: object
required: [type, path]
properties:
type:
type: string
enum: [unix]
path:
type: string
ServerApiSettings:
type: object
required: [url]
properties:
url:
type: ["string", "null"]
ServerWebSettings:
type: object
required: [enabled, url]
properties:
enabled:
type: boolean
url:
type: string
ServerAuthSettings:
type: object
required: [methods, github]
properties:
methods:
type: array
items:
$ref: "#/components/schemas/ServerAuthMethod"
github:
$ref: "#/components/schemas/ServerAuthGithubSettings"
ServerAuthMethod:
type: string
enum: [dev-token, github]
ServerAuthGithubSettings:
type: object
required: [allowed_usernames]
properties:
allowed_usernames:
type: array
items:
type: string
ServerIpAllowlistSettings:
type: object
required: [entries, trusted_proxy_count]
properties:
entries:
type: array
items:
$ref: "#/components/schemas/IpAllowEntry"
trusted_proxy_count:
type: integer
ServerIpAllowlistOverrideSettings:
type: object
required: [entries, trusted_proxy_count]
properties:
entries:
type: ["array", "null"]
items:
$ref: "#/components/schemas/IpAllowEntry"
trusted_proxy_count:
type: ["integer", "null"]
IpAllowEntry:
oneOf:
- $ref: "#/components/schemas/LiteralIpAllowEntry"
- $ref: "#/components/schemas/GitHubMetaHooksEntry"
LiteralIpAllowEntry:
type: object
required: [Literal]
properties:
Literal:
type: string
GitHubMetaHooksEntry:
type: string
enum: [GitHubMetaHooks]
ServerStorageSettings:
type: object
required: [root]
properties:
root:
type: string
ServerArtifactsSettings:
type: object
required: [prefix, store]
properties:
prefix:
type: string
store:
$ref: "#/components/schemas/ObjectStoreSettings"
ServerSlateDbSettings:
type: object
required: [prefix, store, flush_interval, disk_cache]
properties:
prefix:
type: string
store:
$ref: "#/components/schemas/ObjectStoreSettings"
flush_interval:
type: string
disk_cache:
type: boolean
ObjectStoreSettings:
oneOf:
- $ref: "#/components/schemas/ObjectStoreLocalSettings"
- $ref: "#/components/schemas/ObjectStoreS3Settings"
ObjectStoreLocalSettings:
type: object
required: [type, root]
properties:
type:
type: string
enum: [local]
root:
type: string
ObjectStoreS3Settings:
type: object
required: [type, bucket, region, endpoint, path_style]
properties:
type:
type: string
enum: [s3]
bucket:
type: string
region:
type: string
endpoint:
type: ["string", "null"]
path_style:
type: boolean
ServerSchedulerSettings:
type: object
required: [max_concurrent_runs]
properties:
max_concurrent_runs:
type: integer
ServerLoggingSettings:
type: object
required: [level]
properties:
level:
type: ["string", "null"]
ServerIntegrationsSettings:
type: object
required: [github, slack, discord, teams]
properties:
github:
$ref: "#/components/schemas/GithubIntegrationSettings"
slack:
$ref: "#/components/schemas/SlackIntegrationSettings"
discord:
$ref: "#/components/schemas/DiscordIntegrationSettings"
teams:
$ref: "#/components/schemas/TeamsIntegrationSettings"
GithubIntegrationSettings:
type: object
required:
- enabled
- strategy
- app_id
- client_id
- slug
- permissions
- webhooks
properties:
enabled:
type: boolean
strategy:
$ref: "#/components/schemas/GithubIntegrationStrategy"
app_id:
type: ["string", "null"]
client_id:
type: ["string", "null"]
slug:
type: ["string", "null"]
permissions:
type: object
additionalProperties:
type: string
webhooks:
oneOf:
- $ref: "#/components/schemas/IntegrationWebhooksSettings"
- type: "null"
GithubIntegrationStrategy:
type: string
enum: [token, app]
SlackIntegrationSettings:
type: object
required: [enabled, default_channel]
properties:
enabled:
type: boolean
default_channel:
type: ["string", "null"]
DiscordIntegrationSettings:
type: object
required: [enabled]
properties:
enabled:
type: boolean
TeamsIntegrationSettings:
type: object
required: [enabled]
properties:
enabled:
type: boolean
IntegrationWebhooksSettings:
type: object
required: [strategy, ip_allowlist]
properties:
strategy:
oneOf:
- $ref: "#/components/schemas/WebhookStrategy"
- type: "null"
ip_allowlist:
oneOf:
- $ref: "#/components/schemas/ServerIpAllowlistOverrideSettings"
- type: "null"
WebhookStrategy:
type: string
enum: [tailscale_funnel, server_url]
RunSettingsLayer:
description: |
The merged, persisted v2 `[run]` subtree for a specific run, serialized
as the wrapping `SettingsFile` shape (so `settings.run.*` holds the run
config). Matches `fabro_types::settings::SettingsFile` minus secret
subtrees, identical to ServerSettings' redaction rules.
See `lib/crates/fabro-types/src/settings/run.rs` for the full type.
The persisted `SettingsLayer` used for a specific run, serialized as-is.
This matches the stored run manifest shape rather than a resolved view.
type: object
additionalProperties: true

View file

@ -1,14 +1,14 @@
---
title: "Store dump export command"
title: "Dump export command"
date: "2026-03-29"
---
## `fabro store dump`
## `fabro dump`
A new `fabro store dump` command exports the contents of the run store to a human-readable format for debugging and inspection. This is useful for diagnosing issues with run state, verifying data integrity after migrations, or extracting run data for external analysis.
A new `fabro dump` command exports the contents of the run store to a human-readable format for debugging and inspection. This is useful for diagnosing issues with run state, verifying data integrity after migrations, or extracting run data for external analysis.
```bash
fabro store dump <run-id>
fabro dump <run-id>
```
## More

View file

@ -81,7 +81,7 @@ jq '{from: .properties.from_node, to: .properties.to_node, label: .properties.la
<(fabro logs 01JKXYZ...) | head
```
If you need files on disk for offline analysis, `fabro store dump` exports `events.jsonl` plus run-state projections.
If you need files on disk for offline analysis, `fabro dump` exports `events.jsonl` plus run-state projections.
## Event categories
@ -132,6 +132,6 @@ Post-run analysis surfaces include:
|---|---|
| `fabro logs <RUN>` | Full event envelope stream as NDJSON |
| `fabro inspect <RUN>` | Current durable run state, including run/start/checkpoint/conclusion records |
| `fabro store dump --output <DIR> <RUN>` | Exported `events.jsonl` plus reconstructed JSON and node files |
| `fabro dump --output <DIR> <RUN>` | Exported `events.jsonl` plus reconstructed JSON and node files |
See [retros](/execution/retros), [stages](/api-reference/run-internals/list-run-stages), and [turns](/api-reference/run-internals/list-stage-turns) for higher-level analysis views built on top of this event stream.

View file

@ -143,4 +143,4 @@ Retros are also available via the REST API. See the [list retros](/api-reference
## Storage
Retros are stored in durable run state. If you need files on disk, `fabro store dump` materializes retro text under `stages/retro/` alongside `run.json`, stage files, and the rest of the exported run data.
Retros are stored in durable run state. If you need files on disk, `fabro dump` materializes retro text under `stages/retro/` alongside `run.json`, stage files, and the rest of the exported run data.

View file

@ -947,13 +947,13 @@ fabro secret rm ANTHROPIC_API_KEY
---
## `fabro store dump`
## `fabro dump`
Export the contents of a run's store-backed state to a directory for debugging and inspection.
Export the contents of a run's durable state to a directory for debugging and inspection.
```bash
fabro store dump <RUN>
fabro store dump abc123 -o ./debug-output
fabro dump <RUN>
fabro dump abc123 -o ./debug-output
```
| Argument / Flag | Description |

View file

@ -30,18 +30,18 @@ These paths are local runtime state and caches, not the canonical run state.
- **`runtime/`** — Local runtime files. Today this is mainly materialized blob payloads under `runtime/blobs/`.
- **`nodes/{manager_node}_{visit}/child/`** — Nested scratch directories for manager-loop child workflows.
Large durable values, event streams, checkpoints, diffs, conclusions, and retros are no longer projected into live scratch by default. Use `fabro logs`, `fabro inspect`, the API, or `fabro store dump` for those surfaces.
Large durable values, event streams, checkpoints, diffs, conclusions, and retros are no longer projected into live scratch by default. Use `fabro logs`, `fabro inspect`, the API, or `fabro dump` for those surfaces.
## Reconstructed and export-only layouts
Reconstructed metadata branches and `fabro store dump` exports now use the same core layout:
Reconstructed metadata branches and `fabro dump` exports now use the same core layout:
- `run.json` for the current projection snapshot, including the current checkpoint
- `graph.fabro` for workflow source
- `stages/retro/*.md` for retro prompt/response text
- `stages/{node_id}@{visit}/...` for per-stage prompt, response, status, diff, stdout, and stderr files
`fabro store dump` adds export-only history surfaces on top of that shared layout:
`fabro dump` adds export-only history surfaces on top of that shared layout:
- `events.jsonl` for the durable event stream
- `checkpoints/*.json` for checkpoint history snapshots

View file

@ -15,6 +15,7 @@ wildcard_imports = "warn"
[dependencies]
chrono = { workspace = true, features = ["serde"] }
fabro-config = { path = "../fabro-config" }
fabro-types = { path = "../fabro-types" }
progenitor-client = "0.13"
regress = "0.10"

View file

@ -171,6 +171,137 @@ fn main() {
"fabro_types::status::RunControlAction",
&[],
),
(
"RunStatusRecord",
"fabro_types::status::RunStatusRecord",
&[],
),
("ServerSettings", "fabro_config::ServerSettings", &[]),
(
"ServerNamespace",
"fabro_types::settings::ServerNamespace",
&[],
),
(
"FeaturesNamespace",
"fabro_types::settings::FeaturesNamespace",
&[],
),
(
"ServerListenSettings",
"fabro_types::settings::server::ServerListenSettings",
&[],
),
(
"ServerApiSettings",
"fabro_types::settings::server::ServerApiSettings",
&[],
),
(
"ServerWebSettings",
"fabro_types::settings::server::ServerWebSettings",
&[],
),
(
"ServerAuthSettings",
"fabro_types::settings::server::ServerAuthSettings",
&[],
),
(
"ServerAuthMethod",
"fabro_types::settings::server::ServerAuthMethod",
&[],
),
(
"ServerAuthGithubSettings",
"fabro_types::settings::server::ServerAuthGithubSettings",
&[],
),
(
"ServerIpAllowlistSettings",
"fabro_types::settings::server::ServerIpAllowlistSettings",
&[],
),
(
"ServerIpAllowlistOverrideSettings",
"fabro_types::settings::server::ServerIpAllowlistOverrideSettings",
&[],
),
(
"IpAllowEntry",
"fabro_types::settings::server::IpAllowEntry",
&[],
),
(
"ServerStorageSettings",
"fabro_types::settings::server::ServerStorageSettings",
&[],
),
(
"ServerArtifactsSettings",
"fabro_types::settings::server::ServerArtifactsSettings",
&[],
),
(
"ServerSlateDbSettings",
"fabro_types::settings::server::ServerSlateDbSettings",
&[],
),
(
"ObjectStoreSettings",
"fabro_types::settings::server::ObjectStoreSettings",
&[],
),
(
"ServerSchedulerSettings",
"fabro_types::settings::server::ServerSchedulerSettings",
&[],
),
(
"ServerLoggingSettings",
"fabro_types::settings::server::ServerLoggingSettings",
&[],
),
(
"ServerIntegrationsSettings",
"fabro_types::settings::server::ServerIntegrationsSettings",
&[],
),
(
"GithubIntegrationSettings",
"fabro_types::settings::server::GithubIntegrationSettings",
&[],
),
(
"GithubIntegrationStrategy",
"fabro_types::settings::server::GithubIntegrationStrategy",
&[],
),
(
"SlackIntegrationSettings",
"fabro_types::settings::server::SlackIntegrationSettings",
&[],
),
(
"DiscordIntegrationSettings",
"fabro_types::settings::server::DiscordIntegrationSettings",
&[],
),
(
"TeamsIntegrationSettings",
"fabro_types::settings::server::TeamsIntegrationSettings",
&[],
),
(
"IntegrationWebhooksSettings",
"fabro_types::settings::server::IntegrationWebhooksSettings",
&[],
),
(
"WebhookStrategy",
"fabro_types::settings::server::WebhookStrategy",
&[],
),
];
for (name, path, impls) in replacements {
settings.with_replacement(*name, *path, impls.iter().copied());

View file

@ -14,6 +14,17 @@ mod generated {
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
}
pub mod types {
pub use fabro_config::ServerSettings;
pub use fabro_types::settings::server::{
DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy,
IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreSettings, ServerApiSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,
ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings,
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
TeamsIntegrationSettings, WebhookStrategy,
};
pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
pub use fabro_types::status::{
BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason, TerminalStatus,
};

View file

@ -0,0 +1,78 @@
use std::any::{TypeId, type_name};
use fabro_api::types::{
FeaturesNamespace as ApiFeaturesNamespace, ObjectStoreSettings as ApiObjectStoreSettings,
ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings,
};
use fabro_config::{ServerSettings, parse_settings_layer};
use fabro_types::settings::server::ObjectStoreSettings;
use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
#[test]
fn server_settings_family_reuses_domain_types() {
assert_same_type::<ApiServerSettings, ServerSettings>();
assert_same_type::<ApiServerNamespace, ServerNamespace>();
assert_same_type::<ApiFeaturesNamespace, FeaturesNamespace>();
assert_same_type::<ApiObjectStoreSettings, ObjectStoreSettings>();
}
#[test]
fn server_settings_json_matches_openapi_shape() {
let layer = parse_settings_layer(
r#"
_version = 1
[server.listen]
type = "tcp"
address = "127.0.0.1:32276"
[server.api]
url = "https://api.fabro.example.com"
[server.web]
enabled = true
url = "https://fabro.example.com"
[server.auth]
methods = ["dev-token", "github"]
[server.auth.github]
allowed_usernames = ["alice"]
[server.storage]
root = "/srv/fabro"
[server.integrations.github]
enabled = true
strategy = "app"
app_id = "12345"
client_id = "Iv1.abcdef"
slug = "fabro-dev"
[features]
session_sandboxes = true
"#,
)
.expect("settings fixture should parse");
let settings = ServerSettings::from_layer(&layer).expect("settings should resolve");
let json = serde_json::to_value(&settings).expect("server settings should serialize");
assert_eq!(json["server"]["listen"]["type"], "tcp");
assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276");
assert_eq!(json["server"]["storage"]["root"], "/srv/fabro");
assert_eq!(json["features"]["session_sandboxes"], true);
let round_trip: ApiServerSettings =
serde_json::from_value(json).expect("server settings should deserialize");
assert_eq!(round_trip, settings);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -514,7 +514,7 @@ pub(crate) struct InspectArgs {
}
#[derive(Args)]
pub(crate) struct StoreDumpArgs {
pub(crate) struct DumpArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
@ -712,13 +712,6 @@ pub(crate) struct SystemEventsArgs {
pub(crate) struct SettingsArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Show only locally resolved settings and skip the server call
#[arg(long, conflicts_with = "server")]
pub(crate) local: bool,
/// Optional workflow name, .fabro path, or .toml run config to overlay
pub(crate) workflow: Option<PathBuf>,
}
#[derive(Args)]
@ -1008,8 +1001,8 @@ pub(crate) enum Commands {
Parse(ParseArgs),
/// Inspect and copy run artifacts (screenshots, reports, traces)
Artifact(ArtifactNamespace),
/// Export store-backed run state for debugging
Store(StoreNamespace),
/// Export a run's durable state to a directory
Dump(DumpArgs),
#[command(flatten)]
RunsCmd(RunsCommands),
/// List and test LLM models
@ -1092,9 +1085,7 @@ impl Commands {
ArtifactCommand::List(_) => "artifact list",
ArtifactCommand::Cp(_) => "artifact cp",
},
Self::Store(ns) => match &ns.command {
StoreCommand::Dump(_) => "store dump",
},
Self::Dump(_) => "dump",
Self::Exec(_) => "exec",
Self::RunCmd(cmd) => cmd.name(),
Self::Preflight(_) => "preflight",
@ -1206,18 +1197,6 @@ pub(crate) enum ArtifactCommand {
Cp(ArtifactCpArgs),
}
#[derive(Args)]
pub(crate) struct StoreNamespace {
#[command(subcommand)]
pub(crate) command: StoreCommand,
}
#[derive(Subcommand)]
pub(crate) enum StoreCommand {
/// Export a run's durable state to a directory
Dump(StoreDumpArgs),
}
#[derive(Args)]
pub(crate) struct SecretNamespace {
#[command(flatten)]

View file

@ -2,9 +2,10 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use fabro_config::UserSettings;
use fabro_config::merge::combine_files;
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;
@ -33,24 +34,19 @@ pub(crate) struct CommandContext {
cwd: PathBuf,
base_config_path: PathBuf,
machine_settings: SettingsLayer,
cli_settings: CliSettings,
user_settings: UserSettings,
server_mode: ServerMode,
server: OnceCell<Arc<Client>>,
}
impl CommandContext {
pub(crate) fn base(
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(printer, ServerMode::None, cli_settings, cli_layer)
pub(crate) fn base(printer: Printer, cli_layer: &CliLayer) -> Result<Self> {
Self::new(printer, ServerMode::None, cli_layer)
}
pub(crate) fn for_target(
args: &ServerTargetArgs,
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(
@ -58,7 +54,6 @@ impl CommandContext {
ServerMode::ByTarget {
target_override: args.server.clone(),
},
cli_settings,
cli_layer,
)
}
@ -66,7 +61,6 @@ impl CommandContext {
pub(crate) fn for_connection(
args: &ServerConnectionArgs,
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(
@ -75,17 +69,11 @@ impl CommandContext {
target_override: args.target.server.clone(),
storage_dir_override: args.storage_dir.clone_path(),
},
cli_settings,
cli_layer,
)
}
fn new(
printer: Printer,
server_mode: ServerMode,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
fn new(printer: Printer, server_mode: ServerMode, cli_layer: &CliLayer) -> Result<Self> {
let cwd = std::env::current_dir().context("Failed to get current directory")?;
let base_config_path = user_config::active_settings_path(None);
let disk_settings = match &server_mode {
@ -99,13 +87,14 @@ impl CommandContext {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?;
Ok(Self {
printer,
cwd,
base_config_path,
machine_settings,
cli_settings,
user_settings,
server_mode,
server: OnceCell::new(),
})
@ -123,16 +112,12 @@ impl CommandContext {
&self.cwd
}
pub(crate) fn base_config_path(&self) -> &Path {
&self.base_config_path
}
pub(crate) fn machine_settings(&self) -> &SettingsLayer {
&self.machine_settings
}
pub(crate) fn cli_settings(&self) -> &CliSettings {
&self.cli_settings
pub(crate) fn user_settings(&self) -> &UserSettings {
&self.user_settings
}
pub(crate) async fn server(&self) -> Result<Arc<Client>> {

View file

@ -6,7 +6,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, split_run_path};
pub(super) async fn cp_command(
args: &ArtifactCpArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -26,7 +26,6 @@ pub(super) async fn cp_command(
run_id_selector,
args.node.as_deref(),
args.retry,
cli,
cli_layer,
printer,
)

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -10,7 +10,7 @@ use crate::args::ArtifactListArgs;
pub(super) async fn list_command(
args: &ArtifactListArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -19,7 +19,6 @@ pub(super) async fn list_command(
&args.run_id,
args.node.as_deref(),
args.retry,
cli,
cli_layer,
printer,
)

View file

@ -2,7 +2,7 @@ mod cp;
mod list;
use anyhow::{Context, Result};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_types::{RunId, StageId};
use fabro_util::printer::Printer;
@ -26,11 +26,10 @@ pub(super) async fn resolve_artifacts(
run_selector: &str,
node: Option<&str>,
retry: Option<u32>,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(RunId, Client, Vec<ArtifactEntry>)> {
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(run_selector).await?.run_id;
let mut entries = Vec::new();
@ -65,7 +64,7 @@ pub(super) async fn resolve_artifacts(
pub(crate) async fn dispatch(
ns: ArtifactNamespace,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {

View file

@ -4,7 +4,6 @@ use anyhow::{Context as _, Result, bail};
use chrono::{DateTime, Utc};
use fabro_client::{AuthEntry, AuthStore, StoredSubject};
use fabro_http::header::CONTENT_TYPE;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::browser;
use fabro_util::printer::Printer;
@ -36,7 +35,6 @@ struct CliTokenSubject {
pub(super) async fn login_command(
args: AuthLoginArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
@ -45,7 +43,7 @@ pub(super) async fn login_command(
#[cfg(not(unix))]
{
let _ = (args, cli, cli_layer, printer);
let _ = (args, cli_layer, printer);
bail!(
"CLI OAuth login is not supported on Windows in this release. Use WSL, or use a dev-token server."
);
@ -53,7 +51,7 @@ pub(super) async fn login_command(
#[cfg(unix)]
{
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::base(printer, cli_layer)?;
let target = user_config::resolve_server_target(&args.server, ctx.machine_settings())?;
let web_url = browser_origin(&target)?;
let pkce = fabro_oauth::generate_pkce();

View file

@ -1,7 +1,6 @@
use anyhow::{Result, bail};
use fabro_client::{AuthEntry, AuthStore};
use fabro_http::header::AUTHORIZATION;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
@ -12,14 +11,13 @@ use crate::user_config::ServerTarget;
pub(super) async fn logout_command(
args: AuthLogoutArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
require_no_json_override(process_local_json)?;
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::base(printer, cli_layer)?;
let store = AuthStore::default();
if args.all {
let entries = store.list()?;

View file

@ -3,7 +3,6 @@ mod logout;
mod status;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
@ -11,20 +10,19 @@ use crate::args::{AuthCommand, AuthNamespace};
pub(crate) async fn dispatch(
ns: AuthNamespace,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
match ns.command {
AuthCommand::Login(args) => {
login::login_command(args, cli, cli_layer, process_local_json, printer).await
login::login_command(args, cli_layer, process_local_json, printer).await
}
AuthCommand::Logout(args) => {
logout::logout_command(args, cli, cli_layer, process_local_json, printer).await
logout::logout_command(args, cli_layer, process_local_json, printer).await
}
AuthCommand::Status(args) => {
status::status_command(&args, cli, cli_layer, process_local_json, printer)
status::status_command(&args, cli_layer, process_local_json, printer)
}
}
}

View file

@ -1,7 +1,6 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use fabro_client::{AuthEntry, AuthStore};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format};
use fabro_util::printer::Printer;
@ -43,12 +42,11 @@ struct StatusOutput {
pub(super) fn status_command(
args: &AuthStatusArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::base(printer, cli_layer)?;
let store = AuthStore::default();
let now = Utc::now();
let rows = if args.server.as_deref().is_some() {

View file

@ -8,155 +8,46 @@
)]
use std::io::Write;
use std::path::Path;
use fabro_config::effective_settings::{
EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer,
};
use fabro_config::{load_settings_project, project};
use fabro_api::types::ServerSettings;
use fabro_config::UserSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use serde_json::json;
use serde::Serialize;
use crate::args::SettingsArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config;
fn config_layers(
ctx: &CommandContext,
workflow: Option<&Path>,
) -> anyhow::Result<EffectiveSettingsLayers> {
let cwd = ctx.cwd();
let (workflow_layer, project_layer) = match workflow {
Some(path) => workflow_and_project_layers(path, cwd)?,
None => (SettingsLayer::default(), load_settings_project(cwd)?),
};
let user_layer =
user_config::load_settings_with_config_and_storage_dir(Some(ctx.base_config_path()), None)?;
Ok(EffectiveSettingsLayers::new(
SettingsLayer::default(),
workflow_layer,
project_layer,
user_layer,
))
}
fn workflow_and_project_layers(
path: &Path,
cwd: &Path,
) -> anyhow::Result<(SettingsLayer, SettingsLayer)> {
let resolution = project::resolve_workflow_path(path, cwd)?;
if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() {
anyhow::bail!(
"Workflow not found: {}",
resolution.resolved_workflow_path.display()
);
}
let workflow_layer = resolution.workflow_config.unwrap_or_default();
let project_layer = project::discover_project_config(
resolution
.resolved_workflow_path
.parent()
.unwrap_or_else(|| Path::new(".")),
)?
.map(|(_, config)| config)
.unwrap_or_default();
Ok((workflow_layer, project_layer))
}
fn strip_nulls(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for child in map.values_mut() {
strip_nulls(child);
}
map.retain(|_, child| !child.is_null());
}
serde_json::Value::Array(values) => {
for child in values {
strip_nulls(child);
}
}
_ => {}
}
}
fn local_settings_value(
args: &SettingsArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let layers = config_layers(&base_ctx, args.workflow.as_deref())?;
let local_settings =
materialize_settings_layer(layers, None, EffectiveSettingsMode::LocalOnly)?;
let mut value = resolve_local_settings_value(&local_settings)?;
strip_nulls(&mut value);
Ok(value)
}
fn render_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve local settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
fn resolve_local_settings_value(file: &SettingsLayer) -> anyhow::Result<serde_json::Value> {
let file = fabro_config::apply_builtin_defaults(file.clone());
let project = fabro_config::resolve_project_from_file(&file).map_err(render_resolve_errors)?;
let workflow =
fabro_config::resolve_workflow_from_file(&file).map_err(render_resolve_errors)?;
let run = fabro_config::resolve_run_from_file(&file).map_err(render_resolve_errors)?;
let cli = fabro_config::resolve_cli_from_file(&file).map_err(render_resolve_errors)?;
let features =
fabro_config::resolve_features_from_file(&file).map_err(render_resolve_errors)?;
Ok(json!({
"project": project,
"workflow": workflow,
"run": run,
"cli": cli,
"features": features,
}))
#[derive(Serialize)]
struct RenderedConfig {
user: UserSettings,
server: ServerSettings,
}
async fn rendered_config(
args: &SettingsArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
if args.local {
return local_settings_value(args, cli, cli_layer, printer);
}
if args.workflow.is_some() {
anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`");
}
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
ctx.server()
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let user = fabro_config::UserSettings::resolve()?;
let server = ctx
.server()
.await?
.retrieve_resolved_server_settings()
.await
.await?;
serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into)
}
pub(crate) async fn execute(
args: &SettingsArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let config = Box::pin(rendered_config(args, cli, cli_layer, printer)).await?;
let config = Box::pin(rendered_config(args, cli_layer, printer)).await?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&config)?;
return Ok(());

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::Result;
use fabro_api::types as api_types;
use fabro_config::user::active_settings_path;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
pub(crate) use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
@ -143,7 +143,7 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer:
pub(crate) async fn run_doctor(
args: &DoctorArgs,
verbose: bool,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<i32, anyhow::Error> {
@ -179,7 +179,7 @@ pub(crate) async fn run_doctor(
}],
};
let ctx = match CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer) {
let ctx = match CommandContext::for_target(&args.target, printer, cli_layer) {
Ok(ctx) => ctx,
Err(err) => {
report.sections.push(CheckSection {

View file

@ -1,6 +1,6 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `store dump` command: sync file I/O for dump outputs"
reason = "CLI `dump` command: sync file I/O for dump outputs"
)]
use std::io::ErrorKind;
@ -11,28 +11,28 @@ use bytes::Bytes;
#[cfg(test)]
use fabro_store::{ArtifactStore, RunDatabase};
use fabro_store::{EventEnvelope, RunProjection, StageId};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::{RunBlobId, RunId};
use fabro_util::printer::Printer;
use fabro_workflow::run_dump::RunDump;
use futures::future::BoxFuture;
#[cfg(test)]
use serde::de::DeserializeOwned;
use tokio::task::spawn_blocking;
use super::run_export::StoreRunExport;
use crate::args::StoreDumpArgs;
use crate::args::DumpArgs;
use crate::command_context::CommandContext;
use crate::server_client::Client;
use crate::shared::{absolute_or_current, print_json_pretty};
pub(crate) async fn dump_command(
args: &StoreDumpArgs,
cli: &CliSettings,
pub(crate) async fn run(
args: &DumpArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
let state = client.get_run_state(&run_id).await?;
@ -222,7 +222,7 @@ async fn export_run_from_source(
.with_context(|| format!("failed to create {}", staging_parent.display()))?;
let staging_dir = tempfile::Builder::new()
.prefix(".fabro-store-dump-")
.prefix(".fabro-dump-")
.tempdir_in(staging_parent)
.with_context(|| {
format!(
@ -248,7 +248,7 @@ async fn write_run_dump(
output_dir: &Path,
) -> Result<usize> {
let events = source.list_events().await?;
let mut dump = StoreRunExport::from_store_state_and_events(state, &events)?;
let mut dump = RunDump::from_store_state_and_events(state, &events)?;
dump.hydrate_referenced_blobs_with_reader(|blob_id| source.read_blob(blob_id))
.await?;

View file

@ -15,7 +15,7 @@ use fabro_llm::types::{
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat;
use fabro_types::settings::run::McpEntryLayer;
use fabro_types::settings::{CliSettings, InterpString};
use fabro_types::settings::{CliNamespace, InterpString};
use fabro_util::exit::{ErrorExt, ExitClass};
use fabro_util::printer::Printer;
use futures::stream;
@ -358,7 +358,7 @@ impl ProviderAdapter for AuthenticatedFabroServerAdapter {
pub(crate) async fn execute(
mut args: ExecArgs,
cli: &CliSettings,
cli: &CliNamespace,
_printer: Printer,
) -> AnyResult<()> {
use fabro_agent::cli::PermissionLevel as AgentPermissionLevel;

View file

@ -14,7 +14,7 @@ use fabro_api::types;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_types::settings::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use tracing::debug;
@ -28,7 +28,7 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r
pub(crate) async fn run(
args: &GraphArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
@ -37,7 +37,7 @@ pub(crate) async fn run(
require_no_json_override(process_local_json)?;
}
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),

View file

@ -24,7 +24,7 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f
use fabro_config::bind::Bind;
use fabro_config::daemon::ServerDaemon;
use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir};
use fabro_config::{ResolveError, Storage, envfile};
use fabro_config::{Storage, envfile};
use fabro_install::{
InstallListenConfig, PendingSettingsWrite, merge_server_settings as merge_server_settings_impl,
persist_install_outputs_direct, write_github_app_settings, write_token_settings,
@ -34,7 +34,7 @@ use fabro_server::serve;
use fabro_store::ArtifactStore;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::server::ServerAuthMethod;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_types::settings::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::version::FABRO_VERSION;
@ -1262,24 +1262,12 @@ fn persist_github_install_changes(
Ok(())
}
fn render_server_resolve_errors(errors: Vec<ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve server settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
async fn write_artifact_store_metadata(
settings: &SettingsLayer,
fabro_version: &str,
) -> Result<()> {
let resolved =
fabro_config::resolve_server_from_file(settings).map_err(render_server_resolve_errors)?;
let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?;
let resolved = fabro_config::ServerSettings::from_layer(settings)?;
let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?;
let artifact_store = ArtifactStore::new(object_store, prefix);
artifact_store.write_metadata(fabro_version).await?;
Ok(())
@ -1421,7 +1409,7 @@ where
pub(crate) async fn execute(
args: &InstallArgs,
command: Option<InstallCommand>,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
@ -1437,7 +1425,7 @@ pub(crate) async fn execute(
async fn run_install_github_command(
args: &InstallArgs,
github_args: &InstallGithubArgs,
cli: &CliSettings,
cli: &CliNamespace,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
@ -1600,7 +1588,7 @@ async fn run_install_github_inner(
pub(crate) async fn run_install(
args: &InstallArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
@ -1626,7 +1614,7 @@ pub(crate) async fn run_install(
async fn run_install_inner(
args: &InstallArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -1802,8 +1790,7 @@ async fn run_install_inner(
.context("failed to parse generated settings.toml")?,
args.storage_dir.as_deref(),
);
fabro_config::resolve_server_from_file(&install_settings)
.map_err(render_server_resolve_errors)?;
fabro_config::ServerSettings::from_layer(&install_settings)?;
// Secrets and auth material
{

View file

@ -2,6 +2,7 @@ pub(crate) mod artifact;
pub(crate) mod auth;
pub(crate) mod config;
pub(crate) mod doctor;
pub(crate) mod dump;
pub(crate) mod exec;
pub(crate) mod graph;
pub(crate) mod install;
@ -10,6 +11,7 @@ pub(crate) mod parse;
pub(crate) mod pr;
pub(crate) mod preflight;
pub(crate) mod provider;
pub(crate) mod rebuild;
pub(crate) mod render_graph;
pub(crate) mod repo;
pub(crate) mod run;
@ -17,7 +19,6 @@ pub(crate) mod runs;
pub(crate) mod sandbox;
pub(crate) mod secret;
pub(crate) mod server;
pub(crate) mod store;
pub(crate) mod system;
pub(crate) mod uninstall;
pub(crate) mod upgrade;

View file

@ -3,7 +3,7 @@ use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_api::types as api_types;
use fabro_model::{Catalog, Model, Provider};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -42,7 +42,7 @@ struct ModelTestOutput {
pub(crate) async fn execute(
command: Option<ModelsCommand>,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -51,7 +51,7 @@ pub(crate) async fn execute(
ModelsCommand::List(args) => &args.target,
ModelsCommand::Test(args) => &args.target,
};
let ctx = CommandContext::for_target(target_args, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(target_args, printer, cli_layer)?;
let server = ctx.server().await?;
run_models(command, &server, cli.output.format == OutputFormat::Json).await

View file

@ -11,13 +11,13 @@ use std::io::Write;
use fabro_config::project::resolve_workflow;
use fabro_graphviz::parser::parse_ast;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_util::printer::Printer;
use crate::args::ParseArgs;
use crate::shared::read_workflow_file;
pub(crate) fn run(args: &ParseArgs, _cli: &CliSettings, _printer: Printer) -> anyhow::Result<()> {
pub(crate) fn run(args: &ParseArgs, _cli: &CliNamespace, _printer: Printer) -> anyhow::Result<()> {
let stdout = std::io::stdout();
run_to(args, stdout.lock())
}

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
@ -9,14 +9,14 @@ use crate::shared::print_json_pretty;
pub(super) async fn close_command(
args: PrCloseArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
fabro_github::close_pull_request(
&creds,

View file

@ -5,7 +5,7 @@ use fabro_auth::configured_providers_from_process_env;
use fabro_config::Storage;
use fabro_model::Catalog;
use fabro_sandbox::daytona::detect_repo_info;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_vault::Vault;
@ -16,7 +16,7 @@ use tracing::info;
use crate::args::PrCreateArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::commands::rebuild::rebuild_run_store;
use crate::shared::print_json_pretty;
use crate::shared::repo::ensure_matching_repo_origin;
use crate::user_config;
@ -27,11 +27,11 @@ use crate::user_config;
)]
pub(super) async fn create_command(
args: PrCreateArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run_id).await?.run_id;
let events = client.list_run_events(&run_id, None, None).await?;
@ -86,7 +86,7 @@ pub(super) async fn create_command(
let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url)
.map_err(|err| anyhow::anyhow!("{err}"))?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let branch_found = fabro_github::branch_exists(
&creds,

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -25,11 +25,11 @@ struct PrRow {
pub(super) async fn list_command(
args: PrListArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let mut entries = Vec::new();
@ -50,7 +50,7 @@ pub(super) async fn list_command(
return Ok(());
}
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let futures: Vec<_> = entries
.iter()

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
@ -9,14 +9,14 @@ use crate::shared::print_json_pretty;
pub(super) async fn merge_command(
args: PrMergeArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
fabro_github::merge_pull_request(
&creds,

View file

@ -9,7 +9,7 @@ use fabro_config::Storage;
use fabro_github::GitHubCredentials;
use fabro_types::PullRequestRecord;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliSettings, InterpString};
use fabro_types::settings::{CliNamespace, InterpString};
use fabro_util::printer::Printer;
use crate::args::{PrCommand, PrNamespace, ServerTargetArgs};
@ -22,7 +22,7 @@ const GITHUB_CREDENTIALS_REQUIRED: &str =
pub(crate) async fn dispatch(
ns: PrNamespace,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -42,28 +42,19 @@ pub(crate) async fn dispatch(
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
)]
fn load_github_credentials_required(
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<GitHubCredentials> {
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let server_settings =
fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| {
anyhow!(
"failed to resolve server settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
})?;
let ctx = CommandContext::base(printer, cli_layer)?;
let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings())
.map_err(anyhow::Error::from)?;
let vault = user_config::storage_dir(ctx.machine_settings())
.ok()
.and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok());
let creds = build_github_credentials(
server_settings.integrations.github.strategy,
server_settings.server.integrations.github.strategy,
server_settings
.server
.integrations
.github
.app_id
@ -79,11 +70,10 @@ fn load_github_credentials_required(
pub(crate) async fn load_pr_record(
server: &ServerTargetArgs,
run_id: &str,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(PullRequestRecord, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(run_id).await?.run_id;
let state = client.get_run_state(&run_id).await?;

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
@ -9,14 +9,14 @@ use crate::shared::print_json_pretty;
pub(super) async fn view_command(
args: PrViewArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let detail = fabro_github::get_pull_request(
&creds,

View file

@ -1,7 +1,7 @@
use anyhow::bail;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -17,12 +17,12 @@ use crate::shared::print_json_pretty;
pub(crate) async fn execute(
mut args: PreflightArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
let manifest = build_run_manifest(ManifestBuildInput {

View file

@ -1,7 +1,6 @@
use anyhow::Result;
use fabro_api::types;
use fabro_auth::credential_id_for;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -12,14 +11,13 @@ use crate::shared::provider_auth;
pub(super) async fn login_command(
args: ProviderLoginArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
require_no_json_override(process_local_json)?;
let s = Styles::detect_stderr();
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let server = ctx.server().await?;
let credential = if args.api_key_stdin {
provider_auth::authenticate_provider_with_api_key_source(

View file

@ -1,7 +1,6 @@
mod login;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
@ -9,14 +8,13 @@ use crate::args::{ProviderCommand, ProviderNamespace};
pub(crate) async fn dispatch(
ns: ProviderNamespace,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
match ns.command {
ProviderCommand::Login(args) => {
login::login_command(args, cli, cli_layer, process_local_json, printer).await
login::login_command(args, cli_layer, process_local_json, printer).await
}
}
}

View file

@ -1,9 +1,9 @@
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result<Vec<String>> {
pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<String>> {
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();

View file

@ -6,7 +6,7 @@
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::process::Command as TokioCommand;
@ -36,7 +36,7 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
pub(crate) async fn run_init(
args: &RepoInitArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<Vec<String>> {
@ -151,7 +151,7 @@ draft = true
}
if cli.output.format != OutputFormat::Json {
check_github_app_installation(&args.target, cli, cli_layer, printer).await;
check_github_app_installation(&args.target, cli_layer, printer).await;
}
Ok(created)
@ -159,7 +159,6 @@ draft = true
async fn check_github_app_installation(
target: &ServerTargetArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) {
@ -200,7 +199,7 @@ async fn check_github_app_installation(
return; // Not a GitHub repo — skip silently
};
let ctx = match CommandContext::for_target(target, printer, cli.clone(), cli_layer) {
let ctx = match CommandContext::for_target(target, printer, cli_layer) {
Ok(ctx) => ctx,
Err(err) => {
fabro_util::printerr!(

View file

@ -2,7 +2,7 @@ pub(crate) mod deinit;
pub(crate) mod init;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -11,7 +11,7 @@ use crate::shared::print_json_pretty;
pub(crate) async fn dispatch(
ns: RepoNamespace,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {

View file

@ -19,7 +19,6 @@ use anyhow::Result;
use fabro_api::types;
use fabro_interview::{AnswerValue, ConsoleInterviewer, Question, QuestionOption, QuestionType};
use fabro_store::EventEnvelope;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::run::ApprovalMode;
use fabro_types::{EventBody, RunId};
use fabro_util::json::normalize_json_value;
@ -49,6 +48,7 @@ pub(crate) async fn attach_run(
kill_on_detach: bool,
styles: &'static Styles,
json_output: bool,
live_verbose: bool,
) -> Result<ExitCode> {
let inferred_storage_dir = infer_storage_dir(run_dir);
let inferred_run_id = infer_run_id(run_dir);
@ -63,6 +63,7 @@ pub(crate) async fn attach_run(
kill_on_detach,
styles,
json_output,
live_verbose,
Printer::Default,
))
.await;
@ -79,6 +80,7 @@ pub(crate) async fn attach_run_with_client(
kill_on_detach: bool,
styles: &'static Styles,
json_output: bool,
live_verbose: bool,
printer: Printer,
) -> Result<ExitCode> {
let state = client.get_run_state(run_id).await?;
@ -86,10 +88,6 @@ pub(crate) async fn attach_run_with_client(
fabro_config::resolve_run_from_file(&record.settings)
.is_ok_and(|settings| settings.execution.approval == ApprovalMode::Auto)
});
let verbose = state.spec.as_ref().is_some_and(|record| {
fabro_config::resolve_cli_from_file(&record.settings)
.is_ok_and(|settings| settings.output.verbosity == OutputVerbosity::Verbose)
});
let events = client.list_run_events(run_id, None, None).await?;
let replay_events = events.clone();
let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1));
@ -98,7 +96,7 @@ pub(crate) async fn attach_run_with_client(
if state_is_terminal(&state) || initial_exit_code.is_some() {
return replay_run_with_client(
verbose,
live_verbose,
events,
initial_exit_code
.or(state_exit_code)
@ -116,7 +114,7 @@ pub(crate) async fn attach_run_with_client(
styles,
AttachOptions {
auto_approve,
verbose,
verbose: live_verbose,
kill_on_detach,
json_output,
},
@ -544,6 +542,7 @@ mod tests {
false,
no_color_styles(),
false,
false,
))
.await
.unwrap_err();

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -11,17 +11,17 @@ use crate::user_config::load_settings_with_storage_dir;
pub(crate) async fn execute(
mut args: RunArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let cli_defaults = load_settings_with_storage_dir(None)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
let quiet = args.detach;
let prevent_idle_sleep = ctx.cli_settings().exec.prevent_idle_sleep;
let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep;
let created_run = Box::pin(super::create::create_run(
&ctx,
&args,
@ -64,6 +64,7 @@ pub(crate) async fn execute(
true,
styles,
json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
printer,
))
.await?;

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::fs;
@ -28,7 +28,7 @@ enum CopyDirection {
pub(crate) async fn cp_command(
args: CpArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -41,8 +41,7 @@ pub(crate) async fn cp_command(
local_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer)
.await?;
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
let file_count = if args.recursive {
Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?)
@ -73,8 +72,7 @@ pub(crate) async fn cp_command(
remote_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer)
.await?;
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
let file_count = if args.recursive {
Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?)
@ -128,11 +126,10 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
async fn resolve_client_and_run_id(
server: &ServerTargetArgs,
run_prefix: &str,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(Client, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(run_prefix).await?.run_id;
Ok((client.clone_for_reuse(), run_id))

View file

@ -10,7 +10,7 @@
use std::io::{self, IsTerminal, Write};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
@ -22,12 +22,12 @@ use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: DiffArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
info!(run_id = %args.run, "Showing diff");
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
let state = client.get_run_state(&run_id).await?;

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use fabro_checkpoint::git::Store;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -9,19 +9,19 @@ use git2::Repository;
use crate::args::ForkArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::commands::rebuild::rebuild_run_store;
use crate::shared::print_json_pretty;
use crate::shared::repo::ensure_matching_repo_origin;
pub(crate) async fn run(
args: &ForkArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run_id).await?.run_id;
let state = client.get_run_state(&run_id).await?;

View file

@ -13,7 +13,7 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use chrono::{DateTime, Utc};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::json::normalize_json_value;
use fabro_util::printer::Printer;
@ -32,11 +32,11 @@ const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500);
pub(crate) async fn run(
args: &LogsArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
info!(run_id = %run_id, "Showing logs");

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -29,7 +29,7 @@ pub(crate) mod wait;
pub(crate) async fn dispatch(
cmd: RunCommands,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
_process_local_json: bool,
printer: Printer,
@ -39,7 +39,7 @@ pub(crate) async fn dispatch(
RunCommands::Create(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_defaults = load_settings_with_storage_dir(None)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let created_run = Box::pin(create::create_run(
&ctx,
&args,
@ -57,7 +57,7 @@ pub(crate) async fn dispatch(
Ok(())
}
RunCommands::Start(StartArgs { server, run }) => {
let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&run).await?.run_id;
start::start_run_with_client(client.as_ref(), &run_id, false).await?;
@ -68,7 +68,7 @@ pub(crate) async fn dispatch(
}
RunCommands::Attach(AttachArgs { server, run }) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&run).await?.run_id;
let exit_code = Box::pin(attach::attach_run_with_client(
@ -77,6 +77,7 @@ pub(crate) async fn dispatch(
false,
styles,
cli.output.format == OutputFormat::Json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
printer,
))
.await?;
@ -112,9 +113,8 @@ pub(crate) async fn dispatch(
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let ctx =
CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep)
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
crate::sleep_inhibitor::guard(ctx.user_settings().cli.exec.prevent_idle_sleep)
};
Box::pin(resume::resume_command(
args, styles, cli, cli_layer, printer,

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
@ -10,12 +10,12 @@ use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: PreviewArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
let expires_in_secs =

View file

@ -1,5 +1,5 @@
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -15,11 +15,11 @@ use crate::shared::print_json_pretty;
pub(crate) async fn resume_command(
args: ResumeArgs,
styles: &'static Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
@ -39,6 +39,7 @@ pub(crate) async fn resume_command(
true,
styles,
json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
printer,
))
.await?;

View file

@ -3,7 +3,7 @@ use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_checkpoint::git::Store;
use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::{EventBody, RunEvent};
use fabro_util::printer::Printer;
@ -17,7 +17,7 @@ use serde::Serialize;
use crate::args::RewindArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::commands::rebuild::rebuild_run_store;
use crate::server_client::Client;
use crate::shared::repo::ensure_matching_repo_origin;
use crate::shared::{color_if, print_json_pretty};
@ -33,12 +33,12 @@ pub(crate) struct TimelineEntryJson {
pub(crate) async fn run(
args: &RewindArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run_id).await?.run_id;
let state = client.get_run_state(&run_id).await?;

View file

@ -4,6 +4,7 @@
std::io::BufReader; not on a Tokio path"
)]
use std::collections::HashMap;
use std::io::{BufRead as StdBufRead, BufReader as StdBufReader};
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -107,6 +108,7 @@ pub(crate) async fn execute(
artifact_sink,
run_control: Some(run_control),
github_app,
github_permissions: HashMap::new(),
vault,
on_node: None,
registry_override: None,

View file

@ -1,5 +1,5 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
@ -10,7 +10,7 @@ use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: SshArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
@ -19,7 +19,7 @@ pub(crate) async fn run(
require_no_json_override(process_local_json)?;
}
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
let ssh = client.create_run_ssh_access(&run_id, args.ttl).await?;

View file

@ -11,7 +11,7 @@ use std::io::Write;
use anyhow::{Result, bail};
use fabro_types::RunId;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -27,11 +27,11 @@ use crate::shared::{format_duration_ms, format_usd_micros, run_status_kind};
pub(crate) async fn run(
args: &WaitArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
info!(run_id = %run_id, "Waiting for run to complete");

View file

@ -1,5 +1,5 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -11,11 +11,11 @@ use crate::shared::print_json_pretty;
pub(crate) async fn archive_command(
args: &RunsArchiveArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
run_bulk(
Action::Archive,
&args.runs,
@ -28,11 +28,11 @@ pub(crate) async fn archive_command(
pub(crate) async fn unarchive_command(
args: &RunsUnarchiveArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
run_bulk(
Action::Unarchive,
&args.runs,
@ -66,7 +66,7 @@ async fn run_bulk(
action: Action,
identifiers: &[String],
client: &server_client::Client,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;

View file

@ -1,5 +1,4 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_workflow::run_status::RunStatus;
@ -21,13 +20,8 @@ pub(crate) struct InspectOutput {
pub sandbox: Option<serde_json::Value>,
}
pub(crate) async fn run(
args: &InspectArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
pub(crate) async fn run(args: &InspectArgs, cli_layer: &CliLayer, printer: Printer) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let client = ctx.server().await?;
let run = ServerRunSummaryInfo::from_summary(client.resolve_run(&args.run).await?);
let run_id = run.run_id();

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use chrono::Utc;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -20,11 +20,11 @@ use crate::shared::{color_if, format_duration_ms, run_status_kind, tilde_path};
pub(crate) async fn list_command(
args: &RunsListArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_server_runs(

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -13,7 +13,7 @@ pub(crate) mod rm;
pub(crate) async fn dispatch(
cmd: RunsCommands,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
@ -23,7 +23,7 @@ pub(crate) async fn dispatch(
list::list_command(&args, &styles, cli, cli_layer, printer).await
}
RunsCommands::Rm(args) => rm::remove_command(&args, cli, cli_layer, printer).await,
RunsCommands::Inspect(args) => inspect::run(&args, cli, cli_layer, printer).await,
RunsCommands::Inspect(args) => inspect::run(&args, cli_layer, printer).await,
RunsCommands::Archive(args) => {
archive::archive_command(&args, cli, cli_layer, printer).await
}

View file

@ -1,5 +1,5 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -11,18 +11,18 @@ use crate::shared::print_json_pretty;
pub(crate) async fn remove_command(
args: &RunsRemoveArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
remove_from(args, ctx.server().await?.as_ref(), cli, printer).await
}
async fn remove_from(
args: &RunsRemoveArgs,
client: &server_client::Client,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
@ -7,7 +7,7 @@ use crate::args::SandboxCommand;
pub(crate) async fn dispatch(
command: SandboxCommand,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use chrono::{DateTime, Utc};
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -25,7 +25,7 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
pub(super) async fn list_command(
client: &Client,
_args: &SecretListArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let secrets = client.list_secrets().await?;

View file

@ -3,7 +3,7 @@ mod rm;
mod set;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
@ -12,11 +12,11 @@ use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
ns: SecretNamespace,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&ns.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&ns.target, printer, cli_layer)?;
let server = ctx.server().await?;
match ns.command {
SecretCommand::List(args) => list::list_command(&server, &args, cli, printer).await,

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
@ -10,7 +10,7 @@ use crate::shared::print_json_pretty;
pub(super) async fn rm_command(
client: &Client,
args: &SecretRmArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
client.delete_secret_by_name(&args.key).await?;

View file

@ -11,7 +11,7 @@ use std::io::{IsTerminal, Read as _};
use anyhow::{Context as _, Result, bail};
use fabro_api::types;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use tokio::task::spawn_blocking;
@ -60,7 +60,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result<String> {
pub(super) async fn set_command(
client: &Client,
args: &SecretSetArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let value = resolve_value(args).await?;

View file

@ -1,21 +0,0 @@
pub(crate) mod dump;
pub(crate) mod rebuild;
mod run_export;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{StoreCommand, StoreNamespace};
pub(crate) async fn dispatch(
ns: StoreNamespace,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match ns.command {
StoreCommand::Dump(args) => dump::dump_command(&args, cli, cli_layer, printer).await,
}
}

View file

@ -1 +0,0 @@
pub(super) use fabro_workflow::run_dump::RunDump as StoreRunExport;

View file

@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_api::types;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -13,11 +13,11 @@ use crate::shared::{format_size, print_json_pretty};
pub(super) async fn df_command(
args: &DfArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
let server = ctx.server().await?;
let json = cli.output.format == OutputFormat::Json;

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use fabro_client::sse;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use futures::StreamExt;
@ -10,11 +10,11 @@ use crate::command_context::CommandContext;
pub(super) async fn events_command(
args: &SystemEventsArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
let server = ctx.server().await?;
let mut stream = server.attach_events(&args.run_ids).await?;
let mut pending = Vec::new();

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
@ -9,11 +9,11 @@ use crate::shared::print_json_pretty;
pub(super) async fn info_command(
args: &SystemInfoArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
let server = ctx.server().await?;
let response = server.get_system_info().await?;

View file

@ -4,7 +4,7 @@ mod info;
mod prune;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
pub(crate) use prune::parse_duration;
@ -13,7 +13,7 @@ use crate::args::{SystemCommand, SystemNamespace};
pub(crate) async fn dispatch(
ns: SystemNamespace,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use anyhow::{Context, Result, bail};
use fabro_api::types;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
@ -13,11 +13,11 @@ use crate::shared::{format_size, print_json_pretty};
pub(super) async fn prune_command(
args: &RunsPruneArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
let server = ctx.server().await?;
let response = server
.prune_runs(types::PruneRunsRequest {

View file

@ -15,7 +15,7 @@ use std::time::Duration;
use anyhow::{Context, Result};
use fabro_config::Storage;
use fabro_config::daemon::ServerDaemon;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::Home;
use fabro_util::printer::Printer;
@ -45,7 +45,7 @@ struct Inventory {
)]
pub(crate) async fn run_uninstall(
args: &UninstallArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;

View file

@ -13,7 +13,7 @@ use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use semver::Version;
@ -436,7 +436,7 @@ impl UpgradeCheckState {
pub(crate) async fn run_upgrade(
args: UpgradeArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let current_exe = std::env::current_exe()
@ -599,7 +599,7 @@ pub(crate) async fn run_upgrade(
fn run_upgrade_brew(
args: &UpgradeArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
channel: BrewChannel,
) -> Result<()> {
@ -1153,7 +1153,7 @@ mod tests {
#[test]
fn run_upgrade_brew_refuses_by_default() {
let cli = CliSettings::default();
let cli = CliNamespace::default();
let err = run_upgrade_brew(
&brew_args(None, false, false, false),
&cli,
@ -1169,7 +1169,7 @@ mod tests {
#[test]
fn run_upgrade_brew_dry_run_returns_ok() {
let cli = CliSettings::default();
let cli = CliNamespace::default();
let result = run_upgrade_brew(
&brew_args(None, false, false, true),
&cli,
@ -1181,7 +1181,7 @@ mod tests {
#[test]
fn run_upgrade_brew_rejects_version_flag() {
let cli = CliSettings::default();
let cli = CliNamespace::default();
let err = run_upgrade_brew(
&brew_args(Some("0.1.0"), false, false, false),
&cli,
@ -1194,7 +1194,7 @@ mod tests {
#[test]
fn run_upgrade_brew_rejects_prerelease_flag() {
let cli = CliSettings::default();
let cli = CliNamespace::default();
let err = run_upgrade_brew(
&brew_args(None, true, false, false),
&cli,
@ -1207,7 +1207,7 @@ mod tests {
#[test]
fn run_upgrade_brew_rejects_force_flag() {
let cli = CliSettings::default();
let cli = CliNamespace::default();
let err = run_upgrade_brew(
&brew_args(None, false, true, false),
&cli,

View file

@ -2,7 +2,7 @@ use anyhow::bail;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_types::settings::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -15,11 +15,11 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
pub(crate) async fn run(
args: &ValidateArgs,
styles: &Styles,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),

View file

@ -6,7 +6,7 @@
use std::io::IsTerminal;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use serde_json::{Map, Value, json};
@ -18,12 +18,12 @@ use crate::user_config::{self, ServerTarget};
pub(crate) async fn version_command(
args: &VersionArgs,
cli: &CliSettings,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let client = client_info();
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let server_target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?;
let server_address = format_server_target(&server_target);
let server_info = match ctx.server().await {

View file

@ -7,7 +7,7 @@ use std::path::Path;
use anyhow::{Context, Result, bail};
use fabro_config::project::{discover_project_config, resolve_fabro_root};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
@ -16,7 +16,7 @@ use crate::shared::{print_json_pretty, relative_path};
pub(super) fn create_command(
args: &WorkflowCreateArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let cwd = std::env::current_dir()?;

View file

@ -5,7 +5,7 @@ use fabro_config::project::{
WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed,
resolve_fabro_root,
};
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -17,7 +17,7 @@ const GOAL_MAX_LEN: usize = 60;
pub(super) fn list_command(
_args: &WorkflowListArgs,
cli: &CliSettings,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let styles = Styles::detect_stderr();

View file

@ -2,12 +2,12 @@ mod create;
mod list;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::CliNamespace;
use fabro_util::printer::Printer;
use crate::args::{WorkflowCommand, WorkflowNamespace};
pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliSettings, printer: Printer) -> Result<()> {
pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliNamespace, printer: Printer) -> Result<()> {
match ns.command {
WorkflowCommand::List(args) => list::list_command(&args, cli, printer),
WorkflowCommand::Create(args) => create::create_command(&args, cli, printer),

View file

@ -34,8 +34,8 @@ pub(crate) fn bind_request(
}
pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec<ServerAuthMethod> {
fabro_config::resolve_server_from_file(settings)
.map(|resolved| resolved.auth.methods)
fabro_config::ServerSettings::from_layer(settings)
.map(|resolved| resolved.server.auth.methods)
.unwrap_or_default()
}

View file

@ -173,9 +173,9 @@ async fn main_inner() -> (String, Result<()>) {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
let cli_settings = match user_config::resolve_cli_settings(&combined_settings) {
Ok(cli_settings) => cli_settings,
Err(err) => return (command_name, Err(err)),
let cli_settings = match fabro_config::UserSettings::from_layer(&combined_settings) {
Ok(settings) => settings.cli,
Err(err) => return (command_name, Err(err.into())),
};
let printer = printer_from_verbosity(cli_settings.output.verbosity);
@ -247,8 +247,8 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Artifact(ns) => {
commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
}
Commands::Store(ns) => {
commands::store::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
Commands::Dump(args) => {
commands::dump::run(&args, &cli_settings, &cli_layer, printer).await?;
}
Commands::RunsCmd(cmd) => {
commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?;
@ -318,14 +318,7 @@ async fn main_inner() -> (String, Result<()>) {
commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?;
}
Commands::Auth(ns) => {
commands::auth::dispatch(
ns,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?;
}
Commands::Pr(ns) => {
Box::pin(commands::pr::dispatch(
@ -353,14 +346,7 @@ async fn main_inner() -> (String, Result<()>) {
commands::upgrade::run_upgrade(args, &cli_settings, printer).await?;
}
Commands::Provider(ns) => {
commands::provider::dispatch(
ns,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?;
}
Commands::Sandbox { command } => {
commands::sandbox::dispatch(
@ -519,7 +505,7 @@ async fn prepare_server_bootstrap(
mod tests {
use args::{
AuthCommand, AuthNamespace, Commands, InstallGitHubStrategyArg, ModelsCommand,
ProviderCommand, ProviderNamespace, StoreCommand, StoreNamespace,
ProviderCommand, ProviderNamespace,
};
use tokio::runtime::Runtime;
@ -954,13 +940,11 @@ level = "warn"
}
#[test]
fn parse_store_dump_command() {
let cli = Cli::try_parse_from(["fabro", "store", "dump", "ABC123", "-o", "./out"])
.expect("should parse");
fn parse_dump_command() {
let cli =
Cli::try_parse_from(["fabro", "dump", "ABC123", "-o", "./out"]).expect("should parse");
match *cli.command.unwrap() {
Commands::Store(StoreNamespace {
command: StoreCommand::Dump(args),
}) => {
Commands::Dump(args) => {
assert_eq!(args.run, "ABC123");
assert_eq!(args.output, std::path::PathBuf::from("./out"));
}
@ -1077,36 +1061,22 @@ level = "warn"
assert_eq!(cli.command.as_ref().unwrap().name(), "settings");
match *cli.command.unwrap() {
Commands::Settings(args) => {
assert!(!args.local);
assert!(args.target.server.is_none());
assert!(args.workflow.is_none());
}
_ => panic!("unexpected command variant"),
}
}
#[test]
fn parse_settings_with_workflow() {
let cli = Cli::try_parse_from(["fabro", "settings", "demo"]).expect("should parse");
match *cli.command.unwrap() {
Commands::Settings(args) => {
assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo")));
}
_ => panic!("unexpected command variant"),
}
fn parse_settings_rejects_workflow_argument() {
let result = Cli::try_parse_from(["fabro", "settings", "demo"]);
assert!(result.is_err(), "should reject settings workflow argument");
}
#[test]
fn parse_settings_local_mode() {
let cli =
Cli::try_parse_from(["fabro", "settings", "--local", "demo"]).expect("should parse");
match *cli.command.unwrap() {
Commands::Settings(args) => {
assert!(args.local);
assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo")));
}
_ => panic!("unexpected command variant"),
}
fn parse_settings_rejects_local_flag() {
let result = Cli::try_parse_from(["fabro", "settings", "--local"]);
assert!(result.is_err(), "should reject settings --local");
}
#[test]

View file

@ -5,7 +5,7 @@ use anyhow::Result;
pub(crate) use fabro_client::ServerTarget;
pub(crate) use fabro_config::user::*;
use fabro_types::settings::cli::CliTargetSettings;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_types::settings::{CliNamespace, SettingsLayer};
use fabro_util::version::FABRO_VERSION;
use tracing::debug;
@ -30,41 +30,9 @@ pub(crate) fn load_settings_with_config_and_storage_dir(
Ok(apply_storage_dir_override(layer, storage_dir))
}
fn render_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve cli settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result<CliSettings> {
fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors)
}
pub(crate) fn apply_storage_dir_override(
mut layer: SettingsLayer,
storage_dir: Option<&Path>,
) -> SettingsLayer {
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::server::{ServerLayer, ServerStorageLayer};
if let Some(dir) = storage_dir {
let server = layer.server.get_or_insert_with(ServerLayer::default);
let storage = server
.storage
.get_or_insert_with(ServerStorageLayer::default);
storage.root = Some(InterpString::parse(&dir.display().to_string()));
}
layer
}
/// Pull the resolved CLI target configuration out of `[cli.target]`.
/// Returns either an http(s) URL or a unix socket path.
fn cli_target_from_settings(settings: &CliSettings) -> Option<String> {
fn cli_target_from_settings(settings: &CliNamespace) -> Option<String> {
let target = settings.target.as_ref()?;
match target {
CliTargetSettings::Http { url } => Some(url.as_source()),
@ -73,8 +41,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option<String> {
}
fn configured_server_target(settings: &SettingsLayer) -> Result<Option<ServerTarget>> {
let cli_settings = resolve_cli_settings(settings)?;
let Some(value) = cli_target_from_settings(&cli_settings) else {
let user_settings = fabro_config::UserSettings::from_layer(settings)?;
let Some(value) = cli_target_from_settings(&user_settings.cli) else {
return Ok(None);
};
parse_server_target(&value).map(Some)

View file

@ -40,93 +40,10 @@ fn parse_settings(stdout: &[u8]) -> serde_json::Value {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML settings")
}
fn parse_settings_json(stdout: &[u8]) -> serde_json::Value {
serde_json::from_slice(stdout).expect("stdout should be valid JSON settings")
}
fn run_goal_inline(settings: &serde_json::Value) -> Option<&str> {
let goal = settings.get("run")?.get("goal")?;
(goal.get("type")?.as_str() == Some("inline"))
.then(|| goal.get("value")?.as_str())
.flatten()
}
fn run_model_name(settings: &serde_json::Value) -> Option<&str> {
settings.get("run")?.get("model")?.get("name")?.as_str()
}
fn run_model_provider(settings: &serde_json::Value) -> Option<&str> {
settings.get("run")?.get("model")?.get("provider")?.as_str()
}
fn run_inputs(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
settings
.get("run")
.and_then(|run| run.get("inputs"))
.and_then(serde_json::Value::as_object)
.expect("run.inputs")
}
fn run_sandbox(settings: &serde_json::Value) -> &serde_json::Value {
settings
.get("run")
.and_then(|run| run.get("sandbox"))
.expect("run.sandbox")
}
fn run_checkpoint(settings: &serde_json::Value) -> &serde_json::Value {
settings
.get("run")
.and_then(|run| run.get("checkpoint"))
.expect("run.checkpoint")
}
fn run_hooks(settings: &serde_json::Value) -> &[serde_json::Value] {
settings
.get("run")
.and_then(|run| run.get("hooks"))
.and_then(serde_json::Value::as_array)
.expect("run.hooks")
}
fn run_agent_mcps(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
settings
.get("run")
.and_then(|run| run.get("agent"))
.and_then(|agent| agent.get("mcps"))
.and_then(serde_json::Value::as_object)
.expect("run.agent.mcps")
}
fn auto_approve_enabled(settings: &serde_json::Value) -> bool {
settings
.get("run")
.and_then(|run| run.get("execution"))
.and_then(|execution| execution.get("approval"))
.and_then(serde_json::Value::as_str)
== Some("auto")
}
fn run_prepare_commands(settings: &serde_json::Value) -> Vec<String> {
settings
.get("run")
.and_then(|run| run.get("prepare"))
.and_then(|prepare| prepare.get("commands"))
.and_then(serde_json::Value::as_array)
.expect("run.prepare.commands")
.iter()
.map(|value| {
value
.as_str()
.expect("command should be a string")
.to_string()
})
.collect()
}
fn server_storage_root(settings: &serde_json::Value) -> &str {
settings
.get("server")
.and_then(|server| server.get("server"))
.and_then(|server| server.get("storage"))
.and_then(|storage| storage.get("root"))
.and_then(serde_json::Value::as_str)
@ -157,7 +74,7 @@ shared = "server"
}
fn resolved_server_settings_fixture() -> serde_json::Value {
let settings = fabro_config::resolve(&server_settings_layer_fixture())
let settings = fabro_config::ServerSettings::from_layer(&server_settings_layer_fixture())
.expect("server settings fixture should resolve");
serde_json::to_value(settings).expect("resolved settings payload should serialize")
}
@ -389,155 +306,6 @@ script = "workflow-setup"
// Tests
// ---------------------------------------------------------------------------
#[test]
fn settings_local_merges_cli_and_project_defaults() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let output = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
assert_eq!(cfg["run"]["sandbox"]["provider"].as_str(), Some("daytona"));
assert_eq!(run_model_name(&cfg), Some("project-model"));
assert_eq!(run_model_provider(&cfg), Some("openai"));
assert_eq!(run_goal_inline(&cfg), None);
// v2 R22: run.inputs replaces the inherited map wholesale rather than
// merging by key, so the project layer wipes out the CLI layer's inputs.
let vars = run_inputs(&cfg);
assert_eq!(
vars.get("project_only").and_then(serde_json::Value::as_str),
Some("1")
);
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("project")
);
assert!(
!vars.contains_key("cli_only"),
"run.inputs should replace across layers, not merge by key"
);
// v2 R71: provider-native maps such as run.sandbox.daytona.labels remain
// sticky merge-by-key, so CLI labels persist under the project layer.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox["daytona"]["labels"];
assert_eq!(labels["cli_only"].as_str(), Some("1"));
assert_eq!(labels["shared"].as_str(), Some("cli"));
}
#[test]
fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let output = context
.settings()
.current_dir(project.path())
.args(["--local", "demo"])
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert_eq!(run_goal_inline(&cfg), Some("demo goal"));
assert_eq!(run_model_name(&cfg), Some("run-model"));
assert_eq!(run_model_provider(&cfg), Some("anthropic"));
// v2 R22: run.inputs replaces wholesale, so the workflow layer wins
// over project and cli.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("run_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("run"));
// checkpoint.exclude_globs is a security/policy list: replace by default.
let checkpoint = run_checkpoint(&cfg);
assert_eq!(
checkpoint["exclude_globs"],
serde_json::json!(["run-only", "shared"])
);
// Hooks: id-based replacement. The "shared" hook appears in both cli and
// workflow layers and resolves to the workflow entry; project and run-only
// contribute the other two ids.
let hooks = run_hooks(&cfg);
assert!(hooks.len() >= 2);
let shared_hook = hooks
.iter()
.find(|hook| hook["name"].as_str() == Some("shared"))
.expect("shared hook");
assert_eq!(shared_hook["command"].as_str(), Some("echo run"));
assert!(
hooks
.iter()
.any(|hook| hook["name"].as_str() == Some("run-only"))
);
let mcps = run_agent_mcps(&cfg);
let shared = mcps.get("shared").expect("shared mcp");
assert_eq!(shared["transport"]["type"].as_str(), Some("stdio"));
assert_eq!(
shared["transport"]["command"],
serde_json::json!(["echo", "run"])
);
assert!(mcps.contains_key("run_only"));
// run.sandbox.daytona.labels stays sticky merge-by-key per R71.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox["daytona"]["labels"];
assert_eq!(labels["run_only"].as_str(), Some("1"));
assert_eq!(labels["shared"].as_str(), Some("run"));
// run.sandbox.env stays sticky merge-by-key per R71.
let env = &sandbox["env"];
assert_eq!(env["CLI_ONLY"].as_str(), Some("1"));
assert_eq!(env["RUN_ONLY"].as_str(), Some("1"));
assert_eq!(env["SHARED"].as_str(), Some("run"));
}
#[test]
fn settings_local_explicit_workflow_path_uses_workflow_project_layers() {
let mut context = test_context!();
let (project, _storage_dir) = setup_external_workflow_fixture(&mut context);
let cwd = tempfile::tempdir().unwrap();
let workflow = project.path().join("workflow.toml");
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
let output = context
.settings()
.env_remove("FABRO_STORAGE_DIR")
.current_dir(cwd.path())
.args(["--local", workflow.to_str().unwrap()])
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert!(auto_approve_enabled(&cfg));
// v2 R30: run.prepare.steps replaces the whole ordered list across layers.
// The highest-precedence layer (workflow) wins.
assert_eq!(run_prepare_commands(&cfg), vec![
"workflow-setup".to_string()
]);
assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true));
}
#[test]
fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
let mut context = test_context!();
@ -608,254 +376,6 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
);
}
#[test]
fn settings_fabro_path_matches_ambient_defaults() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let ambient = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let graph = context
.settings()
.current_dir(project.path())
.args(["--local", "standalone.fabro"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(parse_settings(&graph), parse_settings(&ambient));
}
#[test]
fn settings_missing_run_config_errors() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let mut cmd = context.settings();
cmd.current_dir(project.path());
cmd.args(["--local", "missing.toml"]);
let output = cmd.output().expect("command should execute");
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("workflow not found:"),
"stderr should report missing workflow path, got:\n{stderr}"
);
assert!(
stderr.contains("missing.toml"),
"stderr should include missing workflow filename, got:\n{stderr}"
);
}
#[test]
fn settings_legacy_cli_config_is_silently_ignored() {
let context = test_context!();
let project = tempfile::tempdir().unwrap();
context.write_home(
".fabro/cli.toml",
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "legacy-model"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success();
assert!(
assert.get_output().stderr.is_empty(),
"settings should not warn about legacy config files: {}",
String::from_utf8_lossy(&assert.get_output().stderr)
);
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
assert!(
cfg["run"]["model"].get("name").is_none(),
"resolved dense settings should omit an unset run.model.name"
);
}
#[test]
fn settings_legacy_user_config_is_silently_ignored() {
let context = test_context!();
let project = tempfile::tempdir().unwrap();
context.write_home(
".fabro/user.toml",
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "legacy-model"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success();
assert!(
assert.get_output().stderr.is_empty(),
"settings should not warn about legacy config files: {}",
String::from_utf8_lossy(&assert.get_output().stderr)
);
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
assert!(
cfg["run"]["model"].get("name").is_none(),
"resolved dense settings should omit an unset run.model.name"
);
}
#[test]
fn settings_legacy_server_config_is_silently_ignored() {
let context = test_context!();
let project = tempfile::tempdir().unwrap();
context.write_home(
".fabro/server.toml",
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "legacy-model"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success();
assert!(
assert.get_output().stderr.is_empty(),
"settings should not warn about legacy config files: {}",
String::from_utf8_lossy(&assert.get_output().stderr)
);
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
assert!(
cfg["run"]["model"].get("name").is_none(),
"resolved dense settings should omit an unset run.model.name"
);
}
#[test]
fn settings_user_config_wins_over_legacy_cli_config() {
let context = test_context!();
let project = setup_settings_fixture(&context);
context.write_home(
".fabro/cli.toml",
r#"
_version = 1
[run.model]
name = "legacy-model"
[run.inputs]
shared = "legacy"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success();
assert!(
assert.get_output().stderr.is_empty(),
"settings should not warn about legacy config files: {}",
String::from_utf8_lossy(&assert.get_output().stderr)
);
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(run_model_name(&cfg), Some("project-model"));
let vars = run_inputs(&cfg);
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("project")
);
}
#[test]
fn settings_uses_fabro_home_for_home_config_resolution() {
let context = test_context!();
let fabro_home = tempfile::tempdir().unwrap();
std::fs::write(
fabro_home.path().join("settings.toml"),
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "from-fabro-home"
"#,
)
.unwrap();
let output = context
.settings()
.args(["--local", "--json"])
.env("FABRO_HOME", fabro_home.path())
.env_remove("FABRO_STORAGE_DIR")
.output()
.expect("command should execute");
assert!(
output.status.success(),
"settings command failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let cfg = parse_settings_json(&output.stdout);
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose"));
assert_eq!(
cfg["run"]["model"]["name"].as_str(),
Some("from-fabro-home")
);
}
#[test]
fn settings_rejects_server_url_flag() {
let context = test_context!();
@ -883,32 +403,27 @@ fn settings_rejects_storage_dir_flag() {
}
#[test]
fn settings_rejects_local_and_server_combination() {
fn settings_rejects_local_flag() {
let context = test_context!();
context
.settings()
.args(["--local", "--server", "https://cli.example.com"])
.arg("--local")
.assert()
.failure()
.stderr(predicate::str::contains(
"the argument '--local' cannot be used with '--server <SERVER>'",
"unexpected argument '--local' found",
));
}
#[test]
fn settings_rejects_workflow_without_local() {
fn settings_rejects_workflow_argument() {
let context = test_context!();
let project = setup_settings_fixture(&context);
context
.settings()
.current_dir(project.path())
.arg("demo")
.assert()
.failure()
.stderr(predicate::str::contains(
"WORKFLOW requires --local; use `fabro settings --local WORKFLOW`",
));
.stderr(predicate::str::contains("unexpected argument 'demo' found"));
}
#[test]
@ -918,12 +433,9 @@ fn settings_fetches_server_resolved_settings() {
let server = MockServer::start();
let server_settings = resolved_server_settings_fixture();
let mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
when.method("GET").path("/api/v1/settings");
then.status(200)
.header("Content-Type", "application/json")
.header("X-Fabro-Settings-View", "resolved")
.body(server_settings_body(&server_settings));
});
context.write_home(
@ -962,30 +474,25 @@ shared = "cli"
mock.assert();
let cfg = parse_settings(&output);
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
assert_eq!(run_model_name(&cfg), Some("server-model"));
assert_eq!(run_model_provider(&cfg), Some("openai"));
assert_eq!(
cfg["user"]["cli"]["output"]["verbosity"].as_str(),
Some("verbose")
);
assert_eq!(
cfg["user"]["features"]["session_sandboxes"].as_bool(),
Some(false)
);
assert_eq!(
cfg["server"]["server"]["auth"]["methods"][0].as_str(),
Some("dev-token")
);
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
// Server-backed mode now returns the selected server's own dense resolved
// settings; local project/user overlays are not merged into the output.
let vars = run_inputs(&cfg);
assert_eq!(
vars.get("server_only").and_then(serde_json::Value::as_str),
Some("1")
);
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("server")
);
assert!(
!vars.contains_key("project_only"),
"server-backed settings output must not include local workflow/project overlays"
cfg["server"]["server"]["artifacts"]["store"]["type"].as_str(),
Some("local")
);
assert!(cfg.get("run").is_none());
assert!(cfg.get("project").is_none());
}
#[test]
@ -994,21 +501,16 @@ fn settings_cli_server_target_overrides_configured_server_target() {
let project = setup_settings_fixture(&context);
let configured_server = MockServer::start();
let configured_mock = configured_server.mock(|when, then| {
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
when.method("GET").path("/api/v1/settings");
then.status(500)
.body("configured-server-should-not-be-used");
});
let cli_server = MockServer::start();
let cli_server_settings = resolved_server_settings_fixture();
let cli_mock = cli_server.mock(|when, then| {
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
when.method("GET").path("/api/v1/settings");
then.status(200)
.header("Content-Type", "application/json")
.header("X-Fabro-Settings-View", "resolved")
.body(server_settings_body(&cli_server_settings));
});
context.write_home(
@ -1044,46 +546,6 @@ verbosity = "verbose"
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
}
#[test]
fn settings_errors_when_server_lacks_resolved_view_marker() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let server = MockServer::start();
let server_settings = resolved_server_settings_fixture();
let mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
then.status(200)
.header("Content-Type", "application/json")
.body(server_settings_body(&server_settings));
});
context.write_home(
".fabro/settings.toml",
format!(
r#"
_version = 1
[cli.target]
type = "http"
url = "{}/api/v1"
"#,
server.base_url()
),
);
context
.settings()
.current_dir(project.path())
.assert()
.failure()
.stderr(predicate::str::contains(
"server does not support resolved settings view; upgrade the server or use --local",
));
mock.assert();
}
#[test]
fn settings_unreachable_http_target_fails_clearly() {
let context = test_context!();

View file

@ -8,7 +8,7 @@ use crate::support::{fabro_json_snapshot, unique_run_id};
fn resolved_run(
settings: &fabro_types::settings::SettingsLayer,
) -> fabro_types::settings::RunSettings {
) -> fabro_types::settings::RunNamespace {
fabro_config::resolve_run_from_file(settings).expect("run settings should resolve")
}

View file

@ -16,14 +16,14 @@ use crate::support::{LightweightCli, unique_run_id};
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["store", "dump", "--help"]);
cmd.args(["dump", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Export a run's durable state to a directory
Usage: fabro store dump [OPTIONS] --output <OUTPUT> <RUN>
Usage: fabro dump [OPTIONS] --output <OUTPUT> <RUN>
Arguments:
<RUN> Run ID prefix or workflow name
@ -42,7 +42,7 @@ fn help() {
}
#[test]
fn store_dump_accepts_server_target_from_separate_home() {
fn dump_accepts_server_target_from_separate_home() {
let context = test_context!();
let run = setup_completed_dry_run(&context);
let cli = LightweightCli::new();
@ -51,7 +51,6 @@ fn store_dump_accepts_server_target_from_separate_home() {
let mut cmd = cli.command();
cmd.args([
"store",
"dump",
"--server",
&server,
@ -63,10 +62,10 @@ fn store_dump_accepts_server_target_from_separate_home() {
cmd.env("FABRO_DEV_TOKEN", dev_token);
}
let output = cmd.output().expect("store dump should execute");
let output = cmd.output().expect("dump should execute");
assert!(
output.status.success(),
"store dump via remote server target failed\nstdout:\n{}\nstderr:\n{}",
"dump via remote server target failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
@ -74,7 +73,7 @@ fn store_dump_accepts_server_target_from_separate_home() {
}
#[test]
fn store_dump_exports_large_command_output_backed_by_blob_refs() {
fn dump_exports_large_command_output_backed_by_blob_refs() {
let context = test_context!();
let workflow = context.temp_dir.join("large-output.fabro");
fs::write(
@ -130,17 +129,11 @@ fn store_dump_exports_large_command_output_backed_by_blob_refs() {
let output_dir = context.temp_dir.join("export");
let mut dump_cmd = context.command();
dump_cmd.args([
"store",
"dump",
"--output",
output_dir.to_str().unwrap(),
&run_id,
]);
let dump_output = dump_cmd.output().expect("store dump should execute");
dump_cmd.args(["dump", "--output", output_dir.to_str().unwrap(), &run_id]);
let dump_output = dump_cmd.output().expect("dump should execute");
assert!(
dump_output.status.success(),
"store dump failed\nstdout:\n{}\nstderr:\n{}",
"dump failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&dump_output.stdout),
String::from_utf8_lossy(&dump_output.stderr)
);
@ -153,7 +146,7 @@ fn store_dump_exports_large_command_output_backed_by_blob_refs() {
}
#[test]
fn store_dump_exports_blob_refs_and_artifacts_together() {
fn dump_exports_blob_refs_and_artifacts_together() {
let context = test_context!();
let workspace_dir = context.temp_dir.join("mixed-export");
fs::create_dir_all(&workspace_dir).unwrap();
@ -233,17 +226,11 @@ include = ["assets/**"]
let output_dir = context.temp_dir.join("export-mixed");
let mut dump_cmd = context.command();
dump_cmd.args([
"store",
"dump",
"--output",
output_dir.to_str().unwrap(),
&run_id,
]);
let dump_output = dump_cmd.output().expect("store dump should execute");
dump_cmd.args(["dump", "--output", output_dir.to_str().unwrap(), &run_id]);
let dump_output = dump_cmd.output().expect("dump should execute");
assert!(
dump_output.status.success(),
"store dump failed\nstdout:\n{}\nstderr:\n{}",
"dump failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&dump_output.stdout),
String::from_utf8_lossy(&dump_output.stderr)
);
@ -260,14 +247,13 @@ include = ["assets/**"]
}
#[test]
fn store_dump_exports_completed_run_snapshot() {
fn dump_exports_completed_run_snapshot() {
let context = test_context!();
let run = setup_completed_dry_run(&context);
let output_dir = context.temp_dir.join("export");
let mut cmd = context.command();
cmd.args([
"store",
"dump",
"--output",
output_dir.to_str().unwrap(),
@ -298,7 +284,7 @@ fn store_dump_exports_completed_run_snapshot() {
}
#[test]
fn store_dump_rejects_non_empty_output_dir() {
fn dump_rejects_non_empty_output_dir() {
let context = test_context!();
let run = setup_completed_dry_run(&context);
let output_dir = context.temp_dir.join("nonempty");
@ -307,7 +293,6 @@ fn store_dump_rejects_non_empty_output_dir() {
let mut cmd = context.command();
cmd.args([
"store",
"dump",
"--output",
output_dir.to_str().unwrap(),

View file

@ -25,7 +25,7 @@ fn help() {
validate Validate a workflow
graph Render a workflow graph as SVG
artifact Inspect and copy run artifacts (screenshots, reports, traces)
store Export store-backed run state for debugging
dump Export a run's durable state to a directory
rm Remove one or more workflow runs
inspect Show detailed information about a workflow run
archive Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings

View file

@ -9,6 +9,7 @@ mod diff;
mod discord;
mod docs;
mod doctor;
mod dump;
mod exec;
mod fabro;
mod fork;
@ -53,8 +54,6 @@ mod server_start;
mod server_status;
mod server_stop;
mod start;
mod store;
mod store_dump;
pub(crate) mod support;
mod system;
mod system_df;

View file

@ -1,29 +0,0 @@
use fabro_test::{fabro_snapshot, test_context};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["store", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Export store-backed run state for debugging
Usage: fabro store [OPTIONS] <COMMAND>
Commands:
dump Export a run's durable state to a directory
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}

Some files were not shown because too many files have changed in this diff Show more