Merge remote-tracking branch 'origin/main'

# Conflicts:
#	lib/crates/fabro-cli/src/command_context.rs
#	lib/crates/fabro-cli/src/main.rs
This commit is contained in:
Bryan Helmkamp 2026-04-23 08:56:40 -04:00
commit e210fe9fed
No known key found for this signature in database
68 changed files with 1680 additions and 939 deletions

View file

@ -0,0 +1,835 @@
---
title: "refactor: CommandContext alignment at CLI command boundaries"
type: refactor
status: completed
date: 2026-04-23
deepened: 2026-04-23
---
# CommandContext Alignment At CLI Command Boundaries
## Overview
Finish the partially landed `CommandContext` refactor in `fabro-cli` by
making `CommandContext` the shared command-boundary abstraction for
invocation plumbing in the CLI command families that already depend on
it or immediately reconstruct it. The goal is to stop threading
`&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`
through command entrypoints when that data is already part of the same
invocation context.
This is a follow-on plan to the earlier server-access refactor. The
current codebase already centralizes `cwd`, merged settings, and server
access in `CommandContext`, but command entrypoints still receive raw
plumbing and then rebuild a context locally. This plan aligns the
surface area around the existing struct instead of introducing a second
context or service wrapper.
## Problem Frame
`CommandContext` exists today in
`lib/crates/fabro-cli/src/command_context.rs`, and many commands
already depend on it for `cwd`, `machine_settings`, `user_settings`, and
`server()` access. The refactor is only half-finished:
- the `printer` field and `printer()` accessor are both marked
`#[allow(dead_code, reason = "...still being wired through")]`
- `main.rs` still passes raw plumbing into most command families
- many command entrypoints still accept some combination of
`&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`
- those same commands often construct `CommandContext` immediately
inside the handler
That leaves the CLI with two overlapping models:
- `CommandContext` as the intended abstraction for shared invocation
state
- raw parameter threading as the de facto command API
The result is more churn on command signatures, more repeated output
format and JSON-gating branches, and a dead-code-marked `printer` field
that signals the abstraction boundary is unfinished.
The refactor needs to finish in a way that preserves current CLI
behavior and avoids turning `CommandContext` into a new god object.
## Requirements Trace
- **R1.** In-scope CLI command entrypoints use `CommandContext` as the
shared invocation-plumbing abstraction instead of separately receiving
`&CliNamespace`, `&CliLayer`, `Printer`, and `process_local_json`.
- **R2.** `CommandContext` remains narrow: it carries invocation-scoped
plumbing and shared derived state, not command-specific args, styles,
or workflow-specific data.
- **R3.** JSON output, verbosity behavior, printer routing, and global
`--json` restrictions remain behaviorally unchanged for existing
commands, including `auth status` remaining explicit-global-`--json`
only rather than switching to persisted `cli.output.format = "json"`.
- **R4.** Target-based and storage-dir-based server resolution semantics
remain unchanged, including `CommandContext::server()` behavior and
`ServerSummaryLookup::from_client(...)` usage.
- **R5.** `main.rs` keeps its current pre-tracing bootstrap ordering for
logging and upgrade checks; `CommandContext` begins after that phase.
- **R6.** The current dead-code allowance on the `printer` field/accessor
is removed by making printer access a real part of the abstraction, or
by deleting redundant surface area if a narrower accessor is better.
- **R7.** Representative unit and integration coverage exists for the
context API, JSON/text output behavior, global `--json` gating, and
target/connection-based command families touched by the refactor.
## Scope Boundaries
- **In scope:** command families in `fabro-cli` that already use
`CommandContext` directly or immediately construct one from raw
plumbing:
`run`, `runs`, `artifact`, `store dump`, `preflight`, `validate`,
`graph`, `model`, `secret`, `pr`, `repo`, `auth`, `provider`,
`config`, `version`, `doctor`, `system`, and the public
`sandbox preview` / `sandbox ssh` command boundary that currently
forwards into `commands/run/*` leaf modules.
- **In scope:** top-level dispatch cleanup in
`lib/crates/fabro-cli/src/main.rs` and family dispatch modules where
the only reason raw CLI plumbing is still passed down is command
signature inertia.
- **Out of scope:** `exec`, `server`, `install`, `upgrade`, `workflow`,
`parse`, `render_graph`, hidden analytics/panic upload commands,
hidden `run worker`, and `sandbox cp`, except for thin compile-only
adapters if needed.
- **Out of scope:** changing CLI text, response payloads, exit-code
semantics, or server connection behavior beyond what is required to
move the plumbing boundary.
- **Out of scope:** moving `Styles` ownership into `CommandContext` or
making low-level pure rendering helpers context-aware when explicit
`Printer` or `Styles` parameters remain clearer.
- **Out of scope:** redesigning `server_client.rs` connection semantics.
This plan consumes the current `CommandContext::server()` model rather
than reopening the earlier server-access design.
## Context & Research
### Relevant Code and Patterns
- `lib/crates/fabro-cli/src/command_context.rs` — current
`CommandContext`, constructors, `user_settings()` / `machine_settings()`
accessors, and dead-code-marked printer storage.
- `lib/crates/fabro-cli/src/main.rs` — top-level command dispatch still
passing raw plumbing into most families after bootstrap.
- `lib/crates/fabro-cli/src/server_client.rs` — current target and
storage-dir connection behavior that must stay unchanged.
- `lib/crates/fabro-cli/src/commands/run/create.rs` — existing example
of a helper that already takes `&CommandContext`.
- `lib/crates/fabro-cli/src/commands/run/mod.rs` — current mixed model:
some subcommands build a `CommandContext`, some still read raw
`cli.output.format`, and `process_local_json` is already vestigial at
this boundary.
- `lib/crates/fabro-cli/src/commands/secret/mod.rs` — good family-level
pattern where a single derived server/client can be shared across
subcommands.
- `lib/crates/fabro-cli/src/commands/pr/mod.rs` — representative mixed
local/server family using both `CommandContext::base(...)` and
`CommandContext::for_target(...)`.
- `lib/crates/fabro-cli/src/commands/auth/mod.rs` and
`lib/crates/fabro-cli/src/commands/provider/mod.rs` — the clearest
examples of raw `process_local_json` still being threaded despite the
rest of the state already belonging to the invocation.
- `files-internal/testing-strategy.md` — CLI integration tests should
stay command-driven and black-box, with implementation-facing behavior
covered by unit tests near the code.
- `lib/crates/fabro-cli/src/commands/sandbox/mod.rs` — the real public
boundary for preview/ssh command dispatch; leaf implementation lives in
`commands/run/preview.rs` and `commands/run/ssh.rs`, but the command
family boundary is `sandbox`.
- `lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs` and
`lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs` — existing
command-owned coverage for those public entrypoints.
### Current-State Inventory
- `CommandContext::{base, for_target, for_connection}` currently has
**42** call sites across **37** command files under
`lib/crates/fabro-cli/src/commands`.
- The command tree contains **one** existing helper that already accepts
`&CommandContext` directly:
`lib/crates/fabro-cli/src/commands/run/create.rs`.
- The `printer()` accessor currently has no call sites, which is why the
dead-code allowance still exists.
- For in-scope command files, the dominant remaining use of raw
`&CliNamespace` is reading `cli.output.format` or
`cli.output.verbosity`; that is a signal that the data belongs on the
shared invocation context rather than each command signature.
- `process_local_json` is now concentrated in `auth`, `provider`,
`graph`, `sandbox preview`, and `sandbox ssh`. That makes it a good
candidate for a dedicated invocation-context field/helper instead of
continued parameter threading.
### Related Context
- `docs/plans/2026-04-08-cli-services-command-context-refactor-plan.md`
— earlier plan that introduced the current `CommandContext` and server
access model. This plan finishes the command-boundary alignment that
document did not fully land.
- `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
— recent owner-first context plan that reinforces the repo preference
for dense, owner-scoped context objects instead of repeated free-form
plumbing.
- `git log -- lib/crates/fabro-cli/src/command_context.rs` shows recent
follow-on commits including `simplify: drop duplicate settings plumbing
from cli/server refactor`, which is consistent with the current goal of
collapsing overlapping command-boundary APIs.
### Institutional Learnings
- No relevant `docs/solutions/` entries currently cover this seam.
### External References
- No external research used. The repo already has sufficient local
context, existing partial implementation, and tests for this refactor.
## Key Technical Decisions
- **Use `CommandContext` as the command-boundary API for in-scope
commands.**
The abstraction already owns the hard parts: working directory,
merged settings, and server access. The remaining refactor should move
entrypoint APIs onto that abstraction instead of continuing to thread
raw plumbing beside it.
- **Keep `CommandContext` narrow and invocation-scoped, not god-shaped.**
It should own:
`printer`, merged CLI-derived settings (`machine_settings`,
`user_settings`), `cwd`, config-path context, server-derivation state,
and the invocation-only global `--json` flag.
It should not own command args, `Styles`, render-only helpers, or
workflow/build-specific state.
- **Do not store or expose the full `CliNamespace` publicly.**
Commands in scope only need a small subset of CLI plumbing:
output format, output verbosity, and global `--json` restrictions.
Output format and verbosity should come from
`ctx.user_settings().cli.output`, which already reflects CLI override
precedence through merged settings. The only truly extra invocation
field is the global `--json` switch, which is not part of persisted
settings and therefore belongs on the context explicitly.
- **Preserve the current error-timing split.**
`CommandContext` construction should keep doing what it does today:
capture cwd, load local settings, and build merged invocation state.
Server-target resolution and server-connection failures should remain
deferred to `ctx.server().await?` or existing explicit
`resolve_server_target(...)` calls. The refactor should not make
malformed target/server resolution errors eager at context-construction
time.
- **Keep `auth status` as an explicit-global-JSON command.**
Most in-scope commands should read output mode from
`ctx.user_settings().cli.output`, but `auth status` is a special case:
it currently emits JSON only for the explicit invocation-wide global
`--json` path, not merely because resolved CLI settings say
`output.format = json`. That distinction should remain intact, so the
context needs both resolved output settings and a separate helper for
the explicit global JSON flag.
- **Create a base invocation context once, then derive target/connection
variants from it.**
`main.rs` should keep using raw settings during pre-tracing bootstrap.
After that, it should build a base `CommandContext` once for each
in-scope dispatch path. Command families then derive
target-based or connection-based contexts from that base without
re-supplying `Printer` and `CliLayer`.
- **Allow private derivation state inside `CommandContext` if that is the
simplest way to avoid raw parameter threading.**
Storing a private `CliLayer` or equivalent internal builder state is
acceptable if it enables methods like `with_target(...)` or
`with_connection(...)` and keeps the raw plumbing hidden behind the
abstraction boundary.
- **Keep render helpers explicit.**
Command entrypoints and family dispatchers should align on
`CommandContext`, but low-level helpers such as table rendering,
summary formatting, and browser-opening routines may keep explicit
`Printer` / `Styles` / boolean parameters where that stays simpler than
threading the full context downward.
- **Keep workflow/manifest layer assembly command-local.**
Commands such as `run`, `preflight`, `validate`, and `graph` should
continue to build their workflow/project/manifests with the existing
command-owned helpers. `CommandContext` can provide `cwd`, merged user
settings for output behavior, and server access, but it should not
absorb manifest-building policy or workflow-layer composition.
- **Migrate family-by-family with temporary compatibility wrappers if
needed.**
This is a cross-cutting refactor with wide signature churn. It is
better to allow short-lived constructor/adapter overlap during the
migration than to force a single giant all-or-nothing patch that is
harder to validate.
## Open Questions
### Resolved During Planning
- **Should this refactor introduce a second wrapper type such as
`Services` or `InvocationContext`?**
No. The repo already has a partially landed `CommandContext`, and the
simplest aligned design is to finish that abstraction rather than
splitting responsibilities across two overlapping context types.
- **Should `CommandContext` absorb the entire `CliNamespace`?**
No. Public command consumers should read output mode and verbosity from
`ctx.user_settings().cli.output`, while command-specific configuration
stays local to the commands that own it.
- **Should `process_local_json` become part of `CommandContext`?**
Yes. It is invocation-scoped, currently leaks across multiple command
signatures, and is the one remaining piece of global command plumbing
that is not already represented by merged settings.
- **Should preview/ssh be treated as `run` work or `sandbox` work in
this plan?**
Treat them as `sandbox` work at the public command boundary. The leaf
implementation modules remain under `commands/run/*`, but the raw
plumbing boundary in the current CLI is `commands/sandbox/mod.rs` and
the plan should align to that boundary and its tests.
- **Should `auth status` remain “explicit global JSON only”?**
Yes. R3 for this plan is behavioral preservation, and the current
contract is that `auth status` emits JSON only when the explicit
invocation-global `--json` switch is active.
- **Should `Styles` move into `CommandContext` in the same pass?**
No. That would enlarge the abstraction without addressing the actual
duplicated plumbing problem.
- **Should out-of-scope local commands be forced onto `CommandContext`
just for uniformity?**
No. This pass should target the families where `CommandContext` already
provides real value or is already partially adopted.
### Deferred to Implementation
- **Exact API names for derived contexts.**
The implementation may settle on `with_target(...)`,
`for_target_from(...)`, or similar naming. The important contract is
that callers no longer pass raw `Printer` and `CliLayer` repeatedly.
- **Whether static constructors remain temporarily during migration.**
If temporary wrappers reduce compile churn while family-by-family
patches land, they are acceptable. Final cleanup should remove the
now-redundant raw-plumbing entrypoints from in-scope call sites.
- **Whether `CommandContext` should be cheaply cloneable or should build
derived variants from private state on demand.**
Either is acceptable if it preserves the abstraction boundary and does
not change runtime behavior.
## High-Level Technical Design
> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*
| Boundary | Current shape | Target shape |
|---|---|---|
| `main.rs` -> command family | `dispatch(args, &cli_settings, &cli_layer, process_local_json, printer)` | `dispatch(args, &base_ctx)` |
| family dispatch -> leaf command | raw CLI plumbing plus `CommandContext::for_target(...)` inside the leaf | derive `target_ctx` / `connection_ctx` once from `base_ctx`, then pass `&CommandContext` or already-resolved client/output helpers |
| output mode lookup | `cli.output.format` / `cli.output.verbosity` | `ctx.user_settings().cli.output.*` |
| global `--json` guard | separate `process_local_json` parameter | `ctx` accessor/helper |
| printing | raw `printer` parameter | `ctx.printer()` or a narrower printer extracted from `ctx` |
Directional flow:
```text
bootstrap raw globals/settings for tracing + upgrade check
-> build base CommandContext once for the in-scope command dispatch
-> family dispatch receives &base_ctx
-> family derives:
target_ctx from target args
connection_ctx from storage-dir-aware args
base/local ctx for settings-only commands
-> leaf command reads:
ctx.cwd()
ctx.machine_settings()
ctx.user_settings().cli.output.*
ctx.require_no_json_override() or equivalent
ctx.printer()
ctx.server().await?
-> render-only helpers stay explicit over Printer / Styles where simpler
```
## Implementation Units
- [x] **Unit 1: Reframe `CommandContext` as the invocation-boundary object**
**Goal:** Make `CommandContext` capable of carrying the invocation
plumbing that is still leaking through command signatures, while keeping
the type narrowly scoped.
**Requirements:** R1, R2, R3, R4, R6
**Dependencies:** None
**Files:**
- Modify: `lib/crates/fabro-cli/src/command_context.rs`
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Test: `lib/crates/fabro-cli/src/command_context.rs`
**Approach:**
- Add the remaining invocation-only plumbing that does not already exist
on the context, specifically the global `--json` switch used today via
`process_local_json`.
- Make printer access part of the live API so the dead-code allowance on
the `printer` field/accessor can be removed.
- Add context-derivation methods that let callers obtain target-based or
connection-based variants from a base invocation context without
re-supplying raw `Printer` and `CliLayer`.
- Keep output format and verbosity sourced from
`ctx.user_settings().cli.output` rather than storing a second public
output-format copy on the side, while still exposing the explicit
invocation-global JSON flag separately for commands like `auth status`
whose contract is not identical to resolved output format.
**Patterns to follow:**
- `lib/crates/fabro-cli/src/command_context.rs`
- `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
for owner-first context boundaries
**Test scenarios:**
- Happy path: a base context exposes the same output format and verbosity
that commands currently read from merged CLI settings.
- Happy path: a base context preserves both resolved output settings and
the explicit invocation-global JSON flag so commands can distinguish
between “resolved output format is JSON” and “user passed global
`--json`”.
- Happy path: deriving a target-based context preserves printer/global
JSON state and resolves server access through the existing
`server_client::connect_server_with_settings(...)` path.
- Edge case: deriving a connection-based context with a storage-dir
override changes only the storage-backed settings path and preserves
other invocation-scoped data.
- Error path: settings-load failures tied to context construction still
surface during context construction, while malformed target resolution
remains deferred until `ctx.server().await?` or explicit
`resolve_server_target(...)` calls.
**Verification:**
- The `printer` field/accessor is no longer dead code.
- There is one clear way to obtain a base context and derive
target/connection variants without raw plumbing at the call site.
- The context API makes the distinction between resolved output format
and explicit global JSON invocation state unambiguous.
- [x] **Unit 2: Move `main.rs` and the run/preflight/graph/sandbox boundary to context-first dispatch**
**Goal:** Eliminate raw plumbing from the top-level dispatch path and the
run-oriented command family plus the public `sandbox preview` /
`sandbox ssh` boundary that already rely heavily on `CommandContext`.
**Requirements:** R1, R3, R4, R5, R6, R7
**Dependencies:** Unit 1
**Files:**
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/command.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/create.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/preview.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/ssh.rs`
- Modify: `lib/crates/fabro-cli/src/commands/sandbox/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/resume.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/rewind.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/fork.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/wait.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/diff.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/logs.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/cp.rs`
- Modify: `lib/crates/fabro-cli/src/commands/preflight.rs`
- Modify: `lib/crates/fabro-cli/src/commands/validate.rs`
- Modify: `lib/crates/fabro-cli/src/commands/graph.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/run.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/create.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/start.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/diff.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/logs.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/preflight.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/validate.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/graph.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`
- Test: `lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs`
- Test: `lib/crates/fabro-cli/tests/it/scenario/recovery.rs`
**Approach:**
- Build a base `CommandContext` in `main.rs` only after the existing
bootstrap phase completes.
- Change the run-family and run-adjacent entrypoints to accept context
instead of raw `cli` / `cli_layer` / `printer` bundles.
- Replace direct reads of `cli.output.format` and
`cli.output.verbosity` with `ctx.user_settings().cli.output.*`.
- Replace direct `process_local_json` threading with a context helper
for the small number of commands that still need it (`graph`,
`commands/sandbox/mod.rs` for the public preview/ssh boundary).
- Move the public `sandbox preview` / `sandbox ssh` dispatch boundary to
the same context-first shape as the run-family boundary, while leaving
the leaf implementation modules in `commands/run/*` if that remains
the cleanest internal organization.
- Keep `Styles` local to the leaf commands and keep `attach` / `start`
client helpers narrow if broadening them adds no value.
**Execution note:** Start by preserving or expanding characterization
coverage for JSON/text output and global `--json` gating before removing
the old raw-plumbing signatures from these entrypoints.
**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/run/create.rs`
- `lib/crates/fabro-cli/src/commands/preflight.rs`
- `lib/crates/fabro-cli/src/commands/graph.rs`
- `lib/crates/fabro-cli/src/commands/sandbox/mod.rs`
**Test scenarios:**
- Happy path: `fabro run --detach` still prints a bare run ID in text
mode and the same JSON payload in JSON mode after output format moves
behind `CommandContext`.
- Happy path: `fabro run` / `resume` / `attach` still inherit verbose
rendering from CLI output verbosity and preserve idle-sleep behavior.
- Edge case: `fabro graph --json` without an explicit output file still
rejects the global JSON override exactly as it does today.
- Edge case: `fabro sandbox preview --open` still opens the browser only
when global JSON mode is not active.
- Edge case: `fabro sandbox ssh` still allows `--print` under global
JSON mode but continues to reject the unsupported interactive
combination.
- Error path: `preflight` and `validate` continue to fail on validation
errors with the same text-vs-JSON contract and exit behavior.
- Integration: create -> start -> attach and resume/recovery flows still
resolve targets, stream output, and summarize results through the same
black-box CLI contracts.
**Verification:**
- `main.rs` no longer passes the raw plumbing bundle into the run /
preflight / validate / graph / sandbox preview-ssh boundary.
- Those command boundaries obtain shared invocation data exclusively via
`CommandContext`.
- [x] **Unit 3: Migrate runs/artifact/store families to context-first family dispatch**
**Goal:** Remove repeated target-context reconstruction from the command
families that already build a target-based `CommandContext` immediately
and mostly use raw CLI state only for output mode.
**Requirements:** R1, R3, R4, R6, R7
**Dependencies:** Units 1-2
**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/runs/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/archive.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/rm.rs`
- Modify: `lib/crates/fabro-cli/src/commands/runs/inspect.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/artifact/cp.rs`
- Modify: `lib/crates/fabro-cli/src/commands/store/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/store/dump.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/archive.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/inspect.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/rm.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/store_dump.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/store.rs`
**Approach:**
- Have each family receive a context-first boundary and derive its
target-based command context once, close to the namespace/selector
boundary.
- Switch output-mode branches to
`ctx.user_settings().cli.output.format`.
- Preserve existing client-sharing patterns such as
`ServerSummaryLookup::from_client(ctx.server().await?)`.
- Keep leaf helpers narrow where they already only need a resolved
client, resolved run ID, or printer.
**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- `lib/crates/fabro-cli/src/commands/artifact/mod.rs`
**Test scenarios:**
- Happy path: `runs list`, `archive`, `unarchive`, and `rm` still render
the same JSON and text shapes after output-mode decisions move behind
the context.
- Happy path: `runs inspect` remains a JSON-only projection of server
state and still resolves the selected run before fetching the state.
- Edge case: “no runs found” and “no artifacts found” text-mode messages
remain unchanged.
- Error path: ambiguous or missing run selectors still fail through the
existing server/client resolution path.
- Integration: artifact listing/copy and store dump continue to hit the
same server-backed data path and respect output-format selection.
**Verification:**
- These family dispatchers no longer need separate `cli_layer` and
`printer` arguments merely to reconstruct a target context.
- [x] **Unit 4: Migrate PR, secret, and system command families**
**Goal:** Align the families that mix local settings, target-based
server access, and storage-dir-aware server access onto the same
context-first boundary.
**Requirements:** R1, R3, R4, R6, R7
**Dependencies:** Units 1-3
**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/pr/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/create.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/view.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/merge.rs`
- Modify: `lib/crates/fabro-cli/src/commands/pr/close.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/list.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/set.rs`
- Modify: `lib/crates/fabro-cli/src/commands/secret/rm.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/info.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/df.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/events.rs`
- Modify: `lib/crates/fabro-cli/src/commands/system/prune.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_create.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_view.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_merge.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/pr_close.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_set.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/secret_rm.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_info.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_df.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_events.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/system_prune.rs`
**Approach:**
- Keep `pr`’s split between base-settings work and target-based run
lookup, but move both sides onto a common context-first boundary so
raw `cli_layer` / `printer` threading disappears from the family API.
- Preserve `secret`’s existing pattern of resolving the server once at
the family boundary and passing the client into subcommands.
- For `system`, derive connection-aware contexts from the base
invocation context so storage-dir override behavior remains explicit
and unchanged.
**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/pr/mod.rs`
- `lib/crates/fabro-cli/src/commands/secret/mod.rs`
- `lib/crates/fabro-cli/src/commands/system/mod.rs`
**Test scenarios:**
- Happy path: PR list/view/create/merge/close continue to render the same
JSON/text contracts and still resolve GitHub credentials from merged
local settings.
- Happy path: secret list/set/rm continue to resolve one server client at
the namespace boundary and honor JSON mode.
- Happy path: system info/df/events/prune continue to use
storage-dir-aware connection mode where appropriate.
- Edge case: missing secret / no matching runs-to-prune / empty PR list
still produce the same user-visible text-mode outcomes.
- Error path: invalid storage-dir or connection resolution still fails
before the command attempts remote work.
- Integration: the `system` family continues to respect explicit
`--storage-dir` overrides and local daemon resolution semantics.
**Verification:**
- These family boundaries no longer accept raw plumbing bundles when the
only reason was to build a `CommandContext` or inspect output mode.
- [x] **Unit 5: Finish remaining base/target context users**
**Goal:** Complete the context-first migration for the remaining in-scope
command surfaces that already use `CommandContext` but still expose raw
plumbing at their entrypoints.
**Requirements:** R1, R2, R3, R5, R6, R7
**Dependencies:** Units 1-4
**Files:**
- Modify: `lib/crates/fabro-cli/src/commands/model.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/init.rs`
- Modify: `lib/crates/fabro-cli/src/commands/repo/deinit.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/login.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/logout.rs`
- Modify: `lib/crates/fabro-cli/src/commands/auth/status.rs`
- Modify: `lib/crates/fabro-cli/src/commands/provider/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/provider/login.rs`
- Modify: `lib/crates/fabro-cli/src/commands/config/mod.rs`
- Modify: `lib/crates/fabro-cli/src/commands/version.rs`
- Modify: `lib/crates/fabro-cli/src/commands/doctor.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model_list.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/model_test.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo_init.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/repo_deinit.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/auth.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/provider.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/provider_login.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/config.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/version.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/doctor.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`
**Approach:**
- Move base-context-only command families (`auth`, `provider`, parts of
`repo`) onto `CommandContext` so JSON gating and printer usage come
from the shared invocation object.
- Use `ctx.machine_settings()` / `ctx.user_settings()` consistently for
local settings lookups rather than parallel raw-CLI arguments.
- Preserve the special-case JSON contract for `auth status`: explicit
invocation-global `--json` remains the only JSON trigger, even if
resolved CLI settings say `output.format = json`.
- Keep `repo deinit` and similar pure-local leaf helpers narrow if a
derived `json_output` boolean or `Printer` extracted from the context
keeps the internal helper clearer than passing the full context.
**Patterns to follow:**
- `lib/crates/fabro-cli/src/commands/auth/login.rs`
- `lib/crates/fabro-cli/src/commands/auth/status.rs`
- `lib/crates/fabro-cli/src/commands/config/mod.rs`
- `lib/crates/fabro-cli/src/commands/version.rs`
**Test scenarios:**
- Happy path: `auth status` and `provider login` preserve global `--json`
restrictions and still resolve the intended server target from merged
settings.
- Happy path: `config`, `version`, `model`, and `doctor` continue to
honor text-vs-JSON output without a direct `CliNamespace` parameter.
- Edge case: persisted `cli.output.format = "json"` without explicit
global `--json` still leaves `auth status` on its current text-mode
path.
- Edge case: explicit global `--json` still forces `auth status` onto
its JSON output path even though the implementation no longer receives
a raw `process_local_json` parameter.
- Edge case: `repo init` non-JSON progress output and JSON result
payloads stay unchanged.
- Edge case: `repo deinit` still stays local and does not accidentally
require server access just because the family boundary now uses
`CommandContext`.
- Error path: missing auth sessions, invalid server targets, and missing
GitHub access continue to fail with the same user-facing contract.
**Verification:**
- Remaining in-scope command entrypoints no longer expose the raw
plumbing bundle as part of their public internal API.
- [x] **Unit 6: Remove migration scaffolding and prove the boundary is clean**
**Goal:** Delete transitional surface area and confirm the in-scope
command tree is consistently aligned on `CommandContext`.
**Requirements:** R1, R2, R6, R7
**Dependencies:** Units 1-5
**Files:**
- Modify: `lib/crates/fabro-cli/src/command_context.rs`
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Modify: in-scope command modules touched by transitional wrappers
- Test: `lib/crates/fabro-cli/tests/it/cmd/top_level.rs`
- Test: `lib/crates/fabro-cli/tests/it/cmd/json_global.rs`
**Approach:**
- Remove temporary wrappers or compatibility constructors that were only
present to get through the migration.
- Delete stale comments and “still being wired through” dead-code
annotations once the boundary is real.
- Do a final sweep to ensure in-scope command boundaries are not still
taking `&CliNamespace`, `&CliLayer`, `Printer`, and
`process_local_json` together out of habit.
**Patterns to follow:**
- Keep cleanup limited to true scaffolding removal; do not reopen command
semantics or formatting behavior in the cleanup pass.
**Test scenarios:**
- Test expectation: none -- cleanup-only unit. Behavioral coverage should
already exist from Units 1-5.
**Verification:**
- The command-boundary API is visibly simpler and consistent across the
in-scope families.
- No in-scope command still looks half-migrated.
## System-Wide Impact
- **Interaction graph:** `main.rs` bootstrap -> base `CommandContext` ->
family dispatchers -> derived target/connection contexts ->
`server_client.rs` / `user_config.rs`. This touches nearly every
user-facing CLI family that already talks to settings or server
resolution.
- **Error propagation:** context-construction failures remain early and
synchronous at the command boundary; server access errors still flow
through `ctx.server().await?` and should not move deeper into render
helpers.
- **State lifecycle risks:** the biggest correctness risk is accidental
reuse of the wrong derived context, especially storage-dir-aware
contexts in the `system` family and target-based contexts in run/PR
flows.
- **API surface parity:** although this is an internal refactor, it
touches external CLI contracts indirectly through JSON/text output,
verbosity, global `--json` restrictions, and server-target resolution.
- **Integration coverage:** black-box command tests and scenario tests
are the main safety net. Unit tests should only cover context
construction/derivation and not replace CLI integration coverage.
- **Unchanged invariants:** tracing and upgrade bootstrap ordering stays
in `main.rs`; `exec` keeps its distinct direct-provider path; server
connection semantics stay in `server_client.rs`; `Styles` remain
command-local.
## Risks & Dependencies
| Risk | Mitigation |
|------|------------|
| Output-mode drift when replacing `cli.output.format` reads with `ctx.user_settings().cli.output.format` | Keep characterization coverage in existing `cmd/*` tests for both text and JSON paths before deleting the raw parameters |
| Global `--json` behavior changes while moving `process_local_json` into the context | Add targeted coverage in `json_global.rs`, `auth.rs`, `provider_login.rs`, `graph.rs`, and sandbox preview/ssh command tests |
| Storage-dir-aware system commands accidentally derive a target-mode context instead of a connection-mode context | Keep connection-specific derivation explicit and cover `system_info`, `system_df`, `system_events`, and `system_prune` with CLI integration tests |
| `CommandContext` grows into a second god object | Keep explicit scope rules: invocation plumbing only, no command args, no `Styles`, no feature-specific render state |
| The refactor becomes a giant compile-fix patch with poor reviewability | Land the work family-by-family with temporary compatibility wrappers where needed, and verify each family with its existing tests before cleanup |
## Documentation / Operational Notes
- No user-facing documentation changes are expected.
- Internal comments in `command_context.rs` and nearby command modules
should be updated to describe the final boundary, not the transitional
“still being wired through” state.
- The earlier April 8 plan should remain as historical context; this plan
supersedes it for the command-boundary alignment work.
## Sources & References
- Prior plan: `docs/plans/2026-04-08-cli-services-command-context-refactor-plan.md`
- Related plan: `docs/plans/2026-04-22-001-refactor-settings-api-entrypoints-plan.md`
- Related code:
`lib/crates/fabro-cli/src/command_context.rs`
`lib/crates/fabro-cli/src/main.rs`
`lib/crates/fabro-cli/src/server_client.rs`
`lib/crates/fabro-cli/src/commands/run/mod.rs`
`lib/crates/fabro-cli/src/commands/pr/mod.rs`
`lib/crates/fabro-cli/src/commands/secret/mod.rs`
`lib/crates/fabro-cli/src/commands/auth/mod.rs`
`lib/crates/fabro-cli/src/commands/provider/mod.rs`
`lib/crates/fabro-cli/src/commands/system/mod.rs`
- Testing guidance: `files-internal/testing-strategy.md`
- Related history:
`93b6577cd simplify: drop duplicate settings plumbing from cli/server refactor`
`367fd9302 refactor(cli): centralize command settings and server access`
`4b30a5f16 refactor(cli): route command output through Printer`

View file

@ -3,12 +3,14 @@ use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use fabro_config::UserSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_types::settings::{Combine, SettingsLayer};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;
use crate::args::{ServerConnectionArgs, ServerTargetArgs};
use crate::args::{
ServerConnectionArgs, ServerTargetArgs, printer_from_verbosity, require_no_json_override,
};
use crate::server_client::Client;
use crate::{server_client, user_config};
@ -25,89 +27,62 @@ pub(crate) enum ServerMode {
}
pub(crate) struct CommandContext {
#[allow(
dead_code,
reason = "This item is kept for command plumbing that is still being wired through."
)]
printer: Printer,
cwd: PathBuf,
base_config_path: PathBuf,
machine_settings: SettingsLayer,
user_settings: UserSettings,
server_mode: ServerMode,
server: OnceCell<Arc<Client>>,
printer: Printer,
process_local_json: bool,
cwd: PathBuf,
base_config_path: PathBuf,
cli_layer: CliLayer,
machine_settings: SettingsLayer,
user_settings: UserSettings,
server_mode: ServerMode,
server: OnceCell<Arc<Client>>,
}
impl CommandContext {
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_layer: &CliLayer,
) -> Result<Self> {
Self::new(
printer,
ServerMode::ByTarget {
target_override: args.server.clone(),
},
cli_layer,
)
}
pub(crate) fn for_connection(
args: &ServerConnectionArgs,
printer: Printer,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(
printer,
ServerMode::ByStorageDir {
target_override: args.target.server.clone(),
storage_dir_override: args.storage_dir.clone_path(),
},
cli_layer,
)
}
fn new(printer: Printer, server_mode: ServerMode, cli_layer: &CliLayer) -> Result<Self> {
pub(crate) fn from_disk(cli_layer: &CliLayer, process_local_json: bool) -> Result<Self> {
let (machine_settings, user_settings) = load_merged_settings(cli_layer, &ServerMode::None)?;
let printer = printer_from_verbosity(user_settings.cli.output.verbosity);
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 {
ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
ServerMode::ByStorageDir {
storage_dir_override,
..
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
};
let machine_settings = SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
}
.combine(disk_settings);
let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?;
Ok(Self {
printer,
process_local_json,
cwd,
base_config_path,
cli_layer: cli_layer.clone(),
machine_settings,
user_settings,
server_mode,
server_mode: ServerMode::None,
server: OnceCell::new(),
})
}
#[allow(
dead_code,
reason = "This item is kept for command plumbing that is still being wired through."
)]
pub(crate) fn with_target(&self, args: &ServerTargetArgs) -> Result<Self> {
self.with_server_mode(ServerMode::ByTarget {
target_override: args.server.clone(),
})
}
pub(crate) fn with_connection(&self, args: &ServerConnectionArgs) -> Result<Self> {
self.with_server_mode(ServerMode::ByStorageDir {
target_override: args.target.server.clone(),
storage_dir_override: args.storage_dir.clone_path(),
})
}
pub(crate) fn printer(&self) -> Printer {
self.printer
}
pub(crate) fn explicit_json_requested(&self) -> bool {
self.process_local_json
}
pub(crate) fn require_no_json_override(&self) -> Result<()> {
require_no_json_override(self.process_local_json)
}
pub(crate) fn cwd(&self) -> &Path {
&self.cwd
}
@ -120,6 +95,14 @@ impl CommandContext {
&self.user_settings
}
pub(crate) fn json_output(&self) -> bool {
self.user_settings.cli.output.format == OutputFormat::Json
}
pub(crate) fn verbose(&self) -> bool {
self.user_settings.cli.output.verbosity == OutputVerbosity::Verbose
}
pub(crate) async fn server(&self) -> Result<Arc<Client>> {
let server_mode = self.server_mode.clone();
let base_config_path = self.base_config_path.clone();
@ -149,4 +132,161 @@ impl CommandContext {
Ok(Arc::clone(client))
}
fn with_server_mode(&self, server_mode: ServerMode) -> Result<Self> {
// Always reload settings for the requested derivation mode so the result
// depends only on the requested mode, not on whichever derived context
// happened to call into this helper.
let (machine_settings, user_settings) =
load_merged_settings(&self.cli_layer, &server_mode)?;
Ok(Self {
printer: self.printer,
process_local_json: self.process_local_json,
cwd: self.cwd.clone(),
base_config_path: self.base_config_path.clone(),
cli_layer: self.cli_layer.clone(),
machine_settings,
user_settings,
server_mode,
server: OnceCell::new(),
})
}
}
fn load_merged_settings(
cli_layer: &CliLayer,
server_mode: &ServerMode,
) -> Result<(SettingsLayer, UserSettings)> {
let disk_settings = match server_mode {
ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
ServerMode::ByStorageDir {
storage_dir_override,
..
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
};
merge_settings_layer(disk_settings, cli_layer)
}
fn merge_settings_layer(
disk_settings: SettingsLayer,
cli_layer: &CliLayer,
) -> Result<(SettingsLayer, UserSettings)> {
let machine_settings = SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
}
.combine(disk_settings);
let user_settings = UserSettings::from_layer(&machine_settings)?;
Ok((machine_settings, user_settings))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use fabro_config::parse_settings_layer;
use fabro_config::user::apply_storage_dir_override;
use fabro_types::settings::InterpString;
use fabro_types::settings::cli::{CliLayer, CliOutputLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;
use super::{CommandContext, ServerMode, merge_settings_layer};
fn cli_layer_with_json_and_verbose() -> CliLayer {
CliLayer {
output: Some(CliOutputLayer {
format: Some(OutputFormat::Json),
verbosity: Some(OutputVerbosity::Verbose),
}),
..CliLayer::default()
}
}
fn synthetic_context(process_local_json: bool, printer: Printer) -> CommandContext {
let cli_layer = cli_layer_with_json_and_verbose();
let (machine_settings, user_settings) =
merge_settings_layer(parse_settings_layer("_version = 1\n").unwrap(), &cli_layer)
.expect("settings should merge");
CommandContext {
printer,
process_local_json,
cwd: PathBuf::from("/tmp/workspace"),
base_config_path: PathBuf::from("/tmp/settings.toml"),
cli_layer,
machine_settings,
user_settings,
server_mode: ServerMode::None,
server: OnceCell::new(),
}
}
#[test]
fn context_exposes_resolved_output_and_explicit_json_state() {
let ctx = synthetic_context(true, Printer::Default);
assert_eq!(ctx.user_settings().cli.output.format, OutputFormat::Json);
assert_eq!(
ctx.user_settings().cli.output.verbosity,
OutputVerbosity::Verbose
);
assert!(ctx.explicit_json_requested());
assert_eq!(ctx.printer(), Printer::Default);
}
#[test]
fn storage_dir_override_only_changes_storage_root_in_merged_settings() {
let cli_layer = cli_layer_with_json_and_verbose();
let base_disk_settings = parse_settings_layer(
r#"
_version = 1
[server.storage]
root = "/srv/fabro/default"
"#,
)
.expect("settings fixture should parse");
let override_disk_settings = apply_storage_dir_override(
base_disk_settings.clone(),
Some(std::path::Path::new("/srv/fabro/override")),
);
let (base_settings, base_user_settings) =
merge_settings_layer(base_disk_settings, &cli_layer)
.expect("base settings should merge");
let (connection_settings, connection_user_settings) =
merge_settings_layer(override_disk_settings, &cli_layer)
.expect("connection settings should merge");
assert_eq!(base_user_settings, connection_user_settings);
assert_eq!(base_user_settings.cli.output.format, OutputFormat::Json);
assert_eq!(
base_settings
.server
.as_ref()
.and_then(|server| server.storage.as_ref())
.and_then(|storage| storage.root.as_ref())
.map(InterpString::as_source),
Some("/srv/fabro/default".to_string())
);
assert_eq!(
connection_settings
.server
.as_ref()
.and_then(|server| server.storage.as_ref())
.and_then(|storage| storage.root.as_ref())
.map(InterpString::as_source),
Some("/srv/fabro/override".to_string())
);
}
#[test]
fn explicit_json_guard_uses_invocation_flag_not_resolved_output_format() {
let json_ctx = synthetic_context(true, Printer::Default);
let text_ctx = synthetic_context(false, Printer::Default);
assert!(json_ctx.require_no_json_override().is_err());
assert!(text_ctx.require_no_json_override().is_ok());
}
}

View file

@ -6,28 +6,21 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::ArtifactCpArgs;
use crate::command_context::CommandContext;
use crate::server_client::Client;
use crate::shared::{print_json_pretty, split_run_path};
pub(super) async fn cp_command(
args: &ArtifactCpArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(super) async fn cp_command(args: &ArtifactCpArgs, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let (run_id_selector, asset_path) = parse_source(&args.source);
let (run_id, client, entries) = super::resolve_artifacts(
base_ctx,
&args.server,
run_id_selector,
args.node.as_deref(),
args.retry,
cli_layer,
printer,
)
.await?;
@ -64,7 +57,7 @@ pub(super) async fn cp_command(
.unwrap_or_else(|| std::ffi::OsStr::new(&entry.relative_path)),
);
write_artifact_file(&client, &run_id, entry, &dest_file).await?;
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
print_json_pretty(&serde_json::json!({
"copied": [{
"relative_path": entry.relative_path,
@ -125,7 +118,7 @@ pub(super) async fn cp_command(
}
}
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
print_json_pretty(&serde_json::json!({ "copied": copied }))?;
} else {
fabro_util::printout!(

View file

@ -1,30 +1,23 @@
use anyhow::Result;
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::ArtifactListArgs;
use crate::command_context::CommandContext;
pub(super) async fn list_command(
args: &ArtifactListArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(super) async fn list_command(args: &ArtifactListArgs, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let (_run_id, _client, entries) = super::resolve_artifacts(
base_ctx,
&args.server,
&args.run_id,
args.node.as_deref(),
args.retry,
cli_layer,
printer,
)
.await?;
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}

View file

@ -2,10 +2,7 @@ mod cp;
mod list;
use anyhow::{Context, Result};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_types::{RunId, StageId};
use fabro_util::printer::Printer;
use crate::args::{ArtifactCommand, ArtifactNamespace, ServerTargetArgs};
use crate::command_context::CommandContext;
@ -22,14 +19,13 @@ pub(super) struct ArtifactEntry {
}
pub(super) async fn resolve_artifacts(
base_ctx: &CommandContext,
server: &ServerTargetArgs,
run_selector: &str,
node: Option<&str>,
retry: Option<u32>,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(RunId, Client, Vec<ArtifactEntry>)> {
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
let ctx = base_ctx.with_target(server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(run_selector).await?.run_id;
let mut entries = Vec::new();
@ -62,14 +58,9 @@ pub(super) async fn resolve_artifacts(
Ok((run_id, client.clone_for_reuse(), entries))
}
pub(crate) async fn dispatch(
ns: ArtifactNamespace,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(ns: ArtifactNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
ArtifactCommand::List(args) => list::list_command(&args, cli, cli_layer, printer).await,
ArtifactCommand::Cp(args) => cp::cp_command(&args, cli, cli_layer, printer).await,
ArtifactCommand::List(args) => list::list_command(&args, base_ctx).await,
ArtifactCommand::Cp(args) => cp::cp_command(&args, base_ctx).await,
}
}

View file

@ -4,13 +4,12 @@ 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::cli::CliLayer;
use fabro_util::browser;
use fabro_util::printer::Printer;
use serde::Deserialize;
use tokio::time::timeout;
use crate::args::{AuthLoginArgs, require_no_json_override};
use crate::args::AuthLoginArgs;
use crate::command_context::CommandContext;
use crate::user_config;
use crate::user_config::ServerTarget;
@ -33,17 +32,13 @@ struct CliTokenSubject {
email: String,
}
pub(super) async fn login_command(
args: AuthLoginArgs,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
require_no_json_override(process_local_json)?;
pub(super) async fn login_command(args: AuthLoginArgs, base_ctx: &CommandContext) -> Result<()> {
base_ctx.require_no_json_override()?;
let printer = base_ctx.printer();
#[cfg(not(unix))]
{
let _ = (args, cli_layer, printer);
let _ = (args, printer);
bail!(
"CLI OAuth login is not supported on Windows in this release. Use WSL, or use a dev-token server."
);
@ -51,8 +46,7 @@ pub(super) async fn login_command(
#[cfg(unix)]
{
let ctx = CommandContext::base(printer, cli_layer)?;
let target = user_config::resolve_server_target(&args.server, ctx.machine_settings())?;
let target = user_config::resolve_server_target(&args.server, base_ctx.machine_settings())?;
let web_url = browser_origin(&target)?;
let pkce = fabro_oauth::generate_pkce();
let state = fabro_oauth::generate_state();

View file

@ -1,23 +1,16 @@
use anyhow::{Result, bail};
use fabro_client::{AuthEntry, AuthStore};
use fabro_http::header::AUTHORIZATION;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{AuthLogoutArgs, require_no_json_override};
use crate::args::AuthLogoutArgs;
use crate::command_context::CommandContext;
use crate::user_config;
use crate::user_config::ServerTarget;
pub(super) async fn logout_command(
args: AuthLogoutArgs,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
require_no_json_override(process_local_json)?;
pub(super) async fn logout_command(args: AuthLogoutArgs, base_ctx: &CommandContext) -> Result<()> {
base_ctx.require_no_json_override()?;
let printer = base_ctx.printer();
let ctx = CommandContext::base(printer, cli_layer)?;
let store = AuthStore::default();
if args.all {
let entries = store.list()?;
@ -41,7 +34,7 @@ pub(super) async fn logout_command(
return Ok(());
}
let target = user_config::resolve_server_target(&args.server, ctx.machine_settings())?;
let target = user_config::resolve_server_target(&args.server, base_ctx.machine_settings())?;
let Some(entry) = store.get(&target)? else {
fabro_util::printerr!(printer, "Not logged in to {}.", target);
return Ok(());

View file

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

View file

@ -1,9 +1,7 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use fabro_client::{AuthEntry, AuthStore};
use fabro_types::settings::cli::CliLayer;
use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format};
use fabro_util::printer::Printer;
use serde::Serialize;
use crate::args::AuthStatusArgs;
@ -40,13 +38,8 @@ struct StatusOutput {
dev_token: &'static str,
}
pub(super) fn status_command(
args: &AuthStatusArgs,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::base(printer, cli_layer)?;
pub(super) fn status_command(args: &AuthStatusArgs, ctx: &CommandContext) -> Result<()> {
let printer = ctx.printer();
let store = AuthStore::default();
let now = Utc::now();
let rows = if args.server.as_deref().is_some() {
@ -61,7 +54,7 @@ pub(super) fn status_command(
"not_set"
};
if process_local_json {
if ctx.explicit_json_requested() {
print_json_pretty(&StatusOutput {
servers: rows,
dev_token,

View file

@ -11,9 +11,6 @@ use std::io::Write;
use fabro_api::types::ServerSettings;
use fabro_config::UserSettings;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use serde::Serialize;
use crate::args::SettingsArgs;
@ -26,29 +23,17 @@ struct RenderedConfig {
server: ServerSettings,
}
async fn rendered_config(
args: &SettingsArgs,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
pub(crate) async fn execute(args: &SettingsArgs, base_ctx: &CommandContext) -> anyhow::Result<()> {
let ctx = base_ctx.with_target(&args.target)?;
let user = fabro_config::UserSettings::resolve()?;
let server = ctx
.server()
.await?
.retrieve_resolved_server_settings()
.await?;
serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into)
}
let config = serde_json::to_value(RenderedConfig { user, server })?;
pub(crate) async fn execute(
args: &SettingsArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let config = Box::pin(rendered_config(args, cli_layer, printer)).await?;
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
print_json_pretty(&config)?;
return Ok(());
}

View file

@ -3,8 +3,6 @@ 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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
pub(crate) use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
};
@ -142,13 +140,12 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer:
pub(crate) async fn run_doctor(
args: &DoctorArgs,
verbose: bool,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<i32, anyhow::Error> {
let verbose = args.verbose || base_ctx.verbose();
let printer = base_ctx.printer();
let styles = Styles::detect_stdout();
let json = cli.output.format == OutputFormat::Json;
let json = base_ctx.json_output();
let spinner = if json {
None
} else {
@ -179,7 +176,7 @@ pub(crate) async fn run_doctor(
}],
};
let ctx = match CommandContext::for_target(&args.target, printer, cli_layer) {
let ctx = match base_ctx.with_target(&args.target) {
Ok(ctx) => ctx,
Err(err) => {
report.sections.push(CheckSection {

View file

@ -11,10 +11,7 @@ use bytes::Bytes;
#[cfg(test)]
use fabro_store::{ArtifactStore, RunDatabase};
use fabro_store::{EventEnvelope, RunProjection, StageId};
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)]
@ -26,19 +23,15 @@ use crate::command_context::CommandContext;
use crate::server_client::Client;
use crate::shared::{absolute_or_current, print_json_pretty};
pub(crate) async fn run(
args: &DumpArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn run(args: &DumpArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
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?;
let source = ServerDumpSource::new(client.as_ref(), &run_id);
let file_count = export_run_from_source(&source, &state, &args.output).await?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"run_id": run_id,
"output_dir": absolute_or_current(&args.output),

View file

@ -13,15 +13,15 @@ use fabro_llm::types::{
FinishReason, Message, Request, Response as LlmResponse, StreamEvent, TokenCounts,
};
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_types::settings::InterpString;
use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat;
use fabro_types::settings::run::McpEntryLayer;
use fabro_types::settings::{CliNamespace, InterpString};
use fabro_util::exit::{ErrorExt, ExitClass};
use fabro_util::printer::Printer;
use futures::stream;
use serde::Deserialize;
use crate::args::ExecArgs;
use crate::command_context::CommandContext;
use crate::{server_client, user_config};
fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
@ -356,14 +356,11 @@ impl ProviderAdapter for AuthenticatedFabroServerAdapter {
}
}
pub(crate) async fn execute(
mut args: ExecArgs,
cli: &CliNamespace,
_printer: Printer,
) -> AnyResult<()> {
pub(crate) async fn execute(mut args: ExecArgs, ctx: &CommandContext) -> AnyResult<()> {
use fabro_agent::cli::PermissionLevel as AgentPermissionLevel;
use fabro_types::settings::run::AgentPermissions;
let cli = &ctx.user_settings().cli;
let raw_settings = user_config::load_settings()?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = crate::sleep_inhibitor::guard(cli.exec.prevent_idle_sleep);

View file

@ -13,13 +13,11 @@ use anyhow::{Context, bail};
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::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_types::settings::SettingsLayer;
use fabro_util::terminal::Styles;
use tracing::debug;
use crate::args::{GraphArgs, GraphDirection, require_no_json_override};
use crate::args::{GraphArgs, GraphDirection};
use crate::command_context::CommandContext;
use crate::commands::run::output::api_diagnostics_to_local;
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
@ -28,16 +26,14 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r
pub(crate) async fn run(
args: &GraphArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
base_ctx: &CommandContext,
) -> anyhow::Result<()> {
if process_local_json && args.output.is_none() {
require_no_json_override(process_local_json)?;
if args.output.is_none() {
base_ctx.require_no_json_override()?;
}
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),
@ -73,7 +69,7 @@ pub(crate) async fn run(
if let Some(ref output_path) = args.output {
std::fs::write(output_path, &rendered)
.with_context(|| format!("writing rendered graph to {}", output_path.display()))?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"path": absolute_or_current(output_path),
"format": args.format.to_string(),

View file

@ -32,9 +32,8 @@ use fabro_install::{
use fabro_model::Provider;
use fabro_server::serve;
use fabro_store::ArtifactStore;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::server::ServerAuthMethod;
use fabro_types::settings::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::version::FABRO_VERSION;
@ -52,6 +51,7 @@ use crate::args::{
DoctorArgs, InstallArgs, InstallCommand, InstallGitHubStrategyArg, InstallGithubArgs,
InstallNonInteractiveArgs, ServerTargetArgs,
};
use crate::command_context::CommandContext;
use crate::commands::server::{start, stop};
use crate::gh::GhCli;
use crate::shared::provider_auth::{
@ -1409,15 +1409,12 @@ where
pub(crate) async fn execute(
args: &InstallArgs,
command: Option<InstallCommand>,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
ctx: &CommandContext,
) -> Result<()> {
match command {
None => run_install(args, cli, cli_layer, process_local_json, printer).await,
None => run_install(args, ctx).await,
Some(InstallCommand::Github(github_args)) => {
run_install_github_command(args, &github_args, cli, process_local_json, printer).await
run_install_github_command(args, &github_args, ctx).await
}
}
}
@ -1425,16 +1422,20 @@ pub(crate) async fn execute(
async fn run_install_github_command(
args: &InstallArgs,
github_args: &InstallGithubArgs,
cli: &CliNamespace,
process_local_json: bool,
printer: Printer,
ctx: &CommandContext,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
if process_local_json && !args.non_interactive {
let json = ctx.json_output();
if ctx.explicit_json_requested() && !args.non_interactive {
bail!("--json is only supported for install with --non-interactive");
}
let result = Box::pin(run_install_github_inner(args, github_args, json, printer)).await;
let result = Box::pin(run_install_github_inner(
args,
github_args,
json,
ctx.printer(),
))
.await;
if json {
let emit_result = match &result {
Ok(()) => emit_install_json_event(&install_complete_event()),
@ -1586,19 +1587,13 @@ async fn run_install_github_inner(
Ok(())
}
pub(crate) async fn run_install(
args: &InstallArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
if process_local_json && !args.non_interactive {
pub(crate) async fn run_install(args: &InstallArgs, ctx: &CommandContext) -> Result<()> {
let json = ctx.json_output();
if ctx.explicit_json_requested() && !args.non_interactive {
bail!("--json is only supported for install with --non-interactive");
}
let result = Box::pin(run_install_inner(args, cli, cli_layer, printer)).await;
let result = Box::pin(run_install_inner(args, ctx)).await;
if json {
let emit_result = match &result {
Ok(()) => emit_install_json_event(&install_complete_event()),
@ -1612,13 +1607,10 @@ pub(crate) async fn run_install(
result
}
async fn run_install_inner(
args: &InstallArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<()> {
let _cli = &ctx.user_settings().cli;
let printer = ctx.printer();
let json = ctx.json_output();
let web_url = &args.web_url;
let s = Styles::detect_stderr();
let emoji = console::Emoji("⚒️ ", "");
@ -1921,7 +1913,7 @@ async fn run_install_inner(
target: ServerTargetArgs::default(),
verbose: false,
};
doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await
doctor::run_doctor(&doctor_args, ctx).await
},
)
.await?

View file

@ -3,9 +3,6 @@ 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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use serde::Serialize;
@ -42,19 +39,17 @@ struct ModelTestOutput {
pub(crate) async fn execute(
command: Option<ModelsCommand>,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let command = command.unwrap_or_default();
let target_args = match &command {
ModelsCommand::List(args) => &args.target,
ModelsCommand::Test(args) => &args.target,
};
let ctx = CommandContext::for_target(target_args, printer, cli_layer)?;
let ctx = base_ctx.with_target(target_args)?;
let server = ctx.server().await?;
run_models(command, &server, cli.output.format == OutputFormat::Json).await
run_models(command, &server, ctx.json_output()).await
}
fn format_context_window(tokens: i64) -> String {

View file

@ -11,13 +11,11 @@ use std::io::Write;
use fabro_config::project::resolve_workflow;
use fabro_graphviz::parser::parse_ast;
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: &CliNamespace, _printer: Printer) -> anyhow::Result<()> {
pub(crate) fn run(args: &ParseArgs) -> anyhow::Result<()> {
let stdout = std::io::stdout();
run_to(args, stdout.lock())
}

View file

@ -1,22 +1,15 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::PrCloseArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(super) async fn close_command(
args: PrCloseArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
pub(super) async fn close_command(args: PrCloseArgs, base_ctx: &CommandContext) -> Result<()> {
let (ctx, record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let creds = super::load_github_credentials_required(&ctx)?;
fabro_github::close_pull_request(
&creds,
@ -29,13 +22,18 @@ pub(super) async fn close_command(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request");
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,
}))?;
} else {
fabro_util::printout!(printer, "Closed #{} ({})", record.number, record.html_url);
fabro_util::printout!(
ctx.printer(),
"Closed #{} ({})",
record.number,
record.html_url
);
}
Ok(())

View file

@ -5,9 +5,6 @@ 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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_vault::Vault;
use fabro_workflow::outcome::StageStatus;
use fabro_workflow::pull_request::maybe_open_pull_request;
@ -25,13 +22,9 @@ use crate::user_config;
deprecated,
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
)]
pub(super) async fn create_command(
args: PrCreateArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(super) async fn create_command(args: PrCreateArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
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 +79,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_layer, printer)?;
let creds = super::load_github_credentials_required(base_ctx)?;
let branch_found = fabro_github::branch_exists(
&creds,
@ -136,14 +129,14 @@ pub(super) async fn create_command(
match pull_request {
Some(record) => {
info!(pr_url = %record.html_url, "Pull request created");
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&record)?;
} else {
fabro_util::printout!(printer, "{}", record.html_url);
}
}
None => {
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::Value::Null)?;
} else {
fabro_util::printout!(printer, "No pull request created (empty diff).");

View file

@ -1,9 +1,6 @@
use anyhow::Result;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use futures::future::join_all;
use serde::Serialize;
@ -23,13 +20,9 @@ struct PrRow {
url: String,
}
pub(super) async fn list_command(
args: PrListArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(super) async fn list_command(args: PrListArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let mut entries = Vec::new();
@ -42,7 +35,7 @@ pub(super) async fn list_command(
}
if entries.is_empty() {
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&Vec::<PrRow>::new())?;
return Ok(());
}
@ -50,7 +43,7 @@ pub(super) async fn list_command(
return Ok(());
}
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let creds = super::load_github_credentials_required(base_ctx)?;
let futures: Vec<_> = entries
.iter()
@ -104,7 +97,7 @@ pub(super) async fn list_command(
.collect()
};
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&rows)?;
return Ok(());
}

View file

@ -1,22 +1,15 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::PrMergeArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(super) async fn merge_command(
args: PrMergeArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
pub(super) async fn merge_command(args: PrMergeArgs, base_ctx: &CommandContext) -> Result<()> {
let (ctx, record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let creds = super::load_github_credentials_required(&ctx)?;
fabro_github::merge_pull_request(
&creds,
@ -30,14 +23,19 @@ pub(super) async fn merge_command(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request");
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,
"method": args.method,
}))?;
} else {
fabro_util::printout!(printer, "Merged #{} ({})", record.number, record.html_url);
fabro_util::printout!(
ctx.printer(),
"Merged #{} ({})",
record.number,
record.html_url
);
}
Ok(())

View file

@ -8,9 +8,7 @@ use anyhow::{Context, Result, anyhow};
use fabro_config::Storage;
use fabro_github::GitHubCredentials;
use fabro_types::PullRequestRecord;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliNamespace, InterpString};
use fabro_util::printer::Printer;
use fabro_types::settings::InterpString;
use crate::args::{PrCommand, PrNamespace, ServerTargetArgs};
use crate::command_context::CommandContext;
@ -20,20 +18,13 @@ use crate::user_config;
const GITHUB_CREDENTIALS_REQUIRED: &str =
"GitHub credentials required — run `fabro install` or set GITHUB_TOKEN";
pub(crate) async fn dispatch(
ns: PrNamespace,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(ns: PrNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
PrCommand::Create(args) => {
Box::pin(create::create_command(args, cli, cli_layer, printer)).await
}
PrCommand::List(args) => list::list_command(args, cli, cli_layer, printer).await,
PrCommand::View(args) => view::view_command(args, cli, cli_layer, printer).await,
PrCommand::Merge(args) => merge::merge_command(args, cli, cli_layer, printer).await,
PrCommand::Close(args) => close::close_command(args, cli, cli_layer, printer).await,
PrCommand::Create(args) => Box::pin(create::create_command(args, base_ctx)).await,
PrCommand::List(args) => list::list_command(args, base_ctx).await,
PrCommand::View(args) => view::view_command(args, base_ctx).await,
PrCommand::Merge(args) => merge::merge_command(args, base_ctx).await,
PrCommand::Close(args) => close::close_command(args, base_ctx).await,
}
}
@ -41,14 +32,10 @@ pub(crate) async fn dispatch(
deprecated,
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
)]
fn load_github_credentials_required(
cli_layer: &CliLayer,
printer: Printer,
) -> Result<GitHubCredentials> {
let ctx = CommandContext::base(printer, cli_layer)?;
let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings())
fn load_github_credentials_required(base_ctx: &CommandContext) -> Result<GitHubCredentials> {
let server_settings = fabro_config::ServerSettings::from_layer(base_ctx.machine_settings())
.map_err(anyhow::Error::from)?;
let vault = user_config::storage_dir(ctx.machine_settings())
let vault = user_config::storage_dir(base_ctx.machine_settings())
.ok()
.and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok());
let creds = build_github_credentials(
@ -70,15 +57,14 @@ fn load_github_credentials_required(
pub(crate) async fn load_pr_record(
server: &ServerTargetArgs,
run_id: &str,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(PullRequestRecord, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
base_ctx: &CommandContext,
) -> Result<(CommandContext, PullRequestRecord, fabro_types::RunId)> {
let ctx = base_ctx.with_target(server)?;
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?;
let record = state.pull_request.with_context(|| {
format!("No pull request found in store. Create one first with: fabro pr create {run_id}")
})?;
Ok((record, run_id))
Ok((ctx, record, run_id))
}

View file

@ -1,22 +1,15 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::PrViewArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(super) async fn view_command(
args: PrViewArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
pub(super) async fn view_command(args: PrViewArgs, base_ctx: &CommandContext) -> Result<()> {
let (ctx, record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
let creds = super::load_github_credentials_required(cli_layer, printer)?;
let creds = super::load_github_credentials_required(&ctx)?;
let detail = fabro_github::get_pull_request(
&creds,
@ -30,11 +23,12 @@ pub(super) async fn view_command(
info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request");
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&detail)?;
return Ok(());
}
let printer = ctx.printer();
fabro_util::printout!(printer, "#{} {}", detail.number, detail.title);
let state_display = if detail.draft { "draft" } else { &detail.state };
fabro_util::printout!(printer, "State: {state_display}");

View file

@ -1,9 +1,6 @@
use anyhow::bail;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::PreflightArgs;
@ -17,13 +14,12 @@ use crate::shared::print_json_pretty;
pub(crate) async fn execute(
mut args: PreflightArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
args.verbose = args.verbose || ctx.verbose();
let manifest = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
@ -38,7 +34,7 @@ pub(crate) async fn execute(
let response = client.run_preflight(manifest.manifest).await?;
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&response)?;
} else {
print_preflight_workflow_summary(

View file

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

View file

@ -1,20 +1,12 @@
mod login;
use anyhow::Result;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{ProviderCommand, ProviderNamespace};
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
ns: ProviderNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(ns: ProviderNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
ProviderCommand::Login(args) => {
login::login_command(args, cli_layer, process_local_json, printer).await
}
ProviderCommand::Login(args) => login::login_command(args, base_ctx).await,
}
}

View file

@ -1,9 +1,9 @@
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<String>> {
use crate::command_context::CommandContext;
pub(crate) fn run_deinit(base_ctx: &CommandContext) -> Result<Vec<String>> {
let printer = base_ctx.printer();
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();
@ -20,7 +20,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<Str
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
removed.push(".fabro/".to_string());
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
" {} {}",
@ -29,7 +29,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<Str
);
}
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
"\n{}",

View file

@ -6,9 +6,6 @@
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::process::Command as TokioCommand;
use tokio::task::spawn_blocking;
@ -36,10 +33,9 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
pub(crate) async fn run_init(
args: &RepoInitArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<Vec<String>> {
let printer = base_ctx.printer();
let repo_root = spawn_blocking(git_repo_root)
.await
.context("git repo root task panicked")??;
@ -79,7 +75,7 @@ draft = true
let green = console::Style::new().green();
let bold = console::Style::new().bold();
let dim = console::Style::new().dim();
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
" {} {}",
@ -112,7 +108,7 @@ draft = true
)
.with_context(|| format!("failed to write {}", dot_path.display()))?;
created.push(".fabro/workflows/hello/workflow.fabro".to_string());
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
" {} {}",
@ -129,7 +125,7 @@ draft = true
)
.with_context(|| format!("failed to write {}", toml_path.display()))?;
created.push(".fabro/workflows/hello/workflow.toml".to_string());
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
" {} {}",
@ -138,7 +134,7 @@ draft = true
);
}
if cli.output.format != OutputFormat::Json {
if !base_ctx.json_output() {
fabro_util::printerr!(
printer,
"\n{} Run a workflow with:\n\n {}",
@ -150,18 +146,15 @@ draft = true
);
}
if cli.output.format != OutputFormat::Json {
check_github_app_installation(&args.target, cli_layer, printer).await;
if !base_ctx.json_output() {
check_github_app_installation(&args.target, base_ctx).await;
}
Ok(created)
}
async fn check_github_app_installation(
target: &ServerTargetArgs,
cli_layer: &CliLayer,
printer: Printer,
) {
async fn check_github_app_installation(target: &ServerTargetArgs, base_ctx: &CommandContext) {
let printer = base_ctx.printer();
// Get the git remote origin URL
let output = match TokioCommand::new("git")
.args(["remote", "get-url", "origin"])
@ -199,7 +192,7 @@ async fn check_github_app_installation(
return; // Not a GitHub repo — skip silently
};
let ctx = match CommandContext::for_target(target, printer, cli_layer) {
let ctx = match base_ctx.with_target(target) {
Ok(ctx) => ctx,
Err(err) => {
fabro_util::printerr!(

View file

@ -2,30 +2,23 @@ pub(crate) mod deinit;
pub(crate) mod init;
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::{RepoCommand, RepoNamespace};
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(crate) async fn dispatch(
ns: RepoNamespace,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(ns: RepoNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
RepoCommand::Init(args) => {
let created = init::run_init(&args, cli, cli_layer, printer).await?;
if cli.output.format == OutputFormat::Json {
let created = init::run_init(&args, base_ctx).await?;
if base_ctx.json_output() {
print_json_pretty(&serde_json::json!({ "created": created }))?;
}
Ok(())
}
RepoCommand::Deinit => {
let removed = deinit::run_deinit(cli, printer)?;
if cli.output.format == OutputFormat::Json {
let removed = deinit::run_deinit(base_ctx)?;
if base_ctx.json_output() {
print_json_pretty(&serde_json::json!({ "removed": removed }))?;
}
Ok(())

View file

@ -1,36 +1,19 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::RunArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config::load_settings_with_storage_dir;
pub(crate) async fn execute(
mut args: RunArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn execute(mut args: RunArgs, base_ctx: &CommandContext) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
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 printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
args.verbose = args.verbose || ctx.verbose();
let quiet = args.detach;
let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep;
let created_run = Box::pin(super::create::create_run(
&ctx,
&args,
cli_defaults,
styles,
quiet,
printer,
))
.await?;
let created_run = Box::pin(super::create::create_run(&ctx, &args, styles, quiet)).await?;
if !quiet {
fabro_util::printerr!(
@ -50,7 +33,7 @@ pub(crate) async fn execute(
let client = ctx.server().await?;
super::start::start_run_with_client(&client, &created_run.run_id, false).await?;
let json = cli.output.format == OutputFormat::Json;
let json = ctx.json_output();
if args.detach {
if json {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
@ -64,7 +47,7 @@ pub(crate) async fn execute(
true,
styles,
json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
ctx.verbose(),
printer,
))
.await?;

View file

@ -1,9 +1,6 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::fs;
use tracing::{debug, info};
@ -26,12 +23,7 @@ enum CopyDirection {
},
}
pub(crate) async fn cp_command(
args: CpArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn cp_command(args: CpArgs, base_ctx: &CommandContext) -> Result<()> {
let direction = parse_direction(&args.src, &args.dst)?;
match direction {
@ -41,7 +33,7 @@ pub(crate) async fn cp_command(
local_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?;
let file_count = if args.recursive {
Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?)
@ -51,7 +43,7 @@ pub(crate) async fn cp_command(
None
};
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
let mut value = serde_json::json!({
"direction": "download",
"recursive": args.recursive,
@ -72,7 +64,7 @@ pub(crate) async fn cp_command(
remote_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?;
let file_count = if args.recursive {
Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?)
@ -82,7 +74,7 @@ pub(crate) async fn cp_command(
None
};
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
let mut value = serde_json::json!({
"direction": "upload",
"recursive": args.recursive,
@ -124,12 +116,11 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
}
async fn resolve_client_and_run_id(
base_ctx: &CommandContext,
server: &ServerTargetArgs,
run_prefix: &str,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(Client, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
let ctx = base_ctx.with_target(server)?;
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

@ -1,8 +1,6 @@
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::RunId;
use fabro_types::settings::SettingsLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use super::output::{api_diagnostics_to_local, print_preflight_workflow_summary};
@ -22,10 +20,8 @@ pub(crate) struct CreatedRun {
pub(crate) async fn create_run(
ctx: &CommandContext,
args: &RunArgs,
_cli_defaults: SettingsLayer,
styles: &Styles,
quiet: bool,
printer: Printer,
) -> anyhow::Result<CreatedRun> {
let workflow_path = args
.workflow
@ -51,6 +47,7 @@ pub(crate) async fn create_run(
})?;
let client = ctx.server().await?;
if !quiet {
let printer = ctx.printer();
let preflight = client.run_preflight(built.manifest.clone()).await?;
let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics);
if !diagnostics

View file

@ -10,9 +10,6 @@
use std::io::{self, IsTerminal, Write};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
use crate::args::DiffArgs;
@ -20,21 +17,16 @@ use crate::command_context::CommandContext;
use crate::server_client::RunProjection;
use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: DiffArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn run(args: DiffArgs, base_ctx: &CommandContext) -> Result<()> {
info!(run_id = %args.run, "Showing diff");
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let ctx = base_ctx.with_target(&args.server)?;
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?;
let patch = resolve_diff(&state, &args)?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
let value = serde_json::json!({
"run_id": run_id,
"node": args.node,

View file

@ -1,8 +1,5 @@
use anyhow::{Context, Result};
use fabro_checkpoint::git::Store;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork};
use git2::Repository;
@ -13,15 +10,10 @@ 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: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
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?;
@ -34,7 +26,7 @@ pub(crate) async fn run(
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
if args.list {
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?;
return Ok(());
}
@ -56,7 +48,7 @@ pub(crate) async fn run(
let run_id_string = run_id.to_string();
let new_run_id_string = new_run_id.to_string();
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
let target = args.target.clone().unwrap_or_else(|| "latest".to_string());
print_json_pretty(&serde_json::json!({
"source_run_id": run_id_string,

View file

@ -13,10 +13,7 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use chrono::{DateTime, Utc};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::json::normalize_json_value;
use fabro_util::printer::Printer;
use fabro_util::redact::redact_jsonl_line;
use fabro_util::terminal::Styles;
use tokio::time;
@ -29,14 +26,8 @@ use crate::shared::format_usd_micros;
const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500);
pub(crate) async fn run(
args: &LogsArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn run(args: &LogsArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
info!(run_id = %run_id, "Showing logs");
@ -60,7 +51,7 @@ pub(crate) async fn run(
let stdout = io::stdout();
let is_tty = stdout.is_terminal();
let mut out = stdout.lock();
let pretty = args.pretty && cli.output.format != OutputFormat::Json;
let pretty = args.pretty && !ctx.json_output();
for line in &filtered {
if pretty {

View file

@ -1,13 +1,9 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs};
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config::load_settings_with_storage_dir;
pub(crate) mod attach;
pub(crate) mod command;
@ -27,29 +23,16 @@ pub(crate) mod ssh;
pub(crate) mod start;
pub(crate) mod wait;
pub(crate) async fn dispatch(
cmd: RunCommands,
cli: &CliNamespace,
cli_layer: &CliLayer,
_process_local_json: bool,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(cmd: RunCommands, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
match cmd {
RunCommands::Run(args) => Box::pin(command::execute(args, cli, cli_layer, printer)).await,
RunCommands::Run(args) => Box::pin(command::execute(args, base_ctx)).await,
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_layer)?;
let created_run = Box::pin(create::create_run(
&ctx,
&args,
cli_defaults,
styles,
true,
printer,
))
.await?;
if cli.output.format == OutputFormat::Json {
let ctx = base_ctx.with_target(&args.target)?;
let created_run = Box::pin(create::create_run(&ctx, &args, styles, true)).await?;
if ctx.json_output() {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
} else {
fabro_util::printout!(printer, "{}", created_run.run_id);
@ -57,27 +40,28 @@ pub(crate) async fn dispatch(
Ok(())
}
RunCommands::Start(StartArgs { server, run }) => {
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
let ctx = base_ctx.with_target(&server)?;
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?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
}
Ok(())
}
RunCommands::Attach(AttachArgs { server, run }) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
let ctx = base_ctx.with_target(&server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&run).await?.run_id;
let json = ctx.json_output();
let exit_code = Box::pin(attach::attach_run_with_client(
client.as_ref(),
&run_id,
false,
styles,
cli.output.format == OutputFormat::Json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
json,
ctx.verbose(),
printer,
))
.await?;
@ -104,34 +88,31 @@ pub(crate) async fn dispatch(
)
.await
}
RunCommands::Diff(args) => diff::run(args, cli, cli_layer, printer).await,
RunCommands::Diff(args) => diff::run(args, base_ctx).await,
RunCommands::Logs(args) => {
let styles = Styles::detect_stdout();
logs::run(&args, &styles, cli, cli_layer, printer).await
logs::run(&args, &styles, base_ctx).await
}
RunCommands::Resume(args) => {
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_layer)?;
let ctx = base_ctx.with_target(&args.server)?;
crate::sleep_inhibitor::guard(ctx.user_settings().cli.exec.prevent_idle_sleep)
};
Box::pin(resume::resume_command(
args, styles, cli, cli_layer, printer,
))
.await
Box::pin(resume::resume_command(args, styles, base_ctx)).await
}
RunCommands::Rewind(args) => {
let styles = Styles::detect_stderr();
Box::pin(rewind::run(&args, &styles, cli, cli_layer, printer)).await
Box::pin(rewind::run(&args, &styles, base_ctx)).await
}
RunCommands::Fork(args) => {
let styles = Styles::detect_stderr();
Box::pin(fork::run(&args, &styles, cli, cli_layer, printer)).await
Box::pin(fork::run(&args, &styles, base_ctx)).await
}
RunCommands::Wait(args) => {
let styles = Styles::detect_stderr();
wait::run(&args, &styles, cli, cli_layer, printer).await
wait::run(&args, &styles, base_ctx).await
}
}
}

View file

@ -1,21 +1,13 @@
use anyhow::{Context, Result};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::PreviewArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: PreviewArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn run(args: PreviewArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
let expires_in_secs =
@ -31,7 +23,8 @@ pub(crate) async fn run(
info!(run_id = %args.run, port = args.port, "Generating preview URL");
if cli.output.format == OutputFormat::Json {
let json = ctx.json_output();
if json {
match response.token {
Some(token) => {
print_json_pretty(&serde_json::json!({ "url": response.url, "token": token }))?;
@ -56,7 +49,7 @@ pub(crate) async fn run(
}
}
if args.open && !process_local_json {
if should_open_browser(args.open, json) {
#[expect(
clippy::disallowed_methods,
reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process."
@ -83,3 +76,23 @@ fn format_standard_output(url: &str, token: &str) -> String {
fn format_signed_output(url: &str) -> String {
format!("{url}\n")
}
fn should_open_browser(open_requested: bool, json: bool) -> bool {
open_requested && !json
}
#[cfg(test)]
mod tests {
use super::should_open_browser;
#[test]
fn json_output_suppresses_browser_opening() {
assert!(!should_open_browser(true, true));
}
#[test]
fn text_output_honors_browser_opening() {
assert!(should_open_browser(true, false));
assert!(!should_open_browser(false, false));
}
}

View file

@ -1,6 +1,3 @@
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::ResumeArgs;
@ -15,17 +12,16 @@ use crate::shared::print_json_pretty;
pub(crate) async fn resume_command(
args: ResumeArgs,
styles: &'static Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.run_id;
super::start::start_run_with_client(client.as_ref(), &run_id, true).await?;
let json = cli.output.format == OutputFormat::Json;
let json = ctx.json_output();
if args.detach {
if json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
@ -39,7 +35,7 @@ pub(crate) async fn resume_command(
true,
styles,
json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
ctx.verbose(),
printer,
))
.await?;

View file

@ -3,8 +3,6 @@ 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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::{EventBody, RunEvent};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -33,12 +31,11 @@ pub(crate) struct TimelineEntryJson {
pub(crate) async fn run(
args: &RewindArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
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?;
@ -54,7 +51,7 @@ pub(crate) async fn run(
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
if args.list || args.target.is_none() {
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&timeline_entries_json(&timeline))?;
return Ok(());
}
@ -79,7 +76,7 @@ pub(crate) async fn run(
let run_id_string = run_id.to_string();
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"run_id": run_id_string,
"target": target_arg,

View file

@ -1,25 +1,17 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{SshArgs, require_no_json_override};
use crate::args::SshArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: SshArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
if process_local_json && !args.print {
require_no_json_override(process_local_json)?;
pub(crate) async fn run(args: SshArgs, base_ctx: &CommandContext) -> Result<()> {
if !args.print {
base_ctx.require_no_json_override()?;
}
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
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?;
@ -27,7 +19,7 @@ pub(crate) async fn run(
info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access");
if args.print {
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({ "command": ssh.command }))?;
} else {
{

View file

@ -11,8 +11,6 @@ use std::io::Write;
use anyhow::{Result, bail};
use fabro_types::RunId;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_workflow::records::Conclusion;
@ -24,14 +22,9 @@ use crate::args::WaitArgs;
use crate::command_context::CommandContext;
use crate::shared::{format_duration_ms, format_usd_micros, run_status_kind};
pub(crate) async fn run(
args: &WaitArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn run(args: &WaitArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
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");
@ -63,7 +56,7 @@ pub(crate) async fn run(
let conclusion = client.get_run_state(&run_id).await?.conclusion;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
let json_value = build_json_output(final_status, &run_id, conclusion.as_ref());
let mut out = std::io::stdout().lock();
serde_json::to_writer_pretty(&mut out, &json_value)?;

View file

@ -1,46 +1,24 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use super::short_run_id;
use crate::args::{RunsArchiveArgs, RunsUnarchiveArgs};
use crate::command_context::CommandContext;
use crate::server_client;
use crate::shared::print_json_pretty;
pub(crate) async fn archive_command(
args: &RunsArchiveArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
run_bulk(
Action::Archive,
&args.runs,
ctx.server().await?.as_ref(),
cli,
printer,
)
.await
let ctx = base_ctx.with_target(&args.server)?;
run_bulk(Action::Archive, &args.runs, &ctx).await
}
pub(crate) async fn unarchive_command(
args: &RunsUnarchiveArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
run_bulk(
Action::Unarchive,
&args.runs,
ctx.server().await?.as_ref(),
cli,
printer,
)
.await
let ctx = base_ctx.with_target(&args.server)?;
run_bulk(Action::Unarchive, &args.runs, &ctx).await
}
#[derive(Clone, Copy)]
@ -62,14 +40,11 @@ impl Action {
}
}
async fn run_bulk(
action: Action,
identifiers: &[String],
client: &server_client::Client,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
async fn run_bulk(action: Action, identifiers: &[String], ctx: &CommandContext) -> Result<()> {
let client = ctx.server().await?;
let client = client.as_ref();
let json = ctx.json_output();
let printer = ctx.printer();
let mut had_errors = false;
let mut changed = Vec::new();
let mut errors = Vec::new();

View file

@ -1,6 +1,4 @@
use anyhow::Result;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_workflow::run_status::RunStatus;
use serde::Serialize;
@ -20,8 +18,9 @@ pub(crate) struct InspectOutput {
pub sandbox: Option<serde_json::Value>,
}
pub(crate) async fn run(args: &InspectArgs, cli_layer: &CliLayer, printer: Printer) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn run(args: &InspectArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
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,9 +4,6 @@ use anyhow::Result;
use chrono::Utc;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::text::strip_goal_decoration;
use fabro_workflow::run_status::RunStatus;
@ -20,11 +17,10 @@ use crate::shared::{color_if, format_duration_ms, run_status_kind, tilde_path};
pub(crate) async fn list_command(
args: &RunsListArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
let ctx = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_server_runs(
@ -35,7 +31,7 @@ pub(crate) async fn list_command(
!args.all,
);
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
let json_rows: Vec<_> = filtered
.iter()
.map(|run| {

View file

@ -1,35 +1,24 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::RunsCommands;
use crate::command_context::CommandContext;
pub(crate) mod archive;
pub(crate) mod inspect;
pub(crate) mod list;
pub(crate) mod rm;
pub(crate) async fn dispatch(
cmd: RunsCommands,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(cmd: RunsCommands, base_ctx: &CommandContext) -> Result<()> {
match cmd {
RunsCommands::Ps(args) => {
let styles = Styles::detect_stdout();
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_layer, printer).await,
RunsCommands::Archive(args) => {
archive::archive_command(&args, cli, cli_layer, printer).await
}
RunsCommands::Unarchive(args) => {
archive::unarchive_command(&args, cli, cli_layer, printer).await
list::list_command(&args, &styles, base_ctx).await
}
RunsCommands::Rm(args) => rm::remove_command(&args, base_ctx).await,
RunsCommands::Inspect(args) => inspect::run(&args, base_ctx).await,
RunsCommands::Archive(args) => archive::archive_command(&args, base_ctx).await,
RunsCommands::Unarchive(args) => archive::unarchive_command(&args, base_ctx).await,
}
}

View file

@ -1,7 +1,4 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use super::short_run_id;
use crate::args::RunsRemoveArgs;
@ -9,23 +6,16 @@ use crate::command_context::CommandContext;
use crate::server_client;
use crate::shared::print_json_pretty;
pub(crate) async fn remove_command(
args: &RunsRemoveArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
remove_from(args, ctx.server().await?.as_ref(), cli, printer).await
pub(crate) async fn remove_command(args: &RunsRemoveArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
remove_from(args, &ctx).await
}
async fn remove_from(
args: &RunsRemoveArgs,
client: &server_client::Client,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
async fn remove_from(args: &RunsRemoveArgs, ctx: &CommandContext) -> Result<()> {
let client = ctx.server().await?;
let client = client.as_ref();
let json = ctx.json_output();
let printer = ctx.printer();
let mut had_errors = false;
let mut removed = Vec::new();
let mut errors = Vec::new();

View file

@ -1,24 +1,12 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::SandboxCommand;
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
command: SandboxCommand,
cli: &CliNamespace,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(command: SandboxCommand, base_ctx: &CommandContext) -> Result<()> {
match command {
SandboxCommand::Cp(args) => super::run::cp::cp_command(args, cli, cli_layer, printer).await,
SandboxCommand::Preview(args) => {
super::run::preview::run(args, cli, cli_layer, process_local_json, printer).await
}
SandboxCommand::Ssh(args) => {
super::run::ssh::run(args, cli, cli_layer, process_local_json, printer).await
}
SandboxCommand::Cp(args) => super::run::cp::cp_command(args, base_ctx).await,
SandboxCommand::Preview(args) => super::run::preview::run(args, base_ctx).await,
SandboxCommand::Ssh(args) => super::run::ssh::run(args, base_ctx).await,
}
}

View file

@ -2,13 +2,10 @@ use anyhow::Result;
use chrono::{DateTime, Utc};
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::SecretListArgs;
use crate::server_client::Client;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
@ -22,14 +19,11 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
}
}
pub(super) async fn list_command(
client: &Client,
_args: &SecretListArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(super) async fn list_command(_args: &SecretListArgs, ctx: &CommandContext) -> Result<()> {
let client = ctx.server().await?;
let printer = ctx.printer();
let secrets = client.list_secrets().await?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&secrets)?;
return Ok(());
}

View file

@ -3,24 +3,15 @@ mod rm;
mod set;
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{SecretCommand, SecretNamespace};
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
ns: SecretNamespace,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&ns.target, printer, cli_layer)?;
let server = ctx.server().await?;
pub(crate) async fn dispatch(ns: SecretNamespace, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&ns.target)?;
match ns.command {
SecretCommand::List(args) => list::list_command(&server, &args, cli, printer).await,
SecretCommand::Rm(args) => rm::rm_command(&server, &args, cli, printer).await,
SecretCommand::Set(args) => set::set_command(&server, &args, cli, printer).await,
SecretCommand::List(args) => list::list_command(&args, &ctx).await,
SecretCommand::Rm(args) => rm::rm_command(&args, &ctx).await,
SecretCommand::Set(args) => set::set_command(&args, &ctx).await,
}
}

View file

@ -1,23 +1,16 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::SecretRmArgs;
use crate::server_client::Client;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(super) async fn rm_command(
client: &Client,
args: &SecretRmArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(super) async fn rm_command(args: &SecretRmArgs, ctx: &CommandContext) -> Result<()> {
let client = ctx.server().await?;
client.delete_secret_by_name(&args.key).await?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
} else {
fabro_util::printerr!(printer, "Removed {}", args.key);
fabro_util::printerr!(ctx.printer(), "Removed {}", args.key);
}
Ok(())
}

View file

@ -11,13 +11,10 @@ use std::io::{IsTerminal, Read as _};
use anyhow::{Context as _, Result, bail};
use fabro_api::types;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use tokio::task::spawn_blocking;
use crate::args::{SecretSetArgs, SecretTypeArg};
use crate::server_client::Client;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::shared::provider_auth::prompt_password;
@ -57,13 +54,9 @@ async fn resolve_value(args: &SecretSetArgs) -> Result<String> {
bail!("secret value required: pass <VALUE>, use --value-stdin, or run interactively")
}
pub(super) async fn set_command(
client: &Client,
args: &SecretSetArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(super) async fn set_command(args: &SecretSetArgs, ctx: &CommandContext) -> Result<()> {
let value = resolve_value(args).await?;
let client = ctx.server().await?;
let meta = client
.create_secret(types::CreateSecretRequest {
name: args.key.clone(),
@ -72,10 +65,10 @@ pub(super) async fn set_command(
description: args.description.clone(),
})
.await?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&meta)?;
} else {
fabro_util::printerr!(printer, "Set {}", meta.name);
fabro_util::printerr!(ctx.printer(), "Set {}", meta.name);
}
Ok(())
}

View file

@ -3,23 +3,15 @@ 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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::DfArgs;
use crate::command_context::CommandContext;
use crate::shared::{format_size, print_json_pretty};
pub(super) async fn df_command(
args: &DfArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
pub(super) async fn df_command(args: &DfArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_connection(&args.connection)?;
let server = ctx.server().await?;
let json = cli.output.format == OutputFormat::Json;
let json = ctx.json_output();
let (output, storage_dir) = if json {
(server.get_system_disk_usage(args.verbose).await?, None)

View file

@ -1,8 +1,5 @@
use anyhow::Result;
use fabro_client::sse;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use futures::StreamExt;
use crate::args::SystemEventsArgs;
@ -10,16 +7,14 @@ use crate::command_context::CommandContext;
pub(super) async fn events_command(
args: &SystemEventsArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
let ctx = base_ctx.with_connection(&args.connection)?;
let server = ctx.server().await?;
let mut stream = server.attach_events(&args.run_ids).await?;
let mut pending = Vec::new();
let json = cli.output.format == OutputFormat::Json;
let json = ctx.json_output();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow::anyhow!("{err}"))?;
pending.extend_from_slice(&chunk);

View file

@ -1,23 +1,15 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::SystemInfoArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(super) async fn info_command(
args: &SystemInfoArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
pub(super) async fn info_command(args: &SystemInfoArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_connection(&args.connection)?;
let server = ctx.server().await?;
let response = server.get_system_info().await?;
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&response)?;
return Ok(());
}

View file

@ -4,23 +4,16 @@ mod info;
mod prune;
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
pub(crate) use prune::parse_duration;
use crate::args::{SystemCommand, SystemNamespace};
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
ns: SystemNamespace,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn dispatch(ns: SystemNamespace, base_ctx: &CommandContext) -> Result<()> {
match ns.command {
SystemCommand::Info(args) => info::info_command(&args, cli, cli_layer, printer).await,
SystemCommand::Prune(args) => prune::prune_command(&args, cli, cli_layer, printer).await,
SystemCommand::Df(args) => df::df_command(&args, cli, cli_layer, printer).await,
SystemCommand::Events(args) => events::events_command(&args, cli, cli_layer, printer).await,
SystemCommand::Info(args) => info::info_command(&args, base_ctx).await,
SystemCommand::Prune(args) => prune::prune_command(&args, base_ctx).await,
SystemCommand::Df(args) => df::df_command(&args, base_ctx).await,
SystemCommand::Events(args) => events::events_command(&args, base_ctx).await,
}
}

View file

@ -2,8 +2,6 @@ use std::collections::HashMap;
use anyhow::{Context, Result, bail};
use fabro_api::types;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
@ -11,13 +9,9 @@ use crate::args::RunsPruneArgs;
use crate::command_context::CommandContext;
use crate::shared::{format_size, print_json_pretty};
pub(super) async fn prune_command(
args: &RunsPruneArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
pub(super) async fn prune_command(args: &RunsPruneArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_connection(&args.connection)?;
let printer = ctx.printer();
let server = ctx.server().await?;
let response = server
.prune_runs(types::PruneRunsRequest {
@ -29,7 +23,7 @@ pub(super) async fn prune_command(
workflow: args.filter.workflow.clone(),
})
.await?;
prune_from(&response, cli.output.format == OutputFormat::Json, printer)
prune_from(&response, ctx.json_output(), printer)
}
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {

View file

@ -15,14 +15,13 @@ use std::time::Duration;
use anyhow::{Context, Result};
use fabro_config::Storage;
use fabro_config::daemon::ServerDaemon;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::Home;
use fabro_util::printer::Printer;
use serde::Serialize;
use tracing::warn;
use crate::args::UninstallArgs;
use crate::command_context::CommandContext;
use crate::commands::server::stop;
use crate::shared::{format_size, print_json_pretty, tilde_path};
use crate::{local_server, user_config};
@ -43,12 +42,9 @@ struct Inventory {
clippy::unused_async,
reason = "The shared command dispatch path expects an async handler."
)]
pub(crate) async fn run_uninstall(
args: &UninstallArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
pub(crate) async fn run_uninstall(args: &UninstallArgs, ctx: &CommandContext) -> Result<()> {
let json = ctx.json_output();
let printer = ctx.printer();
let home = Home::from_env();
let home_root = home.root().to_path_buf();

View file

@ -23,6 +23,7 @@ use tokio::task::JoinHandle;
use tracing::debug;
use crate::args::UpgradeArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
// ── Download backend abstraction ───────────────────────────────────────────
@ -434,11 +435,9 @@ impl UpgradeCheckState {
// ── Main upgrade command ───────────────────────────────────────────────────
pub(crate) async fn run_upgrade(
args: UpgradeArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(crate) async fn run_upgrade(args: UpgradeArgs, ctx: &CommandContext) -> Result<()> {
let cli = &ctx.user_settings().cli;
let printer = ctx.printer();
let current_exe = std::env::current_exe()
.context("resolving current fabro executable path")?
.canonicalize()

View file

@ -1,9 +1,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::{CliNamespace, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_types::settings::SettingsLayer;
use fabro_util::terminal::Styles;
use crate::args::ValidateArgs;
@ -15,11 +13,10 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
pub(crate) async fn run(
args: &ValidateArgs,
styles: &Styles,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),
@ -33,7 +30,7 @@ pub(crate) async fn run(
let response = client.run_preflight(built.manifest).await?;
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"workflow_name": response.workflow.name,
"nodes": response.workflow.nodes,

View file

@ -6,8 +6,6 @@
use std::io::IsTerminal;
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use serde_json::{Map, Value, json};
@ -16,14 +14,10 @@ use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config::{self, ServerTarget};
pub(crate) async fn version_command(
args: &VersionArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
pub(crate) async fn version_command(args: &VersionArgs, base_ctx: &CommandContext) -> Result<()> {
let client = client_info();
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
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 {
@ -49,7 +43,7 @@ pub(crate) async fn version_command(
},
};
if cli.output.format == OutputFormat::Json {
if ctx.json_output() {
print_json_pretty(&json_output(&client, &server_info))?;
return Ok(());
}

View file

@ -7,18 +7,13 @@ use std::path::Path;
use anyhow::{Context, Result, bail};
use fabro_config::project::{discover_project_config, resolve_fabro_root};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::WorkflowCreateArgs;
use crate::command_context::CommandContext;
use crate::shared::{print_json_pretty, relative_path};
pub(super) fn create_command(
args: &WorkflowCreateArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(super) fn create_command(args: &WorkflowCreateArgs, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let cwd = std::env::current_dir()?;
let Some((config_path, config)) = discover_project_config(&cwd)? else {
@ -31,7 +26,7 @@ pub(super) fn create_command(
let fabro_root = resolve_fabro_root(&config_path, &config);
let created = write_workflow_scaffold(args, &fabro_root)?;
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
let created: Vec<_> = created.iter().map(|path| relative_path(path)).collect();
print_json_pretty(&serde_json::json!({
"name": args.name,

View file

@ -5,21 +5,17 @@ use fabro_config::project::{
WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed,
resolve_fabro_root,
};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::WorkflowListArgs;
use crate::command_context::CommandContext;
use crate::shared::{color_if, print_json_pretty, relative_path};
const GOAL_MAX_LEN: usize = 60;
pub(super) fn list_command(
_args: &WorkflowListArgs,
cli: &CliNamespace,
printer: Printer,
) -> Result<()> {
pub(super) fn list_command(_args: &WorkflowListArgs, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let styles = Styles::detect_stderr();
let cwd = std::env::current_dir()?;
@ -36,7 +32,7 @@ pub(super) fn list_command(
let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref());
if cli.output.format == OutputFormat::Json {
if base_ctx.json_output() {
print_json_pretty(&workflows)?;
return Ok(());
}

View file

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

View file

@ -24,12 +24,10 @@ use std::ffi::OsString;
use anyhow::Result;
use args::{
Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace,
global_args_cli_layer, printer_from_verbosity, require_no_json_override,
global_args_cli_layer, require_no_json_override,
};
use clap::{CommandFactory, Parser};
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::{Combine, SettingsLayer};
use fabro_util::exit::ExitClass;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -37,6 +35,8 @@ use fabro_util::{browser, exit};
use rustls::crypto::ring::default_provider;
use tracing::debug;
use crate::command_context::CommandContext;
#[derive(Parser)]
#[command(name = "fabro", version, long_version = LONG_VERSION)]
struct Cli {
@ -164,23 +164,14 @@ async fn main_inner() -> (String, Result<()>) {
Err(err) => return (command_name, Err(err)),
};
let user_settings = match user_config::load_settings() {
Ok(settings) => settings,
let base_ctx = match CommandContext::from_disk(&cli_layer, process_local_json) {
Ok(ctx) => ctx,
Err(err) => return (command_name, Err(err)),
};
let combined_settings = SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
}
.combine(user_settings);
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);
let printer = base_ctx.printer();
let config_log_level = match &pre_tracing_bootstrap.sink {
logging::InternalLogSink::Cli => cli_settings.logging.level.clone(),
logging::InternalLogSink::Cli => base_ctx.user_settings().cli.logging.level.clone(),
logging::InternalLogSink::Server { .. } => pre_tracing_bootstrap.config_log_level.clone(),
};
if let Err(err) = logging::init_tracing(
@ -204,57 +195,44 @@ async fn main_inner() -> (String, Result<()>) {
| Commands::Repo(_)
| Commands::Install { .. }
) {
commands::upgrade::spawn_upgrade_check(cli_settings.updates.check, printer)
commands::upgrade::spawn_upgrade_check(base_ctx.user_settings().cli.updates.check, printer)
} else {
None
};
let result = Box::pin(async move {
match *command {
Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?,
Commands::Exec(args) => {
commands::exec::execute(args, &base_ctx).await?;
}
Commands::RunCmd(cmd) => {
Box::pin(commands::run::dispatch(
cmd,
&cli_settings,
&cli_layer,
process_local_json,
printer,
))
.await?;
Box::pin(commands::run::dispatch(cmd, &base_ctx)).await?;
}
Commands::Preflight(args) => {
commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?;
commands::preflight::execute(args, &base_ctx).await?;
}
Commands::Validate(args) => {
let styles = Styles::detect_stderr();
commands::validate::run(&args, &styles, &cli_settings, &cli_layer, printer).await?;
commands::validate::run(&args, &styles, &base_ctx).await?;
}
Commands::Graph(args) => {
let styles = Styles::detect_stderr();
commands::graph::run(
&args,
&styles,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
commands::graph::run(&args, &styles, &base_ctx).await?;
}
Commands::Parse(args) => {
commands::parse::run(&args, &cli_settings, printer)?;
commands::parse::run(&args)?;
}
Commands::Artifact(ns) => {
commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
commands::artifact::dispatch(ns, &base_ctx).await?;
}
Commands::Dump(args) => {
commands::dump::run(&args, &cli_settings, &cli_layer, printer).await?;
commands::dump::run(&args, &base_ctx).await?;
}
Commands::RunsCmd(cmd) => {
commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?;
commands::runs::dispatch(cmd, &base_ctx).await?;
}
Commands::Model { command } => {
commands::model::execute(command, &cli_settings, &cli_layer, printer).await?;
commands::model::execute(command, &base_ctx).await?;
}
Commands::Server(ns) => {
Box::pin(commands::server::dispatch(
@ -266,21 +244,11 @@ async fn main_inner() -> (String, Result<()>) {
.await?;
}
Commands::Doctor(args) => {
let verbose =
args.verbose || cli_settings.output.verbosity == OutputVerbosity::Verbose;
let exit_code = Box::pin(commands::doctor::run_doctor(
&args,
verbose,
&cli_settings,
&cli_layer,
printer,
))
.await?;
let exit_code = Box::pin(commands::doctor::run_doctor(&args, &base_ctx)).await?;
std::process::exit(exit_code);
}
Commands::Version(args) => {
commands::version::version_command(&args, &cli_settings, &cli_layer, printer)
.await?;
commands::version::version_command(&args, &base_ctx).await?;
}
Commands::Discord => {
if process_local_json {
@ -301,65 +269,40 @@ async fn main_inner() -> (String, Result<()>) {
}
}
Commands::Repo(ns) => {
commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
commands::repo::dispatch(ns, &base_ctx).await?;
}
Commands::Install { args, command } => {
Box::pin(commands::install::execute(
&args,
command,
&cli_settings,
&cli_layer,
process_local_json,
printer,
))
.await?;
Box::pin(commands::install::execute(&args, command, &base_ctx)).await?;
}
Commands::Uninstall(args) => {
commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?;
commands::uninstall::run_uninstall(&args, &base_ctx).await?;
}
Commands::Auth(ns) => {
commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?;
commands::auth::dispatch(ns, &base_ctx).await?;
}
Commands::Pr(ns) => {
Box::pin(commands::pr::dispatch(
ns,
&cli_settings,
&cli_layer,
printer,
))
.await?;
Box::pin(commands::pr::dispatch(ns, &base_ctx)).await?;
}
Commands::Secret(ns) => {
commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
commands::secret::dispatch(ns, &base_ctx).await?;
}
Commands::Settings(args) => {
Box::pin(commands::config::execute(
&args,
&cli_settings,
&cli_layer,
printer,
))
.await?;
Box::pin(commands::config::execute(&args, &base_ctx)).await?;
}
Commands::Workflow(ns) => {
commands::workflow::dispatch(ns, &base_ctx)?;
}
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?,
Commands::Upgrade(args) => {
commands::upgrade::run_upgrade(args, &cli_settings, printer).await?;
commands::upgrade::run_upgrade(args, &base_ctx).await?;
}
Commands::Provider(ns) => {
commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?;
commands::provider::dispatch(ns, &base_ctx).await?;
}
Commands::Sandbox { command } => {
commands::sandbox::dispatch(
command,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
commands::sandbox::dispatch(command, &base_ctx).await?;
}
Commands::System(ns) => {
commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
commands::system::dispatch(ns, &base_ctx).await?;
}
Commands::Completion(args) => {
require_no_json_override(process_local_json)?;

View file

@ -48,6 +48,30 @@ fn settings_uses_json_output_format_from_home_config() {
assert!(value.is_object());
}
#[test]
fn auth_status_ignores_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let output = context
.command()
.args(["auth", "status"])
.output()
.expect("command should run");
assert!(output.status.success());
assert!(
output.stdout.is_empty(),
"stdout should stay empty in text mode"
);
let stderr = output_stderr(&output);
assert!(stderr.contains("Not logged in to any servers."));
assert!(stderr.contains("Dev token:"));
}
#[test]
fn secret_list_uses_json_output_format_from_home_config() {
let context = test_context!();

View file

@ -1,6 +1,31 @@
use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
use serde_json::{Value, json};
use super::support::setup_local_sandbox_run;
use crate::support::unique_run_id;
fn remote_run_summary(run_id: &str) -> serde_json::Value {
json!({
"run_id": run_id,
"workflow_name": "Preview Test",
"workflow_slug": "preview-test",
"goal": "Preview test",
"title": "Preview test",
"labels": {},
"host_repo_path": "/srv/repo",
"repository": { "name": "repo" },
"start_time": "2026-04-19T12:00:00Z",
"created_at": "2026-04-19T12:00:00Z",
"status": {
"kind": "running"
},
"pending_control": null,
"duration_ms": null,
"elapsed_secs": null,
"total_usd_micros": null
})
}
#[test]
fn help() {
@ -49,3 +74,55 @@ fn sandbox_preview_rejects_non_daytona_run() {
error: Sandbox provider does not support this capability.
");
}
#[test]
fn sandbox_preview_open_is_suppressed_by_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let server = MockServer::start();
let run_id = unique_run_id();
let resolve_run = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/runs/resolve")
.query_param("selector", run_id.as_str());
then.status(200)
.header("content-type", "application/json")
.body(remote_run_summary(&run_id).to_string());
});
let preview = server.mock(|when, then| {
when.method("POST")
.path(format!("/api/v1/runs/{run_id}/preview"))
.json_body(json!({
"port": 3000,
"expires_in_secs": 3600,
"signed": true,
}));
then.status(201)
.header("content-type", "application/json")
.body(json!({ "url": "https://preview.example.test/app" }).to_string());
});
let mut cmd = context.preview();
cmd.args([
"--server",
&format!("{}/api/v1", server.base_url()),
"--open",
run_id.as_str(),
"3000",
]);
let output = cmd.output().expect("command should run");
assert!(output.status.success(), "sandbox preview should succeed");
let value: Value = serde_json::from_slice(&output.stdout).expect("preview JSON should parse");
assert_eq!(
value,
json!({
"url": "https://preview.example.test/app"
})
);
resolve_run.assert();
preview.assert();
}

View file

@ -1,4 +1,5 @@
use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
#[test]
fn help() {
@ -26,3 +27,87 @@ fn help() {
----- stderr -----
");
}
#[test]
fn system_events_renders_text_lines_from_sse_payloads() {
let context = test_context!();
let server = MockServer::start();
let run_id = crate::support::unique_run_id();
let payload = serde_json::json!({
"payload": {
"ts": "2026-04-05T12:00:00Z",
"run_id": run_id,
"event": "run.completed",
}
});
let attach_mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/attach")
.query_param("run_id", run_id.as_str());
then.status(200)
.header("Content-Type", "text/event-stream")
.body(format!("data: {payload}\n\n"));
});
let output = context
.command()
.args([
"system",
"events",
"--server",
&format!("{}/api/v1", server.base_url()),
"--run-id",
&run_id,
])
.output()
.expect("command should run");
assert!(output.status.success(), "system events failed");
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
assert_eq!(
stdout.trim(),
format!("2026-04-05T12:00:00Z {} run.completed", &run_id[..12])
);
attach_mock.assert();
}
#[test]
fn system_events_json_emits_raw_sse_payloads() {
let context = test_context!();
let server = MockServer::start();
let run_id = crate::support::unique_run_id();
let payload = serde_json::json!({
"payload": {
"ts": "2026-04-05T12:00:00Z",
"run_id": run_id,
"event": "run.completed",
}
});
let attach_mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/attach")
.query_param("run_id", run_id.as_str());
then.status(200)
.header("Content-Type", "text/event-stream")
.body(format!("data: {payload}\n\n"));
});
let output = context
.command()
.args([
"--json",
"system",
"events",
"--server",
&format!("{}/api/v1", server.base_url()),
"--run-id",
&run_id,
])
.output()
.expect("command should run");
assert!(output.status.success(), "system events failed");
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
assert_eq!(stdout.trim(), payload.to_string());
attach_mock.assert();
}

View file

@ -48,3 +48,41 @@ fn system_info_json_reports_runtime_fields() {
assert!(value["uptime_secs"].is_number());
assert!(value["runs"]["total"].is_number());
}
#[test]
fn system_info_uses_explicit_storage_dir_override() {
let mut context = test_context!();
let storage_dir = context.temp_dir.join("alternate-storage");
std::fs::create_dir_all(&storage_dir).unwrap();
context.write_home(
".fabro/settings.toml",
format!(
"_version = 1\n\n[server.storage]\nroot = {:?}\n",
context.storage_dir.display().to_string()
),
);
context.ensure_home_server_auth_methods();
context.manage_storage_dir(&storage_dir);
let output = context
.command()
.args([
"--json",
"system",
"info",
"--storage-dir",
storage_dir.to_str().unwrap(),
])
.output()
.expect("command should run");
assert!(
output.status.success(),
"system info failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let value: Value =
serde_json::from_slice(&output.stdout).expect("system info JSON should parse");
assert_eq!(value["storage_dir"], storage_dir.display().to_string());
}