mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
Merge remote-tracking branch 'origin/main'
# Conflicts: # lib/crates/fabro-cli/src/command_context.rs # lib/crates/fabro-cli/src/main.rs
This commit is contained in:
commit
e210fe9fed
68 changed files with 1680 additions and 939 deletions
|
|
@ -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`
|
||||
|
|
@ -3,12 +3,14 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use fabro_config::UserSettings;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_types::settings::{Combine, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::args::{ServerConnectionArgs, ServerTargetArgs};
|
||||
use crate::args::{
|
||||
ServerConnectionArgs, ServerTargetArgs, printer_from_verbosity, require_no_json_override,
|
||||
};
|
||||
use crate::server_client::Client;
|
||||
use crate::{server_client, user_config};
|
||||
|
||||
|
|
@ -25,89 +27,62 @@ pub(crate) enum ServerMode {
|
|||
}
|
||||
|
||||
pub(crate) struct CommandContext {
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This item is kept for command plumbing that is still being wired through."
|
||||
)]
|
||||
printer: Printer,
|
||||
cwd: PathBuf,
|
||||
base_config_path: PathBuf,
|
||||
machine_settings: SettingsLayer,
|
||||
user_settings: UserSettings,
|
||||
server_mode: ServerMode,
|
||||
server: OnceCell<Arc<Client>>,
|
||||
printer: Printer,
|
||||
process_local_json: bool,
|
||||
cwd: PathBuf,
|
||||
base_config_path: PathBuf,
|
||||
cli_layer: CliLayer,
|
||||
machine_settings: SettingsLayer,
|
||||
user_settings: UserSettings,
|
||||
server_mode: ServerMode,
|
||||
server: OnceCell<Arc<Client>>,
|
||||
}
|
||||
|
||||
impl CommandContext {
|
||||
pub(crate) fn base(printer: Printer, cli_layer: &CliLayer) -> Result<Self> {
|
||||
Self::new(printer, ServerMode::None, cli_layer)
|
||||
}
|
||||
|
||||
pub(crate) fn for_target(
|
||||
args: &ServerTargetArgs,
|
||||
printer: Printer,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
printer,
|
||||
ServerMode::ByTarget {
|
||||
target_override: args.server.clone(),
|
||||
},
|
||||
cli_layer,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn for_connection(
|
||||
args: &ServerConnectionArgs,
|
||||
printer: Printer,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
printer,
|
||||
ServerMode::ByStorageDir {
|
||||
target_override: args.target.server.clone(),
|
||||
storage_dir_override: args.storage_dir.clone_path(),
|
||||
},
|
||||
cli_layer,
|
||||
)
|
||||
}
|
||||
|
||||
fn new(printer: Printer, server_mode: ServerMode, cli_layer: &CliLayer) -> Result<Self> {
|
||||
pub(crate) fn from_disk(cli_layer: &CliLayer, process_local_json: bool) -> Result<Self> {
|
||||
let (machine_settings, user_settings) = load_merged_settings(cli_layer, &ServerMode::None)?;
|
||||
let printer = printer_from_verbosity(user_settings.cli.output.verbosity);
|
||||
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||
let base_config_path = user_config::active_settings_path(None);
|
||||
let disk_settings = match &server_mode {
|
||||
ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
|
||||
ServerMode::ByStorageDir {
|
||||
storage_dir_override,
|
||||
..
|
||||
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
|
||||
};
|
||||
let machine_settings = SettingsLayer {
|
||||
cli: Some(cli_layer.clone()),
|
||||
..SettingsLayer::default()
|
||||
}
|
||||
.combine(disk_settings);
|
||||
let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?;
|
||||
|
||||
Ok(Self {
|
||||
printer,
|
||||
process_local_json,
|
||||
cwd,
|
||||
base_config_path,
|
||||
cli_layer: cli_layer.clone(),
|
||||
machine_settings,
|
||||
user_settings,
|
||||
server_mode,
|
||||
server_mode: ServerMode::None,
|
||||
server: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This item is kept for command plumbing that is still being wired through."
|
||||
)]
|
||||
pub(crate) fn with_target(&self, args: &ServerTargetArgs) -> Result<Self> {
|
||||
self.with_server_mode(ServerMode::ByTarget {
|
||||
target_override: args.server.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_connection(&self, args: &ServerConnectionArgs) -> Result<Self> {
|
||||
self.with_server_mode(ServerMode::ByStorageDir {
|
||||
target_override: args.target.server.clone(),
|
||||
storage_dir_override: args.storage_dir.clone_path(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn printer(&self) -> Printer {
|
||||
self.printer
|
||||
}
|
||||
|
||||
pub(crate) fn explicit_json_requested(&self) -> bool {
|
||||
self.process_local_json
|
||||
}
|
||||
|
||||
pub(crate) fn require_no_json_override(&self) -> Result<()> {
|
||||
require_no_json_override(self.process_local_json)
|
||||
}
|
||||
|
||||
pub(crate) fn cwd(&self) -> &Path {
|
||||
&self.cwd
|
||||
}
|
||||
|
|
@ -120,6 +95,14 @@ impl CommandContext {
|
|||
&self.user_settings
|
||||
}
|
||||
|
||||
pub(crate) fn json_output(&self) -> bool {
|
||||
self.user_settings.cli.output.format == OutputFormat::Json
|
||||
}
|
||||
|
||||
pub(crate) fn verbose(&self) -> bool {
|
||||
self.user_settings.cli.output.verbosity == OutputVerbosity::Verbose
|
||||
}
|
||||
|
||||
pub(crate) async fn server(&self) -> Result<Arc<Client>> {
|
||||
let server_mode = self.server_mode.clone();
|
||||
let base_config_path = self.base_config_path.clone();
|
||||
|
|
@ -149,4 +132,161 @@ impl CommandContext {
|
|||
|
||||
Ok(Arc::clone(client))
|
||||
}
|
||||
|
||||
fn with_server_mode(&self, server_mode: ServerMode) -> Result<Self> {
|
||||
// Always reload settings for the requested derivation mode so the result
|
||||
// depends only on the requested mode, not on whichever derived context
|
||||
// happened to call into this helper.
|
||||
let (machine_settings, user_settings) =
|
||||
load_merged_settings(&self.cli_layer, &server_mode)?;
|
||||
|
||||
Ok(Self {
|
||||
printer: self.printer,
|
||||
process_local_json: self.process_local_json,
|
||||
cwd: self.cwd.clone(),
|
||||
base_config_path: self.base_config_path.clone(),
|
||||
cli_layer: self.cli_layer.clone(),
|
||||
machine_settings,
|
||||
user_settings,
|
||||
server_mode,
|
||||
server: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_merged_settings(
|
||||
cli_layer: &CliLayer,
|
||||
server_mode: &ServerMode,
|
||||
) -> Result<(SettingsLayer, UserSettings)> {
|
||||
let disk_settings = match server_mode {
|
||||
ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
|
||||
ServerMode::ByStorageDir {
|
||||
storage_dir_override,
|
||||
..
|
||||
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
|
||||
};
|
||||
merge_settings_layer(disk_settings, cli_layer)
|
||||
}
|
||||
|
||||
fn merge_settings_layer(
|
||||
disk_settings: SettingsLayer,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<(SettingsLayer, UserSettings)> {
|
||||
let machine_settings = SettingsLayer {
|
||||
cli: Some(cli_layer.clone()),
|
||||
..SettingsLayer::default()
|
||||
}
|
||||
.combine(disk_settings);
|
||||
let user_settings = UserSettings::from_layer(&machine_settings)?;
|
||||
Ok((machine_settings, user_settings))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fabro_config::parse_settings_layer;
|
||||
use fabro_config::user::apply_storage_dir_override;
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::cli::{CliLayer, CliOutputLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::{CommandContext, ServerMode, merge_settings_layer};
|
||||
|
||||
fn cli_layer_with_json_and_verbose() -> CliLayer {
|
||||
CliLayer {
|
||||
output: Some(CliOutputLayer {
|
||||
format: Some(OutputFormat::Json),
|
||||
verbosity: Some(OutputVerbosity::Verbose),
|
||||
}),
|
||||
..CliLayer::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn synthetic_context(process_local_json: bool, printer: Printer) -> CommandContext {
|
||||
let cli_layer = cli_layer_with_json_and_verbose();
|
||||
let (machine_settings, user_settings) =
|
||||
merge_settings_layer(parse_settings_layer("_version = 1\n").unwrap(), &cli_layer)
|
||||
.expect("settings should merge");
|
||||
CommandContext {
|
||||
printer,
|
||||
process_local_json,
|
||||
cwd: PathBuf::from("/tmp/workspace"),
|
||||
base_config_path: PathBuf::from("/tmp/settings.toml"),
|
||||
cli_layer,
|
||||
machine_settings,
|
||||
user_settings,
|
||||
server_mode: ServerMode::None,
|
||||
server: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_exposes_resolved_output_and_explicit_json_state() {
|
||||
let ctx = synthetic_context(true, Printer::Default);
|
||||
|
||||
assert_eq!(ctx.user_settings().cli.output.format, OutputFormat::Json);
|
||||
assert_eq!(
|
||||
ctx.user_settings().cli.output.verbosity,
|
||||
OutputVerbosity::Verbose
|
||||
);
|
||||
assert!(ctx.explicit_json_requested());
|
||||
assert_eq!(ctx.printer(), Printer::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_dir_override_only_changes_storage_root_in_merged_settings() {
|
||||
let cli_layer = cli_layer_with_json_and_verbose();
|
||||
let base_disk_settings = parse_settings_layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.storage]
|
||||
root = "/srv/fabro/default"
|
||||
"#,
|
||||
)
|
||||
.expect("settings fixture should parse");
|
||||
let override_disk_settings = apply_storage_dir_override(
|
||||
base_disk_settings.clone(),
|
||||
Some(std::path::Path::new("/srv/fabro/override")),
|
||||
);
|
||||
|
||||
let (base_settings, base_user_settings) =
|
||||
merge_settings_layer(base_disk_settings, &cli_layer)
|
||||
.expect("base settings should merge");
|
||||
let (connection_settings, connection_user_settings) =
|
||||
merge_settings_layer(override_disk_settings, &cli_layer)
|
||||
.expect("connection settings should merge");
|
||||
|
||||
assert_eq!(base_user_settings, connection_user_settings);
|
||||
assert_eq!(base_user_settings.cli.output.format, OutputFormat::Json);
|
||||
assert_eq!(
|
||||
base_settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.storage.as_ref())
|
||||
.and_then(|storage| storage.root.as_ref())
|
||||
.map(InterpString::as_source),
|
||||
Some("/srv/fabro/default".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
connection_settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.storage.as_ref())
|
||||
.and_then(|storage| storage.root.as_ref())
|
||||
.map(InterpString::as_source),
|
||||
Some("/srv/fabro/override".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_json_guard_uses_invocation_flag_not_resolved_output_format() {
|
||||
let json_ctx = synthetic_context(true, Printer::Default);
|
||||
let text_ctx = synthetic_context(false, Printer::Default);
|
||||
|
||||
assert!(json_ctx.require_no_json_override().is_err());
|
||||
assert!(text_ctx.require_no_json_override().is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,28 +6,21 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::ArtifactCpArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::server_client::Client;
|
||||
use crate::shared::{print_json_pretty, split_run_path};
|
||||
|
||||
pub(super) async fn cp_command(
|
||||
args: &ArtifactCpArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) async fn cp_command(args: &ArtifactCpArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
let (run_id_selector, asset_path) = parse_source(&args.source);
|
||||
let (run_id, client, entries) = super::resolve_artifacts(
|
||||
base_ctx,
|
||||
&args.server,
|
||||
run_id_selector,
|
||||
args.node.as_deref(),
|
||||
args.retry,
|
||||
cli_layer,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
@ -64,7 +57,7 @@ pub(super) async fn cp_command(
|
|||
.unwrap_or_else(|| std::ffi::OsStr::new(&entry.relative_path)),
|
||||
);
|
||||
write_artifact_file(&client, &run_id, entry, &dest_file).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"copied": [{
|
||||
"relative_path": entry.relative_path,
|
||||
|
|
@ -125,7 +118,7 @@ pub(super) async fn cp_command(
|
|||
}
|
||||
}
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "copied": copied }))?;
|
||||
} else {
|
||||
fabro_util::printout!(
|
||||
|
|
|
|||
|
|
@ -1,30 +1,23 @@
|
|||
use anyhow::Result;
|
||||
use cli_table::format::{Border, Justify, Separator};
|
||||
use cli_table::{Cell, CellStruct, Style, Table};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::ArtifactListArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
pub(super) async fn list_command(
|
||||
args: &ArtifactListArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) async fn list_command(args: &ArtifactListArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
let (_run_id, _client, entries) = super::resolve_artifacts(
|
||||
base_ctx,
|
||||
&args.server,
|
||||
&args.run_id,
|
||||
args.node.as_deref(),
|
||||
args.retry,
|
||||
cli_layer,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ use std::io::Write;
|
|||
|
||||
use fabro_api::types::ServerSettings;
|
||||
use fabro_config::UserSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::args::SettingsArgs;
|
||||
|
|
@ -26,29 +23,17 @@ struct RenderedConfig {
|
|||
server: ServerSettings,
|
||||
}
|
||||
|
||||
async fn rendered_config(
|
||||
args: &SettingsArgs,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
pub(crate) async fn execute(args: &SettingsArgs, base_ctx: &CommandContext) -> anyhow::Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
let user = fabro_config::UserSettings::resolve()?;
|
||||
let server = ctx
|
||||
.server()
|
||||
.await?
|
||||
.retrieve_resolved_server_settings()
|
||||
.await?;
|
||||
serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into)
|
||||
}
|
||||
let config = serde_json::to_value(RenderedConfig { user, server })?;
|
||||
|
||||
pub(crate) async fn execute(
|
||||
args: &SettingsArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = Box::pin(rendered_config(args, cli_layer, printer)).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&config)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ use std::path::PathBuf;
|
|||
use anyhow::Result;
|
||||
use fabro_api::types as api_types;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
pub(crate) use fabro_util::check_report::{
|
||||
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
|
||||
};
|
||||
|
|
@ -142,13 +140,12 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer:
|
|||
|
||||
pub(crate) async fn run_doctor(
|
||||
args: &DoctorArgs,
|
||||
verbose: bool,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<i32, anyhow::Error> {
|
||||
let verbose = args.verbose || base_ctx.verbose();
|
||||
let printer = base_ctx.printer();
|
||||
let styles = Styles::detect_stdout();
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
let json = base_ctx.json_output();
|
||||
let spinner = if json {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -179,7 +176,7 @@ pub(crate) async fn run_doctor(
|
|||
}],
|
||||
};
|
||||
|
||||
let ctx = match CommandContext::for_target(&args.target, printer, cli_layer) {
|
||||
let ctx = match base_ctx.with_target(&args.target) {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
report.sections.push(CheckSection {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ use bytes::Bytes;
|
|||
#[cfg(test)]
|
||||
use fabro_store::{ArtifactStore, RunDatabase};
|
||||
use fabro_store::{EventEnvelope, RunProjection, StageId};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::{RunBlobId, RunId};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_workflow::run_dump::RunDump;
|
||||
use futures::future::BoxFuture;
|
||||
#[cfg(test)]
|
||||
|
|
@ -26,19 +23,15 @@ use crate::command_context::CommandContext;
|
|||
use crate::server_client::Client;
|
||||
use crate::shared::{absolute_or_current, print_json_pretty};
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &DumpArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(crate) async fn run(args: &DumpArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
let state = client.get_run_state(&run_id).await?;
|
||||
let source = ServerDumpSource::new(client.as_ref(), &run_id);
|
||||
let file_count = export_run_from_source(&source, &state, &args.output).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"run_id": run_id,
|
||||
"output_dir": absolute_or_current(&args.output),
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ use fabro_llm::types::{
|
|||
FinishReason, Message, Request, Response as LlmResponse, StreamEvent, TokenCounts,
|
||||
};
|
||||
use fabro_mcp::config::{McpServerSettings, McpTransport};
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat;
|
||||
use fabro_types::settings::run::McpEntryLayer;
|
||||
use fabro_types::settings::{CliNamespace, InterpString};
|
||||
use fabro_util::exit::{ErrorExt, ExitClass};
|
||||
use fabro_util::printer::Printer;
|
||||
use futures::stream;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::args::ExecArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::{server_client, user_config};
|
||||
|
||||
fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
|
||||
|
|
@ -356,14 +356,11 @@ impl ProviderAdapter for AuthenticatedFabroServerAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(
|
||||
mut args: ExecArgs,
|
||||
cli: &CliNamespace,
|
||||
_printer: Printer,
|
||||
) -> AnyResult<()> {
|
||||
pub(crate) async fn execute(mut args: ExecArgs, ctx: &CommandContext) -> AnyResult<()> {
|
||||
use fabro_agent::cli::PermissionLevel as AgentPermissionLevel;
|
||||
use fabro_types::settings::run::AgentPermissions;
|
||||
|
||||
let cli = &ctx.user_settings().cli;
|
||||
let raw_settings = user_config::load_settings()?;
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(cli.exec.prevent_idle_sleep);
|
||||
|
|
|
|||
|
|
@ -13,13 +13,11 @@ use anyhow::{Context, bail};
|
|||
use fabro_api::types;
|
||||
use fabro_config::load::load_settings_user;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::args::{GraphArgs, GraphDirection, require_no_json_override};
|
||||
use crate::args::{GraphArgs, GraphDirection};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::commands::run::output::api_diagnostics_to_local;
|
||||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
|
||||
|
|
@ -28,16 +26,14 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r
|
|||
pub(crate) async fn run(
|
||||
args: &GraphArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> anyhow::Result<()> {
|
||||
if process_local_json && args.output.is_none() {
|
||||
require_no_json_override(process_local_json)?;
|
||||
if args.output.is_none() {
|
||||
base_ctx.require_no_json_override()?;
|
||||
}
|
||||
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
cwd: ctx.cwd().to_path_buf(),
|
||||
|
|
@ -73,7 +69,7 @@ pub(crate) async fn run(
|
|||
if let Some(ref output_path) = args.output {
|
||||
std::fs::write(output_path, &rendered)
|
||||
.with_context(|| format!("writing rendered graph to {}", output_path.display()))?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"path": absolute_or_current(output_path),
|
||||
"format": args.format.to_string(),
|
||||
|
|
|
|||
|
|
@ -32,9 +32,8 @@ use fabro_install::{
|
|||
use fabro_model::Provider;
|
||||
use fabro_server::serve;
|
||||
use fabro_store::ArtifactStore;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_types::settings::server::ServerAuthMethod;
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
|
|
@ -52,6 +51,7 @@ use crate::args::{
|
|||
DoctorArgs, InstallArgs, InstallCommand, InstallGitHubStrategyArg, InstallGithubArgs,
|
||||
InstallNonInteractiveArgs, ServerTargetArgs,
|
||||
};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::commands::server::{start, stop};
|
||||
use crate::gh::GhCli;
|
||||
use crate::shared::provider_auth::{
|
||||
|
|
@ -1409,15 +1409,12 @@ where
|
|||
pub(crate) async fn execute(
|
||||
args: &InstallArgs,
|
||||
command: Option<InstallCommand>,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
match command {
|
||||
None => run_install(args, cli, cli_layer, process_local_json, printer).await,
|
||||
None => run_install(args, ctx).await,
|
||||
Some(InstallCommand::Github(github_args)) => {
|
||||
run_install_github_command(args, &github_args, cli, process_local_json, printer).await
|
||||
run_install_github_command(args, &github_args, ctx).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1425,16 +1422,20 @@ pub(crate) async fn execute(
|
|||
async fn run_install_github_command(
|
||||
args: &InstallArgs,
|
||||
github_args: &InstallGithubArgs,
|
||||
cli: &CliNamespace,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
if process_local_json && !args.non_interactive {
|
||||
let json = ctx.json_output();
|
||||
if ctx.explicit_json_requested() && !args.non_interactive {
|
||||
bail!("--json is only supported for install with --non-interactive");
|
||||
}
|
||||
|
||||
let result = Box::pin(run_install_github_inner(args, github_args, json, printer)).await;
|
||||
let result = Box::pin(run_install_github_inner(
|
||||
args,
|
||||
github_args,
|
||||
json,
|
||||
ctx.printer(),
|
||||
))
|
||||
.await;
|
||||
if json {
|
||||
let emit_result = match &result {
|
||||
Ok(()) => emit_install_json_event(&install_complete_event()),
|
||||
|
|
@ -1586,19 +1587,13 @@ async fn run_install_github_inner(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run_install(
|
||||
args: &InstallArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
if process_local_json && !args.non_interactive {
|
||||
pub(crate) async fn run_install(args: &InstallArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let json = ctx.json_output();
|
||||
if ctx.explicit_json_requested() && !args.non_interactive {
|
||||
bail!("--json is only supported for install with --non-interactive");
|
||||
}
|
||||
|
||||
let result = Box::pin(run_install_inner(args, cli, cli_layer, printer)).await;
|
||||
let result = Box::pin(run_install_inner(args, ctx)).await;
|
||||
if json {
|
||||
let emit_result = match &result {
|
||||
Ok(()) => emit_install_json_event(&install_complete_event()),
|
||||
|
|
@ -1612,13 +1607,10 @@ pub(crate) async fn run_install(
|
|||
result
|
||||
}
|
||||
|
||||
async fn run_install_inner(
|
||||
args: &InstallArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let _cli = &ctx.user_settings().cli;
|
||||
let printer = ctx.printer();
|
||||
let json = ctx.json_output();
|
||||
let web_url = &args.web_url;
|
||||
let s = Styles::detect_stderr();
|
||||
let emoji = console::Emoji("⚒️ ", "");
|
||||
|
|
@ -1921,7 +1913,7 @@ async fn run_install_inner(
|
|||
target: ServerTargetArgs::default(),
|
||||
verbose: false,
|
||||
};
|
||||
doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await
|
||||
doctor::run_doctor(&doctor_args, ctx).await
|
||||
},
|
||||
)
|
||||
.await?
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ use cli_table::format::{Border, Justify, Separator};
|
|||
use cli_table::{Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_api::types as api_types;
|
||||
use fabro_model::{Catalog, Model, Provider};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -42,19 +39,17 @@ struct ModelTestOutput {
|
|||
|
||||
pub(crate) async fn execute(
|
||||
command: Option<ModelsCommand>,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let command = command.unwrap_or_default();
|
||||
let target_args = match &command {
|
||||
ModelsCommand::List(args) => &args.target,
|
||||
ModelsCommand::Test(args) => &args.target,
|
||||
};
|
||||
let ctx = CommandContext::for_target(target_args, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(target_args)?;
|
||||
let server = ctx.server().await?;
|
||||
|
||||
run_models(command, &server, cli.output.format == OutputFormat::Json).await
|
||||
run_models(command, &server, ctx.json_output()).await
|
||||
}
|
||||
|
||||
fn format_context_window(tokens: i64) -> String {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,11 @@ use std::io::Write;
|
|||
|
||||
use fabro_config::project::resolve_workflow;
|
||||
use fabro_graphviz::parser::parse_ast;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::ParseArgs;
|
||||
use crate::shared::read_workflow_file;
|
||||
|
||||
pub(crate) fn run(args: &ParseArgs, _cli: &CliNamespace, _printer: Printer) -> anyhow::Result<()> {
|
||||
pub(crate) fn run(args: &ParseArgs) -> anyhow::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
run_to(args, stdout.lock())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrCloseArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(super) async fn close_command(
|
||||
args: PrCloseArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let (record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
|
||||
pub(super) async fn close_command(args: PrCloseArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let (ctx, record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(cli_layer, printer)?;
|
||||
let creds = super::load_github_credentials_required(&ctx)?;
|
||||
|
||||
fabro_github::close_pull_request(
|
||||
&creds,
|
||||
|
|
@ -29,13 +22,18 @@ pub(super) async fn close_command(
|
|||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request");
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"number": record.number,
|
||||
"html_url": record.html_url,
|
||||
}))?;
|
||||
} else {
|
||||
fabro_util::printout!(printer, "Closed #{} ({})", record.number, record.html_url);
|
||||
fabro_util::printout!(
|
||||
ctx.printer(),
|
||||
"Closed #{} ({})",
|
||||
record.number,
|
||||
record.html_url
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ use fabro_auth::configured_providers_from_process_env;
|
|||
use fabro_config::Storage;
|
||||
use fabro_model::Catalog;
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_vault::Vault;
|
||||
use fabro_workflow::outcome::StageStatus;
|
||||
use fabro_workflow::pull_request::maybe_open_pull_request;
|
||||
|
|
@ -25,13 +22,9 @@ use crate::user_config;
|
|||
deprecated,
|
||||
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
|
||||
)]
|
||||
pub(super) async fn create_command(
|
||||
args: PrCreateArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(super) async fn create_command(args: PrCreateArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run_id).await?.run_id;
|
||||
let events = client.list_run_events(&run_id, None, None).await?;
|
||||
|
|
@ -86,7 +79,7 @@ pub(super) async fn create_command(
|
|||
let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url)
|
||||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
let creds = super::load_github_credentials_required(cli_layer, printer)?;
|
||||
let creds = super::load_github_credentials_required(base_ctx)?;
|
||||
|
||||
let branch_found = fabro_github::branch_exists(
|
||||
&creds,
|
||||
|
|
@ -136,14 +129,14 @@ pub(super) async fn create_command(
|
|||
match pull_request {
|
||||
Some(record) => {
|
||||
info!(pr_url = %record.html_url, "Pull request created");
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&record)?;
|
||||
} else {
|
||||
fabro_util::printout!(printer, "{}", record.html_url);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::Value::Null)?;
|
||||
} else {
|
||||
fabro_util::printout!(printer, "No pull request created (empty diff).");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use cli_table::format::{Border, Separator};
|
||||
use cli_table::{Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use futures::future::join_all;
|
||||
use serde::Serialize;
|
||||
|
|
@ -23,13 +20,9 @@ struct PrRow {
|
|||
url: String,
|
||||
}
|
||||
|
||||
pub(super) async fn list_command(
|
||||
args: PrListArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(super) async fn list_command(args: PrListArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
|
@ -42,7 +35,7 @@ pub(super) async fn list_command(
|
|||
}
|
||||
|
||||
if entries.is_empty() {
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&Vec::<PrRow>::new())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -50,7 +43,7 @@ pub(super) async fn list_command(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let creds = super::load_github_credentials_required(cli_layer, printer)?;
|
||||
let creds = super::load_github_credentials_required(base_ctx)?;
|
||||
|
||||
let futures: Vec<_> = entries
|
||||
.iter()
|
||||
|
|
@ -104,7 +97,7 @@ pub(super) async fn list_command(
|
|||
.collect()
|
||||
};
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&rows)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrMergeArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(super) async fn merge_command(
|
||||
args: PrMergeArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let (record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
|
||||
pub(super) async fn merge_command(args: PrMergeArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let (ctx, record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(cli_layer, printer)?;
|
||||
let creds = super::load_github_credentials_required(&ctx)?;
|
||||
|
||||
fabro_github::merge_pull_request(
|
||||
&creds,
|
||||
|
|
@ -30,14 +23,19 @@ pub(super) async fn merge_command(
|
|||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request");
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"number": record.number,
|
||||
"html_url": record.html_url,
|
||||
"method": args.method,
|
||||
}))?;
|
||||
} else {
|
||||
fabro_util::printout!(printer, "Merged #{} ({})", record.number, record.html_url);
|
||||
fabro_util::printout!(
|
||||
ctx.printer(),
|
||||
"Merged #{} ({})",
|
||||
record.number,
|
||||
record.html_url
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ use anyhow::{Context, Result, anyhow};
|
|||
use fabro_config::Storage;
|
||||
use fabro_github::GitHubCredentials;
|
||||
use fabro_types::PullRequestRecord;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_types::settings::{CliNamespace, InterpString};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_types::settings::InterpString;
|
||||
|
||||
use crate::args::{PrCommand, PrNamespace, ServerTargetArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
|
|
@ -20,20 +18,13 @@ use crate::user_config;
|
|||
const GITHUB_CREDENTIALS_REQUIRED: &str =
|
||||
"GitHub credentials required — run `fabro install` or set GITHUB_TOKEN";
|
||||
|
||||
pub(crate) async fn dispatch(
|
||||
ns: PrNamespace,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn dispatch(ns: PrNamespace, base_ctx: &CommandContext) -> Result<()> {
|
||||
match ns.command {
|
||||
PrCommand::Create(args) => {
|
||||
Box::pin(create::create_command(args, cli, cli_layer, printer)).await
|
||||
}
|
||||
PrCommand::List(args) => list::list_command(args, cli, cli_layer, printer).await,
|
||||
PrCommand::View(args) => view::view_command(args, cli, cli_layer, printer).await,
|
||||
PrCommand::Merge(args) => merge::merge_command(args, cli, cli_layer, printer).await,
|
||||
PrCommand::Close(args) => close::close_command(args, cli, cli_layer, printer).await,
|
||||
PrCommand::Create(args) => Box::pin(create::create_command(args, base_ctx)).await,
|
||||
PrCommand::List(args) => list::list_command(args, base_ctx).await,
|
||||
PrCommand::View(args) => view::view_command(args, base_ctx).await,
|
||||
PrCommand::Merge(args) => merge::merge_command(args, base_ctx).await,
|
||||
PrCommand::Close(args) => close::close_command(args, base_ctx).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,14 +32,10 @@ pub(crate) async fn dispatch(
|
|||
deprecated,
|
||||
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
|
||||
)]
|
||||
fn load_github_credentials_required(
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<GitHubCredentials> {
|
||||
let ctx = CommandContext::base(printer, cli_layer)?;
|
||||
let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings())
|
||||
fn load_github_credentials_required(base_ctx: &CommandContext) -> Result<GitHubCredentials> {
|
||||
let server_settings = fabro_config::ServerSettings::from_layer(base_ctx.machine_settings())
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let vault = user_config::storage_dir(ctx.machine_settings())
|
||||
let vault = user_config::storage_dir(base_ctx.machine_settings())
|
||||
.ok()
|
||||
.and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok());
|
||||
let creds = build_github_credentials(
|
||||
|
|
@ -70,15 +57,14 @@ fn load_github_credentials_required(
|
|||
pub(crate) async fn load_pr_record(
|
||||
server: &ServerTargetArgs,
|
||||
run_id: &str,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<(PullRequestRecord, fabro_types::RunId)> {
|
||||
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<(CommandContext, PullRequestRecord, fabro_types::RunId)> {
|
||||
let ctx = base_ctx.with_target(server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(run_id).await?.run_id;
|
||||
let state = client.get_run_state(&run_id).await?;
|
||||
let record = state.pull_request.with_context(|| {
|
||||
format!("No pull request found in store. Create one first with: fabro pr create {run_id}")
|
||||
})?;
|
||||
Ok((record, run_id))
|
||||
Ok((ctx, record, run_id))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrViewArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(super) async fn view_command(
|
||||
args: PrViewArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let (record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?;
|
||||
pub(super) async fn view_command(args: PrViewArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let (ctx, record, _run_id) =
|
||||
super::load_pr_record(&args.server, &args.run_id, base_ctx).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(cli_layer, printer)?;
|
||||
let creds = super::load_github_credentials_required(&ctx)?;
|
||||
|
||||
let detail = fabro_github::get_pull_request(
|
||||
&creds,
|
||||
|
|
@ -30,11 +23,12 @@ pub(super) async fn view_command(
|
|||
|
||||
info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request");
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&detail)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let printer = ctx.printer();
|
||||
fabro_util::printout!(printer, "#{} {}", detail.number, detail.title);
|
||||
let state_display = if detail.draft { "draft" } else { &detail.state };
|
||||
fabro_util::printout!(printer, "State: {state_display}");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::load::load_settings_user;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::PreflightArgs;
|
||||
|
|
@ -17,13 +14,12 @@ use crate::shared::print_json_pretty;
|
|||
|
||||
pub(crate) async fn execute(
|
||||
mut args: PreflightArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
args.verbose = args.verbose || ctx.verbose();
|
||||
|
||||
let manifest = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
|
|
@ -38,7 +34,7 @@ pub(crate) async fn execute(
|
|||
let response = client.run_preflight(manifest.manifest).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&response)?;
|
||||
} else {
|
||||
print_preflight_workflow_summary(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<String>> {
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
pub(crate) fn run_deinit(base_ctx: &CommandContext) -> Result<Vec<String>> {
|
||||
let printer = base_ctx.printer();
|
||||
let repo_root = super::init::git_repo_root()?;
|
||||
let mut removed = Vec::new();
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<Str
|
|||
std::fs::remove_dir_all(&fabro_dir)
|
||||
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
|
||||
removed.push(".fabro/".to_string());
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} {}",
|
||||
|
|
@ -29,7 +29,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<Str
|
|||
);
|
||||
}
|
||||
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
"\n{}",
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
|
|
@ -36,10 +33,9 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
|
|||
|
||||
pub(crate) async fn run_init(
|
||||
args: &RepoInitArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<Vec<String>> {
|
||||
let printer = base_ctx.printer();
|
||||
let repo_root = spawn_blocking(git_repo_root)
|
||||
.await
|
||||
.context("git repo root task panicked")??;
|
||||
|
|
@ -79,7 +75,7 @@ draft = true
|
|||
let green = console::Style::new().green();
|
||||
let bold = console::Style::new().bold();
|
||||
let dim = console::Style::new().dim();
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} {}",
|
||||
|
|
@ -112,7 +108,7 @@ draft = true
|
|||
)
|
||||
.with_context(|| format!("failed to write {}", dot_path.display()))?;
|
||||
created.push(".fabro/workflows/hello/workflow.fabro".to_string());
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} {}",
|
||||
|
|
@ -129,7 +125,7 @@ draft = true
|
|||
)
|
||||
.with_context(|| format!("failed to write {}", toml_path.display()))?;
|
||||
created.push(".fabro/workflows/hello/workflow.toml".to_string());
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} {}",
|
||||
|
|
@ -138,7 +134,7 @@ draft = true
|
|||
);
|
||||
}
|
||||
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
if !base_ctx.json_output() {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
"\n{} Run a workflow with:\n\n {}",
|
||||
|
|
@ -150,18 +146,15 @@ draft = true
|
|||
);
|
||||
}
|
||||
|
||||
if cli.output.format != OutputFormat::Json {
|
||||
check_github_app_installation(&args.target, cli_layer, printer).await;
|
||||
if !base_ctx.json_output() {
|
||||
check_github_app_installation(&args.target, base_ctx).await;
|
||||
}
|
||||
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
async fn check_github_app_installation(
|
||||
target: &ServerTargetArgs,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) {
|
||||
async fn check_github_app_installation(target: &ServerTargetArgs, base_ctx: &CommandContext) {
|
||||
let printer = base_ctx.printer();
|
||||
// Get the git remote origin URL
|
||||
let output = match TokioCommand::new("git")
|
||||
.args(["remote", "get-url", "origin"])
|
||||
|
|
@ -199,7 +192,7 @@ async fn check_github_app_installation(
|
|||
return; // Not a GitHub repo — skip silently
|
||||
};
|
||||
|
||||
let ctx = match CommandContext::for_target(target, printer, cli_layer) {
|
||||
let ctx = match base_ctx.with_target(target) {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
fabro_util::printerr!(
|
||||
|
|
|
|||
|
|
@ -2,30 +2,23 @@ pub(crate) mod deinit;
|
|||
pub(crate) mod init;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{RepoCommand, RepoNamespace};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn dispatch(
|
||||
ns: RepoNamespace,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn dispatch(ns: RepoNamespace, base_ctx: &CommandContext) -> Result<()> {
|
||||
match ns.command {
|
||||
RepoCommand::Init(args) => {
|
||||
let created = init::run_init(&args, cli, cli_layer, printer).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
let created = init::run_init(&args, base_ctx).await?;
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "created": created }))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
RepoCommand::Deinit => {
|
||||
let removed = deinit::run_deinit(cli, printer)?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
let removed = deinit::run_deinit(base_ctx)?;
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "removed": removed }))?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,36 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::RunArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn execute(
|
||||
mut args: RunArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn execute(mut args: RunArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
let cli_defaults = load_settings_with_storage_dir(None)?;
|
||||
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
args.verbose = args.verbose || ctx.verbose();
|
||||
|
||||
let quiet = args.detach;
|
||||
let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep;
|
||||
let created_run = Box::pin(super::create::create_run(
|
||||
&ctx,
|
||||
&args,
|
||||
cli_defaults,
|
||||
styles,
|
||||
quiet,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
let created_run = Box::pin(super::create::create_run(&ctx, &args, styles, quiet)).await?;
|
||||
|
||||
if !quiet {
|
||||
fabro_util::printerr!(
|
||||
|
|
@ -50,7 +33,7 @@ pub(crate) async fn execute(
|
|||
let client = ctx.server().await?;
|
||||
super::start::start_run_with_client(&client, &created_run.run_id, false).await?;
|
||||
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
let json = ctx.json_output();
|
||||
if args.detach {
|
||||
if json {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
|
||||
|
|
@ -64,7 +47,7 @@ pub(crate) async fn execute(
|
|||
true,
|
||||
styles,
|
||||
json,
|
||||
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
|
||||
ctx.verbose(),
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
|
@ -26,12 +23,7 @@ enum CopyDirection {
|
|||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn cp_command(
|
||||
args: CpArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn cp_command(args: CpArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let direction = parse_direction(&args.src, &args.dst)?;
|
||||
|
||||
match direction {
|
||||
|
|
@ -41,7 +33,7 @@ pub(crate) async fn cp_command(
|
|||
local_path,
|
||||
} => {
|
||||
let (client, run_id) =
|
||||
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
|
||||
resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?;
|
||||
|
||||
let file_count = if args.recursive {
|
||||
Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?)
|
||||
|
|
@ -51,7 +43,7 @@ pub(crate) async fn cp_command(
|
|||
None
|
||||
};
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
let mut value = serde_json::json!({
|
||||
"direction": "download",
|
||||
"recursive": args.recursive,
|
||||
|
|
@ -72,7 +64,7 @@ pub(crate) async fn cp_command(
|
|||
remote_path,
|
||||
} => {
|
||||
let (client, run_id) =
|
||||
resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?;
|
||||
resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?;
|
||||
|
||||
let file_count = if args.recursive {
|
||||
Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?)
|
||||
|
|
@ -82,7 +74,7 @@ pub(crate) async fn cp_command(
|
|||
None
|
||||
};
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
let mut value = serde_json::json!({
|
||||
"direction": "upload",
|
||||
"recursive": args.recursive,
|
||||
|
|
@ -124,12 +116,11 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
|
|||
}
|
||||
|
||||
async fn resolve_client_and_run_id(
|
||||
base_ctx: &CommandContext,
|
||||
server: &ServerTargetArgs,
|
||||
run_prefix: &str,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<(Client, fabro_types::RunId)> {
|
||||
let ctx = CommandContext::for_target(server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(run_prefix).await?.run_id;
|
||||
Ok((client.clone_for_reuse(), run_id))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
use fabro_config::load::load_settings_user;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::RunId;
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use super::output::{api_diagnostics_to_local, print_preflight_workflow_summary};
|
||||
|
|
@ -22,10 +20,8 @@ pub(crate) struct CreatedRun {
|
|||
pub(crate) async fn create_run(
|
||||
ctx: &CommandContext,
|
||||
args: &RunArgs,
|
||||
_cli_defaults: SettingsLayer,
|
||||
styles: &Styles,
|
||||
quiet: bool,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<CreatedRun> {
|
||||
let workflow_path = args
|
||||
.workflow
|
||||
|
|
@ -51,6 +47,7 @@ pub(crate) async fn create_run(
|
|||
})?;
|
||||
let client = ctx.server().await?;
|
||||
if !quiet {
|
||||
let printer = ctx.printer();
|
||||
let preflight = client.run_preflight(built.manifest.clone()).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics);
|
||||
if !diagnostics
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@
|
|||
use std::io::{self, IsTerminal, Write};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::args::DiffArgs;
|
||||
|
|
@ -20,21 +17,16 @@ use crate::command_context::CommandContext;
|
|||
use crate::server_client::RunProjection;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: DiffArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn run(args: DiffArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
info!(run_id = %args.run, "Showing diff");
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
let state = client.get_run_state(&run_id).await?;
|
||||
|
||||
let patch = resolve_diff(&state, &args)?;
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
let value = serde_json::json!({
|
||||
"run_id": run_id,
|
||||
"node": args.node,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_checkpoint::git::Store;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork};
|
||||
use git2::Repository;
|
||||
|
|
@ -13,15 +10,10 @@ use crate::commands::rebuild::rebuild_run_store;
|
|||
use crate::shared::print_json_pretty;
|
||||
use crate::shared::repo::ensure_matching_repo_origin;
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &ForkArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run_id).await?.run_id;
|
||||
let state = client.get_run_state(&run_id).await?;
|
||||
|
|
@ -34,7 +26,7 @@ pub(crate) async fn run(
|
|||
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
|
||||
|
||||
if args.list {
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -56,7 +48,7 @@ pub(crate) async fn run(
|
|||
let run_id_string = run_id.to_string();
|
||||
let new_run_id_string = new_run_id.to_string();
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
let target = args.target.clone().unwrap_or_else(|| "latest".to_string());
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"source_run_id": run_id_string,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::json::normalize_json_value;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tokio::time;
|
||||
|
|
@ -29,14 +26,8 @@ use crate::shared::format_usd_micros;
|
|||
|
||||
const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500);
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &LogsArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(crate) async fn run(args: &LogsArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
info!(run_id = %run_id, "Showing logs");
|
||||
|
|
@ -60,7 +51,7 @@ pub(crate) async fn run(
|
|||
let stdout = io::stdout();
|
||||
let is_tty = stdout.is_terminal();
|
||||
let mut out = stdout.lock();
|
||||
let pretty = args.pretty && cli.output.format != OutputFormat::Json;
|
||||
let pretty = args.pretty && !ctx.json_output();
|
||||
|
||||
for line in &filtered {
|
||||
if pretty {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) mod attach;
|
||||
pub(crate) mod command;
|
||||
|
|
@ -27,29 +23,16 @@ pub(crate) mod ssh;
|
|||
pub(crate) mod start;
|
||||
pub(crate) mod wait;
|
||||
|
||||
pub(crate) async fn dispatch(
|
||||
cmd: RunCommands,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
_process_local_json: bool,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn dispatch(cmd: RunCommands, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
|
||||
match cmd {
|
||||
RunCommands::Run(args) => Box::pin(command::execute(args, cli, cli_layer, printer)).await,
|
||||
RunCommands::Run(args) => Box::pin(command::execute(args, base_ctx)).await,
|
||||
RunCommands::Create(args) => {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = load_settings_with_storage_dir(None)?;
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
let created_run = Box::pin(create::create_run(
|
||||
&ctx,
|
||||
&args,
|
||||
cli_defaults,
|
||||
styles,
|
||||
true,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
let created_run = Box::pin(create::create_run(&ctx, &args, styles, true)).await?;
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
|
||||
} else {
|
||||
fabro_util::printout!(printer, "{}", created_run.run_id);
|
||||
|
|
@ -57,27 +40,28 @@ pub(crate) async fn dispatch(
|
|||
Ok(())
|
||||
}
|
||||
RunCommands::Start(StartArgs { server, run }) => {
|
||||
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&run).await?.run_id;
|
||||
start::start_run_with_client(client.as_ref(), &run_id, false).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
RunCommands::Attach(AttachArgs { server, run }) => {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let ctx = CommandContext::for_target(&server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&run).await?.run_id;
|
||||
let json = ctx.json_output();
|
||||
let exit_code = Box::pin(attach::attach_run_with_client(
|
||||
client.as_ref(),
|
||||
&run_id,
|
||||
false,
|
||||
styles,
|
||||
cli.output.format == OutputFormat::Json,
|
||||
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
|
||||
json,
|
||||
ctx.verbose(),
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
|
|
@ -104,34 +88,31 @@ pub(crate) async fn dispatch(
|
|||
)
|
||||
.await
|
||||
}
|
||||
RunCommands::Diff(args) => diff::run(args, cli, cli_layer, printer).await,
|
||||
RunCommands::Diff(args) => diff::run(args, base_ctx).await,
|
||||
RunCommands::Logs(args) => {
|
||||
let styles = Styles::detect_stdout();
|
||||
logs::run(&args, &styles, cli, cli_layer, printer).await
|
||||
logs::run(&args, &styles, base_ctx).await
|
||||
}
|
||||
RunCommands::Resume(args) => {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
crate::sleep_inhibitor::guard(ctx.user_settings().cli.exec.prevent_idle_sleep)
|
||||
};
|
||||
Box::pin(resume::resume_command(
|
||||
args, styles, cli, cli_layer, printer,
|
||||
))
|
||||
.await
|
||||
Box::pin(resume::resume_command(args, styles, base_ctx)).await
|
||||
}
|
||||
RunCommands::Rewind(args) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
Box::pin(rewind::run(&args, &styles, cli, cli_layer, printer)).await
|
||||
Box::pin(rewind::run(&args, &styles, base_ctx)).await
|
||||
}
|
||||
RunCommands::Fork(args) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
Box::pin(fork::run(&args, &styles, cli, cli_layer, printer)).await
|
||||
Box::pin(fork::run(&args, &styles, base_ctx)).await
|
||||
}
|
||||
RunCommands::Wait(args) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
wait::run(&args, &styles, cli, cli_layer, printer).await
|
||||
wait::run(&args, &styles, base_ctx).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,13 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PreviewArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: PreviewArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(crate) async fn run(args: PreviewArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
let expires_in_secs =
|
||||
|
|
@ -31,7 +23,8 @@ pub(crate) async fn run(
|
|||
|
||||
info!(run_id = %args.run, port = args.port, "Generating preview URL");
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
let json = ctx.json_output();
|
||||
if json {
|
||||
match response.token {
|
||||
Some(token) => {
|
||||
print_json_pretty(&serde_json::json!({ "url": response.url, "token": token }))?;
|
||||
|
|
@ -56,7 +49,7 @@ pub(crate) async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
if args.open && !process_local_json {
|
||||
if should_open_browser(args.open, json) {
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process."
|
||||
|
|
@ -83,3 +76,23 @@ fn format_standard_output(url: &str, token: &str) -> String {
|
|||
fn format_signed_output(url: &str) -> String {
|
||||
format!("{url}\n")
|
||||
}
|
||||
|
||||
fn should_open_browser(open_requested: bool, json: bool) -> bool {
|
||||
open_requested && !json
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_open_browser;
|
||||
|
||||
#[test]
|
||||
fn json_output_suppresses_browser_opening() {
|
||||
assert!(!should_open_browser(true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_output_honors_browser_opening() {
|
||||
assert!(should_open_browser(true, false));
|
||||
assert!(!should_open_browser(false, false));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::ResumeArgs;
|
||||
|
|
@ -15,17 +12,16 @@ use crate::shared::print_json_pretty;
|
|||
pub(crate) async fn resume_command(
|
||||
args: ResumeArgs,
|
||||
styles: &'static Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
|
||||
super::start::start_run_with_client(client.as_ref(), &run_id, true).await?;
|
||||
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
let json = ctx.json_output();
|
||||
if args.detach {
|
||||
if json {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
|
||||
|
|
@ -39,7 +35,7 @@ pub(crate) async fn resume_command(
|
|||
true,
|
||||
styles,
|
||||
json,
|
||||
ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose,
|
||||
ctx.verbose(),
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ use cli_table::format::{Border, Separator};
|
|||
use cli_table::{Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_checkpoint::git::Store;
|
||||
use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::{EventBody, RunEvent};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -33,12 +31,11 @@ pub(crate) struct TimelineEntryJson {
|
|||
pub(crate) async fn run(
|
||||
args: &RewindArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run_id).await?.run_id;
|
||||
let state = client.get_run_state(&run_id).await?;
|
||||
|
|
@ -54,7 +51,7 @@ pub(crate) async fn run(
|
|||
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
|
||||
|
||||
if args.list || args.target.is_none() {
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&timeline_entries_json(&timeline))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -79,7 +76,7 @@ pub(crate) async fn run(
|
|||
|
||||
let run_id_string = run_id.to_string();
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"run_id": run_id_string,
|
||||
"target": target_arg,
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
use anyhow::{Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::{SshArgs, require_no_json_override};
|
||||
use crate::args::SshArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: SshArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
if process_local_json && !args.print {
|
||||
require_no_json_override(process_local_json)?;
|
||||
pub(crate) async fn run(args: SshArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
if !args.print {
|
||||
base_ctx.require_no_json_override()?;
|
||||
}
|
||||
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
let ssh = client.create_run_ssh_access(&run_id, args.ttl).await?;
|
||||
|
|
@ -27,7 +19,7 @@ pub(crate) async fn run(
|
|||
info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access");
|
||||
|
||||
if args.print {
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "command": ssh.command }))?;
|
||||
} else {
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ use std::io::Write;
|
|||
|
||||
use anyhow::{Result, bail};
|
||||
use fabro_types::RunId;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::records::Conclusion;
|
||||
|
|
@ -24,14 +22,9 @@ use crate::args::WaitArgs;
|
|||
use crate::command_context::CommandContext;
|
||||
use crate::shared::{format_duration_ms, format_usd_micros, run_status_kind};
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &WaitArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
pub(crate) async fn run(args: &WaitArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let client = ctx.server().await?;
|
||||
let run_id = client.resolve_run(&args.run).await?.run_id;
|
||||
info!(run_id = %run_id, "Waiting for run to complete");
|
||||
|
|
@ -63,7 +56,7 @@ pub(crate) async fn run(
|
|||
|
||||
let conclusion = client.get_run_state(&run_id).await?.conclusion;
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
let json_value = build_json_output(final_status, &run_id, conclusion.as_ref());
|
||||
let mut out = std::io::stdout().lock();
|
||||
serde_json::to_writer_pretty(&mut out, &json_value)?;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,24 @@
|
|||
use anyhow::{Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use super::short_run_id;
|
||||
use crate::args::{RunsArchiveArgs, RunsUnarchiveArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::server_client;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn archive_command(
|
||||
args: &RunsArchiveArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
run_bulk(
|
||||
Action::Archive,
|
||||
&args.runs,
|
||||
ctx.server().await?.as_ref(),
|
||||
cli,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
run_bulk(Action::Archive, &args.runs, &ctx).await
|
||||
}
|
||||
|
||||
pub(crate) async fn unarchive_command(
|
||||
args: &RunsUnarchiveArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
run_bulk(
|
||||
Action::Unarchive,
|
||||
&args.runs,
|
||||
ctx.server().await?.as_ref(),
|
||||
cli,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
run_bulk(Action::Unarchive, &args.runs, &ctx).await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -62,14 +40,11 @@ impl Action {
|
|||
}
|
||||
}
|
||||
|
||||
async fn run_bulk(
|
||||
action: Action,
|
||||
identifiers: &[String],
|
||||
client: &server_client::Client,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
async fn run_bulk(action: Action, identifiers: &[String], ctx: &CommandContext) -> Result<()> {
|
||||
let client = ctx.server().await?;
|
||||
let client = client.as_ref();
|
||||
let json = ctx.json_output();
|
||||
let printer = ctx.printer();
|
||||
let mut had_errors = false;
|
||||
let mut changed = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ use anyhow::Result;
|
|||
use chrono::Utc;
|
||||
use cli_table::format::{Border, Separator};
|
||||
use cli_table::{Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_util::text::strip_goal_decoration;
|
||||
use fabro_workflow::run_status::RunStatus;
|
||||
|
|
@ -20,11 +17,10 @@ use crate::shared::{color_if, format_duration_ms, run_status_kind, tilde_path};
|
|||
pub(crate) async fn list_command(
|
||||
args: &RunsListArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
let printer = ctx.printer();
|
||||
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
|
||||
let label_filters = parse_label_filters(&args.filter.label);
|
||||
let filtered = filter_server_runs(
|
||||
|
|
@ -35,7 +31,7 @@ pub(crate) async fn list_command(
|
|||
!args.all,
|
||||
);
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
let json_rows: Vec<_> = filtered
|
||||
.iter()
|
||||
.map(|run| {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use anyhow::{Result, bail};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use super::short_run_id;
|
||||
use crate::args::RunsRemoveArgs;
|
||||
|
|
@ -9,23 +6,16 @@ use crate::command_context::CommandContext;
|
|||
use crate::server_client;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(crate) async fn remove_command(
|
||||
args: &RunsRemoveArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?;
|
||||
remove_from(args, ctx.server().await?.as_ref(), cli, printer).await
|
||||
pub(crate) async fn remove_command(args: &RunsRemoveArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&args.server)?;
|
||||
remove_from(args, &ctx).await
|
||||
}
|
||||
|
||||
async fn remove_from(
|
||||
args: &RunsRemoveArgs,
|
||||
client: &server_client::Client,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
async fn remove_from(args: &RunsRemoveArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let client = ctx.server().await?;
|
||||
let client = client.as_ref();
|
||||
let json = ctx.json_output();
|
||||
let printer = ctx.printer();
|
||||
let mut had_errors = false;
|
||||
let mut removed = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@ use anyhow::Result;
|
|||
use chrono::{DateTime, Utc};
|
||||
use cli_table::format::{Border, Separator};
|
||||
use cli_table::{Cell, CellStruct, Style, Table};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::SecretListArgs;
|
||||
use crate::server_client::Client;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
|
||||
|
|
@ -22,14 +19,11 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) async fn list_command(
|
||||
client: &Client,
|
||||
_args: &SecretListArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) async fn list_command(_args: &SecretListArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let client = ctx.server().await?;
|
||||
let printer = ctx.printer();
|
||||
let secrets = client.list_secrets().await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&secrets)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,15 @@ mod rm;
|
|||
mod set;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{SecretCommand, SecretNamespace};
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
pub(crate) async fn dispatch(
|
||||
ns: SecretNamespace,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_target(&ns.target, printer, cli_layer)?;
|
||||
let server = ctx.server().await?;
|
||||
pub(crate) async fn dispatch(ns: SecretNamespace, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_target(&ns.target)?;
|
||||
match ns.command {
|
||||
SecretCommand::List(args) => list::list_command(&server, &args, cli, printer).await,
|
||||
SecretCommand::Rm(args) => rm::rm_command(&server, &args, cli, printer).await,
|
||||
SecretCommand::Set(args) => set::set_command(&server, &args, cli, printer).await,
|
||||
SecretCommand::List(args) => list::list_command(&args, &ctx).await,
|
||||
SecretCommand::Rm(args) => rm::rm_command(&args, &ctx).await,
|
||||
SecretCommand::Set(args) => set::set_command(&args, &ctx).await,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,16 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::SecretRmArgs;
|
||||
use crate::server_client::Client;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(super) async fn rm_command(
|
||||
client: &Client,
|
||||
args: &SecretRmArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) async fn rm_command(args: &SecretRmArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let client = ctx.server().await?;
|
||||
client.delete_secret_by_name(&args.key).await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
|
||||
} else {
|
||||
fabro_util::printerr!(printer, "Removed {}", args.key);
|
||||
fabro_util::printerr!(ctx.printer(), "Removed {}", args.key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,10 @@ use std::io::{IsTerminal, Read as _};
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use crate::args::{SecretSetArgs, SecretTypeArg};
|
||||
use crate::server_client::Client;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::shared::provider_auth::prompt_password;
|
||||
|
||||
|
|
@ -57,13 +54,9 @@ async fn resolve_value(args: &SecretSetArgs) -> Result<String> {
|
|||
bail!("secret value required: pass <VALUE>, use --value-stdin, or run interactively")
|
||||
}
|
||||
|
||||
pub(super) async fn set_command(
|
||||
client: &Client,
|
||||
args: &SecretSetArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) async fn set_command(args: &SecretSetArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let value = resolve_value(args).await?;
|
||||
let client = ctx.server().await?;
|
||||
let meta = client
|
||||
.create_secret(types::CreateSecretRequest {
|
||||
name: args.key.clone(),
|
||||
|
|
@ -72,10 +65,10 @@ pub(super) async fn set_command(
|
|||
description: args.description.clone(),
|
||||
})
|
||||
.await?;
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&meta)?;
|
||||
} else {
|
||||
fabro_util::printerr!(printer, "Set {}", meta.name);
|
||||
fabro_util::printerr!(ctx.printer(), "Set {}", meta.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,15 @@ use chrono::{DateTime, Utc};
|
|||
use cli_table::format::{Border, Justify, Separator};
|
||||
use cli_table::{Cell, CellStruct, Style, Table};
|
||||
use fabro_api::types;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::DfArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::{format_size, print_json_pretty};
|
||||
|
||||
pub(super) async fn df_command(
|
||||
args: &DfArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
|
||||
pub(super) async fn df_command(args: &DfArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_connection(&args.connection)?;
|
||||
let server = ctx.server().await?;
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
let json = ctx.json_output();
|
||||
|
||||
let (output, storage_dir) = if json {
|
||||
(server.get_system_disk_usage(args.verbose).await?, None)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_client::sse;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::args::SystemEventsArgs;
|
||||
|
|
@ -10,16 +7,14 @@ use crate::command_context::CommandContext;
|
|||
|
||||
pub(super) async fn events_command(
|
||||
args: &SystemEventsArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
|
||||
let ctx = base_ctx.with_connection(&args.connection)?;
|
||||
let server = ctx.server().await?;
|
||||
let mut stream = server.attach_events(&args.run_ids).await?;
|
||||
let mut pending = Vec::new();
|
||||
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
let json = ctx.json_output();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
pending.extend_from_slice(&chunk);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::SystemInfoArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
pub(super) async fn info_command(
|
||||
args: &SystemInfoArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
|
||||
pub(super) async fn info_command(args: &SystemInfoArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_connection(&args.connection)?;
|
||||
let server = ctx.server().await?;
|
||||
let response = server.get_system_info().await?;
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&response)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ use std::collections::HashMap;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
|
@ -11,13 +9,9 @@ use crate::args::RunsPruneArgs;
|
|||
use crate::command_context::CommandContext;
|
||||
use crate::shared::{format_size, print_json_pretty};
|
||||
|
||||
pub(super) async fn prune_command(
|
||||
args: &RunsPruneArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?;
|
||||
pub(super) async fn prune_command(args: &RunsPruneArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let ctx = base_ctx.with_connection(&args.connection)?;
|
||||
let printer = ctx.printer();
|
||||
let server = ctx.server().await?;
|
||||
let response = server
|
||||
.prune_runs(types::PruneRunsRequest {
|
||||
|
|
@ -29,7 +23,7 @@ pub(super) async fn prune_command(
|
|||
workflow: args.filter.workflow.clone(),
|
||||
})
|
||||
.await?;
|
||||
prune_from(&response, cli.output.format == OutputFormat::Json, printer)
|
||||
prune_from(&response, ctx.json_output(), printer)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@ use std::time::Duration;
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_config::Storage;
|
||||
use fabro_config::daemon::ServerDaemon;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::Home;
|
||||
use fabro_util::printer::Printer;
|
||||
use serde::Serialize;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::args::UninstallArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::commands::server::stop;
|
||||
use crate::shared::{format_size, print_json_pretty, tilde_path};
|
||||
use crate::{local_server, user_config};
|
||||
|
|
@ -43,12 +42,9 @@ struct Inventory {
|
|||
clippy::unused_async,
|
||||
reason = "The shared command dispatch path expects an async handler."
|
||||
)]
|
||||
pub(crate) async fn run_uninstall(
|
||||
args: &UninstallArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let json = cli.output.format == OutputFormat::Json;
|
||||
pub(crate) async fn run_uninstall(args: &UninstallArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let json = ctx.json_output();
|
||||
let printer = ctx.printer();
|
||||
let home = Home::from_env();
|
||||
let home_root = home.root().to_path_buf();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use tokio::task::JoinHandle;
|
|||
use tracing::debug;
|
||||
|
||||
use crate::args::UpgradeArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
// ── Download backend abstraction ───────────────────────────────────────────
|
||||
|
|
@ -434,11 +435,9 @@ impl UpgradeCheckState {
|
|||
|
||||
// ── Main upgrade command ───────────────────────────────────────────────────
|
||||
|
||||
pub(crate) async fn run_upgrade(
|
||||
args: UpgradeArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn run_upgrade(args: UpgradeArgs, ctx: &CommandContext) -> Result<()> {
|
||||
let cli = &ctx.user_settings().cli;
|
||||
let printer = ctx.printer();
|
||||
let current_exe = std::env::current_exe()
|
||||
.context("resolving current fabro executable path")?
|
||||
.canonicalize()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::load::load_settings_user;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::ValidateArgs;
|
||||
|
|
@ -15,11 +13,10 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
|
|||
pub(crate) async fn run(
|
||||
args: &ValidateArgs,
|
||||
styles: &Styles,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
base_ctx: &CommandContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
cwd: ctx.cwd().to_path_buf(),
|
||||
|
|
@ -33,7 +30,7 @@ pub(crate) async fn run(
|
|||
let response = client.run_preflight(built.manifest).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"workflow_name": response.workflow.name,
|
||||
"nodes": response.workflow.nodes,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
use std::io::IsTerminal;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
|
|
@ -16,14 +14,10 @@ use crate::command_context::CommandContext;
|
|||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::{self, ServerTarget};
|
||||
|
||||
pub(crate) async fn version_command(
|
||||
args: &VersionArgs,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn version_command(args: &VersionArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let client = client_info();
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?;
|
||||
let printer = base_ctx.printer();
|
||||
let ctx = base_ctx.with_target(&args.target)?;
|
||||
let server_target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?;
|
||||
let server_address = format_server_target(&server_target);
|
||||
let server_info = match ctx.server().await {
|
||||
|
|
@ -49,7 +43,7 @@ pub(crate) async fn version_command(
|
|||
},
|
||||
};
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&json_output(&client, &server_info))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,13 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_config::project::{discover_project_config, resolve_fabro_root};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::WorkflowCreateArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::{print_json_pretty, relative_path};
|
||||
|
||||
pub(super) fn create_command(
|
||||
args: &WorkflowCreateArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) fn create_command(args: &WorkflowCreateArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let Some((config_path, config)) = discover_project_config(&cwd)? else {
|
||||
|
|
@ -31,7 +26,7 @@ pub(super) fn create_command(
|
|||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
let created = write_workflow_scaffold(args, &fabro_root)?;
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
let created: Vec<_> = created.iter().map(|path| relative_path(path)).collect();
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"name": args.name,
|
||||
|
|
|
|||
|
|
@ -5,21 +5,17 @@ use fabro_config::project::{
|
|||
WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed,
|
||||
resolve_fabro_root,
|
||||
};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::WorkflowListArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::{color_if, print_json_pretty, relative_path};
|
||||
|
||||
const GOAL_MAX_LEN: usize = 60;
|
||||
|
||||
pub(super) fn list_command(
|
||||
_args: &WorkflowListArgs,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
pub(super) fn list_command(_args: &WorkflowListArgs, base_ctx: &CommandContext) -> Result<()> {
|
||||
let printer = base_ctx.printer();
|
||||
let styles = Styles::detect_stderr();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
|
|
@ -36,7 +32,7 @@ pub(super) fn list_command(
|
|||
|
||||
let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref());
|
||||
|
||||
if cli.output.format == OutputFormat::Json {
|
||||
if base_ctx.json_output() {
|
||||
print_json_pretty(&workflows)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ mod create;
|
|||
mod list;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{WorkflowCommand, WorkflowNamespace};
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliNamespace, printer: Printer) -> Result<()> {
|
||||
pub(crate) fn dispatch(ns: WorkflowNamespace, base_ctx: &CommandContext) -> Result<()> {
|
||||
match ns.command {
|
||||
WorkflowCommand::List(args) => list::list_command(&args, cli, printer),
|
||||
WorkflowCommand::Create(args) => create::create_command(&args, cli, printer),
|
||||
WorkflowCommand::List(args) => list::list_command(&args, base_ctx),
|
||||
WorkflowCommand::Create(args) => create::create_command(&args, base_ctx),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,10 @@ use std::ffi::OsString;
|
|||
use anyhow::Result;
|
||||
use args::{
|
||||
Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace,
|
||||
global_args_cli_layer, printer_from_verbosity, require_no_json_override,
|
||||
global_args_cli_layer, require_no_json_override,
|
||||
};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::{Combine, SettingsLayer};
|
||||
use fabro_util::exit::ExitClass;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -37,6 +35,8 @@ use fabro_util::{browser, exit};
|
|||
use rustls::crypto::ring::default_provider;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "fabro", version, long_version = LONG_VERSION)]
|
||||
struct Cli {
|
||||
|
|
@ -164,23 +164,14 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Err(err) => return (command_name, Err(err)),
|
||||
};
|
||||
|
||||
let user_settings = match user_config::load_settings() {
|
||||
Ok(settings) => settings,
|
||||
let base_ctx = match CommandContext::from_disk(&cli_layer, process_local_json) {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
};
|
||||
let combined_settings = SettingsLayer {
|
||||
cli: Some(cli_layer.clone()),
|
||||
..SettingsLayer::default()
|
||||
}
|
||||
.combine(user_settings);
|
||||
let cli_settings = match fabro_config::UserSettings::from_layer(&combined_settings) {
|
||||
Ok(settings) => settings.cli,
|
||||
Err(err) => return (command_name, Err(err.into())),
|
||||
};
|
||||
let printer = printer_from_verbosity(cli_settings.output.verbosity);
|
||||
let printer = base_ctx.printer();
|
||||
|
||||
let config_log_level = match &pre_tracing_bootstrap.sink {
|
||||
logging::InternalLogSink::Cli => cli_settings.logging.level.clone(),
|
||||
logging::InternalLogSink::Cli => base_ctx.user_settings().cli.logging.level.clone(),
|
||||
logging::InternalLogSink::Server { .. } => pre_tracing_bootstrap.config_log_level.clone(),
|
||||
};
|
||||
if let Err(err) = logging::init_tracing(
|
||||
|
|
@ -204,57 +195,44 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
| Commands::Repo(_)
|
||||
| Commands::Install { .. }
|
||||
) {
|
||||
commands::upgrade::spawn_upgrade_check(cli_settings.updates.check, printer)
|
||||
commands::upgrade::spawn_upgrade_check(base_ctx.user_settings().cli.updates.check, printer)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = Box::pin(async move {
|
||||
match *command {
|
||||
Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?,
|
||||
Commands::Exec(args) => {
|
||||
commands::exec::execute(args, &base_ctx).await?;
|
||||
}
|
||||
Commands::RunCmd(cmd) => {
|
||||
Box::pin(commands::run::dispatch(
|
||||
cmd,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
process_local_json,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
Box::pin(commands::run::dispatch(cmd, &base_ctx)).await?;
|
||||
}
|
||||
Commands::Preflight(args) => {
|
||||
commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::preflight::execute(args, &base_ctx).await?;
|
||||
}
|
||||
Commands::Validate(args) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
commands::validate::run(&args, &styles, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::validate::run(&args, &styles, &base_ctx).await?;
|
||||
}
|
||||
Commands::Graph(args) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
commands::graph::run(
|
||||
&args,
|
||||
&styles,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
process_local_json,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
commands::graph::run(&args, &styles, &base_ctx).await?;
|
||||
}
|
||||
Commands::Parse(args) => {
|
||||
commands::parse::run(&args, &cli_settings, printer)?;
|
||||
commands::parse::run(&args)?;
|
||||
}
|
||||
Commands::Artifact(ns) => {
|
||||
commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::artifact::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Dump(args) => {
|
||||
commands::dump::run(&args, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::dump::run(&args, &base_ctx).await?;
|
||||
}
|
||||
Commands::RunsCmd(cmd) => {
|
||||
commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::runs::dispatch(cmd, &base_ctx).await?;
|
||||
}
|
||||
Commands::Model { command } => {
|
||||
commands::model::execute(command, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::model::execute(command, &base_ctx).await?;
|
||||
}
|
||||
Commands::Server(ns) => {
|
||||
Box::pin(commands::server::dispatch(
|
||||
|
|
@ -266,21 +244,11 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
.await?;
|
||||
}
|
||||
Commands::Doctor(args) => {
|
||||
let verbose =
|
||||
args.verbose || cli_settings.output.verbosity == OutputVerbosity::Verbose;
|
||||
let exit_code = Box::pin(commands::doctor::run_doctor(
|
||||
&args,
|
||||
verbose,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
let exit_code = Box::pin(commands::doctor::run_doctor(&args, &base_ctx)).await?;
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
Commands::Version(args) => {
|
||||
commands::version::version_command(&args, &cli_settings, &cli_layer, printer)
|
||||
.await?;
|
||||
commands::version::version_command(&args, &base_ctx).await?;
|
||||
}
|
||||
Commands::Discord => {
|
||||
if process_local_json {
|
||||
|
|
@ -301,65 +269,40 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
}
|
||||
Commands::Repo(ns) => {
|
||||
commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::repo::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Install { args, command } => {
|
||||
Box::pin(commands::install::execute(
|
||||
&args,
|
||||
command,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
process_local_json,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
Box::pin(commands::install::execute(&args, command, &base_ctx)).await?;
|
||||
}
|
||||
Commands::Uninstall(args) => {
|
||||
commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?;
|
||||
commands::uninstall::run_uninstall(&args, &base_ctx).await?;
|
||||
}
|
||||
Commands::Auth(ns) => {
|
||||
commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?;
|
||||
commands::auth::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Pr(ns) => {
|
||||
Box::pin(commands::pr::dispatch(
|
||||
ns,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
Box::pin(commands::pr::dispatch(ns, &base_ctx)).await?;
|
||||
}
|
||||
Commands::Secret(ns) => {
|
||||
commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::secret::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Settings(args) => {
|
||||
Box::pin(commands::config::execute(
|
||||
&args,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
printer,
|
||||
))
|
||||
.await?;
|
||||
Box::pin(commands::config::execute(&args, &base_ctx)).await?;
|
||||
}
|
||||
Commands::Workflow(ns) => {
|
||||
commands::workflow::dispatch(ns, &base_ctx)?;
|
||||
}
|
||||
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?,
|
||||
Commands::Upgrade(args) => {
|
||||
commands::upgrade::run_upgrade(args, &cli_settings, printer).await?;
|
||||
commands::upgrade::run_upgrade(args, &base_ctx).await?;
|
||||
}
|
||||
Commands::Provider(ns) => {
|
||||
commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?;
|
||||
commands::provider::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Sandbox { command } => {
|
||||
commands::sandbox::dispatch(
|
||||
command,
|
||||
&cli_settings,
|
||||
&cli_layer,
|
||||
process_local_json,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
commands::sandbox::dispatch(command, &base_ctx).await?;
|
||||
}
|
||||
Commands::System(ns) => {
|
||||
commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
|
||||
commands::system::dispatch(ns, &base_ctx).await?;
|
||||
}
|
||||
Commands::Completion(args) => {
|
||||
require_no_json_override(process_local_json)?;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,30 @@ fn settings_uses_json_output_format_from_home_config() {
|
|||
assert!(value.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_status_ignores_json_output_format_from_home_config() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/settings.toml",
|
||||
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
||||
);
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.args(["auth", "status"])
|
||||
.output()
|
||||
.expect("command should run");
|
||||
|
||||
assert!(output.status.success());
|
||||
assert!(
|
||||
output.stdout.is_empty(),
|
||||
"stdout should stay empty in text mode"
|
||||
);
|
||||
let stderr = output_stderr(&output);
|
||||
assert!(stderr.contains("Not logged in to any servers."));
|
||||
assert!(stderr.contains("Dev token:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_list_uses_json_output_format_from_home_config() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,31 @@
|
|||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use httpmock::MockServer;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::support::setup_local_sandbox_run;
|
||||
use crate::support::unique_run_id;
|
||||
|
||||
fn remote_run_summary(run_id: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"run_id": run_id,
|
||||
"workflow_name": "Preview Test",
|
||||
"workflow_slug": "preview-test",
|
||||
"goal": "Preview test",
|
||||
"title": "Preview test",
|
||||
"labels": {},
|
||||
"host_repo_path": "/srv/repo",
|
||||
"repository": { "name": "repo" },
|
||||
"start_time": "2026-04-19T12:00:00Z",
|
||||
"created_at": "2026-04-19T12:00:00Z",
|
||||
"status": {
|
||||
"kind": "running"
|
||||
},
|
||||
"pending_control": null,
|
||||
"duration_ms": null,
|
||||
"elapsed_secs": null,
|
||||
"total_usd_micros": null
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -49,3 +74,55 @@ fn sandbox_preview_rejects_non_daytona_run() {
|
|||
error: Sandbox provider does not support this capability.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_preview_open_is_suppressed_by_json_output_format_from_home_config() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/settings.toml",
|
||||
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
||||
);
|
||||
let server = MockServer::start();
|
||||
let run_id = unique_run_id();
|
||||
let resolve_run = server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/runs/resolve")
|
||||
.query_param("selector", run_id.as_str());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(remote_run_summary(&run_id).to_string());
|
||||
});
|
||||
let preview = server.mock(|when, then| {
|
||||
when.method("POST")
|
||||
.path(format!("/api/v1/runs/{run_id}/preview"))
|
||||
.json_body(json!({
|
||||
"port": 3000,
|
||||
"expires_in_secs": 3600,
|
||||
"signed": true,
|
||||
}));
|
||||
then.status(201)
|
||||
.header("content-type", "application/json")
|
||||
.body(json!({ "url": "https://preview.example.test/app" }).to_string());
|
||||
});
|
||||
|
||||
let mut cmd = context.preview();
|
||||
cmd.args([
|
||||
"--server",
|
||||
&format!("{}/api/v1", server.base_url()),
|
||||
"--open",
|
||||
run_id.as_str(),
|
||||
"3000",
|
||||
]);
|
||||
let output = cmd.output().expect("command should run");
|
||||
|
||||
assert!(output.status.success(), "sandbox preview should succeed");
|
||||
let value: Value = serde_json::from_slice(&output.stdout).expect("preview JSON should parse");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"url": "https://preview.example.test/app"
|
||||
})
|
||||
);
|
||||
resolve_run.assert();
|
||||
preview.assert();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use httpmock::MockServer;
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -26,3 +27,87 @@ fn help() {
|
|||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_events_renders_text_lines_from_sse_payloads() {
|
||||
let context = test_context!();
|
||||
let server = MockServer::start();
|
||||
let run_id = crate::support::unique_run_id();
|
||||
let payload = serde_json::json!({
|
||||
"payload": {
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"run_id": run_id,
|
||||
"event": "run.completed",
|
||||
}
|
||||
});
|
||||
let attach_mock = server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/attach")
|
||||
.query_param("run_id", run_id.as_str());
|
||||
then.status(200)
|
||||
.header("Content-Type", "text/event-stream")
|
||||
.body(format!("data: {payload}\n\n"));
|
||||
});
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.args([
|
||||
"system",
|
||||
"events",
|
||||
"--server",
|
||||
&format!("{}/api/v1", server.base_url()),
|
||||
"--run-id",
|
||||
&run_id,
|
||||
])
|
||||
.output()
|
||||
.expect("command should run");
|
||||
|
||||
assert!(output.status.success(), "system events failed");
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
|
||||
assert_eq!(
|
||||
stdout.trim(),
|
||||
format!("2026-04-05T12:00:00Z {} run.completed", &run_id[..12])
|
||||
);
|
||||
attach_mock.assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_events_json_emits_raw_sse_payloads() {
|
||||
let context = test_context!();
|
||||
let server = MockServer::start();
|
||||
let run_id = crate::support::unique_run_id();
|
||||
let payload = serde_json::json!({
|
||||
"payload": {
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"run_id": run_id,
|
||||
"event": "run.completed",
|
||||
}
|
||||
});
|
||||
let attach_mock = server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/attach")
|
||||
.query_param("run_id", run_id.as_str());
|
||||
then.status(200)
|
||||
.header("Content-Type", "text/event-stream")
|
||||
.body(format!("data: {payload}\n\n"));
|
||||
});
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.args([
|
||||
"--json",
|
||||
"system",
|
||||
"events",
|
||||
"--server",
|
||||
&format!("{}/api/v1", server.base_url()),
|
||||
"--run-id",
|
||||
&run_id,
|
||||
])
|
||||
.output()
|
||||
.expect("command should run");
|
||||
|
||||
assert!(output.status.success(), "system events failed");
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
|
||||
assert_eq!(stdout.trim(), payload.to_string());
|
||||
attach_mock.assert();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,3 +48,41 @@ fn system_info_json_reports_runtime_fields() {
|
|||
assert!(value["uptime_secs"].is_number());
|
||||
assert!(value["runs"]["total"].is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_info_uses_explicit_storage_dir_override() {
|
||||
let mut context = test_context!();
|
||||
let storage_dir = context.temp_dir.join("alternate-storage");
|
||||
std::fs::create_dir_all(&storage_dir).unwrap();
|
||||
context.write_home(
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
"_version = 1\n\n[server.storage]\nroot = {:?}\n",
|
||||
context.storage_dir.display().to_string()
|
||||
),
|
||||
);
|
||||
context.ensure_home_server_auth_methods();
|
||||
context.manage_storage_dir(&storage_dir);
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.args([
|
||||
"--json",
|
||||
"system",
|
||||
"info",
|
||||
"--storage-dir",
|
||||
storage_dir.to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.expect("command should run");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"system info failed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let value: Value =
|
||||
serde_json::from_slice(&output.stdout).expect("system info JSON should parse");
|
||||
assert_eq!(value["storage_dir"], storage_dir.display().to_string());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue