From fbe0bdfbc162327a27df9f849d18905bfca845f9 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 01:08:14 -0400 Subject: [PATCH] refactor: align cli command boundaries on CommandContext --- ...refactor-command-context-alignment-plan.md | 835 ++++++++++++++++++ lib/crates/fabro-cli/src/command_context.rs | 281 ++++-- .../fabro-cli/src/commands/artifact/cp.rs | 20 +- .../fabro-cli/src/commands/artifact/list.rs | 18 +- .../fabro-cli/src/commands/artifact/mod.rs | 19 +- .../fabro-cli/src/commands/auth/login.rs | 18 +- .../fabro-cli/src/commands/auth/logout.rs | 17 +- lib/crates/fabro-cli/src/commands/auth/mod.rs | 22 +- .../fabro-cli/src/commands/auth/status.rs | 13 +- .../fabro-cli/src/commands/config/mod.rs | 20 +- lib/crates/fabro-cli/src/commands/doctor.rs | 15 +- lib/crates/fabro-cli/src/commands/graph.rs | 21 +- lib/crates/fabro-cli/src/commands/install.rs | 4 +- lib/crates/fabro-cli/src/commands/model.rs | 17 +- lib/crates/fabro-cli/src/commands/pr/close.rs | 21 +- .../fabro-cli/src/commands/pr/create.rs | 20 +- lib/crates/fabro-cli/src/commands/pr/list.rs | 20 +- lib/crates/fabro-cli/src/commands/pr/merge.rs | 21 +- lib/crates/fabro-cli/src/commands/pr/mod.rs | 38 +- lib/crates/fabro-cli/src/commands/pr/view.rs | 21 +- .../fabro-cli/src/commands/preflight.rs | 16 +- .../fabro-cli/src/commands/provider/login.rs | 13 +- .../fabro-cli/src/commands/provider/mod.rs | 14 +- .../fabro-cli/src/commands/repo/deinit.rs | 11 +- .../fabro-cli/src/commands/repo/init.rs | 30 +- lib/crates/fabro-cli/src/commands/repo/mod.rs | 20 +- .../fabro-cli/src/commands/run/command.rs | 19 +- lib/crates/fabro-cli/src/commands/run/cp.rs | 24 +- lib/crates/fabro-cli/src/commands/run/diff.rs | 15 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 19 +- lib/crates/fabro-cli/src/commands/run/logs.rs | 16 +- lib/crates/fabro-cli/src/commands/run/mod.rs | 46 +- .../fabro-cli/src/commands/run/preview.rs | 19 +- .../fabro-cli/src/commands/run/resume.rs | 13 +- .../fabro-cli/src/commands/run/rewind.rs | 14 +- lib/crates/fabro-cli/src/commands/run/ssh.rs | 23 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 16 +- .../fabro-cli/src/commands/runs/archive.rs | 29 +- .../fabro-cli/src/commands/runs/inspect.rs | 7 +- .../fabro-cli/src/commands/runs/list.rs | 13 +- lib/crates/fabro-cli/src/commands/runs/mod.rs | 25 +- lib/crates/fabro-cli/src/commands/runs/rm.rs | 26 +- .../fabro-cli/src/commands/sandbox/mod.rs | 22 +- .../fabro-cli/src/commands/secret/list.rs | 6 +- .../fabro-cli/src/commands/secret/mod.rs | 21 +- .../fabro-cli/src/commands/secret/rm.rs | 6 +- .../fabro-cli/src/commands/secret/set.rs | 6 +- .../fabro-cli/src/commands/store/dump.rs | 16 +- .../fabro-cli/src/commands/store/mod.rs | 13 +- .../fabro-cli/src/commands/system/df.rs | 15 +- .../fabro-cli/src/commands/system/events.rs | 12 +- .../fabro-cli/src/commands/system/info.rs | 15 +- .../fabro-cli/src/commands/system/mod.rs | 19 +- .../fabro-cli/src/commands/system/prune.rs | 19 +- lib/crates/fabro-cli/src/commands/validate.rs | 14 +- lib/crates/fabro-cli/src/commands/version.rs | 15 +- lib/crates/fabro-cli/src/main.rs | 103 +-- 57 files changed, 1445 insertions(+), 746 deletions(-) create mode 100644 docs/plans/2026-04-23-001-refactor-command-context-alignment-plan.md diff --git a/docs/plans/2026-04-23-001-refactor-command-context-alignment-plan.md b/docs/plans/2026-04-23-001-refactor-command-context-alignment-plan.md new file mode 100644 index 000000000..348d5cfd6 --- /dev/null +++ b/docs/plans/2026-04-23-001-refactor-command-context-alignment-plan.md @@ -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` diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index b5351d640..ce1741bb1 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -26,73 +26,55 @@ pub(crate) enum ServerMode { } pub(crate) struct CommandContext { - #[allow( - dead_code, - reason = "This item is kept for command plumbing that is still being wired through." - )] - printer: Printer, - cwd: PathBuf, - base_config_path: PathBuf, - machine_settings: SettingsLayer, - user_settings: UserSettings, - server_mode: ServerMode, - server: OnceCell>, + 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>, } impl CommandContext { - pub(crate) fn base(printer: Printer, cli_layer: &CliLayer) -> Result { - Self::new(printer, ServerMode::None, cli_layer) - } - - pub(crate) fn for_target( - args: &ServerTargetArgs, + pub(crate) fn base( printer: Printer, cli_layer: &CliLayer, + process_local_json: bool, ) -> Result { - Self::new( - printer, - ServerMode::ByTarget { - target_override: args.server.clone(), - }, - cli_layer, - ) + Self::new(printer, process_local_json, ServerMode::None, cli_layer) } - pub(crate) fn for_connection( - args: &ServerConnectionArgs, + pub(crate) fn with_target(&self, args: &ServerTargetArgs) -> Result { + self.with_server_mode(ServerMode::ByTarget { + target_override: args.server.clone(), + }) + } + + pub(crate) fn with_connection(&self, args: &ServerConnectionArgs) -> Result { + self.with_server_mode(ServerMode::ByStorageDir { + target_override: args.target.server.clone(), + storage_dir_override: args.storage_dir.clone_path(), + }) + } + + fn new( printer: Printer, + process_local_json: bool, + server_mode: ServerMode, cli_layer: &CliLayer, ) -> Result { - Self::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 { let cwd = std::env::current_dir().context("Failed to get current directory")?; let base_config_path = user_config::active_settings_path(None); - let disk_settings = match &server_mode { - ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?, - ServerMode::ByStorageDir { - storage_dir_override, - .. - } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, - }; - let machine_settings = combine_files(disk_settings, SettingsLayer { - cli: Some(cli_layer.clone()), - ..SettingsLayer::default() - }); - let user_settings = fabro_config::UserSettings::from_layer(&machine_settings)?; + let (machine_settings, user_settings) = load_merged_settings(cli_layer, &server_mode)?; Ok(Self { printer, + process_local_json, cwd, base_config_path, + cli_layer: cli_layer.clone(), machine_settings, user_settings, server_mode, @@ -100,14 +82,18 @@ impl CommandContext { }) } - #[allow( - dead_code, - reason = "This item is kept for command plumbing that is still being wired through." - )] pub(crate) fn printer(&self) -> Printer { self.printer } + pub(crate) fn explicit_json_requested(&self) -> bool { + self.process_local_json + } + + pub(crate) fn require_no_json_override(&self) -> Result<()> { + crate::args::require_no_json_override(self.process_local_json) + } + pub(crate) fn cwd(&self) -> &Path { &self.cwd } @@ -149,4 +135,193 @@ impl CommandContext { Ok(Arc::clone(client)) } + + fn with_server_mode(&self, server_mode: ServerMode) -> Result { + let (machine_settings, user_settings) = match &server_mode { + ServerMode::ByStorageDir { .. } => load_merged_settings(&self.cli_layer, &server_mode)?, + ServerMode::None | ServerMode::ByTarget { .. } => { + (self.machine_settings.clone(), self.user_settings.clone()) + } + }; + + Ok(Self { + printer: self.printer, + process_local_json: self.process_local_json, + cwd: self.cwd.clone(), + base_config_path: self.base_config_path.clone(), + cli_layer: self.cli_layer.clone(), + machine_settings, + user_settings, + server_mode, + server: OnceCell::new(), + }) + } +} + +fn load_merged_settings( + cli_layer: &CliLayer, + server_mode: &ServerMode, +) -> Result<(SettingsLayer, UserSettings)> { + let disk_settings = match server_mode { + ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?, + ServerMode::ByStorageDir { + storage_dir_override, + .. + } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, + }; + merge_settings_layer(disk_settings, cli_layer) +} + +fn merge_settings_layer( + disk_settings: SettingsLayer, + cli_layer: &CliLayer, +) -> Result<(SettingsLayer, UserSettings)> { + let machine_settings = combine_files(disk_settings, SettingsLayer { + cli: Some(cli_layer.clone()), + ..SettingsLayer::default() + }); + let user_settings = UserSettings::from_layer(&machine_settings)?; + Ok((machine_settings, user_settings)) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use fabro_config::parse_settings_layer; + use fabro_types::settings::cli::{CliLayer, CliOutputLayer, OutputFormat, OutputVerbosity}; + use fabro_util::printer::Printer; + use tokio::sync::OnceCell; + + use super::{CommandContext, ServerMode, merge_settings_layer}; + use crate::args::ServerTargetArgs; + + fn cli_layer_with_json_and_verbose() -> CliLayer { + CliLayer { + output: Some(CliOutputLayer { + format: Some(OutputFormat::Json), + verbosity: Some(OutputVerbosity::Verbose), + }), + ..CliLayer::default() + } + } + + fn synthetic_context(process_local_json: bool, printer: Printer) -> CommandContext { + let cli_layer = cli_layer_with_json_and_verbose(); + let (machine_settings, user_settings) = + merge_settings_layer(parse_settings_layer("_version = 1\n").unwrap(), &cli_layer) + .expect("settings should merge"); + + CommandContext { + printer, + process_local_json, + cwd: PathBuf::from("/tmp/workspace"), + base_config_path: PathBuf::from("/tmp/settings.toml"), + cli_layer, + machine_settings, + user_settings, + server_mode: ServerMode::None, + server: OnceCell::new(), + } + } + + #[test] + fn context_exposes_resolved_output_and_explicit_json_state() { + let ctx = synthetic_context(true, Printer::Default); + + assert_eq!(ctx.user_settings().cli.output.format, OutputFormat::Json); + assert_eq!( + ctx.user_settings().cli.output.verbosity, + OutputVerbosity::Verbose + ); + assert!(ctx.explicit_json_requested()); + assert_eq!(ctx.printer(), Printer::Default); + } + + #[test] + fn deriving_target_context_preserves_invocation_state() { + let base = synthetic_context(true, Printer::Verbose); + let derived = base + .with_target(&ServerTargetArgs { + server: Some("https://fabro.example.com".to_string()), + }) + .expect("target context should derive"); + + assert_eq!(derived.printer(), Printer::Verbose); + assert!(derived.explicit_json_requested()); + assert_eq!(derived.user_settings(), base.user_settings()); + assert_eq!(derived.machine_settings(), base.machine_settings()); + assert_eq!(derived.cwd(), base.cwd()); + assert!(matches!( + derived.server_mode, + ServerMode::ByTarget { target_override: Some(ref target) } + if target == "https://fabro.example.com" + )); + } + + #[test] + fn storage_dir_override_only_changes_storage_root_in_merged_settings() { + let cli_layer = cli_layer_with_json_and_verbose(); + let base_disk_settings = parse_settings_layer( + r#" +_version = 1 + +[server.storage] +root = "/srv/fabro/default" +"#, + ) + .expect("settings fixture should parse"); + let override_disk_settings = fabro_config::user::apply_storage_dir_override( + base_disk_settings.clone(), + Some(std::path::Path::new("/srv/fabro/override")), + ); + + let (base_settings, base_user_settings) = + merge_settings_layer(base_disk_settings, &cli_layer) + .expect("base settings should merge"); + let (connection_settings, connection_user_settings) = + merge_settings_layer(override_disk_settings, &cli_layer) + .expect("connection settings should merge"); + + assert_eq!(base_user_settings, connection_user_settings); + assert_eq!(base_user_settings.cli.output.format, OutputFormat::Json); + assert_eq!( + base_settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.as_ref()) + .map(|root| root.as_source()), + Some("/srv/fabro/default".to_string()) + ); + assert_eq!( + connection_settings + .server + .as_ref() + .and_then(|server| server.storage.as_ref()) + .and_then(|storage| storage.root.as_ref()) + .map(|root| root.as_source()), + Some("/srv/fabro/override".to_string()) + ); + } + + #[test] + fn explicit_json_guard_uses_invocation_flag_not_resolved_output_format() { + let json_ctx = synthetic_context(true, Printer::Default); + let text_ctx = synthetic_context(false, Printer::Default); + + assert!(json_ctx.require_no_json_override().is_err()); + assert!(text_ctx.require_no_json_override().is_ok()); + } + + #[test] + fn target_resolution_errors_remain_deferred() { + let ctx = synthetic_context(false, Printer::Default) + .with_target(&ServerTargetArgs { + server: Some("not-a-valid-target".to_string()), + }) + .expect("target derivation should not resolve the target eagerly"); + + assert!(matches!(ctx.server_mode, ServerMode::ByTarget { .. })); + } } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index 2756912c1..5f6652326 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -6,28 +6,22 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use crate::args::ArtifactCpArgs; +use crate::command_context::CommandContext; use crate::server_client::Client; use crate::shared::{print_json_pretty, split_run_path}; -pub(super) async fn cp_command( - args: &ArtifactCpArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(super) async fn cp_command(args: &ArtifactCpArgs, base_ctx: &CommandContext) -> Result<()> { + let printer = base_ctx.printer(); let (run_id_selector, asset_path) = parse_source(&args.source); let (run_id, client, entries) = super::resolve_artifacts( + base_ctx, &args.server, run_id_selector, args.node.as_deref(), args.retry, - cli_layer, - printer, ) .await?; @@ -64,7 +58,7 @@ pub(super) async fn cp_command( .unwrap_or_else(|| std::ffi::OsStr::new(&entry.relative_path)), ); write_artifact_file(&client, &run_id, entry, &dest_file).await?; - if cli.output.format == OutputFormat::Json { + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "copied": [{ "relative_path": entry.relative_path, @@ -125,7 +119,7 @@ pub(super) async fn cp_command( } } - if cli.output.format == OutputFormat::Json { + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "copied": copied }))?; } else { fabro_util::printout!( diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index e7de1d4a4..e5375b659 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,30 +1,24 @@ use anyhow::Result; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use crate::args::ArtifactListArgs; +use crate::command_context::CommandContext; -pub(super) async fn list_command( - args: &ArtifactListArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(super) async fn list_command(args: &ArtifactListArgs, base_ctx: &CommandContext) -> Result<()> { + let printer = base_ctx.printer(); let (_run_id, _client, entries) = super::resolve_artifacts( + base_ctx, &args.server, &args.run_id, args.node.as_deref(), args.retry, - cli_layer, - printer, ) .await?; - if cli.output.format == OutputFormat::Json { + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?); return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 605d1beec..b9a5ffcf7 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -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, - cli_layer: &CliLayer, - printer: Printer, ) -> Result<(RunId, Client, Vec)> { - 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, } } diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/crates/fabro-cli/src/commands/auth/login.rs index 7968fb78e..926dd23a9 100644 --- a/lib/crates/fabro-cli/src/commands/auth/login.rs +++ b/lib/crates/fabro-cli/src/commands/auth/login.rs @@ -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(); diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/crates/fabro-cli/src/commands/auth/logout.rs index 6acf20275..f1ad4b806 100644 --- a/lib/crates/fabro-cli/src/commands/auth/logout.rs +++ b/lib/crates/fabro-cli/src/commands/auth/logout.rs @@ -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(()); diff --git a/lib/crates/fabro-cli/src/commands/auth/mod.rs b/lib/crates/fabro-cli/src/commands/auth/mod.rs index dd4ca8a83..0802ac73d 100644 --- a/lib/crates/fabro-cli/src/commands/auth/mod.rs +++ b/lib/crates/fabro-cli/src/commands/auth/mod.rs @@ -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), } } diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/crates/fabro-cli/src/commands/auth/status.rs index aa82b6f7e..808745e7f 100644 --- a/lib/crates/fabro-cli/src/commands/auth/status.rs +++ b/lib/crates/fabro-cli/src/commands/auth/status.rs @@ -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, diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 1535bfa0e..34ced43a3 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -11,9 +11,7 @@ use std::io::Write; use fabro_api::types::ServerSettings; use fabro_config::UserSettings; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use serde::Serialize; use crate::args::SettingsArgs; @@ -28,10 +26,9 @@ struct RenderedConfig { async fn rendered_config( args: &SettingsArgs, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> anyhow::Result { - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.target)?; let user = fabro_config::UserSettings::resolve()?; let server = ctx .server() @@ -41,14 +38,9 @@ async fn rendered_config( serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into) } -pub(crate) async fn execute( - args: &SettingsArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> anyhow::Result<()> { - let config = Box::pin(rendered_config(args, cli_layer, printer)).await?; - if cli.output.format == OutputFormat::Json { +pub(crate) async fn execute(args: &SettingsArgs, base_ctx: &CommandContext) -> anyhow::Result<()> { + let config = Box::pin(rendered_config(args, base_ctx)).await?; + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&config)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index 85c8517c6..e88731564 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -3,8 +3,7 @@ use std::path::PathBuf; use anyhow::Result; use fabro_api::types as api_types; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; pub(crate) use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, }; @@ -142,13 +141,13 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer: pub(crate) async fn run_doctor( args: &DoctorArgs, - verbose: bool, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result { + let verbose = + args.verbose || base_ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose; + let printer = base_ctx.printer(); let styles = Styles::detect_stdout(); - let json = cli.output.format == OutputFormat::Json; + let json = base_ctx.user_settings().cli.output.format == OutputFormat::Json; let spinner = if json { None } else { @@ -179,7 +178,7 @@ pub(crate) async fn run_doctor( }], }; - let ctx = match CommandContext::for_target(&args.target, printer, cli_layer) { + let ctx = match base_ctx.with_target(&args.target) { Ok(ctx) => ctx, Err(err) => { report.sections.push(CheckSection { diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 71ffa9f68..066d41cad 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -13,13 +13,12 @@ use anyhow::{Context, bail}; use fabro_api::types; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliNamespace, SettingsLayer}; -use fabro_util::printer::Printer; +use fabro_types::settings::SettingsLayer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use tracing::debug; -use crate::args::{GraphArgs, GraphDirection, require_no_json_override}; +use crate::args::{GraphArgs, GraphDirection}; use crate::command_context::CommandContext; use crate::commands::run::output::api_diagnostics_to_local; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest}; @@ -28,16 +27,14 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r pub(crate) async fn run( args: &GraphArgs, styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - process_local_json: bool, - printer: Printer, + base_ctx: &CommandContext, ) -> anyhow::Result<()> { - if process_local_json && args.output.is_none() { - require_no_json_override(process_local_json)?; + if base_ctx.explicit_json_requested() && args.output.is_none() { + base_ctx.require_no_json_override()?; } - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -73,7 +70,7 @@ pub(crate) async fn run( if let Some(ref output_path) = args.output { std::fs::write(output_path, &rendered) .with_context(|| format!("writing rendered graph to {}", output_path.display()))?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "path": absolute_or_current(output_path), "format": args.format.to_string(), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 3e0753af4..07a7574f9 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -52,6 +52,7 @@ use crate::args::{ DoctorArgs, InstallArgs, InstallCommand, InstallGitHubStrategyArg, InstallGithubArgs, InstallNonInteractiveArgs, ServerTargetArgs, }; +use crate::command_context::CommandContext; use crate::commands::server::{start, stop}; use crate::gh::GhCli; use crate::shared::provider_auth::{ @@ -1941,7 +1942,8 @@ async fn run_install_inner( target: ServerTargetArgs::default(), verbose: false, }; - doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await + let base_ctx = CommandContext::base(printer, cli_layer, false)?; + doctor::run_doctor(&doctor_args, &base_ctx).await }, ) .await? diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index d7c26c2a5..e336696b1 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -3,9 +3,7 @@ use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::types as api_types; use fabro_model::{Catalog, Model, Provider}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use serde::Serialize; @@ -42,19 +40,22 @@ struct ModelTestOutput { pub(crate) async fn execute( command: Option, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { let command = command.unwrap_or_default(); let target_args = match &command { ModelsCommand::List(args) => &args.target, ModelsCommand::Test(args) => &args.target, }; - let ctx = CommandContext::for_target(target_args, printer, cli_layer)?; + let ctx = base_ctx.with_target(target_args)?; let server = ctx.server().await?; - run_models(command, &server, cli.output.format == OutputFormat::Json).await + run_models( + command, + &server, + ctx.user_settings().cli.output.format == OutputFormat::Json, + ) + .await } fn format_context_window(tokens: i64) -> String { diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index c48132ba0..cdc9bcba7 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -1,22 +1,17 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::info; use crate::args::PrCloseArgs; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(super) async fn close_command( - args: PrCloseArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; +pub(super) async fn close_command(args: PrCloseArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, base_ctx).await?; - let creds = super::load_github_credentials_required(cli_layer, printer)?; + let creds = super::load_github_credentials_required(base_ctx)?; fabro_github::close_pull_request( &creds, @@ -29,7 +24,7 @@ pub(super) async fn close_command( .map_err(|err| anyhow::anyhow!("{err}"))?; info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request"); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "number": record.number, "html_url": record.html_url, diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index e42c633e2..907ccef08 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -5,9 +5,7 @@ use fabro_auth::configured_providers_from_process_env; use fabro_config::Storage; use fabro_model::Catalog; use fabro_sandbox::daytona::detect_repo_info; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_vault::Vault; use fabro_workflow::outcome::StageStatus; use fabro_workflow::pull_request::maybe_open_pull_request; @@ -25,13 +23,9 @@ use crate::user_config; deprecated, reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side" )] -pub(super) async fn create_command( - args: PrCreateArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(super) async fn create_command(args: PrCreateArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let events = client.list_run_events(&run_id, None, None).await?; @@ -86,7 +80,7 @@ pub(super) async fn create_command( let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url) .map_err(|err| anyhow::anyhow!("{err}"))?; - let creds = super::load_github_credentials_required(cli_layer, printer)?; + let creds = super::load_github_credentials_required(base_ctx)?; let branch_found = fabro_github::branch_exists( &creds, @@ -136,14 +130,14 @@ pub(super) async fn create_command( match pull_request { Some(record) => { info!(pr_url = %record.html_url, "Pull request created"); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&record)?; } else { fabro_util::printout!(printer, "{}", record.html_url); } } None => { - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::Value::Null)?; } else { fabro_util::printout!(printer, "No pull request created (empty diff)."); diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index bf86031eb..c38c59b61 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -1,9 +1,7 @@ use anyhow::Result; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use futures::future::join_all; use serde::Serialize; @@ -23,13 +21,9 @@ struct PrRow { url: String, } -pub(super) async fn list_command( - args: PrListArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(super) async fn list_command(args: PrListArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let mut entries = Vec::new(); @@ -42,7 +36,7 @@ pub(super) async fn list_command( } if entries.is_empty() { - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&Vec::::new())?; return Ok(()); } @@ -50,7 +44,7 @@ pub(super) async fn list_command( return Ok(()); } - let creds = super::load_github_credentials_required(cli_layer, printer)?; + let creds = super::load_github_credentials_required(base_ctx)?; let futures: Vec<_> = entries .iter() @@ -104,7 +98,7 @@ pub(super) async fn list_command( .collect() }; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&rows)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 3ee3f3338..ac043b654 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -1,22 +1,17 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::info; use crate::args::PrMergeArgs; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(super) async fn merge_command( - args: PrMergeArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; +pub(super) async fn merge_command(args: PrMergeArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, base_ctx).await?; - let creds = super::load_github_credentials_required(cli_layer, printer)?; + let creds = super::load_github_credentials_required(base_ctx)?; fabro_github::merge_pull_request( &creds, @@ -30,7 +25,7 @@ pub(super) async fn merge_command( .map_err(|err| anyhow::anyhow!("{err}"))?; info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request"); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "number": record.number, "html_url": record.html_url, diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index dd70c83f7..a0c83c21b 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -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 { - 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 { + let server_settings = fabro_config::ServerSettings::from_layer(base_ctx.machine_settings()) .map_err(anyhow::Error::from)?; - let vault = user_config::storage_dir(ctx.machine_settings()) + let vault = user_config::storage_dir(base_ctx.machine_settings()) .ok() .and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok()); let creds = build_github_credentials( @@ -70,10 +57,9 @@ fn load_github_credentials_required( pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server, printer, cli_layer)?; + let ctx = base_ctx.with_target(server)?; let client = ctx.server().await?; let run_id = client.resolve_run(run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index d766e7dd4..e0f934d15 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -1,22 +1,17 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::info; use crate::args::PrViewArgs; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(super) async fn view_command( - args: PrViewArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let (record, _run_id) = - super::load_pr_record(&args.server, &args.run_id, cli_layer, printer).await?; +pub(super) async fn view_command(args: PrViewArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, base_ctx).await?; - let creds = super::load_github_credentials_required(cli_layer, printer)?; + let creds = super::load_github_credentials_required(base_ctx)?; let detail = fabro_github::get_pull_request( &creds, @@ -30,7 +25,7 @@ pub(super) async fn view_command( info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request"); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&detail)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 1a49e8cb5..a3fb819bb 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -1,9 +1,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_util::terminal::Styles; use crate::args::PreflightArgs; @@ -17,13 +15,13 @@ use crate::shared::print_json_pretty; pub(crate) async fn execute( mut args: PreflightArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; - args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.target)?; + args.verbose = + args.verbose || ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), @@ -38,7 +36,7 @@ pub(crate) async fn execute( let response = client.run_preflight(manifest.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&response)?; } else { print_preflight_workflow_summary( diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 86a393a09..32babd47d 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -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( diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index 07e3d9adf..8dd8f0080 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -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, } } diff --git a/lib/crates/fabro-cli/src/commands/repo/deinit.rs b/lib/crates/fabro-cli/src/commands/repo/deinit.rs index 8ed8ae707..ff0e0d1f7 100644 --- a/lib/crates/fabro-cli/src/commands/repo/deinit.rs +++ b/lib/crates/fabro-cli/src/commands/repo/deinit.rs @@ -1,9 +1,10 @@ use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliNamespace; use fabro_types::settings::cli::OutputFormat; -use fabro_util::printer::Printer; -pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result> { +use crate::command_context::CommandContext; + +pub(crate) fn run_deinit(base_ctx: &CommandContext) -> Result> { + let printer = base_ctx.printer(); let repo_root = super::init::git_repo_root()?; let mut removed = Vec::new(); @@ -20,7 +21,7 @@ pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result Result Result { pub(crate) async fn run_init( args: &RepoInitArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result> { + let printer = base_ctx.printer(); let repo_root = spawn_blocking(git_repo_root) .await .context("git repo root task panicked")??; @@ -79,7 +76,7 @@ draft = true let green = console::Style::new().green(); let bold = console::Style::new().bold(); let dim = console::Style::new().dim(); - if cli.output.format != OutputFormat::Json { + if base_ctx.user_settings().cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -112,7 +109,7 @@ draft = true ) .with_context(|| format!("failed to write {}", dot_path.display()))?; created.push(".fabro/workflows/hello/workflow.fabro".to_string()); - if cli.output.format != OutputFormat::Json { + if base_ctx.user_settings().cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -129,7 +126,7 @@ draft = true ) .with_context(|| format!("failed to write {}", toml_path.display()))?; created.push(".fabro/workflows/hello/workflow.toml".to_string()); - if cli.output.format != OutputFormat::Json { + if base_ctx.user_settings().cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -138,7 +135,7 @@ draft = true ); } - if cli.output.format != OutputFormat::Json { + if base_ctx.user_settings().cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, "\n{} Run a workflow with:\n\n {}", @@ -150,18 +147,15 @@ draft = true ); } - if cli.output.format != OutputFormat::Json { - check_github_app_installation(&args.target, cli_layer, printer).await; + if base_ctx.user_settings().cli.output.format != OutputFormat::Json { + check_github_app_installation(&args.target, base_ctx).await; } Ok(created) } -async fn check_github_app_installation( - target: &ServerTargetArgs, - cli_layer: &CliLayer, - printer: Printer, -) { +async fn check_github_app_installation(target: &ServerTargetArgs, base_ctx: &CommandContext) { + let printer = base_ctx.printer(); // Get the git remote origin URL let output = match TokioCommand::new("git") .args(["remote", "get-url", "origin"]) @@ -199,7 +193,7 @@ async fn check_github_app_installation( return; // Not a GitHub repo — skip silently }; - let ctx = match CommandContext::for_target(target, printer, cli_layer) { + let ctx = match base_ctx.with_target(target) { Ok(ctx) => ctx, Err(err) => { fabro_util::printerr!( diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/crates/fabro-cli/src/commands/repo/mod.rs index 5ddeaa95b..8c768e576 100644 --- a/lib/crates/fabro-cli/src/commands/repo/mod.rs +++ b/lib/crates/fabro-cli/src/commands/repo/mod.rs @@ -2,30 +2,24 @@ pub(crate) mod deinit; pub(crate) mod init; use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use crate::args::{RepoCommand, RepoNamespace}; +use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(crate) async fn dispatch( - ns: RepoNamespace, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn dispatch(ns: RepoNamespace, base_ctx: &CommandContext) -> Result<()> { match ns.command { RepoCommand::Init(args) => { - let created = init::run_init(&args, cli, cli_layer, printer).await?; - if cli.output.format == OutputFormat::Json { + let created = init::run_init(&args, base_ctx).await?; + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "created": created }))?; } Ok(()) } RepoCommand::Deinit => { - let removed = deinit::run_deinit(cli, printer)?; - if cli.output.format == OutputFormat::Json { + let removed = deinit::run_deinit(base_ctx)?; + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "removed": removed }))?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 20572cf2b..663cad207 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,7 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_util::terminal::Styles; use crate::args::RunArgs; @@ -9,16 +7,13 @@ use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config::load_settings_with_storage_dir; -pub(crate) async fn execute( - mut args: RunArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn execute(mut args: RunArgs, base_ctx: &CommandContext) -> Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.target)?; let cli_defaults = load_settings_with_storage_dir(None)?; - args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; + args.verbose = + args.verbose || ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose; let quiet = args.detach; let prevent_idle_sleep = ctx.user_settings().cli.exec.prevent_idle_sleep; @@ -50,7 +45,7 @@ pub(crate) async fn execute( let client = ctx.server().await?; super::start::start_run_with_client(&client, &created_run.run_id, false).await?; - let json = cli.output.format == OutputFormat::Json; + let json = ctx.user_settings().cli.output.format == OutputFormat::Json; if args.detach { if json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 84c05258e..170ea62e1 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,9 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tokio::fs; use tracing::{debug, info}; @@ -26,12 +24,7 @@ enum CopyDirection { }, } -pub(crate) async fn cp_command( - args: CpArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn cp_command(args: CpArgs, base_ctx: &CommandContext) -> Result<()> { let direction = parse_direction(&args.src, &args.dst)?; match direction { @@ -41,7 +34,7 @@ pub(crate) async fn cp_command( local_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?; + resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?; let file_count = if args.recursive { Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?) @@ -51,7 +44,7 @@ pub(crate) async fn cp_command( None }; - if cli.output.format == OutputFormat::Json { + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { let mut value = serde_json::json!({ "direction": "download", "recursive": args.recursive, @@ -72,7 +65,7 @@ pub(crate) async fn cp_command( remote_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, cli_layer, printer).await?; + resolve_client_and_run_id(base_ctx, &args.server, &run_prefix).await?; let file_count = if args.recursive { Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?) @@ -82,7 +75,7 @@ pub(crate) async fn cp_command( None }; - if cli.output.format == OutputFormat::Json { + if base_ctx.user_settings().cli.output.format == OutputFormat::Json { let mut value = serde_json::json!({ "direction": "upload", "recursive": args.recursive, @@ -124,12 +117,11 @@ fn parse_direction(src: &str, dst: &str) -> Result { } 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)) diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 51da824cc..2bebdda23 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -10,9 +10,7 @@ use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::{debug, info}; use crate::args::DiffArgs; @@ -20,21 +18,16 @@ use crate::command_context::CommandContext; use crate::server_client::RunProjection; use crate::shared::print_json_pretty; -pub(crate) async fn run( - args: DiffArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn run(args: DiffArgs, base_ctx: &CommandContext) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let state = client.get_run_state(&run_id).await?; let patch = resolve_diff(&state, &args)?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { let value = serde_json::json!({ "run_id": run_id, "node": args.node, diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 70d5ebba6..23ba16eda 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -1,8 +1,6 @@ use anyhow::{Context, Result}; use fabro_checkpoint::git::Store; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork}; use git2::Repository; @@ -13,15 +11,10 @@ use crate::commands::store::rebuild::rebuild_run_store; use crate::shared::print_json_pretty; use crate::shared::repo::ensure_matching_repo_origin; -pub(crate) async fn run( - args: &ForkArgs, - styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn run(args: &ForkArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; @@ -34,7 +27,7 @@ pub(crate) async fn run( let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?; if args.list { - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?; return Ok(()); } @@ -56,7 +49,7 @@ pub(crate) async fn run( let run_id_string = run_id.to_string(); let new_run_id_string = new_run_id.to_string(); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { let target = args.target.clone().unwrap_or_else(|| "latest".to_string()); print_json_pretty(&serde_json::json!({ "source_run_id": run_id_string, diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 879d3f68b..2592c43cd 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -13,10 +13,8 @@ use std::time::Duration; use anyhow::{Context, Result, bail}; use chrono::{DateTime, Utc}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_util::json::normalize_json_value; -use fabro_util::printer::Printer; use fabro_util::redact::redact_jsonl_line; use fabro_util::terminal::Styles; use tokio::time; @@ -29,14 +27,8 @@ use crate::shared::format_usd_micros; const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); -pub(crate) async fn run( - args: &LogsArgs, - styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(crate) async fn run(args: &LogsArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; info!(run_id = %run_id, "Showing logs"); @@ -60,7 +52,7 @@ pub(crate) async fn run( let stdout = io::stdout(); let is_tty = stdout.is_terminal(); let mut out = stdout.lock(); - let pretty = args.pretty && cli.output.format != OutputFormat::Json; + let pretty = args.pretty && ctx.user_settings().cli.output.format != OutputFormat::Json; for line in &filtered { if pretty { diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 7acd35e3e..0256ca566 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,7 +1,5 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_util::terminal::Styles; use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs}; @@ -27,19 +25,15 @@ pub(crate) mod ssh; pub(crate) mod start; pub(crate) mod wait; -pub(crate) async fn dispatch( - cmd: RunCommands, - cli: &CliNamespace, - cli_layer: &CliLayer, - _process_local_json: bool, - printer: Printer, -) -> Result<()> { +pub(crate) async fn dispatch(cmd: RunCommands, base_ctx: &CommandContext) -> Result<()> { + let printer = base_ctx.printer(); + match cmd { - RunCommands::Run(args) => Box::pin(command::execute(args, cli, cli_layer, printer)).await, + RunCommands::Run(args) => Box::pin(command::execute(args, base_ctx)).await, RunCommands::Create(args) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); let cli_defaults = load_settings_with_storage_dir(None)?; - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.target)?; let created_run = Box::pin(create::create_run( &ctx, &args, @@ -49,7 +43,7 @@ pub(crate) async fn dispatch( printer, )) .await?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { fabro_util::printout!(printer, "{}", created_run.run_id); @@ -57,26 +51,27 @@ pub(crate) async fn dispatch( Ok(()) } RunCommands::Start(StartArgs { server, run }) => { - let ctx = CommandContext::for_target(&server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&run).await?.run_id; start::start_run_with_client(client.as_ref(), &run_id, false).await?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id }))?; } Ok(()) } RunCommands::Attach(AttachArgs { server, run }) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&run).await?.run_id; + let json = ctx.user_settings().cli.output.format == OutputFormat::Json; let exit_code = Box::pin(attach::attach_run_with_client( client.as_ref(), &run_id, false, styles, - cli.output.format == OutputFormat::Json, + json, ctx.user_settings().cli.output.verbosity == OutputVerbosity::Verbose, printer, )) @@ -104,34 +99,31 @@ pub(crate) async fn dispatch( ) .await } - RunCommands::Diff(args) => diff::run(args, cli, cli_layer, printer).await, + RunCommands::Diff(args) => diff::run(args, base_ctx).await, RunCommands::Logs(args) => { let styles = Styles::detect_stdout(); - logs::run(&args, &styles, cli, cli_layer, printer).await + logs::run(&args, &styles, base_ctx).await } RunCommands::Resume(args) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; crate::sleep_inhibitor::guard(ctx.user_settings().cli.exec.prevent_idle_sleep) }; - Box::pin(resume::resume_command( - args, styles, cli, cli_layer, printer, - )) - .await + Box::pin(resume::resume_command(args, styles, base_ctx)).await } RunCommands::Rewind(args) => { let styles = Styles::detect_stderr(); - Box::pin(rewind::run(&args, &styles, cli, cli_layer, printer)).await + Box::pin(rewind::run(&args, &styles, base_ctx)).await } RunCommands::Fork(args) => { let styles = Styles::detect_stderr(); - Box::pin(fork::run(&args, &styles, cli, cli_layer, printer)).await + Box::pin(fork::run(&args, &styles, base_ctx)).await } RunCommands::Wait(args) => { let styles = Styles::detect_stderr(); - wait::run(&args, &styles, cli, cli_layer, printer).await + wait::run(&args, &styles, base_ctx).await } } } diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index f344ffd76..bf916c27e 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,21 +1,14 @@ use anyhow::{Context, Result}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::info; use crate::args::PreviewArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(crate) async fn run( - args: PreviewArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - process_local_json: bool, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(crate) async fn run(args: PreviewArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let expires_in_secs = @@ -31,7 +24,7 @@ pub(crate) async fn run( info!(run_id = %args.run, port = args.port, "Generating preview URL"); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { match response.token { Some(token) => { print_json_pretty(&serde_json::json!({ "url": response.url, "token": token }))?; @@ -56,7 +49,7 @@ pub(crate) async fn run( } } - if args.open && !process_local_json { + if args.open && !ctx.explicit_json_requested() { #[expect( clippy::disallowed_methods, reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process." diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index 9c892ea14..ca05661f9 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,6 +1,4 @@ -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_util::terminal::Styles; use crate::args::ResumeArgs; @@ -15,17 +13,16 @@ use crate::shared::print_json_pretty; pub(crate) async fn resume_command( args: ResumeArgs, styles: &'static Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; super::start::start_run_with_client(client.as_ref(), &run_id, true).await?; - let json = cli.output.format == OutputFormat::Json; + let json = ctx.user_settings().cli.output.format == OutputFormat::Json; if args.detach { if json { print_json_pretty(&serde_json::json!({ "run_id": run_id }))?; diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index a57e0f9d0..96e7c450b 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -3,8 +3,7 @@ use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_checkpoint::git::Store; use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_types::{EventBody, RunEvent}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -33,12 +32,11 @@ pub(crate) struct TimelineEntryJson { pub(crate) async fn run( args: &RewindArgs, styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run_id).await?.run_id; let state = client.get_run_state(&run_id).await?; @@ -54,7 +52,7 @@ pub(crate) async fn run( let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?; if args.list || args.target.is_none() { - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&timeline_entries_json(&timeline))?; return Ok(()); } @@ -79,7 +77,7 @@ pub(crate) async fn run( let run_id_string = run_id.to_string(); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id_string, "target": target_arg, diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index d3ce4f9f8..537c781e9 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,25 +1,18 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use tracing::info; -use crate::args::{SshArgs, require_no_json_override}; +use crate::args::SshArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(crate) async fn run( - args: SshArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - process_local_json: bool, - printer: Printer, -) -> Result<()> { - if process_local_json && !args.print { - require_no_json_override(process_local_json)?; +pub(crate) async fn run(args: SshArgs, base_ctx: &CommandContext) -> Result<()> { + if base_ctx.explicit_json_requested() && !args.print { + base_ctx.require_no_json_override()?; } - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let ssh = client.create_run_ssh_access(&run_id, args.ttl).await?; @@ -27,7 +20,7 @@ pub(crate) async fn run( info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access"); if args.print { - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "command": ssh.command }))?; } else { { diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 26bf2d181..f43e33510 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -11,8 +11,7 @@ use std::io::Write; use anyhow::{Result, bail}; use fabro_types::RunId; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::records::Conclusion; @@ -24,14 +23,9 @@ use crate::args::WaitArgs; use crate::command_context::CommandContext; use crate::shared::{format_duration_ms, format_usd_micros, run_status_kind}; -pub(crate) async fn run( - args: &WaitArgs, - styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(crate) async fn run(args: &WaitArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> { + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.server)?; let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; info!(run_id = %run_id, "Waiting for run to complete"); @@ -63,7 +57,7 @@ pub(crate) async fn run( let conclusion = client.get_run_state(&run_id).await?.conclusion; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { let json_value = build_json_output(final_status, &run_id, conclusion.as_ref()); let mut out = std::io::stdout().lock(); serde_json::to_writer_pretty(&mut out, &json_value)?; diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/crates/fabro-cli/src/commands/runs/archive.rs index dcc996fc9..ac4b2e39c 100644 --- a/lib/crates/fabro-cli/src/commands/runs/archive.rs +++ b/lib/crates/fabro-cli/src/commands/runs/archive.rs @@ -1,7 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use super::short_run_id; use crate::args::{RunsArchiveArgs, RunsUnarchiveArgs}; @@ -11,34 +9,30 @@ use crate::shared::print_json_pretty; pub(crate) async fn archive_command( args: &RunsArchiveArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; run_bulk( Action::Archive, &args.runs, ctx.server().await?.as_ref(), - cli, - printer, + ctx.user_settings().cli.output.format == OutputFormat::Json, + ctx.printer(), ) .await } pub(crate) async fn unarchive_command( args: &RunsUnarchiveArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; run_bulk( Action::Unarchive, &args.runs, ctx.server().await?.as_ref(), - cli, - printer, + ctx.user_settings().cli.output.format == OutputFormat::Json, + ctx.printer(), ) .await } @@ -66,10 +60,9 @@ async fn run_bulk( action: Action, identifiers: &[String], client: &server_client::Client, - cli: &CliNamespace, - printer: Printer, + json: bool, + printer: fabro_util::printer::Printer, ) -> Result<()> { - let json = cli.output.format == OutputFormat::Json; let mut had_errors = false; let mut changed = Vec::new(); let mut errors = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index dfde883ae..80f7b2015 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -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, } -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(); diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index c26f4c73e..25c6daab3 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -4,9 +4,7 @@ use anyhow::Result; use chrono::Utc; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use fabro_util::text::strip_goal_decoration; use fabro_workflow::run_status::RunStatus; @@ -20,11 +18,10 @@ use crate::shared::{color_if, format_duration_ms, run_status_kind, tilde_path}; pub(crate) async fn list_command( args: &RunsListArgs, styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let label_filters = parse_label_filters(&args.filter.label); let filtered = filter_server_runs( @@ -35,7 +32,7 @@ pub(crate) async fn list_command( !args.all, ); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { let json_rows: Vec<_> = filtered .iter() .map(|run| { diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index 3804d4052..49ea0561c 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -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, } } diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index 8990d4efc..1fcff5364 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,7 +1,5 @@ use anyhow::{Result, bail}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use super::short_run_id; use crate::args::RunsRemoveArgs; @@ -9,23 +7,23 @@ use crate::command_context::CommandContext; use crate::server_client; use crate::shared::print_json_pretty; -pub(crate) async fn remove_command( - args: &RunsRemoveArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; - remove_from(args, ctx.server().await?.as_ref(), cli, printer).await +pub(crate) async fn remove_command(args: &RunsRemoveArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + remove_from( + args, + ctx.server().await?.as_ref(), + ctx.user_settings().cli.output.format == OutputFormat::Json, + ctx.printer(), + ) + .await } async fn remove_from( args: &RunsRemoveArgs, client: &server_client::Client, - cli: &CliNamespace, - printer: Printer, + json: bool, + printer: fabro_util::printer::Printer, ) -> Result<()> { - let json = cli.output.format == OutputFormat::Json; let mut had_errors = false; let mut removed = Vec::new(); let mut errors = Vec::new(); diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs index 3adc7bd13..96c41bb50 100644 --- a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -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, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index 25d7f7680..fb664124d 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -2,8 +2,6 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -25,11 +23,11 @@ fn format_age(dt: DateTime, now: DateTime) -> String { pub(super) async fn list_command( client: &Client, _args: &SecretListArgs, - cli: &CliNamespace, + json_output: bool, printer: Printer, ) -> Result<()> { let secrets = client.list_secrets().await?; - if cli.output.format == OutputFormat::Json { + if json_output { print_json_pretty(&secrets)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index 19fdeef73..458136baf 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -3,24 +3,19 @@ mod rm; mod set; use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::CliLayer; -use fabro_util::printer::Printer; use crate::args::{SecretCommand, SecretNamespace}; use crate::command_context::CommandContext; -pub(crate) async fn dispatch( - ns: SecretNamespace, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&ns.target, printer, cli_layer)?; +pub(crate) async fn dispatch(ns: SecretNamespace, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&ns.target)?; let server = ctx.server().await?; + let json_output = + ctx.user_settings().cli.output.format == fabro_types::settings::cli::OutputFormat::Json; + let printer = ctx.printer(); match ns.command { - SecretCommand::List(args) => list::list_command(&server, &args, cli, printer).await, - SecretCommand::Rm(args) => rm::rm_command(&server, &args, cli, printer).await, - SecretCommand::Set(args) => set::set_command(&server, &args, cli, printer).await, + SecretCommand::List(args) => list::list_command(&server, &args, json_output, printer).await, + SecretCommand::Rm(args) => rm::rm_command(&server, &args, json_output, printer).await, + SecretCommand::Set(args) => set::set_command(&server, &args, json_output, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 253ca09d0..ec3a4922b 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -1,6 +1,4 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use crate::args::SecretRmArgs; @@ -10,11 +8,11 @@ use crate::shared::print_json_pretty; pub(super) async fn rm_command( client: &Client, args: &SecretRmArgs, - cli: &CliNamespace, + json_output: bool, printer: Printer, ) -> Result<()> { client.delete_secret_by_name(&args.key).await?; - if cli.output.format == OutputFormat::Json { + if json_output { print_json_pretty(&serde_json::json!({ "key": args.key }))?; } else { fabro_util::printerr!(printer, "Removed {}", args.key); diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index 1ec66418e..1ad6b4ff4 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -11,8 +11,6 @@ use std::io::{IsTerminal, Read as _}; use anyhow::{Context as _, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use tokio::task::spawn_blocking; @@ -60,7 +58,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result { pub(super) async fn set_command( client: &Client, args: &SecretSetArgs, - cli: &CliNamespace, + json_output: bool, printer: Printer, ) -> Result<()> { let value = resolve_value(args).await?; @@ -72,7 +70,7 @@ pub(super) async fn set_command( description: args.description.clone(), }) .await?; - if cli.output.format == OutputFormat::Json { + if json_output { print_json_pretty(&meta)?; } else { fabro_util::printerr!(printer, "Set {}", meta.name); diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index df1cf1f6a..b12422a35 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -11,10 +11,8 @@ use bytes::Bytes; #[cfg(test)] use fabro_store::{ArtifactStore, RunDatabase}; use fabro_store::{EventEnvelope, RunProjection, StageId}; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_types::{RunBlobId, RunId}; -use fabro_util::printer::Printer; use futures::future::BoxFuture; #[cfg(test)] use serde::de::DeserializeOwned; @@ -26,19 +24,15 @@ use crate::command_context::CommandContext; use crate::server_client::Client; use crate::shared::{absolute_or_current, print_json_pretty}; -pub(crate) async fn dump_command( - args: &StoreDumpArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer, cli_layer)?; +pub(crate) async fn dump_command(args: &StoreDumpArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_target(&args.server)?; + let printer = ctx.printer(); let client = ctx.server().await?; let run_id = client.resolve_run(&args.run).await?.run_id; let state = client.get_run_state(&run_id).await?; let source = ServerDumpSource::new(client.as_ref(), &run_id); let file_count = export_run_from_source(&source, &state, &args.output).await?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id, "output_dir": absolute_or_current(&args.output), diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index 2c885231b..07089c243 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -3,19 +3,12 @@ pub(crate) mod rebuild; mod run_export; use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::CliLayer; -use fabro_util::printer::Printer; use crate::args::{StoreCommand, StoreNamespace}; +use crate::command_context::CommandContext; -pub(crate) async fn dispatch( - ns: StoreNamespace, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn dispatch(ns: StoreNamespace, base_ctx: &CommandContext) -> Result<()> { match ns.command { - StoreCommand::Dump(args) => dump::dump_command(&args, cli, cli_layer, printer).await, + StoreCommand::Dump(args) => dump::dump_command(&args, base_ctx).await, } } diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index 67a099a2e..2504dbf09 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -3,23 +3,16 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use crate::args::DfArgs; use crate::command_context::CommandContext; use crate::shared::{format_size, print_json_pretty}; -pub(super) async fn df_command( - args: &DfArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; +pub(super) async fn df_command(args: &DfArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_connection(&args.connection)?; let server = ctx.server().await?; - let json = cli.output.format == OutputFormat::Json; + let json = ctx.user_settings().cli.output.format == OutputFormat::Json; let (output, storage_dir) = if json { (server.get_system_disk_usage(args.verbose).await?, None) diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index 1e8d69346..48165e774 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -1,8 +1,6 @@ use anyhow::Result; use fabro_client::sse; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use futures::StreamExt; use crate::args::SystemEventsArgs; @@ -10,16 +8,14 @@ use crate::command_context::CommandContext; pub(super) async fn events_command( args: &SystemEventsArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; + let ctx = base_ctx.with_connection(&args.connection)?; let server = ctx.server().await?; let mut stream = server.attach_events(&args.run_ids).await?; let mut pending = Vec::new(); - let json = cli.output.format == OutputFormat::Json; + let json = ctx.user_settings().cli.output.format == OutputFormat::Json; while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|err| anyhow::anyhow!("{err}"))?; pending.extend_from_slice(&chunk); diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index 314b3f347..7d0efaf55 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,23 +1,16 @@ use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_util::printer::Printer; +use fabro_types::settings::cli::OutputFormat; use crate::args::SystemInfoArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; -pub(super) async fn info_command( - args: &SystemInfoArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; +pub(super) async fn info_command(args: &SystemInfoArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_connection(&args.connection)?; let server = ctx.server().await?; let response = server.get_system_info().await?; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&response)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/crates/fabro-cli/src/commands/system/mod.rs index 45f35dee6..d70cae9c6 100644 --- a/lib/crates/fabro-cli/src/commands/system/mod.rs +++ b/lib/crates/fabro-cli/src/commands/system/mod.rs @@ -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, } } diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 376ae2003..b0956389c 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -2,8 +2,7 @@ use std::collections::HashMap; use anyhow::{Context, Result, bail}; use fabro_api::types; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use tracing::{debug, info}; @@ -11,13 +10,9 @@ use crate::args::RunsPruneArgs; use crate::command_context::CommandContext; use crate::shared::{format_size, print_json_pretty}; -pub(super) async fn prune_command( - args: &RunsPruneArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer, cli_layer)?; +pub(super) async fn prune_command(args: &RunsPruneArgs, base_ctx: &CommandContext) -> Result<()> { + let ctx = base_ctx.with_connection(&args.connection)?; + let printer = ctx.printer(); let server = ctx.server().await?; let response = server .prune_runs(types::PruneRunsRequest { @@ -29,7 +24,11 @@ pub(super) async fn prune_command( workflow: args.filter.workflow.clone(), }) .await?; - prune_from(&response, cli.output.format == OutputFormat::Json, printer) + prune_from( + &response, + ctx.user_settings().cli.output.format == OutputFormat::Json, + printer, + ) } pub(crate) fn parse_duration(s: &str) -> Result { diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index 74bba2d11..0c0a5b460 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -1,9 +1,8 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; -use fabro_types::settings::{CliNamespace, SettingsLayer}; -use fabro_util::printer::Printer; +use fabro_types::settings::SettingsLayer; +use fabro_types::settings::cli::OutputFormat; use fabro_util::terminal::Styles; use crate::args::ValidateArgs; @@ -15,11 +14,10 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( args: &ValidateArgs, styles: &Styles, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, + base_ctx: &CommandContext, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -33,7 +31,7 @@ pub(crate) async fn run( let response = client.run_preflight(built.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "workflow_name": response.workflow.name, "nodes": response.workflow.nodes, diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index 993557834..73c4709c3 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -6,8 +6,7 @@ use std::io::IsTerminal; use anyhow::Result; -use fabro_types::settings::CliNamespace; -use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use serde_json::{Map, Value, json}; @@ -16,14 +15,10 @@ use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config::{self, ServerTarget}; -pub(crate) async fn version_command( - args: &VersionArgs, - cli: &CliNamespace, - cli_layer: &CliLayer, - printer: Printer, -) -> Result<()> { +pub(crate) async fn version_command(args: &VersionArgs, base_ctx: &CommandContext) -> Result<()> { let client = client_info(); - let ctx = CommandContext::for_target(&args.target, printer, cli_layer)?; + let printer = base_ctx.printer(); + let ctx = base_ctx.with_target(&args.target)?; let server_target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?; let server_address = format_server_target(&server_target); let server_info = match ctx.server().await { @@ -49,7 +44,7 @@ pub(crate) async fn version_command( }, }; - if cli.output.format == OutputFormat::Json { + if ctx.user_settings().cli.output.format == OutputFormat::Json { print_json_pretty(&json_output(&client, &server_info))?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 6e54808ff..754e38243 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -30,7 +30,6 @@ use clap::{CommandFactory, Parser}; use fabro_config::merge::combine_files; use fabro_telemetry::{git, panic as tel_panic, sanitize, sender}; use fabro_types::settings::SettingsLayer; -use fabro_types::settings::cli::OutputVerbosity; use fabro_util::exit::ExitClass; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -38,6 +37,8 @@ use fabro_util::{browser, exit}; use rustls::crypto::ring::default_provider; use tracing::debug; +use crate::command_context::CommandContext; + #[derive(Parser)] #[command(name = "fabro", version, long_version = LONG_VERSION)] struct Cli { @@ -210,51 +211,46 @@ async fn main_inner() -> (String, Result<()>) { }; let result = Box::pin(async move { + let build_base_ctx = || CommandContext::base(printer, &cli_layer, process_local_json); + match *command { Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?, Commands::RunCmd(cmd) => { - Box::pin(commands::run::dispatch( - cmd, - &cli_settings, - &cli_layer, - process_local_json, - printer, - )) - .await?; + let base_ctx = build_base_ctx()?; + Box::pin(commands::run::dispatch(cmd, &base_ctx)).await?; } Commands::Preflight(args) => { - commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::preflight::execute(args, &base_ctx).await?; } Commands::Validate(args) => { let styles = Styles::detect_stderr(); - commands::validate::run(&args, &styles, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::validate::run(&args, &styles, &base_ctx).await?; } Commands::Graph(args) => { let styles = Styles::detect_stderr(); - commands::graph::run( - &args, - &styles, - &cli_settings, - &cli_layer, - process_local_json, - printer, - ) - .await?; + let base_ctx = build_base_ctx()?; + commands::graph::run(&args, &styles, &base_ctx).await?; } Commands::Parse(args) => { commands::parse::run(&args, &cli_settings, printer)?; } Commands::Artifact(ns) => { - commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::artifact::dispatch(ns, &base_ctx).await?; } Commands::Store(ns) => { - commands::store::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::store::dispatch(ns, &base_ctx).await?; } Commands::RunsCmd(cmd) => { - commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::runs::dispatch(cmd, &base_ctx).await?; } Commands::Model { command } => { - commands::model::execute(command, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::model::execute(command, &base_ctx).await?; } Commands::Server(ns) => { Box::pin(commands::server::dispatch( @@ -266,21 +262,13 @@ async fn main_inner() -> (String, Result<()>) { .await?; } Commands::Doctor(args) => { - let verbose = - args.verbose || cli_settings.output.verbosity == OutputVerbosity::Verbose; - let exit_code = Box::pin(commands::doctor::run_doctor( - &args, - verbose, - &cli_settings, - &cli_layer, - printer, - )) - .await?; + let base_ctx = build_base_ctx()?; + let exit_code = Box::pin(commands::doctor::run_doctor(&args, &base_ctx)).await?; std::process::exit(exit_code); } Commands::Version(args) => { - commands::version::version_command(&args, &cli_settings, &cli_layer, printer) - .await?; + let base_ctx = build_base_ctx()?; + commands::version::version_command(&args, &base_ctx).await?; } Commands::Discord => { if process_local_json { @@ -301,7 +289,8 @@ async fn main_inner() -> (String, Result<()>) { } } Commands::Repo(ns) => { - commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::repo::dispatch(ns, &base_ctx).await?; } Commands::Install { args, command } => { Box::pin(commands::install::execute( @@ -318,48 +307,36 @@ async fn main_inner() -> (String, Result<()>) { commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?; } Commands::Auth(ns) => { - commands::auth::dispatch(ns, &cli_layer, process_local_json, printer).await?; + let base_ctx = build_base_ctx()?; + commands::auth::dispatch(ns, &base_ctx).await?; } Commands::Pr(ns) => { - Box::pin(commands::pr::dispatch( - ns, - &cli_settings, - &cli_layer, - printer, - )) - .await?; + let base_ctx = build_base_ctx()?; + Box::pin(commands::pr::dispatch(ns, &base_ctx)).await?; } Commands::Secret(ns) => { - commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::secret::dispatch(ns, &base_ctx).await?; } Commands::Settings(args) => { - Box::pin(commands::config::execute( - &args, - &cli_settings, - &cli_layer, - printer, - )) - .await?; + let base_ctx = build_base_ctx()?; + Box::pin(commands::config::execute(&args, &base_ctx)).await?; } Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?, Commands::Upgrade(args) => { commands::upgrade::run_upgrade(args, &cli_settings, printer).await?; } Commands::Provider(ns) => { - commands::provider::dispatch(ns, &cli_layer, process_local_json, printer).await?; + let base_ctx = build_base_ctx()?; + commands::provider::dispatch(ns, &base_ctx).await?; } Commands::Sandbox { command } => { - commands::sandbox::dispatch( - command, - &cli_settings, - &cli_layer, - process_local_json, - printer, - ) - .await?; + let base_ctx = build_base_ctx()?; + commands::sandbox::dispatch(command, &base_ctx).await?; } Commands::System(ns) => { - commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + let base_ctx = build_base_ctx()?; + commands::system::dispatch(ns, &base_ctx).await?; } Commands::Completion(args) => { require_no_json_override(process_local_json)?;