refactor: align cli command boundaries on CommandContext

This commit is contained in:
Bryan Helmkamp 2026-04-23 01:08:14 -04:00
parent ec18c1864b
commit fbe0bdfbc1
No known key found for this signature in database
57 changed files with 1445 additions and 746 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

@ -26,73 +26,55 @@ 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,
pub(crate) fn base(
printer: Printer,
cli_layer: &CliLayer,
process_local_json: bool,
) -> Result<Self> {
Self::new(
printer,
ServerMode::ByTarget {
target_override: args.server.clone(),
},
cli_layer,
)
Self::new(printer, process_local_json, ServerMode::None, cli_layer)
}
pub(crate) fn for_connection(
args: &ServerConnectionArgs,
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(),
})
}
fn new(
printer: Printer,
process_local_json: bool,
server_mode: ServerMode,
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> {
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 = combine_files(disk_settings, SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?;
let (machine_settings, user_settings) = load_merged_settings(cli_layer, &server_mode)?;
Ok(Self {
printer,
process_local_json,
cwd,
base_config_path,
cli_layer: cli_layer.clone(),
machine_settings,
user_settings,
server_mode,
@ -100,14 +82,18 @@ impl CommandContext {
})
}
#[allow(
dead_code,
reason = "This item is kept for command plumbing that is still being wired through."
)]
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<()> {
crate::args::require_no_json_override(self.process_local_json)
}
pub(crate) fn cwd(&self) -> &Path {
&self.cwd
}
@ -149,4 +135,193 @@ impl CommandContext {
Ok(Arc::clone(client))
}
fn with_server_mode(&self, server_mode: ServerMode) -> Result<Self> {
let (machine_settings, user_settings) = match &server_mode {
ServerMode::ByStorageDir { .. } => load_merged_settings(&self.cli_layer, &server_mode)?,
ServerMode::None | ServerMode::ByTarget { .. } => {
(self.machine_settings.clone(), self.user_settings.clone())
}
};
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 = combine_files(disk_settings, SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
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_types::settings::cli::{CliLayer, CliOutputLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;
use super::{CommandContext, ServerMode, merge_settings_layer};
use crate::args::ServerTargetArgs;
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 deriving_target_context_preserves_invocation_state() {
let base = synthetic_context(true, Printer::Verbose);
let derived = base
.with_target(&ServerTargetArgs {
server: Some("https://fabro.example.com".to_string()),
})
.expect("target context should derive");
assert_eq!(derived.printer(), Printer::Verbose);
assert!(derived.explicit_json_requested());
assert_eq!(derived.user_settings(), base.user_settings());
assert_eq!(derived.machine_settings(), base.machine_settings());
assert_eq!(derived.cwd(), base.cwd());
assert!(matches!(
derived.server_mode,
ServerMode::ByTarget { target_override: Some(ref target) }
if target == "https://fabro.example.com"
));
}
#[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 = fabro_config::user::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(|root| root.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(|root| root.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());
}
#[test]
fn target_resolution_errors_remain_deferred() {
let ctx = synthetic_context(false, Printer::Default)
.with_target(&ServerTargetArgs {
server: Some("not-a-valid-target".to_string()),
})
.expect("target derivation should not resolve the target eagerly");
assert!(matches!(ctx.server_mode, ServerMode::ByTarget { .. }));
}
}

View file

@ -6,28 +6,22 @@
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 fabro_types::settings::cli::OutputFormat;
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 +58,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"copied": [{
"relative_path": entry.relative_path,
@ -125,7 +119,7 @@ pub(super) async fn cp_command(
}
}
if cli.output.format == OutputFormat::Json {
if base_ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "copied": copied }))?;
} else {
fabro_util::printout!(

View file

@ -1,30 +1,24 @@
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_types::settings::cli::OutputFormat;
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.user_settings().cli.output.format == OutputFormat::Json {
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,7 @@ 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 fabro_types::settings::cli::OutputFormat;
use serde::Serialize;
use crate::args::SettingsArgs;
@ -28,10 +26,9 @@ struct RenderedConfig {
async fn rendered_config(
args: &SettingsArgs,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> anyhow::Result<serde_json::Value> {
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
let ctx = base_ctx.with_target(&args.target)?;
let user = fabro_config::UserSettings::resolve()?;
let server = ctx
.server()
@ -41,14 +38,9 @@ async fn rendered_config(
serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into)
}
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 {
pub(crate) async fn execute(args: &SettingsArgs, base_ctx: &CommandContext) -> anyhow::Result<()> {
let config = Box::pin(rendered_config(args, base_ctx)).await?;
if base_ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&config)?;
return Ok(());
}

View file

@ -3,8 +3,7 @@ use std::path::PathBuf;
use anyhow::Result;
use fabro_api::types as api_types;
use fabro_config::user::active_settings_path;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
pub(crate) use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
};
@ -142,13 +141,13 @@ 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.user_settings().cli.output.verbosity == OutputVerbosity::Verbose;
let printer = base_ctx.printer();
let styles = Styles::detect_stdout();
let json = cli.output.format == OutputFormat::Json;
let json = base_ctx.user_settings().cli.output.format == OutputFormat::Json;
let spinner = if json {
None
} else {
@ -179,7 +178,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

@ -13,13 +13,12 @@ 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_types::settings::cli::OutputFormat;
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 +27,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 base_ctx.explicit_json_requested() && 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 +70,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"path": absolute_or_current(output_path),
"format": args.format.to_string(),

View file

@ -52,6 +52,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::{
@ -1941,7 +1942,8 @@ async fn run_install_inner(
target: ServerTargetArgs::default(),
verbose: false,
};
doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await
let base_ctx = CommandContext::base(printer, cli_layer, false)?;
doctor::run_doctor(&doctor_args, &base_ctx).await
},
)
.await?

View file

@ -3,9 +3,7 @@ use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_api::types as api_types;
use fabro_model::{Catalog, Model, Provider};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::terminal::Styles;
use serde::Serialize;
@ -42,19 +40,22 @@ 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.user_settings().cli.output.format == OutputFormat::Json,
)
.await
}
fn format_context_window(tokens: i64) -> String {

View file

@ -1,22 +1,17 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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 = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let (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(base_ctx)?;
fabro_github::close_pull_request(
&creds,
@ -29,7 +24,7 @@ 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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,

View file

@ -5,9 +5,7 @@ use fabro_auth::configured_providers_from_process_env;
use fabro_config::Storage;
use fabro_model::Catalog;
use fabro_sandbox::daytona::detect_repo_info;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
use fabro_vault::Vault;
use fabro_workflow::outcome::StageStatus;
use fabro_workflow::pull_request::maybe_open_pull_request;
@ -25,13 +23,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 +80,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 +130,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&record)?;
} else {
fabro_util::printout!(printer, "{}", record.html_url);
}
}
None => {
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::Value::Null)?;
} else {
fabro_util::printout!(printer, "No pull request created (empty diff).");

View file

@ -1,9 +1,7 @@
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_types::settings::cli::OutputFormat;
use fabro_util::terminal::Styles;
use futures::future::join_all;
use serde::Serialize;
@ -23,13 +21,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 +36,7 @@ pub(super) async fn list_command(
}
if entries.is_empty() {
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&Vec::<PrRow>::new())?;
return Ok(());
}
@ -50,7 +44,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 +98,7 @@ pub(super) async fn list_command(
.collect()
};
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&rows)?;
return Ok(());
}

View file

@ -1,22 +1,17 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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 = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let (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(base_ctx)?;
fabro_github::merge_pull_request(
&creds,
@ -30,7 +25,7 @@ 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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,

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,10 +57,9 @@ fn load_github_credentials_required(
pub(crate) async fn load_pr_record(
server: &ServerTargetArgs,
run_id: &str,
cli_layer: &CliLayer,
printer: Printer,
base_ctx: &CommandContext,
) -> Result<(PullRequestRecord, 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_id).await?.run_id;
let state = client.get_run_state(&run_id).await?;

View file

@ -1,22 +1,17 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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 = base_ctx.with_target(&args.server)?;
let printer = ctx.printer();
let (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(base_ctx)?;
let detail = fabro_github::get_pull_request(
&creds,
@ -30,7 +25,7 @@ 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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&detail)?;
return Ok(());
}

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::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_util::terminal::Styles;
use crate::args::PreflightArgs;
@ -17,13 +15,13 @@ 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.user_settings().cli.output.verbosity == OutputVerbosity::Verbose;
let manifest = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
@ -38,7 +36,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.user_settings().cli.output.format == OutputFormat::Json {
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,10 @@
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 +21,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.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -29,7 +30,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<Str
);
}
if cli.output.format != OutputFormat::Json {
if base_ctx.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
"\n{}",

View file

@ -6,9 +6,7 @@
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 fabro_types::settings::cli::OutputFormat;
use tokio::process::Command as TokioCommand;
use tokio::task::spawn_blocking;
@ -36,10 +34,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 +76,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.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -112,7 +109,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.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -129,7 +126,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.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -138,7 +135,7 @@ draft = true
);
}
if cli.output.format != OutputFormat::Json {
if base_ctx.user_settings().cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
"\n{} Run a workflow with:\n\n {}",
@ -150,18 +147,15 @@ draft = true
);
}
if cli.output.format != OutputFormat::Json {
check_github_app_installation(&args.target, cli_layer, printer).await;
if base_ctx.user_settings().cli.output.format != OutputFormat::Json {
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 +193,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,24 @@ 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 fabro_types::settings::cli::OutputFormat;
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.user_settings().cli.output.format == OutputFormat::Json {
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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "removed": removed }))?;
}
Ok(())

View file

@ -1,7 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_util::terminal::Styles;
use crate::args::RunArgs;
@ -9,16 +7,13 @@ 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 printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.target)?;
let cli_defaults = load_settings_with_storage_dir(None)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
args.verbose =
args.verbose || ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose;
let quiet = args.detach;
let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep;
@ -50,7 +45,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.user_settings().cli.output.format == OutputFormat::Json;
if args.detach {
if json {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;

View file

@ -1,9 +1,7 @@
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 fabro_types::settings::cli::OutputFormat;
use tokio::fs;
use tracing::{debug, info};
@ -26,12 +24,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 +34,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 +44,7 @@ pub(crate) async fn cp_command(
None
};
if cli.output.format == OutputFormat::Json {
if base_ctx.user_settings().cli.output.format == OutputFormat::Json {
let mut value = serde_json::json!({
"direction": "download",
"recursive": args.recursive,
@ -72,7 +65,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 +75,7 @@ pub(crate) async fn cp_command(
None
};
if cli.output.format == OutputFormat::Json {
if base_ctx.user_settings().cli.output.format == OutputFormat::Json {
let mut value = serde_json::json!({
"direction": "upload",
"recursive": args.recursive,
@ -124,12 +117,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

@ -10,9 +10,7 @@
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 fabro_types::settings::cli::OutputFormat;
use tracing::{debug, info};
use crate::args::DiffArgs;
@ -20,21 +18,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.user_settings().cli.output.format == OutputFormat::Json {
let value = serde_json::json!({
"run_id": run_id,
"node": args.node,

View file

@ -1,8 +1,6 @@
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_types::settings::cli::OutputFormat;
use fabro_util::terminal::Styles;
use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork};
use git2::Repository;
@ -13,15 +11,10 @@ use crate::commands::store::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 +27,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?;
return Ok(());
}
@ -56,7 +49,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.user_settings().cli.output.format == OutputFormat::Json {
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,8 @@ 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_types::settings::cli::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 +27,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 +52,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.user_settings().cli.output.format != OutputFormat::Json;
for line in &filtered {
if pretty {

View file

@ -1,7 +1,5 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_util::terminal::Styles;
use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs};
@ -27,19 +25,15 @@ 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 ctx = base_ctx.with_target(&args.target)?;
let created_run = Box::pin(create::create_run(
&ctx,
&args,
@ -49,7 +43,7 @@ pub(crate) async fn dispatch(
printer,
))
.await?;
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
} else {
fabro_util::printout!(printer, "{}", created_run.run_id);
@ -57,26 +51,27 @@ 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.user_settings().cli.output.format == OutputFormat::Json {
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.user_settings().cli.output.format == OutputFormat::Json;
let exit_code = Box::pin(attach::attach_run_with_client(
client.as_ref(),
&run_id,
false,
styles,
cli.output.format == OutputFormat::Json,
json,
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
printer,
))
@ -104,34 +99,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,14 @@
use anyhow::{Context, Result};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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 +24,7 @@ pub(crate) async fn run(
info!(run_id = %args.run, port = args.port, "Generating preview URL");
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::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 args.open && !ctx.explicit_json_requested() {
#[expect(
clippy::disallowed_methods,
reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process."

View file

@ -1,6 +1,4 @@
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_util::terminal::Styles;
use crate::args::ResumeArgs;
@ -15,17 +13,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.user_settings().cli.output.format == OutputFormat::Json;
if args.detach {
if json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;

View file

@ -3,8 +3,7 @@ use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_checkpoint::git::Store;
use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::cli::OutputFormat;
use fabro_types::{EventBody, RunEvent};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -33,12 +32,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 +52,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&timeline_entries_json(&timeline))?;
return Ok(());
}
@ -79,7 +77,7 @@ pub(crate) async fn run(
let run_id_string = run_id.to_string();
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"run_id": run_id_string,
"target": target_arg,

View file

@ -1,25 +1,18 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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 base_ctx.explicit_json_requested() && !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 +20,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "command": ssh.command }))?;
} else {
{

View file

@ -11,8 +11,7 @@ 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_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_workflow::records::Conclusion;
@ -24,14 +23,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 +57,7 @@ pub(crate) async fn run(
let conclusion = client.get_run_state(&run_id).await?.conclusion;
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
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,7 +1,5 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
use super::short_run_id;
use crate::args::{RunsArchiveArgs, RunsUnarchiveArgs};
@ -11,34 +9,30 @@ 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)?;
let ctx = base_ctx.with_target(&args.server)?;
run_bulk(
Action::Archive,
&args.runs,
ctx.server().await?.as_ref(),
cli,
printer,
ctx.user_settings().cli.output.format == OutputFormat::Json,
ctx.printer(),
)
.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)?;
let ctx = base_ctx.with_target(&args.server)?;
run_bulk(
Action::Unarchive,
&args.runs,
ctx.server().await?.as_ref(),
cli,
printer,
ctx.user_settings().cli.output.format == OutputFormat::Json,
ctx.printer(),
)
.await
}
@ -66,10 +60,9 @@ async fn run_bulk(
action: Action,
identifiers: &[String],
client: &server_client::Client,
cli: &CliNamespace,
printer: Printer,
json: bool,
printer: fabro_util::printer::Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
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,7 @@ use anyhow::Result;
use chrono::Utc;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::terminal::Styles;
use fabro_util::text::strip_goal_decoration;
use fabro_workflow::run_status::RunStatus;
@ -20,11 +18,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 +32,7 @@ pub(crate) async fn list_command(
!args.all,
);
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
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,5 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
use super::short_run_id;
use crate::args::RunsRemoveArgs;
@ -9,23 +7,23 @@ 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.server().await?.as_ref(),
ctx.user_settings().cli.output.format == OutputFormat::Json,
ctx.printer(),
)
.await
}
async fn remove_from(
args: &RunsRemoveArgs,
client: &server_client::Client,
cli: &CliNamespace,
printer: Printer,
json: bool,
printer: fabro_util::printer::Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
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,8 +2,6 @@ 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;
@ -25,11 +23,11 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
pub(super) async fn list_command(
client: &Client,
_args: &SecretListArgs,
cli: &CliNamespace,
json_output: bool,
printer: Printer,
) -> Result<()> {
let secrets = client.list_secrets().await?;
if cli.output.format == OutputFormat::Json {
if json_output {
print_json_pretty(&secrets)?;
return Ok(());
}

View file

@ -3,24 +3,19 @@ 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)?;
pub(crate) async fn dispatch(ns: SecretNamespace, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&ns.target)?;
let server = ctx.server().await?;
let json_output =
ctx.user_settings().cli.output.format == fabro_types::settings::cli::OutputFormat::Json;
let printer = ctx.printer();
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(&server, &args, json_output, printer).await,
SecretCommand::Rm(args) => rm::rm_command(&server, &args, json_output, printer).await,
SecretCommand::Set(args) => set::set_command(&server, &args, json_output, printer).await,
}
}

View file

@ -1,6 +1,4 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::SecretRmArgs;
@ -10,11 +8,11 @@ use crate::shared::print_json_pretty;
pub(super) async fn rm_command(
client: &Client,
args: &SecretRmArgs,
cli: &CliNamespace,
json_output: bool,
printer: Printer,
) -> Result<()> {
client.delete_secret_by_name(&args.key).await?;
if cli.output.format == OutputFormat::Json {
if json_output {
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
} else {
fabro_util::printerr!(printer, "Removed {}", args.key);

View file

@ -11,8 +11,6 @@ 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;
@ -60,7 +58,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result<String> {
pub(super) async fn set_command(
client: &Client,
args: &SecretSetArgs,
cli: &CliNamespace,
json_output: bool,
printer: Printer,
) -> Result<()> {
let value = resolve_value(args).await?;
@ -72,7 +70,7 @@ pub(super) async fn set_command(
description: args.description.clone(),
})
.await?;
if cli.output.format == OutputFormat::Json {
if json_output {
print_json_pretty(&meta)?;
} else {
fabro_util::printerr!(printer, "Set {}", meta.name);

View file

@ -11,10 +11,8 @@ 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::settings::cli::OutputFormat;
use fabro_types::{RunBlobId, RunId};
use fabro_util::printer::Printer;
use futures::future::BoxFuture;
#[cfg(test)]
use serde::de::DeserializeOwned;
@ -26,19 +24,15 @@ use crate::command_context::CommandContext;
use crate::server_client::Client;
use crate::shared::{absolute_or_current, print_json_pretty};
pub(crate) async fn dump_command(
args: &StoreDumpArgs,
cli: &CliNamespace,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
pub(crate) async fn dump_command(args: &StoreDumpArgs, 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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"run_id": run_id,
"output_dir": absolute_or_current(&args.output),

View file

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

View file

@ -3,23 +3,16 @@ 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 fabro_types::settings::cli::OutputFormat;
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.user_settings().cli.output.format == OutputFormat::Json;
let (output, storage_dir) = if json {
(server.get_system_disk_usage(args.verbose).await?, None)

View file

@ -1,8 +1,6 @@
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 fabro_types::settings::cli::OutputFormat;
use futures::StreamExt;
use crate::args::SystemEventsArgs;
@ -10,16 +8,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.user_settings().cli.output.format == OutputFormat::Json;
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,16 @@
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_types::settings::cli::OutputFormat;
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.user_settings().cli.output.format == OutputFormat::Json {
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,7 @@ 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_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use tracing::{debug, info};
@ -11,13 +10,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 +24,11 @@ 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.user_settings().cli.output.format == OutputFormat::Json,
printer,
)
}
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {

View file

@ -1,9 +1,8 @@
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_types::settings::cli::OutputFormat;
use fabro_util::terminal::Styles;
use crate::args::ValidateArgs;
@ -15,11 +14,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 +31,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.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"workflow_name": response.workflow.name,
"nodes": response.workflow.nodes,

View file

@ -6,8 +6,7 @@
use std::io::IsTerminal;
use anyhow::Result;
use fabro_types::settings::CliNamespace;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use serde_json::{Map, Value, json};
@ -16,14 +15,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 +44,7 @@ pub(crate) async fn version_command(
},
};
if cli.output.format == OutputFormat::Json {
if ctx.user_settings().cli.output.format == OutputFormat::Json {
print_json_pretty(&json_output(&client, &server_info))?;
return Ok(());
}

View file

@ -30,7 +30,6 @@ use clap::{CommandFactory, Parser};
use fabro_config::merge::combine_files;
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_util::exit::ExitClass;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -38,6 +37,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 {
@ -210,51 +211,46 @@ async fn main_inner() -> (String, Result<()>) {
};
let result = Box::pin(async move {
let build_base_ctx = || CommandContext::base(printer, &cli_layer, process_local_json);
match *command {
Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?,
Commands::RunCmd(cmd) => {
Box::pin(commands::run::dispatch(
cmd,
&cli_settings,
&cli_layer,
process_local_json,
printer,
))
.await?;
let base_ctx = build_base_ctx()?;
Box::pin(commands::run::dispatch(cmd, &base_ctx)).await?;
}
Commands::Preflight(args) => {
commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
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?;
let base_ctx = build_base_ctx()?;
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?;
let base_ctx = build_base_ctx()?;
commands::graph::run(&args, &styles, &base_ctx).await?;
}
Commands::Parse(args) => {
commands::parse::run(&args, &cli_settings, printer)?;
}
Commands::Artifact(ns) => {
commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::artifact::dispatch(ns, &base_ctx).await?;
}
Commands::Store(ns) => {
commands::store::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::store::dispatch(ns, &base_ctx).await?;
}
Commands::RunsCmd(cmd) => {
commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::runs::dispatch(cmd, &base_ctx).await?;
}
Commands::Model { command } => {
commands::model::execute(command, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::model::execute(command, &base_ctx).await?;
}
Commands::Server(ns) => {
Box::pin(commands::server::dispatch(
@ -266,21 +262,13 @@ 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 base_ctx = build_base_ctx()?;
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?;
let base_ctx = build_base_ctx()?;
commands::version::version_command(&args, &base_ctx).await?;
}
Commands::Discord => {
if process_local_json {
@ -301,7 +289,8 @@ async fn main_inner() -> (String, Result<()>) {
}
}
Commands::Repo(ns) => {
commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::repo::dispatch(ns, &base_ctx).await?;
}
Commands::Install { args, command } => {
Box::pin(commands::install::execute(
@ -318,48 +307,36 @@ async fn main_inner() -> (String, Result<()>) {
commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?;
}
Commands::Auth(ns) => {
commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?;
let base_ctx = build_base_ctx()?;
commands::auth::dispatch(ns, &base_ctx).await?;
}
Commands::Pr(ns) => {
Box::pin(commands::pr::dispatch(
ns,
&cli_settings,
&cli_layer,
printer,
))
.await?;
let base_ctx = build_base_ctx()?;
Box::pin(commands::pr::dispatch(ns, &base_ctx)).await?;
}
Commands::Secret(ns) => {
commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::secret::dispatch(ns, &base_ctx).await?;
}
Commands::Settings(args) => {
Box::pin(commands::config::execute(
&args,
&cli_settings,
&cli_layer,
printer,
))
.await?;
let base_ctx = build_base_ctx()?;
Box::pin(commands::config::execute(&args, &base_ctx)).await?;
}
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?,
Commands::Upgrade(args) => {
commands::upgrade::run_upgrade(args, &cli_settings, printer).await?;
}
Commands::Provider(ns) => {
commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?;
let base_ctx = build_base_ctx()?;
commands::provider::dispatch(ns, &base_ctx).await?;
}
Commands::Sandbox { command } => {
commands::sandbox::dispatch(
command,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
let base_ctx = build_base_ctx()?;
commands::sandbox::dispatch(command, &base_ctx).await?;
}
Commands::System(ns) => {
commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
let base_ctx = build_base_ctx()?;
commands::system::dispatch(ns, &base_ctx).await?;
}
Commands::Completion(args) => {
require_no_json_override(process_local_json)?;