Makes §7 ownership usable end to end from the CLI. - manifest: a `[project]` block (slug + optional name), independent of the [app]/[group] XOR; threaded onto every apply from the repo. --dir reads it from the tree ROOT's picloud.toml (a [project] elsewhere is ignored with a note). - client: apply_node/apply_tree carry project + takeover; the 409 branch now surfaces the server message verbatim (covers StateMoved AND OwnershipConflict, both self-contained). New groups_list_with_owner captures the owner slug. - pic apply --takeover flag; pic groups ls gains an `owner` column (the owning project's slug, or — when unclaimed). - server: /api/v1/admin/groups now returns each group flattened with its owner slug (via list_with_owner) — the shared Group deserialize ignores the extra field, so pic groups tree / the dashboard are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2122 lines
64 KiB
Rust
2122 lines
64 KiB
Rust
//! 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,
|
|
},
|
|
|
|
/// 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 <id>`.
|
|
Invoke(InvokeArgs),
|
|
|
|
/// Top-level alias for `pic scripts deploy <file> --app <slug>`.
|
|
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 `@<file>`, 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/<name>.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 (`<set>`/`<unset>`), 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<PathBuf>,
|
|
/// 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.<env>.toml` overlay (per-env slug + secrets) on
|
|
/// top of the base manifest before applying.
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
}
|
|
|
|
#[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<PathBuf>,
|
|
/// Merge the `picloud.<env>.toml` overlay (per-env slug + secrets) on
|
|
/// top of the base manifest before diffing.
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
/// 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<String>,
|
|
/// 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.<env>.toml` overlay (per-env slug +
|
|
/// secrets), matching `pic plan --env` / `pic apply --env`.
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
#[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<String>,
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
#[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 <path>` or stdout).
|
|
Get {
|
|
#[arg(long)]
|
|
app: String,
|
|
#[arg(long)]
|
|
collection: String,
|
|
#[arg(long = "id")]
|
|
file_id: String,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
/// 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<String>,
|
|
|
|
/// 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<String>,
|
|
|
|
/// Username for non-interactive password login. Pair with
|
|
/// `--password-stdin`. Omit both for the interactive prompt.
|
|
#[arg(long)]
|
|
username: Option<String>,
|
|
|
|
/// 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<String>,
|
|
#[arg(long)]
|
|
description: Option<String>,
|
|
/// Parent group (slug or id). Defaults to the instance root.
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
},
|
|
|
|
/// 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 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<String>,
|
|
#[arg(long)]
|
|
description: Option<String>,
|
|
/// Parent group (slug or id).
|
|
#[arg(long)]
|
|
parent: Option<String>,
|
|
},
|
|
/// 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<String>,
|
|
#[arg(long)]
|
|
description: Option<String>,
|
|
},
|
|
/// Move a group under a new parent (`--to` slug/id, or omit for root).
|
|
Reparent {
|
|
ident: String,
|
|
#[arg(long)]
|
|
to: Option<String>,
|
|
},
|
|
/// 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<String>,
|
|
#[arg(long, conflicts_with = "app")]
|
|
group: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[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<String>,
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[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<String>,
|
|
/// Phase 4: list a group's own (non-inherited) scripts.
|
|
#[arg(long, conflicts_with = "app")]
|
|
group: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
/// 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<String>,
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
#[arg(long)]
|
|
description: Option<String>,
|
|
/// Per-script wall-clock timeout in seconds (overrides the instance
|
|
/// default; still clamped by `PICLOUD_SANDBOX_MAX_*` ceilings).
|
|
#[arg(long)]
|
|
timeout: Option<i32>,
|
|
/// Per-script memory ceiling in MB.
|
|
#[arg(long)]
|
|
memory: Option<i32>,
|
|
/// Script kind: `endpoint` (default) or `module` (importable, no route).
|
|
#[arg(long, value_enum)]
|
|
kind: Option<ScriptKindArg>,
|
|
/// 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<String>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, clap::ValueEnum)]
|
|
enum ScriptKindArg {
|
|
Endpoint,
|
|
Module,
|
|
}
|
|
|
|
impl From<ScriptKindArg> 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<client::ScriptConfig> {
|
|
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<Option<picloud_shared::ScriptSandbox>> {
|
|
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<String>,
|
|
#[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<String>,
|
|
/// Bind the key to a single app (slug or id). Rejects
|
|
/// `instance:*` scopes when set.
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
/// Absolute RFC 3339 (`2026-12-31T23:59:59Z`) or shorthand
|
|
/// `<N>d`/`<N>h`/`<N>m`.
|
|
#[arg(long)]
|
|
expires: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, ValueEnum)]
|
|
enum DispatchModeArg {
|
|
Sync,
|
|
Async,
|
|
}
|
|
|
|
impl From<DispatchModeArg> 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,
|
|
/// `*.<host>` — wildcard subdomain.
|
|
Wildcard,
|
|
}
|
|
|
|
impl From<HostKindArg> 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<PathKindArg> 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<InstanceRoleArg> 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<String>,
|
|
/// List the group's §11 tail route templates instead.
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
/// `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<String>,
|
|
/// `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<String>,
|
|
#[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<String>,
|
|
/// List the group's §11 tail trigger templates instead.
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
/// Trigger id filter — match only DL rows from one trigger.
|
|
#[arg(long = "trigger-id-filter")]
|
|
trigger_id_filter: Option<String>,
|
|
/// Script id filter — match only DL rows from one script.
|
|
#[arg(long = "script-id-filter")]
|
|
script_id_filter: Option<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[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<u32>,
|
|
#[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<String>,
|
|
},
|
|
|
|
/// 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, `@<file>`, 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<bool>,
|
|
#[arg(long = "auth-mode", value_enum)]
|
|
auth_mode: Option<TopicAuthModeArg>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
/// Environment scope (group owners only).
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
name: String,
|
|
/// Environment scope (group owners only).
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
/// 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<String>,
|
|
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
name: String,
|
|
/// Environment scope (group owners only).
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
},
|
|
}
|
|
|
|
#[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<String>,
|
|
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
/// Environment scope (`*` = env-agnostic, the default).
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
/// 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<String>,
|
|
#[arg(long)]
|
|
app: Option<String>,
|
|
#[arg(long)]
|
|
env: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[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<String>,
|
|
/// `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<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
/// Use `--password -` to read from stdin. An inline value is
|
|
/// rejected (it leaks into shell history, `ps`, and /proc).
|
|
#[arg(long)]
|
|
password: Option<String>,
|
|
/// `true` to (re)activate, `false` to deactivate (deactivation
|
|
/// also expires every API key owned by the user).
|
|
#[arg(long)]
|
|
active: Option<bool>,
|
|
#[arg(long = "instance-role", value_enum)]
|
|
instance_role: Option<InstanceRoleArg>,
|
|
},
|
|
|
|
/// 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) => {
|
|
cmds::apply::run_tree(
|
|
dir,
|
|
args.prune,
|
|
args.yes,
|
|
args.force,
|
|
args.takeover,
|
|
args.env.as_deref(),
|
|
mode,
|
|
)
|
|
.await
|
|
}
|
|
None => {
|
|
cmds::apply::run(
|
|
&args.file,
|
|
args.env.as_deref(),
|
|
args.prune,
|
|
args.yes,
|
|
args.force,
|
|
args.takeover,
|
|
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::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
|
|
}
|
|
}
|
|
}
|