Skip to main content

faucet_cli/
cli.rs

1//! Argument parser shared by `main.rs` and the integration tests.
2
3use crate::commands::completions;
4use clap::{Args, Parser, Subcommand};
5use clap_complete::engine::ArgValueCandidates;
6use std::path::PathBuf;
7
8/// Runtime matrix-row selection flags, shared by `run`/`validate`/`preview`/
9/// `plan` via `#[command(flatten)]`. Implements the selection model of
10/// #370 (identity), #371 (status), #376 (tags), and #377 (include_parents).
11#[derive(Debug, Args, Default, Clone)]
12pub struct SelectionArgs {
13    /// Run only matrix rows whose id exactly matches. Repeatable and/or
14    /// comma-joined (`--select people --select time_off` or `--select a,b`).
15    /// Force-includes by name, bypassing the `status` gate. (#370)
16    #[arg(long = "select", value_delimiter = ',', env = "FAUCET_SELECT",
17          add = ArgValueCandidates::new(completions::matrix_id_candidates))]
18    pub select: Vec<String>,
19
20    /// Like `--select` but glob-matched against row ids (`--only 'timeoff_*'`).
21    /// Also bypasses the `status` gate. Repeatable / comma-joined. (#370)
22    #[arg(long = "only", value_delimiter = ',',
23          add = ArgValueCandidates::new(completions::matrix_id_candidates))]
24    pub only: Vec<String>,
25
26    /// Remove matching rows (exact id or glob) from the run set, applied last.
27    /// A `mandatory` row is removable only by an exact `--skip <id>`. (#370)
28    #[arg(long = "skip", value_delimiter = ',', env = "FAUCET_SKIP",
29          add = ArgValueCandidates::new(completions::matrix_id_candidates))]
30    pub skip: Vec<String>,
31
32    /// Additively include a readiness tier beyond the default
33    /// `{mandatory, active}` set: `available` / `draft` / `archived`.
34    /// Repeatable / comma-joined. (#371)
35    #[arg(long = "status", value_delimiter = ',', env = "FAUCET_STATUS",
36          add = ArgValueCandidates::new(completions::status_candidates))]
37    pub status: Vec<String>,
38
39    /// Narrow the eligible set to rows carrying any listed tag (union).
40    /// Cannot resurrect a non-eligible row — raise `--status` for that.
41    /// Repeatable / comma-joined. (#376)
42    #[arg(long = "tag", value_delimiter = ',', env = "FAUCET_TAGS",
43          add = ArgValueCandidates::new(completions::tag_candidates))]
44    pub tags: Vec<String>,
45
46    /// How a selected row's `parent:` / `depends_on:` ancestors are resolved
47    /// when not independently selected: `off` (default, strict — error on a
48    /// missing ancestor), `eligible`, or `all`. Overrides
49    /// `selection.include_parents` in the config. (#377)
50    #[arg(long = "include-parents", env = "FAUCET_INCLUDE_PARENTS")]
51    pub include_parents: Option<String>,
52}
53
54/// `faucet` — config-driven runner for faucet-stream pipelines.
55#[derive(Debug, Parser)]
56#[command(name = "faucet", version, about, long_about = None)]
57pub struct Cli {
58    /// Override the global log level (also honors `FAUCET_LOG`).
59    #[arg(long, global = true, env = "FAUCET_LOG", default_value = "info")]
60    pub log_level: String,
61
62    #[command(subcommand)]
63    pub command: Command,
64}
65
66/// Top-level subcommands.
67#[derive(Debug, Subcommand)]
68pub enum Command {
69    /// Execute a pipeline config end-to-end.
70    Run(RunArgs),
71    /// Replay a bounded historical window of a pipeline: chunk --from/--to
72    /// into window units, run them with bounded parallelism, and record
73    /// durable, resumable progress. Exits non-zero if any unit fails.
74    Backfill(BackfillArgs),
75    /// Bulk-snapshot a database table, then stream CDC from a position captured
76    /// before the snapshot (a true mirror with `write_mode: upsert`).
77    /// Long-running when `replication.continuous` is true (Ctrl-C / SIGTERM to stop).
78    Replicate(ReplicateArgs),
79    /// Connect to a config's source, enumerate the datasets behind it
80    /// (tables / collections / indices / prefixes), and emit a ready-to-run
81    /// config with one matrix row per dataset.
82    Discover(DiscoverArgs),
83    /// Parse + validate a pipeline config without running it.
84    Validate(ValidateArgs),
85    /// Print the JSON Schema for a specific connector.
86    Schema(SchemaArgs),
87    /// List every compiled-in source, sink, and transform with a one-line
88    /// description (`--available` lists the whole connector registry instead).
89    List(ListArgs),
90    /// Search the connector registry index for connectors by name / keyword.
91    Search(SearchArgs),
92    /// Score each connector's conformance to the faucet SDK contract and print
93    /// its maturity tier (Stable / Experimental / Beta / Draft) + capabilities.
94    Conformance(ConformanceArgs),
95    /// Show how to install or enable a connector from the registry index
96    /// (prints the recipe; never executes anything).
97    Install(InstallArgs),
98    /// Run only the source side and print records to stdout (uses the stdout sink).
99    Preview(PreviewArgs),
100    /// Read-only preview of what a config would do: resolved pipeline, inferred
101    /// output schema, sink schema delta, lineage, and target sinks — zero writes.
102    Plan(PlanArgs),
103    /// Watch a config and re-run a sample offline on every save, printing a
104    /// live diff of the output. Requires the `cli-dev` build feature.
105    #[cfg(feature = "cli-dev")]
106    Dev(DevArgs),
107    /// Scaffold a starter `pipeline.yaml` to disk.
108    Init(InitArgs),
109    /// Scaffold a new artifact — currently a third-party connector crate.
110    New(NewArgs),
111    /// Probe every connector in a config (auth / network / permissions) and
112    /// print a green/red checklist. Exits non-zero if any probe fails.
113    Doctor(DoctorArgs),
114    /// Run fixture-based offline pipeline tests from one or more spec files.
115    /// No real source or sink is touched. Exits non-zero if any case fails.
116    Test(TestArgs),
117    /// Inspect, replay, or discard dead-letter-queue envelopes written by a
118    /// pipeline's `dlq:` sink.
119    Dlq(DlqArgs),
120    /// Validate a config's `contract:` block and print a summary, or export
121    /// it in a machine-readable format (`--export`).
122    #[cfg(feature = "contract")]
123    Contract(ContractArgs),
124    /// Validate a config's `masking:` block and print which rules apply to
125    /// each destination sink.
126    #[cfg(feature = "masking")]
127    Masking(MaskingArgs),
128    /// Run a pipeline on a cron schedule (long-running; Ctrl-C / SIGTERM to stop).
129    #[cfg(feature = "schedule")]
130    Schedule(ScheduleArgs),
131    /// Run a long-running HTTP control plane (submit / poll / cancel pipeline runs).
132    #[cfg(feature = "serve")]
133    Serve(ServeArgs),
134    /// Send a synthetic notification through a config's `notifications:` rules
135    /// to validate channel setup end-to-end (no pipeline runs).
136    #[cfg(feature = "notify")]
137    Notify(NotifyArgs),
138    /// Browse the Data Movement Catalog accumulated by a config's `catalog:`
139    /// store — datasets, schema timelines, volume/freshness, lineage.
140    #[cfg(feature = "catalog")]
141    Catalog(CatalogArgs),
142    /// Generate a shell tab-completion script (bash / zsh / fish / powershell /
143    /// elvish). For registry- and config-aware *dynamic* completion, enable the
144    /// `COMPLETE` hook instead, e.g. `source <(COMPLETE=zsh faucet)`.
145    Completions(CompletionsArgs),
146    /// Upgrade a config written against an older `faucet` grammar to the current
147    /// shape (e.g. pre-`pipeline:` top-level source/sink, legacy inline auth).
148    /// Idempotent; rewrites in place unless `--check` / `--stdout`.
149    Migrate(MigrateArgs),
150    /// Canonicalize a config: stable key order, normalized style. Idempotent;
151    /// rewrites in place unless `--check` / `--stdout`. Comments are not
152    /// preserved (the config is parsed and re-serialized).
153    Fmt(FmtArgs),
154    /// Explain, in plain English, what a pipeline config does — source →
155    /// transforms → sink, matrix expansion, replication, delivery guarantee,
156    /// and state store. Read-only and fully offline (no source is touched).
157    Explain(ExplainArgs),
158    /// Show recent run history recorded in a config's `catalog:` store —
159    /// status, duration, throughput, and bookmark. Read-only; requires the
160    /// `catalog` build feature.
161    #[cfg(feature = "catalog")]
162    History(HistoryArgs),
163}
164
165/// `faucet migrate` arguments.
166#[derive(Debug, Args)]
167pub struct MigrateArgs {
168    /// Config file to migrate. Auto-discovered (`faucet.yaml` → `.yml` →
169    /// `.json`) when omitted.
170    #[arg(value_hint = clap::ValueHint::FilePath)]
171    pub config: Option<PathBuf>,
172    /// Report whether a migration is needed without writing (exits non-zero if
173    /// the config is not current). For CI / pre-upgrade checks.
174    #[arg(long)]
175    pub check: bool,
176    /// Write the migrated config to stdout instead of rewriting the file.
177    #[arg(long, conflicts_with = "check")]
178    pub stdout: bool,
179}
180
181/// `faucet fmt` arguments.
182#[derive(Debug, Args)]
183pub struct FmtArgs {
184    /// Config file(s) to format. Auto-discovered (`faucet.yaml` → `.yml` →
185    /// `.json`) when none are given.
186    #[arg(value_hint = clap::ValueHint::FilePath)]
187    pub configs: Vec<PathBuf>,
188    /// Report whether each file is already canonical without writing (exits
189    /// non-zero and prints a unified diff for any file that is not). For CI.
190    #[arg(long)]
191    pub check: bool,
192    /// Write the formatted result to stdout instead of rewriting the file(s).
193    #[arg(long, conflicts_with = "check")]
194    pub stdout: bool,
195}
196
197/// `faucet explain` arguments.
198#[derive(Debug, Args)]
199pub struct ExplainArgs {
200    /// Path to a `.yaml`/`.yml`/`.json` config (auto-discovered if omitted).
201    pub config: Option<PathBuf>,
202    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
203    /// Defaults to `.env` in cwd if present.
204    #[arg(long, conflicts_with = "no_env_file")]
205    pub env_file: Option<PathBuf>,
206    /// Skip auto-loading `.env` from cwd.
207    #[arg(long)]
208    pub no_env_file: bool,
209    /// Select a named overlay from the config's `profiles:` block.
210    #[arg(long, env = "FAUCET_PROFILE")]
211    pub profile: Option<String>,
212    /// Emit the narration as structured JSON instead of prose.
213    #[arg(long)]
214    pub json: bool,
215    /// Narrate every matrix row instead of summarizing a large matrix.
216    #[arg(long)]
217    pub rows: bool,
218}
219
220/// `faucet history` arguments.
221#[cfg(feature = "catalog")]
222#[derive(Debug, Args)]
223pub struct HistoryArgs {
224    /// Path to a config carrying a `catalog:` block (auto-discovered if omitted).
225    pub config: Option<PathBuf>,
226    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
227    #[arg(long, conflicts_with = "no_env_file")]
228    pub env_file: Option<PathBuf>,
229    /// Skip auto-loading `.env` from cwd.
230    #[arg(long)]
231    pub no_env_file: bool,
232    /// Select a named overlay from the config's `profiles:` block.
233    #[arg(long, env = "FAUCET_PROFILE")]
234    pub profile: Option<String>,
235    /// Maximum number of runs to show, newest first.
236    #[arg(long, default_value_t = 20)]
237    pub limit: usize,
238    /// Show only runs that contain an invocation for this matrix row id.
239    #[arg(long)]
240    pub row: Option<String>,
241    /// Emit the history as JSON instead of a table.
242    #[arg(long)]
243    pub json: bool,
244}
245
246/// `faucet completions` arguments.
247#[derive(Debug, Args)]
248pub struct CompletionsArgs {
249    /// Target shell.
250    pub shell: clap_complete::aot::Shell,
251}
252
253/// `faucet catalog` arguments.
254#[cfg(feature = "catalog")]
255#[derive(Debug, Parser)]
256pub struct CatalogArgs {
257    #[command(subcommand)]
258    pub command: CatalogCommand,
259}
260
261/// `faucet catalog` subcommands.
262#[cfg(feature = "catalog")]
263#[derive(Debug, Subcommand)]
264pub enum CatalogCommand {
265    /// List every catalogued dataset (newest activity first).
266    Datasets(CatalogDatasetsArgs),
267    /// Show one dataset's detail: schema timeline, volume points, edges.
268    Show(CatalogShowArgs),
269    /// Print the dataset lineage graph (optionally rooted at a dataset).
270    Lineage(CatalogLineageArgs),
271}
272
273/// Shared config-loading flags for the `faucet catalog` subcommands.
274#[cfg(feature = "catalog")]
275#[derive(Debug, Parser)]
276pub struct CatalogConfigArgs {
277    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a
278    /// `catalog:` block naming the store. If omitted, auto-discover
279    /// `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
280    #[arg(long)]
281    pub config: Option<PathBuf>,
282    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
283    /// Defaults to `.env` in cwd if present.
284    #[arg(long, conflicts_with = "no_env_file")]
285    pub env_file: Option<PathBuf>,
286    /// Skip auto-loading `.env` from cwd.
287    #[arg(long)]
288    pub no_env_file: bool,
289    /// Select a named overlay from the config's `profiles:` block.
290    /// Overrides the `FAUCET_PROFILE` env var.
291    #[arg(long, env = "FAUCET_PROFILE")]
292    pub profile: Option<String>,
293    /// Emit machine-readable JSON instead of the human summary.
294    #[arg(long)]
295    pub json: bool,
296}
297
298/// `faucet catalog datasets` arguments.
299#[cfg(feature = "catalog")]
300#[derive(Debug, Parser)]
301pub struct CatalogDatasetsArgs {
302    #[command(flatten)]
303    pub common: CatalogConfigArgs,
304    /// Only datasets of this connector kind (e.g. `postgres`, `csv`).
305    #[arg(long)]
306    pub kind: Option<String>,
307    /// Case-insensitive substring match on the dataset URI.
308    #[arg(long)]
309    pub q: Option<String>,
310    /// Max datasets to list.
311    #[arg(long, default_value_t = 100)]
312    pub limit: usize,
313}
314
315/// `faucet catalog show <id>` arguments.
316#[cfg(feature = "catalog")]
317#[derive(Debug, Parser)]
318pub struct CatalogShowArgs {
319    /// Dataset id (from `faucet catalog datasets`), or a unique prefix of one.
320    pub id: String,
321    #[command(flatten)]
322    pub common: CatalogConfigArgs,
323}
324
325/// `faucet catalog lineage` arguments.
326#[cfg(feature = "catalog")]
327#[derive(Debug, Parser)]
328pub struct CatalogLineageArgs {
329    #[command(flatten)]
330    pub common: CatalogConfigArgs,
331    /// Dataset id to root the graph at (whole graph when omitted).
332    #[arg(long)]
333    pub root: Option<String>,
334    /// BFS hop bound around --root.
335    #[arg(long, default_value_t = 5)]
336    pub depth: u32,
337}
338
339/// `faucet notify test` arguments.
340#[cfg(feature = "notify")]
341#[derive(Debug, Parser)]
342pub struct NotifyArgs {
343    #[command(subcommand)]
344    pub command: NotifyCommand,
345}
346
347/// `faucet notify` subcommands.
348#[cfg(feature = "notify")]
349#[derive(Debug, Subcommand)]
350pub enum NotifyCommand {
351    /// Fire one synthetic event at every matching rule in the config.
352    Test(NotifyTestArgs),
353}
354
355/// `faucet notify test <config>` arguments.
356#[cfg(feature = "notify")]
357#[derive(Debug, Parser)]
358pub struct NotifyTestArgs {
359    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a
360    /// `notifications:` block. If omitted, auto-discover in cwd.
361    pub config: Option<PathBuf>,
362    /// Which event to synthesize (defaults to `run_failure`).
363    #[arg(long, default_value = "run_failure")]
364    pub event: String,
365    /// Path to a `.env` file for `${env:VAR}` interpolation.
366    #[arg(long, conflicts_with = "no_env_file")]
367    pub env_file: Option<PathBuf>,
368    /// Disable `.env` auto-discovery.
369    #[arg(long)]
370    pub no_env_file: bool,
371}
372
373/// `faucet test` arguments.
374#[derive(Debug, Parser)]
375pub struct TestArgs {
376    /// One or more test-spec files (`.yaml`, `.yml`, or `.json`), e.g.
377    /// `faucet test tests/*.yaml`.
378    #[arg(required = true)]
379    pub specs: Vec<PathBuf>,
380    /// Run only cases whose name contains this substring.
381    #[arg(long)]
382    pub filter: Option<String>,
383    /// Emit a machine-readable JSON report instead of the human checklist.
384    #[arg(long)]
385    pub json: bool,
386    /// Default `${now.*}` clock for cases without their own `clock:` field
387    /// (RFC3339 like `2026-01-31T00:00:00Z`, or a date `2026-01-31`).
388    /// Defaults to process start (UTC).
389    #[arg(long)]
390    pub clock: Option<String>,
391    /// Path to a `.env` file to load for `${env:VAR}` interpolation in
392    /// referenced pipeline configs. Defaults to `.env` in cwd if present.
393    #[arg(long, conflicts_with = "no_env_file")]
394    pub env_file: Option<PathBuf>,
395    /// Skip auto-loading `.env` from cwd.
396    #[arg(long)]
397    pub no_env_file: bool,
398    /// Select a named overlay from each referenced config's `profiles:` block.
399    /// Overrides the `FAUCET_PROFILE` env var.
400    #[arg(long, env = "FAUCET_PROFILE")]
401    pub profile: Option<String>,
402    /// Resolve `${vault:…}` / `${aws-sm:…}` / … secret directives in
403    /// referenced configs (requires network + credentials). By default tests
404    /// load configs offline and leave secret directives unresolved — safe
405    /// because the real source/sink configs holding them are never used.
406    #[arg(long)]
407    pub resolve_secrets: bool,
408}
409
410/// `faucet dlq` arguments.
411#[derive(Debug, Parser)]
412pub struct DlqArgs {
413    #[command(subcommand)]
414    pub command: DlqCommand,
415}
416
417/// `faucet dlq` subcommands.
418#[derive(Debug, Subcommand)]
419pub enum DlqCommand {
420    /// Read a DLQ location back and print a per-reason / per-error-kind
421    /// breakdown plus a sample of quarantined records.
422    Inspect(DlqInspectArgs),
423    /// Re-feed quarantined records through a pipeline config (transforms →
424    /// quality → contract → sink). Rows that fail again land in a *fresh* DLQ.
425    Replay(DlqReplayArgs),
426    /// Remove processed envelopes from a DLQ location (archive by default,
427    /// or `--delete`), filtered by reason and/or age.
428    Discard(DlqDiscardArgs),
429}
430
431/// `faucet dlq inspect <location>` arguments.
432#[derive(Debug, Parser)]
433pub struct DlqInspectArgs {
434    /// DLQ location: a `.jsonl` file, a directory of `*.jsonl` files, or a glob.
435    pub location: String,
436    /// Only include envelopes with this DLQ reason (`partial` / `dlq_all` /
437    /// `quality` / `schema_drift` / `contract`).
438    #[arg(long)]
439    pub reason: Option<String>,
440    /// Number of sample records to show. Default: 5.
441    #[arg(long, default_value_t = 5)]
442    pub limit: usize,
443    /// Key for a DLQ sealed at rest by the jsonl sink's `encryption` block.
444    /// Repeat the flag to also try older (rotated) keys. Requires a build
445    /// with the `encryption` feature.
446    #[arg(long = "encryption-key")]
447    pub encryption_key: Vec<String>,
448    /// Emit a machine-readable JSON summary instead of the human report.
449    #[arg(long)]
450    pub json: bool,
451}
452
453/// `faucet dlq replay <config> --from <location>` arguments.
454#[derive(Debug, Parser)]
455pub struct DlqReplayArgs {
456    /// Path to the pipeline config whose sink / transforms / quality / contract
457    /// the replayed records flow through. If omitted, auto-discover in cwd.
458    pub config: Option<PathBuf>,
459    /// DLQ location to replay from: a `.jsonl` file, a directory, or a glob.
460    #[arg(long)]
461    pub from: String,
462    /// Only replay envelopes with this DLQ reason.
463    #[arg(long)]
464    pub reason: Option<String>,
465    /// Where replayed rows that fail *again* are quarantined. Defaults to a
466    /// `replay-failed.jsonl` sibling of the source (never the source itself).
467    #[arg(long)]
468    pub failed_dlq: Option<String>,
469    /// Which root row of the config to replay through. Defaults to the first root.
470    #[arg(long)]
471    pub row: Option<String>,
472    /// Report what would be replayed without writing to the sink.
473    #[arg(long)]
474    pub dry_run: bool,
475    /// Key for a DLQ sealed at rest by the jsonl sink's `encryption` block.
476    /// Repeat the flag to also try older (rotated) keys. Requires a build
477    /// with the `encryption` feature.
478    #[arg(long = "encryption-key")]
479    pub encryption_key: Vec<String>,
480    /// (Replay picks up the config's own dlq `encryption` block automatically
481    /// when no key is passed.)
482    /// Emit a machine-readable JSON result instead of the human summary.
483    #[arg(long)]
484    pub json: bool,
485    /// Path to a `.env` file for `${env:VAR}` interpolation in the config.
486    #[arg(long, conflicts_with = "no_env_file")]
487    pub env_file: Option<PathBuf>,
488    /// Skip auto-loading `.env` from cwd.
489    #[arg(long)]
490    pub no_env_file: bool,
491    /// Select a named overlay from the config's `profiles:` block.
492    #[arg(long, env = "FAUCET_PROFILE")]
493    pub profile: Option<String>,
494}
495
496/// `faucet dlq discard <location>` arguments.
497#[derive(Debug, Parser)]
498pub struct DlqDiscardArgs {
499    /// DLQ location: a `.jsonl` file, a directory of `*.jsonl` files, or a glob.
500    pub location: String,
501    /// Only discard envelopes with this DLQ reason.
502    #[arg(long)]
503    pub reason: Option<String>,
504    /// Only discard envelopes older than this: an RFC3339 timestamp
505    /// (`2026-06-01T00:00:00Z`) or a relative age (`7d`, `24h`, `30m`).
506    #[arg(long)]
507    pub before: Option<String>,
508    /// Permanently delete matching envelopes instead of archiving them to a
509    /// `<file>.archived.jsonl` sibling.
510    #[arg(long)]
511    pub delete: bool,
512    /// Key for a DLQ sealed at rest by the jsonl sink's `encryption` block.
513    /// Repeat the flag to also try older (rotated) keys. Requires a build
514    /// with the `encryption` feature.
515    #[arg(long = "encryption-key")]
516    pub encryption_key: Vec<String>,
517    /// Emit a machine-readable JSON result instead of the human summary.
518    #[arg(long)]
519    pub json: bool,
520}
521
522/// `faucet doctor` arguments.
523#[derive(Debug, Parser)]
524pub struct DoctorArgs {
525    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config. If omitted,
526    /// auto-discover `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
527    pub config: Option<PathBuf>,
528    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
529    /// Defaults to `.env` in cwd if present.
530    #[arg(long, conflicts_with = "no_env_file")]
531    pub env_file: Option<PathBuf>,
532    /// Skip auto-loading `.env` from cwd.
533    #[arg(long)]
534    pub no_env_file: bool,
535    /// Per-probe timeout in seconds.
536    #[arg(long, default_value_t = 10)]
537    pub timeout_secs: u64,
538    /// Emit machine-readable JSON instead of the human checklist.
539    #[arg(long)]
540    pub json: bool,
541    /// Run only the offline static config lints (no network probes): dangling /
542    /// unreferenced `auth:` providers, unused `vars:`, and no-op sink
543    /// `batch_size: 0`. Fast and credential-free — ideal for CI. Exits non-zero
544    /// on any lint *error* (warnings don't fail).
545    #[arg(long)]
546    pub offline: bool,
547    /// Select a named overlay from the config's `profiles:` block and deep-merge
548    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
549    #[arg(long, env = "FAUCET_PROFILE")]
550    pub profile: Option<String>,
551}
552
553/// `faucet contract` arguments.
554#[cfg(feature = "contract")]
555#[derive(Debug, Parser)]
556pub struct ContractArgs {
557    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a
558    /// `pipeline.contract:` block. If omitted, auto-discover
559    /// `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
560    pub config: Option<PathBuf>,
561    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
562    /// Defaults to `.env` in cwd if present.
563    #[arg(long, conflicts_with = "no_env_file")]
564    pub env_file: Option<PathBuf>,
565    /// Skip auto-loading `.env` from cwd.
566    #[arg(long)]
567    pub no_env_file: bool,
568    /// Select a named overlay from the config's `profiles:` block and deep-merge
569    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
570    #[arg(long, env = "FAUCET_PROFILE")]
571    pub profile: Option<String>,
572    /// Export the contract in a machine-readable format instead of the
573    /// human summary: the canonical contract JSON, a standalone JSON Schema,
574    /// or an OpenLineage schema facet.
575    #[arg(long, value_enum)]
576    pub export: Option<ContractExportFormat>,
577}
578
579/// Arguments for `faucet masking`.
580#[cfg(feature = "masking")]
581#[derive(Debug, Parser)]
582pub struct MaskingArgs {
583    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a
584    /// `pipeline.masking:` block. If omitted, auto-discover
585    /// `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
586    pub config: Option<PathBuf>,
587    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
588    /// Defaults to `.env` in cwd if present.
589    #[arg(long, conflicts_with = "no_env_file")]
590    pub env_file: Option<PathBuf>,
591    /// Skip auto-loading `.env` from cwd.
592    #[arg(long)]
593    pub no_env_file: bool,
594    /// Select a named overlay from the config's `profiles:` block and deep-merge
595    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
596    #[arg(long, env = "FAUCET_PROFILE")]
597    pub profile: Option<String>,
598}
599
600/// Export format for `faucet contract --export`.
601#[cfg(feature = "contract")]
602#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
603pub enum ContractExportFormat {
604    /// The canonical contract document as JSON.
605    Contract,
606    /// A standalone JSON Schema (draft 2020-12) for the promised records.
607    JsonSchema,
608    /// An OpenLineage `SchemaDatasetFacet` JSON document.
609    Openlineage,
610}
611
612/// `faucet schedule` arguments.
613#[cfg(feature = "schedule")]
614#[derive(Debug, Parser)]
615pub struct ScheduleArgs {
616    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a `schedule:`
617    /// block. If omitted, auto-discover `faucet.yaml` / `.yml` / `.json` in cwd.
618    pub config: Option<PathBuf>,
619    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
620    /// Defaults to `.env` in cwd if present.
621    #[arg(long, conflicts_with = "no_env_file")]
622    pub env_file: Option<PathBuf>,
623    /// Skip auto-loading `.env` from cwd.
624    #[arg(long)]
625    pub no_env_file: bool,
626    /// Run exactly one pipeline run immediately, then exit (ignores cron timing).
627    /// Useful for platform-driven invocation (k8s CronJob / systemd OnCalendar).
628    #[arg(long)]
629    pub once: bool,
630    /// Select a named overlay from the config's `profiles:` block and deep-merge
631    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
632    #[arg(long, env = "FAUCET_PROFILE")]
633    pub profile: Option<String>,
634}
635
636/// `faucet serve` arguments.
637#[cfg(feature = "serve")]
638#[derive(Debug, Clone, Parser)]
639pub struct ServeArgs {
640    /// Bind address. Defaults to loopback; set 0.0.0.0:PORT to expose externally.
641    #[arg(long, env = "FAUCET_SERVE_LISTEN", default_value = "127.0.0.1:8080")]
642    pub listen: String,
643    /// Bearer token required on /v1/* requests. Prefer the env var (avoids `ps` leakage).
644    #[arg(long, env = "FAUCET_SERVE_AUTH_TOKEN", conflicts_with = "no_auth")]
645    pub auth_token: Option<String>,
646    /// Explicitly disable authentication. Required if no token is set, so an
647    /// unauthenticated server is never accidental.
648    #[arg(long)]
649    pub no_auth: bool,
650    /// Path to an RBAC auth config (YAML/JSON) defining principals — each a
651    /// `{ name, token, role }` where role is `viewer` / `operator` / `admin`.
652    /// Enables role-based access control + an audit log. Mutually exclusive with
653    /// `--auth-token` / `--no-auth`.
654    #[arg(long, conflicts_with_all = ["auth_token", "no_auth"])]
655    pub auth_config: Option<std::path::PathBuf>,
656    /// Max pipeline runs executing at once. Default: min(16, cpu count).
657    #[arg(long)]
658    pub max_concurrent_runs: Option<usize>,
659    /// Max queued (not-yet-running) runs before POST /v1/runs returns 429.
660    /// Default: 8 × max-concurrent-runs.
661    #[arg(long)]
662    pub max_queued_runs: Option<usize>,
663    /// Workspace-default config merged under every submitted run.
664    #[arg(long)]
665    pub default_config: Option<std::path::PathBuf>,
666    /// Run-history backend URL: omitted = in-memory; postgres://… ; sqlite:… .
667    #[arg(long)]
668    pub history: Option<String>,
669    /// CORS allow-list origin (repeatable). Omitted = CORS disabled.
670    #[arg(long)]
671    pub cors_origin: Vec<String>,
672    /// Max POST /v1/runs body size in bytes (413 on exceed).
673    #[arg(long, default_value_t = 1_048_576)]
674    pub body_limit_bytes: usize,
675    /// SIGTERM/SIGINT drain window in seconds.
676    #[arg(long, default_value_t = 60)]
677    pub shutdown_grace_secs: u64,
678    /// Retain terminal run records this long (seconds).
679    #[arg(long, default_value_t = 604_800)]
680    pub retain_terminal_runs_secs: u64,
681    /// Idempotency-key replay window (seconds).
682    #[arg(long, default_value_t = 86_400)]
683    pub idempotency_retention_secs: u64,
684    /// Run-ownership lease TTL in seconds (multi-instance orphan fencing). A run
685    /// is owned by the instance executing it and its lease is heartbeated at
686    /// ~⅓ of this interval; only a run whose lease has expired (owner presumed
687    /// dead) is recovered as failed. Make this comfortably larger than expected
688    /// GC/IO stalls so a healthy-but-slow instance is never falsely reclaimed.
689    /// Only relevant with a persistent (postgres/sqlite) history backend.
690    #[arg(long, default_value_t = 30)]
691    pub lease_ttl_secs: u64,
692    /// Per-probe timeout for `doctor_first` preflight (seconds).
693    #[arg(long, default_value_t = 10)]
694    pub probe_timeout_secs: u64,
695    /// Path to a `.env` file loaded for the server's own startup interpolation.
696    #[arg(long, conflicts_with = "no_env_file")]
697    pub env_file: Option<std::path::PathBuf>,
698    /// Skip auto-loading `.env` from cwd at startup.
699    #[arg(long)]
700    pub no_env_file: bool,
701    /// Disable serving the embedded web console (only meaningful in a build that
702    /// includes the `serve-ui` feature; the API is unaffected).
703    #[arg(long)]
704    pub no_ui: bool,
705    /// Enable clustered execution: run a claim loop that pulls Pending runs from
706    /// the shared history DB so N instances pull-balance and fail over. Requires
707    /// a postgres/sqlite --history backend.
708    #[arg(long)]
709    pub cluster: bool,
710    /// Claim-loop poll interval (seconds) in cluster mode. Also the
711    /// cross-instance cancel-propagation lag. Must be > 0.
712    #[arg(long, default_value_t = 2)]
713    pub cluster_poll_secs: u64,
714    /// Max failover re-runs of an orphaned run before it is marked Failed
715    /// (poison). Must be > 0.
716    #[arg(long, default_value_t = 3)]
717    pub cluster_max_attempts: u32,
718    /// Path to a triggers file (YAML/JSON) defining event-driven pipeline
719    /// triggers (object-arrival / webhook / queue-depth). Requires a build with
720    /// the `triggers` feature. See `faucet schema triggers`.
721    #[arg(long)]
722    pub triggers: Option<std::path::PathBuf>,
723}
724
725/// `faucet run` arguments.
726#[derive(Debug, Parser)]
727pub struct RunArgs {
728    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config.
729    /// If omitted (and `--from-env` is not set), auto-discover
730    /// `faucet.yaml` / `faucet.yml` / `faucet.json` in the current directory.
731    /// Mutually exclusive with `--from-env`.
732    #[arg(conflicts_with = "from_env")]
733    pub config: Option<PathBuf>,
734    /// Build the pipeline entirely from `FAUCET_*` environment variables —
735    /// no YAML required. See `cli/README.md` for the variable schema.
736    #[arg(long)]
737    pub from_env: bool,
738    /// Path to a `.env` file to load before reading variables. Works in both
739    /// YAML mode (for `${env:VAR}` interpolation) and `--from-env` mode.
740    /// When omitted, `.env` in the current directory is auto-loaded if present.
741    /// Existing process-env values always win over file-supplied ones.
742    #[arg(long, conflicts_with = "no_env_file")]
743    pub env_file: Option<PathBuf>,
744    /// Skip auto-loading `.env` from the current directory.
745    #[arg(long)]
746    pub no_env_file: bool,
747    /// Stop after fetching from the source — write nothing to the sink.
748    #[arg(long)]
749    pub dry_run: bool,
750    /// Stop after writing this many records to the sink. Default: unlimited.
751    #[arg(long)]
752    pub limit: Option<usize>,
753    /// Override the state-store directory (file backend only).
754    #[arg(long)]
755    pub state_path: Option<PathBuf>,
756    /// Override the `${now.*}` interpolation clock (RFC3339 like
757    /// `2026-01-31T00:00:00Z`, or a date `2026-01-31`). Default: process start (UTC).
758    /// Use for backfills.
759    #[arg(long)]
760    pub clock: Option<String>,
761    /// Select a named overlay from the config's `profiles:` block and deep-merge
762    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
763    /// Not applicable in `--from-env` mode (no config file to compose).
764    #[arg(long, env = "FAUCET_PROFILE")]
765    pub profile: Option<String>,
766    /// Show a live full-screen terminal UI (per-invocation throughput, errors,
767    /// DLQ counts, bookmark age) while the pipeline runs. Requires a binary
768    /// built with the `cli-tui` feature and a real terminal on stdout —
769    /// on a non-TTY (CI, pipes) the run proceeds normally with a notice.
770    /// Press `q` to cancel cooperatively (in-flight work flushes at the next
771    /// page boundary).
772    #[arg(long)]
773    pub tui: bool,
774
775    /// Suppress the inline live progress line (records in/out, rows/s, pages,
776    /// elapsed) that `faucet run` shows on an interactive terminal. The
777    /// progress line is already auto-disabled on a non-TTY stdout (CI, pipes)
778    /// and when `--tui` is used; `--quiet` turns it off explicitly, keeping
779    /// only the periodic log output.
780    #[arg(long)]
781    pub quiet: bool,
782
783    /// Format for the end-of-run summary printed to stdout: `text` (default,
784    /// human), `json` (a single machine-readable document), or `ndjson` (one
785    /// JSON object per matrix row). With `json`/`ndjson`, stdout carries only
786    /// the summary — logs stay on stderr — so `faucet run` is scriptable.
787    #[arg(long, value_enum, default_value_t = RunOutput::Text)]
788    pub output: RunOutput,
789
790    /// Runtime matrix-row selection (`--select`/`--only`/`--skip`/`--status`/
791    /// `--tag`/`--include-parents`).
792    #[command(flatten)]
793    pub selection: SelectionArgs,
794}
795
796/// Format for `faucet run`'s end-of-run summary.
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
798pub enum RunOutput {
799    /// Human-readable one-line summary (default).
800    #[default]
801    Text,
802    /// A single machine-readable JSON document with per-row + total stats.
803    Json,
804    /// One JSON object per matrix row (newline-delimited) for streaming consumers.
805    Ndjson,
806}
807
808/// `faucet backfill` arguments.
809#[derive(Debug, Parser)]
810pub struct BackfillArgs {
811    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config. If omitted,
812    /// auto-discover `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
813    pub config: Option<PathBuf>,
814    /// Window start (inclusive): RFC3339 (`2026-06-01T00:00:00Z`) or a date
815    /// (`2026-06-01`, midnight in --timezone). Requires --to.
816    #[arg(long, requires = "to", conflicts_with = "from_bookmark")]
817    pub from: Option<String>,
818    /// Window end (exclusive): RFC3339 or a date.
819    #[arg(long, requires = "from", conflicts_with = "from_bookmark")]
820    pub to: Option<String>,
821    /// Chunk the range into windows of this duration (`45s`, `30m`, `6h`,
822    /// `1d`, `1w`) so each chunk is an independent, resumable unit. Defaults
823    /// to the config's `backfill.window`; omitted = one unit for the whole
824    /// range.
825    #[arg(long)]
826    pub window: Option<String>,
827    /// Replay from this explicit bookmark value instead of a wall-clock
828    /// range (seeded into the backfill's scoped state key; the source's own
829    /// incremental logic reads forward from it). JSON or a bare string.
830    #[arg(long)]
831    pub from_bookmark: Option<String>,
832    /// Upper bookmark bound: records whose --bookmark-field orders after
833    /// this value are dropped before the sink.
834    #[arg(long, requires_all = ["from_bookmark", "bookmark_field"])]
835    pub to_bookmark: Option<String>,
836    /// Record field the --to-bookmark bound applies to.
837    #[arg(long)]
838    pub bookmark_field: Option<String>,
839    /// Max concurrently-running window units. Defaults to the config's
840    /// `backfill.concurrency`, else 1 (sequential).
841    #[arg(long)]
842    pub concurrency: Option<usize>,
843    /// IANA timezone for date boundaries and `${now.*}` rendering. Defaults
844    /// to the config's `backfill.timezone`, else UTC.
845    #[arg(long)]
846    pub timezone: Option<String>,
847    /// Root row of the config to backfill. Defaults to the only root.
848    #[arg(long)]
849    pub row: Option<String>,
850    /// Redirect writes to this named sink template under `pipeline.sinks`
851    /// (backfill into a staging table first).
852    #[arg(long)]
853    pub into: Option<String>,
854    /// Print the planned units without running anything.
855    #[arg(long)]
856    pub dry_run: bool,
857    /// Continue a previously-interrupted backfill of the same range: skip
858    /// units already done, re-run failed and pending ones.
859    #[arg(long, conflicts_with = "restart")]
860    pub resume: bool,
861    /// Discard a previous progress marker for this range and start over.
862    #[arg(long)]
863    pub restart: bool,
864    /// Emit a machine-readable JSON report instead of the human summary.
865    #[arg(long)]
866    pub json: bool,
867    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
868    /// Defaults to `.env` in cwd if present.
869    #[arg(long, conflicts_with = "no_env_file")]
870    pub env_file: Option<PathBuf>,
871    /// Skip auto-loading `.env` from cwd.
872    #[arg(long)]
873    pub no_env_file: bool,
874    /// Select a named overlay from the config's `profiles:` block.
875    /// Overrides the `FAUCET_PROFILE` env var.
876    #[arg(long, env = "FAUCET_PROFILE")]
877    pub profile: Option<String>,
878}
879
880/// `faucet replicate` arguments.
881#[derive(Debug, Parser)]
882pub struct ReplicateArgs {
883    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config with a
884    /// `replication:` block. If omitted, auto-discover
885    /// `faucet.yaml` / `.yml` / `.json` in cwd.
886    pub config: Option<PathBuf>,
887    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
888    /// Defaults to `.env` in cwd if present.
889    #[arg(long, conflicts_with = "no_env_file")]
890    pub env_file: Option<PathBuf>,
891    /// Skip auto-loading `.env` from cwd.
892    #[arg(long)]
893    pub no_env_file: bool,
894    /// Select a named overlay from the config's `profiles:` block and deep-merge
895    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
896    #[arg(long, env = "FAUCET_PROFILE")]
897    pub profile: Option<String>,
898}
899
900/// `faucet discover` arguments.
901#[derive(Debug, Parser)]
902pub struct DiscoverArgs {
903    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config whose source
904    /// points at the system to introspect. If omitted, auto-discover
905    /// `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
906    pub config: Option<PathBuf>,
907    /// Which source template to introspect (an entry under `pipeline.sources`).
908    /// Defaults to `default` (the legacy singular `pipeline.source`).
909    #[arg(long)]
910    pub source: Option<String>,
911    /// Only include datasets whose name matches this `*`-wildcard pattern
912    /// (repeatable; no patterns = include everything).
913    #[arg(long)]
914    pub include: Vec<String>,
915    /// Exclude datasets whose name matches this `*`-wildcard pattern
916    /// (repeatable; applied after --include).
917    #[arg(long)]
918    pub exclude: Vec<String>,
919    /// Write the generated config to this file instead of stdout.
920    #[arg(long, short = 'o')]
921    pub output: Option<PathBuf>,
922    /// Overwrite the --output file if it already exists.
923    #[arg(long)]
924    pub force: bool,
925    /// Emit the discovered datasets as machine-readable JSON instead of a
926    /// generated config.
927    #[arg(long)]
928    pub json: bool,
929    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
930    /// Defaults to `.env` in cwd if present.
931    #[arg(long, conflicts_with = "no_env_file")]
932    pub env_file: Option<PathBuf>,
933    /// Skip auto-loading `.env` from cwd.
934    #[arg(long)]
935    pub no_env_file: bool,
936    /// Select a named overlay from the config's `profiles:` block.
937    /// Overrides the `FAUCET_PROFILE` env var.
938    #[arg(long, env = "FAUCET_PROFILE")]
939    pub profile: Option<String>,
940}
941
942/// `faucet validate` arguments.
943#[derive(Debug, Parser)]
944pub struct ValidateArgs {
945    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config. If omitted,
946    /// auto-discover `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
947    pub config: Option<PathBuf>,
948    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
949    /// Defaults to `.env` in cwd if present.
950    #[arg(long, conflicts_with = "no_env_file")]
951    pub env_file: Option<PathBuf>,
952    /// Skip auto-loading `.env` from cwd.
953    #[arg(long)]
954    pub no_env_file: bool,
955    /// Validate grammar and structure only — skip fetching from secrets
956    /// managers (no network / credentials needed).
957    #[arg(long)]
958    pub no_secrets: bool,
959    /// Select a named overlay from the config's `profiles:` block and deep-merge
960    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
961    #[arg(long, env = "FAUCET_PROFILE")]
962    pub profile: Option<String>,
963    /// Print the fully-composed config (after extends/!include/profile, before
964    /// `${...}` interpolation) and exit. For debugging composition precedence.
965    /// `--no-secrets` is redundant here (no interpolation or secret fetch occurs).
966    #[arg(long)]
967    pub show_composed: bool,
968
969    /// Runtime matrix-row selection — `validate` reports each row's resolved
970    /// status/tags and whether the selection would run or skip it.
971    #[command(flatten)]
972    pub selection: SelectionArgs,
973}
974
975/// `faucet schema` arguments.
976#[derive(Debug, Parser)]
977pub struct SchemaArgs {
978    #[command(subcommand)]
979    pub target: SchemaTarget,
980}
981
982/// Schema subcommand target — which connector or system component to describe.
983#[derive(Debug, Subcommand)]
984pub enum SchemaTarget {
985    /// Composed JSON Schema for the **entire** `faucet.yaml` / `faucet.json`
986    /// config document (top-level grammar + per-connector `type` discrimination).
987    /// Point an editor at it with a `# yaml-language-server: $schema=…` header.
988    Config,
989    /// JSON Schema for a source connector config.
990    Source {
991        /// Connector name (e.g. `rest`, `graphql`, `postgres`).
992        #[arg(add = ArgValueCandidates::new(completions::source_kind_candidates))]
993        name: String,
994    },
995    /// JSON Schema for a sink connector config.
996    Sink {
997        /// Connector name (e.g. `jsonl`, `bigquery`, `postgres`).
998        #[arg(add = ArgValueCandidates::new(completions::sink_kind_candidates))]
999        name: String,
1000    },
1001    /// JSON Schema for a transform's inline config.
1002    Transform {
1003        /// Transform name (e.g. `flatten`, `keys_case`, `cast`).
1004        /// Run `faucet list` to see what is compiled in.
1005        #[arg(add = ArgValueCandidates::new(completions::transform_candidates))]
1006        name: String,
1007    },
1008    /// JSON Schema for the DLQ (Dead Letter Queue) specification.
1009    Dlq,
1010    /// JSON Schema for the `replication:` (snapshot→CDC) block.
1011    Replication,
1012    /// JSON Schema for the `backfill:` (window replay defaults) block.
1013    Backfill,
1014    /// JSON Schema for the top-level `execution:` block.
1015    Execution,
1016    /// JSON Schema for the top-level `resilience:` block.
1017    Resilience,
1018    /// JSON Schema for the top-level `sla:` (freshness/volume SLA) block.
1019    Sla,
1020    /// JSON Schema for the `quality:` block.
1021    #[cfg(feature = "quality")]
1022    Quality,
1023    /// JSON Schema for the `contract:` block.
1024    #[cfg(feature = "contract")]
1025    Contract,
1026    /// JSON Schema for the `masking:` (PII masking) block.
1027    #[cfg(feature = "masking")]
1028    Masking,
1029    /// JSON Schema for the `faucet test` spec file.
1030    Test,
1031    /// Grammar reference for secrets-manager interpolation directives.
1032    Secrets,
1033    /// JSON Schema for the `schedule:` block.
1034    #[cfg(feature = "schedule")]
1035    Schedule,
1036    /// JSON Schema for the `lineage:` (OpenLineage) block.
1037    #[cfg(feature = "lineage")]
1038    Lineage,
1039    /// JSON Schema for the `--triggers` file (event-driven pipeline triggers).
1040    #[cfg(feature = "triggers")]
1041    Triggers,
1042    /// JSON Schema for the `notifications:` (incident-routing) block.
1043    #[cfg(feature = "notify")]
1044    Notifications,
1045    /// JSON Schema for the `catalog:` (Data Movement Catalog store) block.
1046    #[cfg(feature = "catalog")]
1047    Catalog,
1048}
1049
1050/// `faucet preview` arguments.
1051#[derive(Debug, Parser)]
1052pub struct PreviewArgs {
1053    /// Path to a `.yaml`, `.yml`, or `.json` pipeline config. If omitted,
1054    /// auto-discover `faucet.yaml` / `faucet.yml` / `faucet.json` in cwd.
1055    pub config: Option<PathBuf>,
1056    /// Stop after this many records. Default: 10.
1057    #[arg(long, default_value_t = 10)]
1058    pub limit: usize,
1059    /// Path to a `.env` file to load for `${env:VAR}` interpolation.
1060    /// Defaults to `.env` in cwd if present.
1061    #[arg(long, conflicts_with = "no_env_file")]
1062    pub env_file: Option<PathBuf>,
1063    /// Skip auto-loading `.env` from cwd.
1064    #[arg(long)]
1065    pub no_env_file: bool,
1066    /// Select a named overlay from the config's `profiles:` block and deep-merge
1067    /// it over the composed base. Overrides the `FAUCET_PROFILE` env var.
1068    #[arg(long, env = "FAUCET_PROFILE")]
1069    pub profile: Option<String>,
1070
1071    /// Runtime matrix-row selection — `preview` previews the first root row of
1072    /// the selected run set.
1073    #[command(flatten)]
1074    pub selection: SelectionArgs,
1075}
1076
1077/// `faucet init` arguments.
1078#[derive(Debug, Parser)]
1079pub struct InitArgs {
1080    /// Name written into the generated file's `name:` field. Defaults to
1081    /// `my-pipeline` when omitted.
1082    pub name: Option<String>,
1083    /// Source connector kind to scaffold (e.g. `rest`, `postgres`, `s3`).
1084    /// Defaults to `rest`. Run `faucet list` to see what is compiled in.
1085    #[arg(long)]
1086    pub source: Option<String>,
1087    /// Sink connector kind to scaffold (e.g. `jsonl`, `bigquery`).
1088    /// Defaults to `jsonl`. Run `faucet list` to see what is compiled in.
1089    #[arg(long)]
1090    pub sink: Option<String>,
1091    /// Output file path. Defaults to `pipeline.yaml`.
1092    #[arg(long, short = 'o', default_value = "pipeline.yaml")]
1093    pub output: PathBuf,
1094    /// Overwrite the output file if it already exists.
1095    #[arg(long)]
1096    pub force: bool,
1097    /// Prompt for the source and sink kinds interactively instead of using
1098    /// `--source` / `--sink`. Requires the `cli-interactive` build feature
1099    /// and a TTY on stdin; falls back to the arg-driven path otherwise.
1100    #[arg(long)]
1101    pub interactive: bool,
1102    /// Name of the template under which to register the scaffolded source
1103    /// and sink. The generated config uses `pipeline.sources.<TEMPLATE>` and
1104    /// `pipeline.sinks.<TEMPLATE>`. Defaults to `default` so a matrix row
1105    /// without a `ref:` field still resolves through the new schema.
1106    #[arg(long, default_value = "default")]
1107    pub template: String,
1108    /// (singer only) Run `<executable> --discover` to fetch the tap's catalog,
1109    /// write it next to the output, and scaffold the config with the discovered
1110    /// streams listed. Requires `--source singer` and `--executable`.
1111    #[arg(long)]
1112    pub discover: bool,
1113    /// (singer only) The Singer tap executable to discover with (used by
1114    /// `--discover`), e.g. `tap-github` or `/opt/taps/tap-csv`.
1115    #[arg(long)]
1116    pub executable: Option<String>,
1117    /// (singer only) The target stream to emit. When given with `--discover`,
1118    /// the written catalog marks this stream — and any inferable parent
1119    /// streams (e.g. a parent-keyed tap's parent) — `selected`, and the
1120    /// scaffolded config's `stream:` is set to it. Most DB / SDK taps sync
1121    /// nothing unless a stream is selected in the catalog.
1122    #[arg(long)]
1123    pub stream: Option<String>,
1124}
1125
1126/// `faucet plan` arguments.
1127#[derive(Debug, Parser)]
1128pub struct PlanArgs {
1129    /// Path to a `.yaml`/`.yml`/`.json` config (auto-discovered if omitted).
1130    pub config: Option<PathBuf>,
1131    /// Which row to plan (default: the first root row).
1132    #[arg(long)]
1133    pub row: Option<String>,
1134    /// Offline sample of input records (`.jsonl` or a `.json` array) to preview
1135    /// the output schema, volume, and sink delta through — no source is touched.
1136    #[arg(long)]
1137    pub sample: Option<PathBuf>,
1138    /// Pull a capped, read-only sample from the real source instead of a
1139    /// fixture (bounded by `--limit`; no bookmark is advanced).
1140    #[arg(long)]
1141    pub live: bool,
1142    /// Cap for `--live` sampling.
1143    #[arg(long, default_value_t = 10)]
1144    pub limit: usize,
1145    /// Emit the plan as JSON.
1146    #[arg(long)]
1147    pub json: bool,
1148    /// Show a `terraform plan`-style diff of the current config against the last
1149    /// recorded run, instead of the resolved-pipeline preview (#374). Requires a
1150    /// `catalog:` block. Resolves secrets so the diff matches what `run` records.
1151    #[arg(long)]
1152    pub diff: bool,
1153    /// Resolve secrets-manager directives (needs network/credentials). Off by
1154    /// default so `plan` works offline like `faucet test`. Implied by `--diff`.
1155    #[arg(long)]
1156    pub resolve_secrets: bool,
1157    /// Select a `profiles:` overlay.
1158    #[arg(long, env = "FAUCET_PROFILE")]
1159    pub profile: Option<String>,
1160}
1161
1162/// `faucet dev` arguments.
1163#[derive(Debug, Parser)]
1164pub struct DevArgs {
1165    /// Path to the `.yaml`/`.yml`/`.json` config to watch.
1166    pub config: PathBuf,
1167    /// Which row to run (default: the first root row).
1168    #[arg(long)]
1169    pub row: Option<String>,
1170    /// Offline sample of input records (`.jsonl` or `.json` array). Required
1171    /// for the offline loop.
1172    #[arg(long)]
1173    pub sample: Option<PathBuf>,
1174    /// (reserved) pull a capped read-only sample from the real source.
1175    #[arg(long)]
1176    pub live: bool,
1177    /// Cap for `--live` sampling.
1178    #[arg(long, default_value_t = 10)]
1179    pub limit: usize,
1180    /// Run once and exit instead of watching (also the non-TTY fallback).
1181    #[arg(long)]
1182    pub once: bool,
1183    /// Debounce window between re-runs, in milliseconds.
1184    #[arg(long, default_value_t = 300)]
1185    pub debounce_ms: u64,
1186    /// Select a `profiles:` overlay.
1187    #[arg(long, env = "FAUCET_PROFILE")]
1188    pub profile: Option<String>,
1189}
1190
1191/// `faucet list` arguments.
1192#[derive(Debug, Parser)]
1193pub struct ListArgs {
1194    /// List every connector in the registry index (not just the compiled-in
1195    /// ones), marking which are already in this binary.
1196    #[arg(long)]
1197    pub available: bool,
1198    /// Read a custom registry index instead of the built-in one.
1199    #[arg(long)]
1200    pub index: Option<PathBuf>,
1201}
1202
1203/// `faucet conformance` arguments.
1204#[derive(Debug, Parser)]
1205pub struct ConformanceArgs {
1206    /// Only score the connector with this system name (e.g. `postgres`); prints
1207    /// a detailed scorecard. Omit to score every compiled-in connector.
1208    pub name: Option<String>,
1209    /// Restrict to `source` or `sink`.
1210    #[arg(long)]
1211    pub kind: Option<String>,
1212    /// Score every compiled-in connector (the default when no NAME is given;
1213    /// accepted explicitly for clarity in CI).
1214    #[arg(long)]
1215    pub all: bool,
1216    /// Emit the full scorecards as JSON.
1217    #[arg(long)]
1218    pub json: bool,
1219    /// Fail (exit non-zero) if any scored connector is below this maturity tier
1220    /// — an opt-in CI gate. One of `stable` / `experimental` / `beta` / `draft`.
1221    #[arg(long, value_name = "TIER")]
1222    pub min_tier: Option<String>,
1223}
1224
1225/// `faucet search` arguments.
1226#[derive(Debug, Parser)]
1227pub struct SearchArgs {
1228    /// Term to match against connector name / description / keywords / crate.
1229    pub term: String,
1230    /// Read a custom registry index instead of the built-in one.
1231    #[arg(long)]
1232    pub index: Option<PathBuf>,
1233    /// Emit matches as JSON.
1234    #[arg(long)]
1235    pub json: bool,
1236}
1237
1238/// `faucet install` arguments.
1239#[derive(Debug, Parser)]
1240pub struct InstallArgs {
1241    /// Connector system name (e.g. `kafka`).
1242    pub name: String,
1243    /// Disambiguate when a name exists as both a source and a sink.
1244    #[arg(long)]
1245    pub kind: Option<String>,
1246    /// Read a custom registry index instead of the built-in one.
1247    #[arg(long)]
1248    pub index: Option<PathBuf>,
1249}
1250
1251/// `faucet new` arguments.
1252#[derive(Debug, Parser)]
1253pub struct NewArgs {
1254    #[command(subcommand)]
1255    pub target: NewTarget,
1256}
1257
1258/// What `faucet new` scaffolds.
1259#[derive(Debug, Subcommand)]
1260pub enum NewTarget {
1261    /// Scaffold a ready-to-build `faucet-source-<name>` / `faucet-sink-<name>`
1262    /// connector crate following every repo convention.
1263    Connector(NewConnectorArgs),
1264}
1265
1266/// `faucet new connector` arguments.
1267#[derive(Debug, Parser)]
1268pub struct NewConnectorArgs {
1269    /// Connector system name (lowercase, e.g. `acme` or `acme-widgets`). Becomes
1270    /// the crate name `faucet-<kind>-<name>` and the YAML `type:` value.
1271    pub name: String,
1272    /// Whether to scaffold a `source` or a `sink`.
1273    #[arg(long)]
1274    pub kind: String,
1275    /// Also scaffold a `faucet-common-<name>` crate for config shared between a
1276    /// source/sink pair.
1277    #[arg(long)]
1278    pub common: bool,
1279    /// Directory to write the new crate(s) into. Defaults to the current dir.
1280    #[arg(long, short = 'o', default_value = ".")]
1281    pub output: PathBuf,
1282    /// Overwrite any existing files.
1283    #[arg(long)]
1284    pub force: bool,
1285}