//! PiCloud command-line client. //! //! Thin client over the existing admin + execute HTTP surface — the //! server gains nothing for the CLI; the CLI is just a developer //! ergonomics layer over endpoints the dashboard already uses. use std::path::PathBuf; use std::process::ExitCode; use clap::{Args, Parser, Subcommand, ValueEnum}; mod client; mod cmds; mod config; mod discover; mod linkstate; mod manifest; mod output; use crate::output::OutputMode; #[derive(Parser)] #[command(name = "pic", version, about = "PiCloud command-line client")] struct Cli { /// Output format for `ls` / `show` / `whoami` / `logs` commands. /// TSV stays pipe-friendly; JSON is `jq`-ready. #[arg(long, value_enum, global = true, default_value_t = OutputMode::Tsv)] output: OutputMode, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Authenticate with the server. Default flow prompts for username /// + password and saves the returned session token; `--token` skips /// the password exchange and persists a bearer string directly (use /// this for long-lived API keys minted via `pic api-keys mint`). Login(LoginArgs), /// Revoke the saved session server-side and delete the local /// credentials file. Idempotent. Logout, /// Print the principal the saved token resolves to. Whoami, /// App management. Apps { #[command(subcommand)] cmd: AppsCmd, }, /// Group (org-tree) management. Groups { #[command(subcommand)] cmd: GroupsCmd, }, /// §7 multi-repo ownership projects (read-only). Projects { #[command(subcommand)] cmd: ProjectsCmd, }, /// Script management. Scripts { #[command(subcommand)] cmd: ScriptsCmd, }, /// Long-lived bearer API key management. #[command(name = "api-keys")] ApiKeys { #[command(subcommand)] cmd: ApiKeysCmd, }, /// Tail recent execution logs for a script. Logs(LogsArgs), /// Top-level alias for `pic scripts invoke `. Invoke(InvokeArgs), /// Top-level alias for `pic scripts deploy --app `. Deploy(DeployArgs), /// Route management — bind script ids to HTTP method+host+path. Routes { #[command(subcommand)] cmd: RoutesCmd, }, /// Admin user management — list / create / patch / delete the /// per-instance admin accounts. All endpoints require /// `instance:admin` or `instance:owner`. Admins { #[command(subcommand)] cmd: AdminsCmd, }, /// Trigger management — list / create / delete the triggers that /// fire scripts on KV mutations, cron schedules, dead-letter rows, /// etc. `create-from-json` is the escape hatch for kinds the CLI /// doesn't expose a per-kind wrapper for (docs/files/pubsub/email/ /// queue) — pass the body JSON inline, via `@`, or `-` to /// read from stdin. Note: a `pubsub` trigger runs a script when a /// message is published; to let an *external* client subscribe to a /// topic over SSE, register it with `pic topics` instead. Triggers { #[command(subcommand)] cmd: TriggersCmd, }, /// Pub/sub topic registry — control which topics external clients /// can subscribe to over SSE (`/realtime/topics/{name}`). Publishing /// from a script needs no registration; only outside subscribers do. Topics { #[command(subcommand)] cmd: TopicsCmd, }, /// App end-user management — list / inspect the users who registered /// via `users::*` in scripts, mint reset tokens, revoke sessions. /// Distinct from `pic admins` (instance accounts). Users { #[command(subcommand)] cmd: UsersCmd, }, /// Dead-letter management — inspect, replay, and resolve rows the /// dispatcher wrote after exhausting a trigger's retries. #[command(name = "dead-letters")] DeadLetters { #[command(subcommand)] cmd: DeadLettersCmd, }, /// Per-app secret storage — list names, set values from stdin, /// delete by name. Values never leave the server after `set`. Secrets { #[command(subcommand)] cmd: SecretsCmd, }, /// App membership — list members and grant / change / revoke their /// per-app role (app_admin | editor | viewer). Members { #[command(subcommand)] cmd: MembersCmd, }, /// Config vars (Phase 3) — set / list / delete group- or app-owned /// env-scoped vars. Values inherit down the group tree; an app value /// overrides an inherited one (proximity wins). Vars { #[command(subcommand)] cmd: VarsCmd, }, /// Extension points (§5.5) — read-only view of the module names a node /// marks as overridable. Authored declaratively via the manifest /// `extension_points = [...]`; this lists them + (for an app) each one's /// resolved provider. ExtensionPoints { #[command(subcommand)] cmd: ExtensionPointsCmd, }, /// Shared group collections (§11.6) — read-only view of the KV collection /// names a group offers as cross-app-shared. Authored declaratively via the /// `[group]` manifest `collections = [...]`; scripts read/write them with /// `kv::shared_collection("name")`. Collections { #[command(subcommand)] cmd: CollectionsCmd, }, /// Per-app opt-out of inherited group templates (§11 tail). Authoring is /// declarative via the `[suppress]` manifest block; this lists what an app /// declines. Suppress { #[command(subcommand)] cmd: SuppressCmd, }, /// Files inspection — list a collection's blobs, download bytes, or /// delete a file. Read + delete only; writes go through scripts. Files { #[command(subcommand)] cmd: FilesCmd, }, /// Queue inspection — list queues with depth counts, or drill into /// one queue's stats + registered consumer. Read-only. Queues { #[command(subcommand)] cmd: QueuesCmd, }, /// KV inspection — list keys in a collection or fetch one value. /// Read-only; writes go through `kv::set` in scripts. Kv { #[command(subcommand)] cmd: KvCmd, }, /// Reconcile the live app to a `picloud.toml` manifest in one /// transaction (creates + updates; `--prune` also deletes resources /// absent from the manifest). Apply(ApplyArgs), /// Diff a `picloud.toml` manifest against the live app and print the /// changes (create / update / no-op / delete). Read-only. Plan(PlanArgs), /// Export an app's current server state into a `picloud.toml` manifest /// (+ `scripts/.rhai` sources) for declarative management with /// `pic plan` / `pic apply`. Pull(PullArgs), /// Scaffold a new declarative project: a `picloud.toml` (minimal working /// app), `scripts/hello.rhai`, and a `.gitignore`. Offline; deploy with /// `pic plan` then `pic apply`. Init(InitArgs), /// Show an app's resolved configuration. `--effective` lists secrets with /// values masked (``/``), cross-referenced against the manifest. Config(ConfigArgs), } #[derive(Args)] struct ApplyArgs { /// Path to the manifest. #[arg(long, default_value = "picloud.toml")] file: PathBuf, /// Phase 5: apply a whole project TREE — every `picloud.toml` under this /// directory (groups + apps) reconciled in one atomic transaction. /// Mutually exclusive with `--file`. #[arg(long, conflicts_with = "file")] dir: Option, /// Delete live scripts/routes/triggers absent from the manifest. /// Secrets are never pruned. #[arg(long)] prune: bool, /// Skip the `--prune` confirmation prompt. Required to prune /// non-interactively (CI). #[arg(long)] yes: bool, /// Skip the bound-plan staleness check (apply even if the app changed /// since the last `pic plan`). #[arg(long)] force: bool, /// §7 multi-repo ownership: reassign a group node owned by another /// `[project]` to this manifest's project. Requires group-admin on the /// node; without it an apply to an owned node is refused (409). #[arg(long)] takeover: bool, /// Merge the `picloud..toml` overlay (per-env slug + secrets) on /// top of the base manifest before applying. #[arg(long)] env: Option, /// §6 M2: reparent a group whose server parent diverges from the /// manifest's directory nesting (default: refuse on divergence). #[arg(long, conflicts_with = "adopt_server_structure")] force_local_structure: bool, /// §6 M2: accept the server's group placement on a structural /// divergence and reconcile content in place (no reparent). #[arg(long)] adopt_server_structure: bool, /// §3 M3: approve applying to a confirm-required environment /// (`[project.environments]`). Repeatable; each must match the `--env`. #[arg(long = "approve")] approve: Vec, } #[derive(Args)] struct PlanArgs { /// Path to the manifest. #[arg(long, default_value = "picloud.toml")] file: PathBuf, /// Phase 5: plan a whole project TREE — every `picloud.toml` under this /// directory (groups + apps). Mutually exclusive with `--file`. #[arg(long, conflicts_with = "file")] dir: Option, /// Merge the `picloud..toml` overlay (per-env slug + secrets) on /// top of the base manifest before diffing. #[arg(long)] env: Option, } #[derive(Args)] struct PullArgs { /// App slug or id to export. app: String, /// Directory to write `picloud.toml` + `scripts/` into. #[arg(long, default_value = ".")] dir: PathBuf, /// Overwrite an existing `picloud.toml` (and colliding `scripts/*.rhai`). #[arg(long)] force: bool, } #[derive(Args)] struct InitArgs { /// App slug for the new project. Defaults to a slug derived from the /// target directory's name. slug: Option, /// Directory to scaffold into. #[arg(long, default_value = ".")] dir: PathBuf, /// Display name for the app. Defaults to a title-cased slug. #[arg(long)] name: Option, /// Overwrite an existing `picloud.toml`. #[arg(long)] force: bool, } #[derive(Args)] struct ConfigArgs { /// Path to the manifest. #[arg(long, default_value = "picloud.toml")] file: PathBuf, /// Show the effective (resolved) config with secrets masked. #[arg(long)] effective: bool, /// With `--effective`, also print each resolved var's `merged_from` /// provenance — the ordered (depth, scope) layers it merged from. #[arg(long)] explain: bool, /// Resolve against the `picloud..toml` overlay (per-env slug + /// secrets), matching `pic plan --env` / `pic apply --env`. #[arg(long)] env: Option, } #[derive(Subcommand)] enum KvCmd { /// List keys in a collection. Exactly one of `--app` (per-app KV) or /// `--group` (a group's §11.6 shared KV). Ls { #[arg(long, conflicts_with = "group")] app: Option, #[arg(long)] group: Option, #[arg(long)] collection: String, #[arg(long, default_value_t = 100)] limit: u32, }, /// Fetch one key's value (printed as JSON). `--app` or `--group`. Get { #[arg(long, conflicts_with = "group")] app: Option, #[arg(long)] group: Option, #[arg(long)] collection: String, key: String, }, } #[derive(Subcommand)] enum MembersCmd { /// List app members. Ls { #[arg(long)] app: String, }, /// Grant a user a role on the app. Add { #[arg(long)] app: String, #[arg(long = "user")] user_id: String, /// `app_admin` | `editor` | `viewer`. #[arg(long)] role: String, }, /// Change an existing member's role. Set { #[arg(long)] app: String, #[arg(long = "user")] user_id: String, #[arg(long)] role: String, }, /// Remove a member. Rm { #[arg(long)] app: String, #[arg(long = "user")] user_id: String, }, } #[derive(Subcommand)] enum FilesCmd { /// List files in a collection. Ls { #[arg(long)] app: String, #[arg(long)] collection: String, #[arg(long, default_value_t = 100)] limit: u32, }, /// Download a file's bytes (to `--out ` or stdout). Get { #[arg(long)] app: String, #[arg(long)] collection: String, #[arg(long = "id")] file_id: String, #[arg(long)] out: Option, }, /// Delete a file. Rm { #[arg(long)] app: String, #[arg(long)] collection: String, #[arg(long = "id")] file_id: String, }, } #[derive(Subcommand)] enum QueuesCmd { /// List queues with depth counts. Ls { #[arg(long)] app: String, }, /// Show one queue's stats + consumer. Show { #[arg(long)] app: String, queue_name: String, }, } #[derive(Args)] struct LoginArgs { /// Override the URL prompt non-interactively. Also reads /// `PICLOUD_URL`. #[arg(long)] url: Option, /// Skip the username + password exchange and persist a bearer /// directly (validated against `/auth/me` first). Pass `--token -` /// to read it from stdin; an inline value is rejected (it leaks into /// shell history / `ps`). For CI, set `PICLOUD_TOKEN` instead. #[arg(long)] token: Option, /// Username for non-interactive password login. Pair with /// `--password-stdin`. Omit both for the interactive prompt. #[arg(long)] username: Option, /// Read the password from stdin instead of prompting (CI path). The /// password is never accepted inline — that leaks into shell history, /// `ps`, and /proc. #[arg(long = "password-stdin")] password_stdin: bool, } #[derive(Subcommand)] enum AppsCmd { /// List apps the caller can see. Ls, /// Create a new app. Create { slug: String, #[arg(long)] name: Option, #[arg(long)] description: Option, /// Parent group (slug or id). Defaults to the instance root. #[arg(long)] group: Option, }, /// Show a single app, including the caller's role in it. Show { ident: String }, /// Delete an app. Without `--force`, the server rejects if the app /// still owns scripts. Delete { ident: String, #[arg(long)] force: bool, }, /// Manage an app's domain claims. A non-default app's routes only /// match once it claims the request `Host` — without a claim every /// route 404s. Domains { #[command(subcommand)] cmd: DomainsCmd, }, } #[derive(Subcommand)] enum ProjectsCmd { /// List all projects + how many group nodes each owns. Ls, } #[derive(Subcommand)] enum GroupsCmd { /// List all groups (flat). Ls, /// Render the group hierarchy as an indented tree. Tree, /// Create a new group. Omit `--parent` for a root-level group. Create { slug: String, #[arg(long)] name: Option, #[arg(long)] description: Option, /// Parent group (slug or id). #[arg(long)] parent: Option, }, /// Show a group with its path, subgroups, and apps. Show { ident: String }, /// Rename a group (name/description only — the slug is frozen). Rename { ident: String, #[arg(long)] name: Option, #[arg(long)] description: Option, }, /// Move a group under a new parent (`--to` slug/id, or omit for root). Reparent { ident: String, #[arg(long)] to: Option, }, /// Delete a group. Refused (409) if non-empty unless `--recursive`, /// which deletes child groups leaf-first (apps are never auto-deleted). Rm { ident: String, #[arg(long)] recursive: bool, }, /// Manage a group's members (inherited down the tree). Members { #[command(subcommand)] cmd: GroupMembersCmd, }, } #[derive(Subcommand)] enum GroupMembersCmd { /// List a group's members. Ls { group: String }, /// Grant a member a role on the group. Add { group: String, user_id: String, #[arg(long, default_value = "viewer")] role: String, }, /// Change a member's role. Set { group: String, user_id: String, #[arg(long)] role: String, }, /// Remove a member from the group. Rm { group: String, user_id: String }, } #[derive(Subcommand)] enum DomainsCmd { /// List the app's domain claims. Ls { app: String }, /// Claim a domain for the app. Patterns: exact (`app.example.com`), /// wildcard (`*.example.com`), or parameterized (`{tenant}.example.com` /// — `{name}` syntax, never `:name`). Add { app: String, pattern: String }, /// Release a domain claim by its id. Rm { app: String, domain_id: String }, } #[derive(Subcommand)] enum ExtensionPointsCmd { /// List a node's extension points. `--app` shows every EP visible on the /// app's chain with its resolved provider; `--group` shows the group's own. Ls { #[arg(long)] app: Option, #[arg(long, conflicts_with = "app")] group: Option, }, } #[derive(Subcommand)] enum CollectionsCmd { /// List the shared collections a group declares (§11.6). Group-only — /// shared collections are owned by groups, not apps. Ls { #[arg(long)] group: String, }, } #[derive(Subcommand)] enum SuppressCmd { /// List the inherited templates an owner declines (§11 tail). Exactly one /// of `--app` (declines for itself) or `--group` (declines for its whole /// subtree, §11 tail M1). Ls { #[arg(long, conflicts_with = "group")] app: Option, #[arg(long)] group: Option, }, } #[derive(Subcommand)] enum ScriptsCmd { /// List scripts. With `--app`, scoped to one app; with `--group`, a /// group's own scripts (Phase 4); without either, one /// `GET /admin/scripts` for everything the caller can see. Ls { #[arg(long)] app: Option, /// Phase 4: list a group's own (non-inherited) scripts. #[arg(long, conflicts_with = "app")] group: Option, }, /// Upload a `.rhai` file. Patches the existing script with the /// matching name in `--app`/`--group` if one exists, otherwise creates it. Deploy(DeployArgs), /// POST to `/api/v1/execute/{id}`. Body via `--body @path`, /// `--body @-` for stdin, or inline JSON. Invoke(InvokeArgs), /// Delete a script. Requires AppAdmin on the owning app. Delete { id: String }, } #[derive(Args)] struct DeployArgs { file: PathBuf, /// Owning app (slug or id). Exactly one of `--app` / `--group`. #[arg(long)] app: Option, /// Phase 4: deploy a group-owned script (template inherited by /// descendant apps) instead of an app-owned one. Exactly one of /// `--app` / `--group`. #[arg(long, conflicts_with = "app")] group: Option, #[arg(long)] name: Option, #[arg(long)] description: Option, /// Per-script wall-clock timeout in seconds (overrides the instance /// default; still clamped by `PICLOUD_SANDBOX_MAX_*` ceilings). #[arg(long)] timeout: Option, /// Per-script memory ceiling in MB. #[arg(long)] memory: Option, /// Script kind: `endpoint` (default) or `module` (importable, no route). #[arg(long, value_enum)] kind: Option, /// Sandbox override as `key=value`, repeatable. Keys: `max_operations`, /// `max_string_size`, `max_array_size`, `max_map_size`, /// `max_call_levels`, `max_expr_depth`. E.g. `--sandbox max_operations=500000`. #[arg(long = "sandbox", value_name = "KEY=VALUE")] sandbox: Vec, } #[derive(Copy, Clone, clap::ValueEnum)] enum ScriptKindArg { Endpoint, Module, } impl From for picloud_shared::ScriptKind { fn from(v: ScriptKindArg) -> Self { match v { ScriptKindArg::Endpoint => Self::Endpoint, ScriptKindArg::Module => Self::Module, } } } impl DeployArgs { /// Fold the runtime-config flags into a `ScriptConfig`, parsing and /// validating the `--sandbox key=value` pairs. fn script_config(&self) -> anyhow::Result { let sandbox = parse_sandbox_overrides(&self.sandbox)?; Ok(client::ScriptConfig { timeout_seconds: self.timeout, memory_limit_mb: self.memory, kind: self.kind.map(Into::into), sandbox, }) } } /// Parse repeatable `--sandbox key=value` flags into a `ScriptSandbox`. /// Unknown keys and non-integer values are hard errors so a typo doesn't /// silently deploy an unrestricted script. fn parse_sandbox_overrides( pairs: &[String], ) -> anyhow::Result> { use anyhow::{anyhow, Context}; if pairs.is_empty() { return Ok(None); } let mut sb = picloud_shared::ScriptSandbox::default(); for raw in pairs { let (key, val) = raw .split_once('=') .ok_or_else(|| anyhow!("--sandbox expects key=value, got {raw:?}"))?; let n: u64 = val .trim() .parse() .with_context(|| format!("--sandbox {key}: {val:?} is not a non-negative integer"))?; match key.trim() { "max_operations" => sb.max_operations = Some(n), "max_string_size" => sb.max_string_size = Some(n), "max_array_size" => sb.max_array_size = Some(n), "max_map_size" => sb.max_map_size = Some(n), "max_call_levels" => sb.max_call_levels = Some(n), "max_expr_depth" => sb.max_expr_depth = Some(n), other => return Err(anyhow!("unknown --sandbox key: {other:?}")), } } Ok(Some(sb)) } #[derive(Args)] struct InvokeArgs { id: String, #[arg(long)] body: Option, #[arg(short = 'H', long = "header", value_parser = client::parse_kv_header)] headers: Vec<(String, String)>, } #[derive(Subcommand)] enum ApiKeysCmd { /// Mint a new long-lived bearer key. Token printed exactly once. Mint { name: String, /// Repeat for multiple scopes: `--scope script:read --scope log:read`. #[arg(long = "scope", required = true)] scopes: Vec, /// Bind the key to a single app (slug or id). Rejects /// `instance:*` scopes when set. #[arg(long)] app: Option, /// Absolute RFC 3339 (`2026-12-31T23:59:59Z`) or shorthand /// `d`/`h`/`m`. #[arg(long)] expires: Option, }, /// List the caller's keys (no `raw_token` after mint). Ls, /// Revoke a key by id. Rm { id: String }, } #[derive(Args)] struct LogsArgs { script_id: String, #[arg(long, default_value_t = 50)] limit: u32, /// Filter by execution origin: `http`, `kv`, `cron`, `queue`, /// `invoke`, `dead_letter`, `docs`, `files`, `pubsub`, `email`. /// Omit (or `all`) to show every source. #[arg(long)] source: Option, } #[derive(Clone, Copy, ValueEnum)] enum DispatchModeArg { Sync, Async, } impl From for picloud_shared::DispatchMode { fn from(v: DispatchModeArg) -> Self { match v { DispatchModeArg::Sync => Self::Sync, DispatchModeArg::Async => Self::Async, } } } /// Wire form for trigger dispatch — the trigger handlers accept /// `"sync"` / `"async"` as JSON strings, so we send that directly. const fn dispatch_wire(d: DispatchModeArg) -> &'static str { match d { DispatchModeArg::Sync => "sync", DispatchModeArg::Async => "async", } } #[derive(Clone, Copy, ValueEnum)] enum HostKindArg { /// Any host (default). Any, /// Exact `host` only. Strict, /// `*.` — wildcard subdomain. Wildcard, } impl From for picloud_shared::HostKind { fn from(v: HostKindArg) -> Self { match v { HostKindArg::Any => Self::Any, HostKindArg::Strict => Self::Strict, HostKindArg::Wildcard => Self::Wildcard, } } } #[derive(Clone, Copy, ValueEnum)] enum PathKindArg { Exact, Prefix, Param, } impl From for picloud_shared::PathKind { fn from(v: PathKindArg) -> Self { match v { PathKindArg::Exact => Self::Exact, PathKindArg::Prefix => Self::Prefix, PathKindArg::Param => Self::Param, } } } #[derive(Clone, Copy, ValueEnum)] enum InstanceRoleArg { Owner, Admin, Member, } impl From for picloud_shared::InstanceRole { fn from(v: InstanceRoleArg) -> Self { match v { InstanceRoleArg::Owner => Self::Owner, InstanceRoleArg::Admin => Self::Admin, InstanceRoleArg::Member => Self::Member, } } } #[derive(Subcommand)] enum RoutesCmd { /// List routes bound to a script, or a group's route TEMPLATES (`--group`). Ls { #[arg(conflicts_with = "group", required_unless_present = "group")] script_id: Option, /// List the group's §11 tail route templates instead. #[arg(long)] group: Option, }, /// Create a new route. Host defaults to `*` (any). Path-kind /// defaults to `exact`. Dispatch defaults to `sync`. Create { /// The script to bind. Routes inherit the script's `app_id`. #[arg(long)] script: String, /// HTTP path (e.g. `/webhook`, `/users/:id`, `/blobs/*`). #[arg(long)] path: String, /// HTTP method to match. Omit for ANY. #[arg(long)] method: Option, /// `exact`, `prefix`, or `param`. #[arg(long = "path-kind", value_enum, default_value_t = PathKindArg::Exact)] path_kind: PathKindArg, /// Host pattern (e.g. `*`, `api.example.com`, `*.example.com`). /// Pair with `--host-kind` when ambiguous; the CLI does not /// auto-detect. #[arg(long, default_value = "")] host: String, /// `any` (default), `strict`, or `wildcard`. #[arg(long = "host-kind", value_enum, default_value_t = HostKindArg::Any)] host_kind: HostKindArg, /// Name to bind the host-wildcard segment to (for /// `host-kind=wildcard` routes that want to read the subdomain). #[arg(long = "host-param-name")] host_param_name: Option, /// `sync` (default) or `async`. Async returns 202 immediately; /// the dispatcher fires the script in the background. #[arg(long, value_enum, default_value_t = DispatchModeArg::Sync)] dispatch: DispatchModeArg, }, /// Delete a route by id. Rm { route_id: String }, /// Dry-run conflict check for a hypothetical route. Check { /// App slug or UUID (conflict checks are intra-app). #[arg(long)] app: String, #[arg(long)] path: String, #[arg(long)] method: Option, #[arg(long = "path-kind", value_enum, default_value_t = PathKindArg::Exact)] path_kind: PathKindArg, #[arg(long, default_value = "")] host: String, #[arg(long = "host-kind", value_enum, default_value_t = HostKindArg::Any)] host_kind: HostKindArg, }, /// What route would the given URL+method match, if any? Match { /// App slug or UUID. #[arg(long)] app: String, /// Full URL — host+path must be present. url: String, #[arg(long, default_value = "GET")] method: String, }, } #[derive(Subcommand)] enum TriggersCmd { /// List an app's triggers, or a group's trigger TEMPLATES (`--group`). Ls { #[arg(long, conflicts_with = "group", required_unless_present = "group")] app: Option, /// List the group's §11 tail trigger templates instead. #[arg(long)] group: Option, }, /// Delete a trigger by id. Rm { #[arg(long)] app: String, trigger_id: String, }, /// Create a KV trigger. #[command(name = "create-kv")] CreateKv { #[arg(long)] app: String, #[arg(long)] script: String, /// Glob over collection names (`*`, `users`, `events_*`, …). #[arg(long)] collection: String, /// Repeat to filter ops: `--op insert --op delete`. Empty /// (the default) means any op fires the trigger. #[arg(long = "op")] ops: Vec, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a cron trigger. #[command(name = "create-cron")] CreateCron { #[arg(long)] app: String, #[arg(long)] script: String, /// 6-field cron expression (with seconds). #[arg(long)] schedule: String, /// IANA timezone name. Defaults to UTC. #[arg(long)] timezone: Option, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a dead-letter trigger — fires when ANOTHER trigger's /// retries exhaust. Filters narrow the match: omit to fire on /// every DL in the app. #[command(name = "create-dead-letter")] CreateDeadLetter { #[arg(long)] app: String, #[arg(long)] script: String, /// Source filter — match only DL rows that originated from /// `kv` / `docs` / `queue` / etc. #[arg(long = "source-filter")] source_filter: Option, /// Trigger id filter — match only DL rows from one trigger. #[arg(long = "trigger-id-filter")] trigger_id_filter: Option, /// Script id filter — match only DL rows from one script. #[arg(long = "script-id-filter")] script_id_filter: Option, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a docs trigger — fires on document mutations in matching /// collections. #[command(name = "create-docs")] CreateDocs { #[arg(long)] app: String, #[arg(long)] script: String, /// Glob over collection names (`*`, `posts`, `events_*`, …). #[arg(long)] collection: String, /// Repeat to filter ops: `--op insert --op delete`. Empty fires on any. #[arg(long = "op")] ops: Vec, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a files trigger — fires on blob create/update/delete in /// matching collections. #[command(name = "create-files")] CreateFiles { #[arg(long)] app: String, #[arg(long)] script: String, #[arg(long)] collection: String, #[arg(long = "op")] ops: Vec, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a pub/sub trigger — fires on messages published to topics /// matching the pattern. #[command(name = "create-pubsub")] CreatePubsub { #[arg(long)] app: String, #[arg(long)] script: String, /// Topic glob (`*`, `orders.*`, `user.signup`, …). #[arg(long = "topic")] topic_pattern: String, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create a queue consumer trigger — fires per message claimed off /// the named queue. #[command(name = "create-queue")] CreateQueue { #[arg(long)] app: String, #[arg(long)] script: String, #[arg(long = "queue")] queue_name: String, /// Per-message visibility timeout in seconds (claim lease). #[arg(long = "visibility-timeout")] visibility_timeout_secs: Option, #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)] dispatch: DispatchModeArg, }, /// Create an email trigger — fires on inbound mail POSTed to the /// webhook receiver. No dispatch mode (inbound webhook only). #[command(name = "create-email")] CreateEmail { #[arg(long)] app: String, #[arg(long)] script: String, /// Shared HMAC secret the provider signs inbound POSTs with. /// Omit to accept unsigned POSTs. #[arg(long = "inbound-secret")] inbound_secret: Option, }, /// Create a trigger of any kind from a raw JSON body — escape /// hatch for advanced retry/dispatch settings beyond the per-kind /// wrappers' defaults. #[command(name = "create-from-json")] CreateFromJson { #[arg(long)] app: String, /// Trigger kind: `kv`, `docs`, `cron`, `files`, `pubsub`, /// `dead_letter`, `email`, or `queue`. #[arg(long)] kind: String, /// Body JSON. Pass inline, `@`, or `-` for stdin. #[arg(long)] body: String, }, } #[derive(Clone, Copy, ValueEnum)] enum TopicAuthModeArg { /// No auth required to subscribe. Public, /// Subscriber bearer token required. Token, /// Per-app user session required. Session, } /// Wire form — the topics API accepts the lowercase strings directly. const fn topic_auth_wire(m: TopicAuthModeArg) -> &'static str { match m { TopicAuthModeArg::Public => "public", TopicAuthModeArg::Token => "token", TopicAuthModeArg::Session => "session", } } #[derive(Subcommand)] enum TopicsCmd { /// List registered topics for an app. Ls { #[arg(long)] app: String, }, /// Register a topic. Defaults to not externally subscribable; pass /// `--external` to open it to outside SSE subscribers. Create { #[arg(long)] app: String, /// Concrete topic name (no `*` wildcards). name: String, /// Allow external clients to subscribe over SSE. #[arg(long)] external: bool, /// Auth required of external subscribers. Defaults to `public`. #[arg(long = "auth-mode", value_enum, default_value_t = TopicAuthModeArg::Public)] auth_mode: TopicAuthModeArg, }, /// Update a topic's external/auth settings. Only the flags you pass /// change; the rest are left as-is. Update { #[arg(long)] app: String, name: String, /// `true` to open external SSE subscription, `false` to close it. #[arg(long)] external: Option, #[arg(long = "auth-mode", value_enum)] auth_mode: Option, }, /// Unregister a topic. Live SSE subscribers are disconnected. Rm { #[arg(long)] app: String, name: String, }, } #[derive(Subcommand)] enum UsersCmd { /// List an app's registered end-users. Ls { #[arg(long)] app: String, #[arg(long, default_value_t = 50)] limit: u32, }, /// Show a single end-user by id. Show { #[arg(long)] app: String, user_id: String, }, /// Mint a one-shot password-reset token for an end-user. Printed /// exactly once — paste it into a manual reset link. #[command(name = "reset-password")] ResetPassword { #[arg(long)] app: String, user_id: String, }, /// Revoke all of an end-user's active sessions. #[command(name = "revoke-sessions")] RevokeSessions { #[arg(long)] app: String, user_id: String, }, } #[derive(Subcommand)] enum DeadLettersCmd { /// Print the unresolved DL count. Cheap probe for alerting / /// dashboards — no row payloads, just the bare integer. Count { #[arg(long)] app: String, }, /// List dead-letter rows for an app. Ls { #[arg(long)] app: String, /// Show only unresolved rows. #[arg(long)] unresolved: bool, #[arg(long, default_value_t = 50)] limit: u32, }, /// Show a single dead-letter row's full payload. Show { #[arg(long)] app: String, dl_id: String, }, /// Re-enqueue the original event so the trigger fires again. The /// DL row is marked resolved with reason `replayed`. Replay { #[arg(long)] app: String, dl_id: String, }, /// Mark a DL row resolved without replaying. Useful for permanent /// failures the operator has already addressed manually. Resolve { #[arg(long)] app: String, dl_id: String, /// Free-form resolution reason. Required by the server. #[arg(long)] reason: String, }, } #[derive(Subcommand)] enum SecretsCmd { /// List secret names + last-modified for the owner. Values never /// leave the server. `--env` filters group secrets (app secrets are /// env-agnostic; the `env` column shows the scope for groups). Ls { /// Owning group (slug or id). Mutually exclusive with `--app`. #[arg(long)] group: Option, /// Owning app (slug or id). Mutually exclusive with `--group`. #[arg(long)] app: Option, /// Environment scope (group owners only). #[arg(long)] env: Option, }, /// Set a secret. Reads the value from stdin (the only safe /// channel — inline values would land in shell history). Pipe /// the value in: `echo -n "mysecret" | pic secrets set --app foo my_key`. /// For group owners, `--env` scopes the secret. Set { /// Owning group (slug or id). Mutually exclusive with `--app`. #[arg(long)] group: Option, /// Owning app (slug or id). Mutually exclusive with `--group`. #[arg(long)] app: Option, name: String, /// Environment scope (group owners only). #[arg(long)] env: Option, /// Treat stdin as raw JSON (numbers, maps, …) instead of a /// string literal. #[arg(long)] json: bool, }, /// Delete a secret by name (optionally env-scoped for groups). Rm { /// Owning group (slug or id). Mutually exclusive with `--app`. #[arg(long)] group: Option, /// Owning app (slug or id). Mutually exclusive with `--group`. #[arg(long)] app: Option, name: String, /// Environment scope (group owners only). #[arg(long)] env: Option, }, /// Fetch and print a secret's PLAINTEXT value. This is the ONLY /// command that reveals a secret value, and it is gated server-side /// at the owning group — hence `--group` only, no `--app`. Read { /// Owning group (slug or id). #[arg(long)] group: String, name: String, /// Environment scope. #[arg(long)] env: Option, }, } #[derive(Subcommand)] enum VarsCmd { /// List the owner's OWN vars (not the resolved/inherited view). Ls { /// Owning group (slug or id). Mutually exclusive with `--app`. #[arg(long)] group: Option, /// Owning app (slug or id). Mutually exclusive with `--group`. #[arg(long)] app: Option, }, /// Set a var. The value is stored as a JSON string by default; pass /// `--json` to parse it as raw JSON. `--tombstone` writes a deletion /// marker that suppresses an inherited key. Set { key: String, /// Ignored (but accepted) when `--tombstone` is set. #[arg(default_value = "")] value: String, #[arg(long)] group: Option, #[arg(long)] app: Option, /// Environment scope (`*` = env-agnostic, the default). #[arg(long)] env: Option, /// Parse `value` as raw JSON instead of a string literal. #[arg(long)] json: bool, /// Write a tombstone (suppress an inherited key) instead of a value. #[arg(long)] tombstone: bool, }, /// Delete a var by key (optionally scoped to one environment). Rm { key: String, #[arg(long)] group: Option, #[arg(long)] app: Option, #[arg(long)] env: Option, }, } #[derive(Subcommand)] enum AdminsCmd { /// List admin accounts. Ls, /// Create a new admin. Password via `--password -` reads from /// stdin (the recommended path for non-interactive use). Without /// `--instance-role`, defaults to `admin`. Create { username: String, /// Password. Use `--password -` to read from stdin (interactive /// no-echo prompt, or piped). An inline value is rejected — it /// would leak into shell history, `ps`, and /proc. #[arg(long)] password: Option, /// `owner`, `admin`, or `member`. Defaults to `admin`. #[arg(long = "instance-role", value_enum, default_value_t = InstanceRoleArg::Admin)] instance_role: InstanceRoleArg, #[arg(long)] email: Option, }, /// Show a single admin by id. Show { id: String }, /// Patch an admin. Pass only the fields you want to change. Set { id: String, #[arg(long)] username: Option, /// Use `--password -` to read from stdin. An inline value is /// rejected (it leaks into shell history, `ps`, and /proc). #[arg(long)] password: Option, /// `true` to (re)activate, `false` to deactivate (deactivation /// also expires every API key owned by the user). #[arg(long)] active: Option, #[arg(long = "instance-role", value_enum)] instance_role: Option, }, /// Delete an admin by id. Rm { id: String }, } #[tokio::main(flavor = "current_thread")] async fn main() -> ExitCode { let cli = Cli::parse(); let mode = cli.output; let result = match cli.cmd { Cmd::Login(args) => { cmds::login::run( args.url.as_deref(), args.token.as_deref(), args.username.as_deref(), args.password_stdin, ) .await } Cmd::Logout => cmds::logout::run().await, Cmd::Whoami => cmds::whoami::run(mode).await, Cmd::Apply(args) => match &args.dir { Some(dir) => { let structure_mode = if args.force_local_structure { "force-local" } else if args.adopt_server_structure { "adopt-server" } else { "refuse" }; cmds::apply::run_tree( dir, args.prune, args.yes, args.force, args.takeover, structure_mode, &args.approve, args.env.as_deref(), mode, ) .await } None => { cmds::apply::run( &args.file, args.env.as_deref(), args.prune, args.yes, args.force, args.takeover, &args.approve, mode, ) .await } }, Cmd::Plan(args) => match &args.dir { Some(dir) => cmds::plan::run_tree(dir, args.env.as_deref(), mode).await, None => cmds::plan::run(&args.file, args.env.as_deref(), mode).await, }, Cmd::Pull(args) => cmds::pull::run(&args.app, &args.dir, args.force, mode).await, Cmd::Config(args) => { cmds::config::run( &args.file, args.effective, args.explain, args.env.as_deref(), mode, ) .await } Cmd::Init(args) => cmds::init::run( &args.dir, args.slug.as_deref(), args.name.as_deref(), args.force, mode, ), Cmd::Apps { cmd: AppsCmd::Ls } => cmds::apps::ls(mode).await, Cmd::Apps { cmd: AppsCmd::Create { slug, name, description, group, }, } => { cmds::apps::create( &slug, name.as_deref(), description.as_deref(), group.as_deref(), mode, ) .await } Cmd::Apps { cmd: AppsCmd::Show { ident }, } => cmds::apps::show(&ident, mode).await, Cmd::Apps { cmd: AppsCmd::Delete { ident, force }, } => cmds::apps::delete(&ident, force).await, Cmd::Apps { cmd: AppsCmd::Domains { cmd: DomainsCmd::Ls { app }, }, } => cmds::apps_domains::ls(&app, mode).await, Cmd::Apps { cmd: AppsCmd::Domains { cmd: DomainsCmd::Add { app, pattern }, }, } => cmds::apps_domains::add(&app, &pattern, mode).await, Cmd::Apps { cmd: AppsCmd::Domains { cmd: DomainsCmd::Rm { app, domain_id }, }, } => cmds::apps_domains::rm(&app, &domain_id).await, Cmd::Projects { cmd: ProjectsCmd::Ls, } => cmds::projects::ls(mode).await, Cmd::Groups { cmd: GroupsCmd::Ls } => cmds::groups::ls(mode).await, Cmd::Groups { cmd: GroupsCmd::Tree, } => cmds::groups::tree(mode).await, Cmd::Groups { cmd: GroupsCmd::Create { slug, name, description, parent, }, } => { cmds::groups::create( &slug, name.as_deref(), description.as_deref(), parent.as_deref(), mode, ) .await } Cmd::Groups { cmd: GroupsCmd::Show { ident }, } => cmds::groups::show(&ident, mode).await, Cmd::Groups { cmd: GroupsCmd::Rename { ident, name, description, }, } => cmds::groups::rename(&ident, name.as_deref(), description.as_deref(), mode).await, Cmd::Groups { cmd: GroupsCmd::Reparent { ident, to }, } => cmds::groups::reparent(&ident, to.as_deref(), mode).await, Cmd::Groups { cmd: GroupsCmd::Rm { ident, recursive }, } => cmds::groups::rm(&ident, recursive).await, Cmd::Groups { cmd: GroupsCmd::Members { cmd: GroupMembersCmd::Ls { group }, }, } => cmds::groups::members_ls(&group, mode).await, Cmd::Groups { cmd: GroupsCmd::Members { cmd: GroupMembersCmd::Add { group, user_id, role, }, }, } => cmds::groups::members_add(&group, &user_id, &role, mode).await, Cmd::Groups { cmd: GroupsCmd::Members { cmd: GroupMembersCmd::Set { group, user_id, role, }, }, } => cmds::groups::members_set(&group, &user_id, &role, mode).await, Cmd::Groups { cmd: GroupsCmd::Members { cmd: GroupMembersCmd::Rm { group, user_id }, }, } => cmds::groups::members_rm(&group, &user_id).await, Cmd::Scripts { cmd: ScriptsCmd::Ls { app, group }, } => cmds::scripts::ls(app.as_deref(), group.as_deref(), mode).await, Cmd::Scripts { cmd: ScriptsCmd::Deploy(args), } => match args.script_config() { Ok(cfg) => { cmds::scripts::deploy( &args.file, args.app.as_deref(), args.group.as_deref(), args.name.as_deref(), args.description.as_deref(), &cfg, mode, ) .await } Err(e) => Err(e), }, Cmd::Scripts { cmd: ScriptsCmd::Invoke(args), } => cmds::scripts::invoke(&args.id, args.body.as_deref(), &args.headers).await, Cmd::Scripts { cmd: ScriptsCmd::Delete { id }, } => cmds::scripts::delete(&id).await, Cmd::ApiKeys { cmd: ApiKeysCmd::Mint { name, scopes, app, expires, }, } => cmds::api_keys::mint(&name, &scopes, app.as_deref(), expires.as_deref(), mode).await, Cmd::ApiKeys { cmd: ApiKeysCmd::Ls, } => cmds::api_keys::ls(mode).await, Cmd::ApiKeys { cmd: ApiKeysCmd::Rm { id }, } => cmds::api_keys::rm(&id).await, Cmd::Logs(LogsArgs { script_id, limit, source, }) => cmds::logs::run(&script_id, limit, source.as_deref(), mode).await, Cmd::Invoke(args) => { cmds::scripts::invoke(&args.id, args.body.as_deref(), &args.headers).await } Cmd::Deploy(args) => match args.script_config() { Ok(cfg) => { cmds::scripts::deploy( &args.file, args.app.as_deref(), args.group.as_deref(), args.name.as_deref(), args.description.as_deref(), &cfg, mode, ) .await } Err(e) => Err(e), }, Cmd::Routes { cmd: RoutesCmd::Ls { script_id, group }, } => match (script_id, group) { (_, Some(group)) => cmds::routes::ls_group(&group, mode).await, (Some(script_id), None) => cmds::routes::ls(&script_id, mode).await, (None, None) => Err(anyhow::anyhow!("provide a script id or --group")), }, Cmd::Routes { cmd: RoutesCmd::Create { script, path, method, path_kind, host, host_kind, host_param_name, dispatch, }, } => { cmds::routes::create( &script, method.as_deref(), &path, path_kind.into(), &host, host_kind.into(), host_param_name.as_deref(), dispatch.into(), ) .await } Cmd::Routes { cmd: RoutesCmd::Rm { route_id }, } => cmds::routes::rm(&route_id).await, Cmd::Routes { cmd: RoutesCmd::Check { app, path, method, path_kind, host, host_kind, }, } => { cmds::routes::check( &app, method.as_deref(), &path, path_kind.into(), &host, host_kind.into(), mode, ) .await } Cmd::Routes { cmd: RoutesCmd::Match { app, url, method }, } => cmds::routes::match_route(&app, &url, &method, mode).await, Cmd::Admins { cmd: AdminsCmd::Ls } => cmds::admins::ls(mode).await, Cmd::Admins { cmd: AdminsCmd::Create { username, password, instance_role, email, }, } => { cmds::admins::create( &username, password.as_deref(), instance_role.into(), email.as_deref(), mode, ) .await } Cmd::Admins { cmd: AdminsCmd::Show { id }, } => cmds::admins::show(&id, mode).await, Cmd::Admins { cmd: AdminsCmd::Set { id, username, password, active, instance_role, }, } => { cmds::admins::set( &id, username.as_deref(), password.as_deref(), active, instance_role.map(Into::into), mode, ) .await } Cmd::Admins { cmd: AdminsCmd::Rm { id }, } => cmds::admins::rm(&id).await, Cmd::Triggers { cmd: TriggersCmd::Ls { app, group }, } => match (app, group) { (_, Some(group)) => cmds::triggers::ls_group(&group, mode).await, (Some(app), None) => cmds::triggers::ls(&app, mode).await, (None, None) => Err(anyhow::anyhow!("provide --app or --group")), }, Cmd::Triggers { cmd: TriggersCmd::Rm { app, trigger_id }, } => cmds::triggers::rm(&app, &trigger_id).await, Cmd::Triggers { cmd: TriggersCmd::CreateKv { app, script, collection, ops, dispatch, }, } => { cmds::triggers::create_kv( &app, &script, &collection, &ops, dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateCron { app, script, schedule, timezone, dispatch, }, } => { cmds::triggers::create_cron( &app, &script, &schedule, timezone.as_deref(), dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateDeadLetter { app, script, source_filter, trigger_id_filter, script_id_filter, dispatch, }, } => { cmds::triggers::create_dead_letter( &app, &script, source_filter.as_deref(), trigger_id_filter.as_deref(), script_id_filter.as_deref(), dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateDocs { app, script, collection, ops, dispatch, }, } => { cmds::triggers::create_docs( &app, &script, &collection, &ops, dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateFiles { app, script, collection, ops, dispatch, }, } => { cmds::triggers::create_files( &app, &script, &collection, &ops, dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreatePubsub { app, script, topic_pattern, dispatch, }, } => { cmds::triggers::create_pubsub( &app, &script, &topic_pattern, dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateQueue { app, script, queue_name, visibility_timeout_secs, dispatch, }, } => { cmds::triggers::create_queue( &app, &script, &queue_name, visibility_timeout_secs, dispatch_wire(dispatch), mode, ) .await } Cmd::Triggers { cmd: TriggersCmd::CreateEmail { app, script, inbound_secret, }, } => cmds::triggers::create_email(&app, &script, inbound_secret.as_deref(), mode).await, Cmd::Triggers { cmd: TriggersCmd::CreateFromJson { app, kind, body }, } => cmds::triggers::create_from_json(&app, &kind, &body, mode).await, Cmd::Topics { cmd: TopicsCmd::Ls { app }, } => cmds::topics::ls(&app, mode).await, Cmd::Topics { cmd: TopicsCmd::Create { app, name, external, auth_mode, }, } => cmds::topics::create(&app, &name, external, topic_auth_wire(auth_mode), mode).await, Cmd::Topics { cmd: TopicsCmd::Update { app, name, external, auth_mode, }, } => { cmds::topics::update(&app, &name, external, auth_mode.map(topic_auth_wire), mode).await } Cmd::Topics { cmd: TopicsCmd::Rm { app, name }, } => cmds::topics::rm(&app, &name).await, Cmd::Users { cmd: UsersCmd::Ls { app, limit }, } => cmds::users::ls(&app, limit, mode).await, Cmd::Users { cmd: UsersCmd::Show { app, user_id }, } => cmds::users::show(&app, &user_id, mode).await, Cmd::Users { cmd: UsersCmd::ResetPassword { app, user_id }, } => cmds::users::reset_password(&app, &user_id, mode).await, Cmd::Users { cmd: UsersCmd::RevokeSessions { app, user_id }, } => cmds::users::revoke_sessions(&app, &user_id, mode).await, Cmd::DeadLetters { cmd: DeadLettersCmd::Count { app }, } => cmds::dead_letters::count(&app, mode).await, Cmd::DeadLetters { cmd: DeadLettersCmd::Ls { app, unresolved, limit, }, } => cmds::dead_letters::ls(&app, unresolved, limit, mode).await, Cmd::DeadLetters { cmd: DeadLettersCmd::Show { app, dl_id }, } => cmds::dead_letters::show(&app, &dl_id, mode).await, Cmd::DeadLetters { cmd: DeadLettersCmd::Replay { app, dl_id }, } => cmds::dead_letters::replay(&app, &dl_id).await, Cmd::DeadLetters { cmd: DeadLettersCmd::Resolve { app, dl_id, reason }, } => cmds::dead_letters::resolve(&app, &dl_id, &reason).await, Cmd::Secrets { cmd: SecretsCmd::Ls { group, app, env }, } => cmds::secrets::ls(group.as_deref(), app.as_deref(), env.as_deref(), mode).await, Cmd::Secrets { cmd: SecretsCmd::Set { group, app, name, env, json, }, } => { cmds::secrets::set( group.as_deref(), app.as_deref(), &name, env.as_deref(), json, ) .await } Cmd::Secrets { cmd: SecretsCmd::Rm { group, app, name, env, }, } => cmds::secrets::rm(group.as_deref(), app.as_deref(), &name, env.as_deref()).await, Cmd::Secrets { cmd: SecretsCmd::Read { group, name, env }, } => cmds::secrets::read(&group, &name, env.as_deref()).await, Cmd::Members { cmd: MembersCmd::Ls { app }, } => cmds::members::ls(&app, mode).await, Cmd::Members { cmd: MembersCmd::Add { app, user_id, role }, } => cmds::members::add(&app, &user_id, &role, mode).await, Cmd::Members { cmd: MembersCmd::Set { app, user_id, role }, } => cmds::members::set(&app, &user_id, &role, mode).await, Cmd::Members { cmd: MembersCmd::Rm { app, user_id }, } => cmds::members::rm(&app, &user_id).await, Cmd::Vars { cmd: VarsCmd::Ls { group, app }, } => cmds::vars::ls(group.as_deref(), app.as_deref(), mode).await, Cmd::Vars { cmd: VarsCmd::Set { key, value, group, app, env, json, tombstone, }, } => { cmds::vars::set( group.as_deref(), app.as_deref(), &key, &value, env.as_deref(), json, tombstone, ) .await } Cmd::Vars { cmd: VarsCmd::Rm { key, group, app, env, }, } => cmds::vars::rm(group.as_deref(), app.as_deref(), &key, env.as_deref()).await, Cmd::ExtensionPoints { cmd: ExtensionPointsCmd::Ls { app, group }, } => cmds::extension_points::ls(app.as_deref(), group.as_deref(), mode).await, Cmd::Collections { cmd: CollectionsCmd::Ls { group }, } => cmds::collections::ls(&group, mode).await, Cmd::Suppress { cmd: SuppressCmd::Ls { app, group }, } => cmds::suppress::ls(app.as_deref(), group.as_deref(), mode).await, Cmd::Files { cmd: FilesCmd::Ls { app, collection, limit, }, } => cmds::files::ls(&app, &collection, limit, mode).await, Cmd::Files { cmd: FilesCmd::Get { app, collection, file_id, out, }, } => cmds::files::get(&app, &collection, &file_id, out.as_deref()).await, Cmd::Files { cmd: FilesCmd::Rm { app, collection, file_id, }, } => cmds::files::rm(&app, &collection, &file_id).await, Cmd::Queues { cmd: QueuesCmd::Ls { app }, } => cmds::queues::ls(&app, mode).await, Cmd::Queues { cmd: QueuesCmd::Show { app, queue_name }, } => cmds::queues::show(&app, &queue_name, mode).await, Cmd::Kv { cmd: KvCmd::Ls { app, group, collection, limit, }, } => cmds::kv::ls(app.as_deref(), group.as_deref(), &collection, limit, mode).await, Cmd::Kv { cmd: KvCmd::Get { app, group, collection, key, }, } => cmds::kv::get(app.as_deref(), group.as_deref(), &collection, &key).await, }; match result { Ok(()) => ExitCode::SUCCESS, Err(err) => { output::print_error(&err); ExitCode::FAILURE } } }