udb 0.4.22

Universal Data Broker — a Rust gRPC broker over multiple databases (Postgres, MySQL, SQLite, MongoDB, ClickHouse, Cassandra, MSSQL, Redis, Qdrant, S3, Neo4j, …) with per-tenant RLS, 2PC, sagas, and CDC.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! CLI discoverability layer (stopgap pending the full clap migration in
//! `CLI_UPGRADE_PLAN.md`). The hand-rolled `parse_args` has no `--help`/`-h`/
//! `--version`; this module answers them from a static registry so `udb` is
//! explorable without reading source. It does NOT parse — it prints and exits
//! before `parse_args` runs, so existing parsing/back-compat is untouched.

use std::process;

/// One command's help entry. `usage`/`details` are empty for commands that only
/// need a one-line summary in the top-level list.
struct CmdHelp {
    /// Invocation token(s), e.g. "serve" or "auth bootstrap user".
    name: &'static str,
    group: &'static str,
    summary: &'static str,
    usage: &'static str,
    details: &'static str,
}

const GROUPS: &[&str] = &[
    "Core",
    "Schema & SQL",
    "Auth & policy",
    "SDK & native",
    "Scaffold",
    "Admin",
    "Diagnostics",
];

const COMMANDS: &[CmdHelp] = &[
    CmdHelp {
        name: "serve",
        group: "Core",
        summary: "Start the broker (syncs schema from protos; control plane on data-port+10).",
        usage: "udb serve [<proto-root>] [<namespace>] [<data-addr>]",
        details: "\
  <proto-root>  proto dir to load (default: ./proto; \"\" keeps the default)
  <namespace>   UDB_PROTO_NAMESPACE filter (\"\" = none; an over-eager filter
                that loads 0 custom schemas is warned at startup)
  <data-addr>   data-plane bind (default 0.0.0.0:50051); auth plane = port+10
  Wait for the \"UDB DataBroker is ready: data=… auth=…\" line before clients.
  Run `udb requirements` and `udb doctor --enterprise` FIRST.
  Example: udb serve proto \"\" 0.0.0.0:50051",
    },
    CmdHelp {
        name: "requirements",
        group: "Core",
        summary: "Print the backend contract this project's manifest declares (run before first start).",
        usage: "udb requirements [--json]",
        details: "\
  Lists each required backend (Postgres/Qdrant/object-store/Redis), its env
  vars, whether it's configured, and fatal-vs-degraded. Exits non-zero if a
  fatal backend is unset.",
    },
    CmdHelp {
        name: "doctor",
        group: "Core",
        summary: "Env + backend readiness; --enterprise adds a manifest-aware prerequisite preflight.",
        usage: "udb doctor [--enterprise] [--probe] [--human]",
        details: "\
  --enterprise  also check encryption/password/session/auth-plane/redis/ABAC
                AND report any required backend your protos declare but you
                haven't configured (the same condition that stops `serve`).
  --probe       actively probe backend connectivity.  --human  text output.
  Exit: 0 clean, 2 warnings, 1 fail.",
    },
    CmdHelp {
        name: "lint",
        group: "Schema & SQL",
        summary: "Lint the catalog built from your annotated protos (CI-safe; nonzero on errors).",
        usage: "udb lint [--human]",
        details: "",
    },
    CmdHelp {
        name: "drift",
        group: "Schema & SQL",
        summary: "Diff the manifest against a prior one; flags destructive/blocked changes.",
        usage: "udb drift [--prior <manifest.json>]",
        details: "",
    },
    CmdHelp {
        name: "sql",
        group: "Schema & SQL",
        summary: "Emit the generated bootstrap SQL artifacts as JSON (no DB touched).",
        usage: "udb sql",
        details: "",
    },
    CmdHelp {
        name: "plan",
        group: "Schema & SQL",
        summary: "Build a migration plan (optionally vs a prior manifest) as JSON.",
        usage: "udb plan [--prior <manifest.json>]",
        details: "",
    },
    CmdHelp {
        name: "catalog",
        group: "Schema & SQL",
        summary: "Dump the parsed proto catalog as JSON.",
        usage: "udb catalog",
        details: "",
    },
    CmdHelp {
        name: "dsn",
        group: "Schema & SQL",
        summary: "Print the unified DSN catalog derived from the protos.",
        usage: "udb dsn",
        details: "",
    },
    CmdHelp {
        name: "manifest-export",
        group: "Schema & SQL",
        summary: "Export the current CatalogManifest to JSON (for CI plan-approval).",
        usage: "udb manifest-export",
        details: "",
    },
    CmdHelp {
        name: "auth migrate-grants",
        group: "Auth & policy",
        summary: "Migrate legacy profile-attribute service grants into the typed service_account_grants table.",
        usage: "udb auth migrate-grants [--dry-run]",
        details: "\
  Needs UDB_PG_DSN. Deterministic: scans ACTIVE service accounts, validates each
  profile grant (admin/wildcard scopes and duplicate identities are REJECTED and
  reported, never partially written), and creates one typed grant per account.
  --dry-run reports without writing. After migration the typed grant is the
  authoritative source for password login, API keys, and mTLS bindings.",
    },
    CmdHelp {
        name: "auth bootstrap user",
        group: "Auth & policy",
        summary: "Mint the FIRST admin OFFLINE (no running broker needed); prints the canonical tenant UUID.",
        usage: "udb auth bootstrap user --username <u> --email <e> --password <p> --tenant <code> --project <p>",
        details: "\
  Needs UDB_PG_DSN + UDB_PASSWORD_HASH_SECRET. Defaults: --username admin,
  --tenant acme, --project default. CAPTURE the printed tenant_id (UUID) —
  it, not the human code, goes in the login JWT and tenant-scoped filters.
  After this you MUST seed ABAC (default-deny) before any data CRUD works.",
    },
    CmdHelp {
        name: "auth api-key create",
        group: "Auth & policy",
        summary: "Mint an API key (tenant-scoped).",
        usage: "udb auth api-key create --owner <id> --name <n> --scope <s> [--scope <s>…]",
        details: "",
    },
    CmdHelp {
        name: "auth api-key list/revoke",
        group: "Auth & policy",
        summary: "List an owner's API keys or revoke one OFFLINE (UDB-AUTH-009 rotation/reconciliation).",
        usage: "udb auth api-key list --owner <id>  |  udb auth api-key revoke --key <key_prefix>",
        details: "  Needs UDB_PG_DSN (offline, same operator trust model as `auth bootstrap user`).
  list prints every key for the owner (prefix/name/tenant/scopes/revoked) so a
  provisioner can reconcile instead of minting duplicates; revoke deactivates by
  key prefix. Create now rejects a duplicate ACTIVE name for the same owner.",
    },
    CmdHelp {
        name: "auth grant",
        group: "Auth & policy",
        summary: "Manage typed service-account grants through the authenticated native API.",
        usage: "udb auth grant <create|get|list|replace|rotate-identity|revoke> --tenant <tenant-id> [flags]",
        details: "\
  Needs UDB_AUTH_TARGET (or UDB_GRPC_TARGET) and UDB_AUTH_TOKEN.
  create:  --tenant <tenant-id> --user <uuid> --identity <svc-id> --scope <s> [--scope <s>…]
           [--project <p>] [--reason <r>]
  get:     --tenant <tenant-id> --user <uuid>
  list:    --tenant <tenant-id>
  replace: --tenant <tenant-id> --user <uuid> --scope <s> [--scope <s>…]
           --expected-revision <n> [--project <p>] [--reason <r>]
  rotate-identity: --tenant <tenant-id> --user <uuid> --identity <new-id>
           --expected-revision <n> [--reason <r>]
  revoke:  --tenant <tenant-id> --user <uuid> [--reason <r>]
  Wildcard/admin/owner scopes are always rejected. Replace and identity rotation
  bump the revision, invalidating dependent keys and bindings until re-issued.",
    },
    CmdHelp {
        name: "auth cert-binding",
        group: "Auth & policy",
        summary: "Manage mTLS certificate bindings through the authenticated native API.",
        usage: "udb auth cert-binding <create|list|revoke> --tenant <tenant-id> [flags]",
        details: "\
  Needs UDB_AUTH_TARGET (or UDB_GRPC_TARGET) and UDB_AUTH_TOKEN.
  create: --tenant <tenant-id> --user <uuid> --selector-kind <k> --selector-value <v>
          [--scope <s>…] [--not-before-unix <seconds>] [--not-after-unix <seconds>]
          [--reason <r>]
  list:   --tenant <tenant-id>
  revoke: --tenant <tenant-id> --binding <id> [--reason <r>]
  Selector kinds: SPIFFE_URI, DNS_SAN, SUBJECT_CN, FINGERPRINT_SHA256. The
  account must hold an ACTIVE grant; --scope attenuates it (empty = full grant).
  Re-creating a REVOKED selector supersedes the old row in place (same id).",
    },
    CmdHelp {
        name: "auth policy put",
        group: "Auth & policy",
        summary: "Write a control-plane Casbin governance rule (NOT the data-plane ABAC gate).",
        usage: "udb auth policy put --subject <s> --action <a> --resource <r> --effect <ALLOW|DENY> --tenant <t> --project <p>",
        details: "",
    },
    CmdHelp {
        name: "policy-lint",
        group: "Auth & policy",
        summary: "Lint ABAC policy files from UDB_ABAC_POLICY_FILE (nonzero on broken files).",
        usage: "udb policy-lint",
        details: "",
    },
    CmdHelp {
        name: "policy-seed",
        group: "Auth & policy",
        summary: "Generate INSERT SQL to seed ABAC policies into the UDB ABAC table.",
        usage: "udb policy-seed",
        details: "",
    },
    CmdHelp {
        name: "proto export",
        group: "SDK & native",
        summary: "Vendor UDB's annotation protos so app protos can import udb/core/common/v1/db.proto.",
        usage: "udb proto export --out <dir> [--no-buf-yaml] [--fmt]",
        details: "\
  Sibling verb: `udb proto fmt [<dir>] [--check]` re-wraps long UDB field
  annotations onto one physical line (narrower than `buf format`).",
    },
    CmdHelp {
        name: "sdk generate",
        group: "SDK & native",
        summary: "Generate/refresh a language SDK from the embedded RPC manifest + templates.",
        usage: "udb sdk generate --lang <ts|python|go|java|csharp|php|all> [--out <dir>] [--surface <…>]",
        details: "\
  Sibling verbs: `udb sdk manifest` (dump the RPC surface as JSON),
  `udb sdk list-langs` (available template dirs).",
    },
    CmdHelp {
        name: "native list",
        group: "SDK & native",
        summary: "Inspect the descriptor-derived native-service contract.",
        usage: "udb native <list|manifest|docs|lint|contract-diff|contract-baseline> [--json]",
        details: "",
    },
    CmdHelp {
        name: "init",
        group: "Scaffold",
        summary: "Project-aware scaffold planner/executor.",
        usage: "udb init [--profile <p>] [--backend <b>…] [--native-service <s>…] [--yes] [--dry-run]",
        details: "",
    },
    CmdHelp {
        name: "init-project",
        group: "Scaffold",
        summary: "Scaffold a minimal project (sample proto, config, DDL, docker-compose).",
        usage: "udb init-project",
        details: "",
    },
    CmdHelp {
        name: "app init",
        group: "Scaffold",
        summary: "Scaffold an app integration wiring the UdbProject facade.",
        usage: "udb app init --lang <l> --services <s,…> --tenant <t> --project <p> --out <dir>",
        details: "",
    },
    CmdHelp {
        name: "dev up",
        group: "Scaffold",
        summary: "Start/stop/test the local multi-backend sandbox (from a repo checkout).",
        usage: "udb dev <up|down|smoke> [<service>] [--yes]",
        details: "",
    },
    CmdHelp {
        name: "admin force-sync",
        group: "Admin",
        summary: "Force the startup lifecycle from the CLI and exit with a JSON report.",
        usage: "udb admin force-sync",
        details: "",
    },
    CmdHelp {
        name: "admin release-lock",
        group: "Admin",
        summary: "Terminate the PG session(s) holding the startup advisory lock (clear a stale lock).",
        usage: "udb admin release-lock",
        details: "Run against the DIRECT DSN, not a pooler.",
    },
    CmdHelp {
        name: "admin reset-db",
        group: "Admin",
        summary: "Drop all UDB-managed schemas + ledger tables (DESTRUCTIVE; needs --yes).",
        usage: "udb admin reset-db --yes",
        details: "",
    },
    CmdHelp {
        name: "admin dry-run",
        group: "Admin",
        summary: "Generate the SQL plan and exit WITHOUT applying (safe on production).",
        usage: "udb admin dry-run",
        details: "",
    },
    CmdHelp {
        name: "admin verify-audit",
        group: "Admin",
        summary: "Verify the tamper-evident admin audit-log hash chain.",
        usage: "udb admin verify-audit [--limit <n>]",
        details: "",
    },
    CmdHelp {
        name: "sync-migrations",
        group: "Admin",
        summary: "Sync db_ops/migrations with the current proto AST.",
        usage: "udb sync-migrations [--force-bootstrap] [--backend <b>]",
        details: "",
    },
    CmdHelp {
        name: "compat-matrix",
        group: "Diagnostics",
        summary: "Print the authoritative supported proto-annotation matrix as JSON.",
        usage: "udb compat-matrix",
        details: "",
    },
    CmdHelp {
        name: "explain",
        group: "Diagnostics",
        summary: "Explain the generated DDL/DSN/policies for a message type.",
        usage: "udb explain",
        details: "",
    },
    CmdHelp {
        name: "health-check",
        group: "Diagnostics",
        summary: "Lightweight Docker HEALTHCHECK — exit 0 if healthy.",
        usage: "udb health-check",
        details: "",
    },
    CmdHelp {
        name: "tracker-ddl",
        group: "Diagnostics",
        summary: "Emit the migration-ledger table DDL to stdout.",
        usage: "udb tracker-ddl",
        details: "",
    },
    CmdHelp {
        name: "config-skeleton",
        group: "Diagnostics",
        summary: "Emit a default MigrationOptions config skeleton as JSON.",
        usage: "udb config-skeleton",
        details: "",
    },
];

/// Answer `--help`/`-h`/`help [cmd]`/no-args and `--version`/`-V`, then exit.
/// Returns normally only when the args are a real command to parse.
pub(crate) fn handle_help_or_version(args: &[String]) {
    if args.iter().any(|a| a == "--version" || a == "-V") {
        println!("udb {}", env!("CARGO_PKG_VERSION"));
        process::exit(0);
    }

    let first = args.first().map(String::as_str);

    // `udb help [cmd...]`
    if first == Some("help") {
        let topic = args[1..].join(" ");
        if topic.is_empty() {
            print_top_level();
        } else {
            print_command_help(&topic);
        }
        process::exit(0);
    }

    // bare `udb`, or `udb --help` / `udb -h`
    if args.is_empty() || matches!(first, Some("--help") | Some("-h")) {
        print_top_level();
        process::exit(0);
    }

    // `udb <cmd> [sub...] --help|-h` — show that command's help.
    if args.iter().skip(1).any(|a| a == "--help" || a == "-h") {
        // Match the longest command prefix from the args (so "auth bootstrap
        // user --help" resolves to the 3-token command, "serve --help" to one).
        let non_flag: Vec<&str> = args
            .iter()
            .map(String::as_str)
            .take_while(|a| !a.starts_with('-'))
            .collect();
        let topic = non_flag.join(" ");
        print_command_help(&topic);
        process::exit(0);
    }
}

fn print_top_level() {
    println!(
        "udb {} — proto-driven multi-database broker\n",
        env!("CARGO_PKG_VERSION")
    );
    println!(
        "USAGE:\n  udb <command> [args] [flags]\n  udb help <command>      show a command's flags + example\n  udb <command> --help    same\n  udb --version\n"
    );
    println!("COMMANDS:");
    for group in GROUPS {
        let mut printed_group = false;
        for cmd in COMMANDS.iter().filter(|c| &c.group == group) {
            if !printed_group {
                println!("\n  {group}:");
                printed_group = true;
            }
            println!("    {:<22} {}", cmd.name, cmd.summary);
        }
    }
    println!(
        "\nNew project? The bootstrap runbook (proto → bootstrap admin → seed ABAC →\n\
         login → CRUD) is in docs/enterprise-deployment.md and examples/ts_enterprise.\n\
         Ground truth for RPCs/annotations: `udb sdk manifest`, `udb native list`, `udb compat-matrix`."
    );
}

fn print_command_help(topic: &str) {
    let topic = topic.trim();
    // Exact match, else longest-prefix match (so "auth bootstrap user xyz" still
    // finds "auth bootstrap user"), else a near-name suggestion.
    let found = COMMANDS.iter().find(|c| c.name == topic).or_else(|| {
        COMMANDS
            .iter()
            .filter(|c| topic.starts_with(c.name) || c.name.starts_with(topic))
            .max_by_key(|c| c.name.len())
    });
    match found {
        Some(cmd) => {
            println!("udb {}{}\n", cmd.name, cmd.summary);
            if !cmd.usage.is_empty() {
                println!("USAGE:\n  {}\n", cmd.usage);
            }
            if !cmd.details.is_empty() {
                println!("{}", cmd.details);
            }
        }
        None => {
            println!("No help for '{topic}'.\n");
            print_top_level();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_command_has_a_known_group() {
        for cmd in COMMANDS {
            assert!(
                GROUPS.contains(&cmd.group),
                "command {} has unlisted group {}",
                cmd.name,
                cmd.group
            );
        }
    }

    #[test]
    fn core_commands_are_documented() {
        for want in [
            "serve",
            "doctor",
            "requirements",
            "auth bootstrap user",
            "sdk generate",
        ] {
            assert!(
                COMMANDS.iter().any(|c| c.name == want),
                "missing help entry for {want}"
            );
        }
    }
}